Make an initial attempt at a better factoring

This splits the overall example rack service into many sub-services.
This leads to a much cleaner factoring of the code.  Note that there are
some missing properties -- it's hard to eyeball this without a real compiler.
But the essence of the example is pretty spot on.
This commit is contained in:
joeduffy 2016-12-15 19:40:34 -08:00
parent 68a3d27a73
commit 2e941bbc57
8 changed files with 911 additions and 784 deletions

View file

@ -15,795 +15,28 @@ import "aws/sns"
service Rack {
// TODO: lambda code.
// TODO: that big nasty UserData shell script.
// TODO: factor things out into separate initialization helpers, perhaps.
// TODO: possibly even refactor individual things into services (e.g., the networks).
// TODO: we probably need a ToString()-like thing for services (e.g., ARN/ID for most AWS ones).
new() {
// IAM goo.
customTopicRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "Administrator"
policyDocument: {
version: "2012-10-17"
statement: [
{ effect: "Allow", action: "*", resource: "*" }
{ effect: "Deny", action: "s3:DeleteObject", resource: "*" }
]
}
}]
}
resources {
security := new rackSecurity {}
network := new rackNetwork {
existingVpc: existingVpc
private: private
privateApi: privateApi
subnetCIDRs: subnetCIDRs
subnetPrivateCIDRs: subnetPrivateCIDRs
vpccidr: vpccidr
}
kerneluser := new iam.User {
path: "xovnoc"
policies: [{
policyName: "Administrator"
policyDocument: {
version: "2012-10-17"
statement: [{ effect: "Allow", action: "*", resource: "*"}]
}
}]
logging := new rackLogging {
role: security.logSubscriptionFilterRole
}
kernelAccess := new iam.AccessKey {
serial: 1
status: "Active"
userName: kernelUser
}
logSubscriptionFilterRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "LogSubscriptionFilterRole"
policyDocument: {
version: "2012-10-17"
statement: [
{
effect: "Allow"
action: [
"logs:CreateLogGroup"
"logs:CreateLogStream"
"logs:PutLogEvents"
]
resource: "arn:aws:logs:*:*:*"
}
{
effect: "Allow"
action: [ "cloudwatch:PutMetricData" ]
resource: "*"
}
]
}
}]
}
}
iamRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "ec2.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
}
path: "/xovnoc/"
policies: [{
policyName: "ClusterInstanceRole"
policyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
action: [
"autoscaling:CompleteLifecycleAction"
"autoscaling:DescribeAutoScalingInstances"
"autoscaling:DescribeLifecycleHooks"
"autoscaling:SetInstanceHealth"
"ecr:GetAuthorizationToken"
"ecr:GetDownloadUrlForLayer"
"ecr:BatchGetImage"
"ecr:BatchCheckLayerAvailability"
"ec2:DescribeInstances"
"ecs:CreateCluster"
"ecs:DeregisterContainerInstance"
"ecs:DiscoverPollEndpoint"
"ecs:Poll"
"ecs:RegisterContainerInstance"
"ecs:StartTelemetrySession"
"ecs:Submit*"
"kinesis:PutRecord"
"kinesis:PutRecords"
"logs:CreateLogStream"
"logs:DescribeLogStreams"
"logs:PutLogEvents"
]
resource: [ "*" ]
}]
}
}]
}
instanceProfile := new aim.InstanceProfile {
path: "/xovnoc/"
roles: [ iamRole ]
}
instancesLifecycleRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "InstancesLifecycleRole"
policyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
action: [ "sns:Publish" ]
resource: instancesLifecycleTopic
}]
}
}]
}
}
instancesHandlerRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "InstancesLifecycleHandlerRole"
policyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
action: [
"autoscaling:CompleteLifecycleAction",
"ecs:DeregisterContainerInstance",
"ecs:DescribeContainerInstances",
"ecs:DescribeServices",
"ecs:ListContainerInstances",
"ecs:ListServices",
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
"elasticloadbalancing:DescribeInstanceHealth",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeTags",
"lambda:GetFunction",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resource: "*"
}]
}
}]
}
}
encryptionKey := new KMSKey {
serviceToken: customTopic.Arn
description: "Xovnoc Master Encryption"
keyUsage: "ENCRYPT_DECRYPT"
}
// Logging resources.
logGroup := new logs.LogGroup {}
logSubscriptionFilterPermission := new lambda.Permission {
action: "lambda:InvokeFunction"
functionName: logSubscriptionFilterFunction
principal: "logs." + context.region + ".amazonaws.com"
sourceAccount: context.accountId
sourceArn: logGroup.Arn
}
logSubscriptionFilterFunction := new lambda.Function {
code: // TODO
handler: "index.handler"
memorySize: 128
role: logSubscriptionFilterRole.Arn
runtime: "nodejs"
timeout: 30
}
logSubscriptionFilter := new logs.SubscriptionFilter {
destinationArn: logSubscriptionFilterFunction.Arn
filterPattern: ""
logGroupName: logGroup
}
// Topic resources.
notificationTopic := new sns.Topic {
topicName: context.stack.name + "-notifications"
}
customTopic := new lambda.Function {
code: // TODO
handler: "index.external"
memorySize: 128
role: customTopicRole
runtime: "nodejs"
timeout: 300
}
instancesLifecycleTopic := new sns.Topic {
subscription: {
endpoint: instancesLifecycleHandler
protocol: lambda
}
topicName: context.stack.name + "-lifecycle"
}
instancesLifecycleHandler := new lambda.Function {
code: // TODO
description: `{ "Cluster": "${cluster}", "Rack": "${context.stack.name}" }`
handler: "index.external"
memorySize: 128
role: instancesLifecycleHandlerRole
runtime: nodejs
timeout: 300
}
instancesLifecycleHandlerPermission := new lambda.Permission {
source: instancesLifecycleTopic
function: instancesLifecycleHandler
action: "lambda:InvokeFunction"
principal: "sns.amazonaws.com"
}
cluster := new ecs.Cluster {}
var vpc: ec2.VPC
if existingVpc == "" {
vpc = new ec2.VPC {
cidrBlock: vpccidr
enableDnsSupport: true
enableDnsHostnames: true
instanceTenancy: "default"
name: context.stack.name
}
gateway := new ec2.InternetGateway {}
gatewayAttachment := new ec2.VPCGatewayAttachment {
internetGateway: gateway
vpc: vpc
}
routes := new ec2.RouteTable {
vpc: vpc
}
routeDefault := new ec2.Route {
destinationCidrBlock: "0.0.0.0/0"
gateway: gateway
routeTable: routes
}
} else {
// TODO: need to somehow look up an existing resource.
vpc = existingVpc
}
availabilityZones := new EC2AvailabilityZones {
serviceToken: customTopic
vpc: vpc
}
var subnets: ec2.Subnet[]
for zone in availabilityZones {
append(subnets, new ec2.Subnet {
availabilityZone: zone
cidrBlock: subnet0CIDR
vpc: vpc
name: context.stack.name + " public " + i
})
}
if private {
var natAddresses: ec2.EIP[]
var nats: ec2.NatGateway[]
var routeTablePrivates: ec2.RouteTable[]
var routeTableDefaultPrivates: ec2.Route[]
for i, subnet in subnets {
append(natAddresses, new ec.EIP {
domain: vpc
})
append(nats, new ec2.NatGateway {
allocation: natAddresses[i]
subnet: subnets
})
append(routeTablePrivates, new ec2.RouteTable {
vpc: vpc
})
append(routeTableDefaultPrivates, new ec2.Route {
destinationCidrBlock: "0.0.0.0/0"
natGateway: nats[i]
routeTable: routeTablePrivates[i]
})
}
var subnetPrivates: ec2.Subnet[]
for i, zone in availabilityZones {
append(subnetPrivates, ec2.Subnet {
availabilityZone: zone
cidrBlock: subnetPrivateCIDR[i]
vpc: vpc
name: context.stack.name + " private " + i
}
}
}
if existingVpc == "" {
var subnetRoutes: ec2.SubnetRouteTableAssociation[]
for i, subnet in subnets {
append(subnetRoutes, new ec2.SubnetRouteTableAssociation {
subnet: subnet0
routesTable: routes
})
}
if private {
var subnetPrivateRoutes: ec2.SubnetRouteTableAssociation[]
for i, subnetPrivate in subnetPrivates {
append(subnetPrivateRoutes, ec2.SubnetRouteTableAssociation {
subnet: subnetPrivate
routesTable: routeTablePrivates[i]
})
}
}
}
securityGroup := new ec2.SecurityGroup: {
groupDescription: "Instances"
securityGroupIngress: [
{ ipProtocol: "tcp", fromPort: 22, toPort: 22, cidrIp: vpccidr }
{ ipProtocol: "tcp", fromPort: 0, toPort: 65535, cidrIp: vpccidr }
{ ipProtocol: "udp", fromPort: 0, toPort: 65535, cidrIp: vpccidr }
]
vpc: vpc
}
launchConfiguration := new autoscaling.LaunchConfiguration {
associatePublicIpAddress: !private
blockDeviceMappings: [
{
deviceName: "/dev/sdb"
ebs: {
volumeSize: swapSize
volumeType: "gp2"
}
}
{
deviceName: "/dev/xvdcz"
ebs: {
volumeSize: volumeSize
volumeType: "gp2"
}
}
]
iamInstanceProfile: instanceProfile
imageId: ami ?? regionConfig[context.region].ami
instanceMonitoring: true
instanceType: instanceType
keyName: key ?? undefined
placementTenancy: tenancy
securityGroups: [ securityGroup ]
userData: base64(makeUserData)
}
instances := new autoscaling.AutoScalingGroup {
launchConfiguration: launchConfiguration
availabilityZones: availabilityZones
vpcZoneIdentifier: private ? subnetPrivates : subnets
cooldown: 5
desiredCapacity: instanceCount
healthCheckType: "EC2"
healthCheckGracePeriod: 120
minSize: 1
maxSize: 1000
metricsCollection: [ { granularity: "1Minute" } ]
name: context.stack.name
tags: [
{
key: "Name"
value: context.stack.name
propagateAtLaunch: true
}
{
key: "Rack"
value: context.stack.name
propagateAtLaunch: true
}
{
key: "GatewayAttachment"
value: existingVpc == "" ? gatewayAttachment : "existing"
propagateAtLaunch: false
}
]
updatePolicy: {
// TODO: in CF, this isn't a "property"; it's a peer to properties.
autoScalingRollingUpdate: {
maxBatchSize: instanceUpdateBatchSize
minInstancesInService: instanceCount
pauseTime: "PT15M"
suspendProcesses: [ "ScheduledActions" ]
waitOnResourceSignals: "true"
}
}
}
instancesLifecycleLaunching := new autoscaling.LifecycleHook: {
autoScalingGroup: instances
defaultResult: "CONTINUE"
heartbeatTimeout: 600
lifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING"
notificationTarget: instancesLifecycleTopic
roleARN: instancesLifecycleRole.Arn
}
instancesLifecycleTerminating := new autoscaling.LifecycleHook: {
autoScalingGroup: instances
defaultResult: "CONTINUE"
heartbeatTimeout: 300
lifecycleTransition: "autoscaling:EC2_INSTANCE_TERMINATING"
notificationTarget: instancesLifecycleTopic
roleARN: instancesLifecycleRole.Arn
}
registryBucket := new s3.bucket: {
deletionPolicy: "Retain" // TODO: not actually a property, it's a peer.
accessControl: "Private"
}
registryUser := new iam.User {
path: "/xovnoc/"
policies: [{
policyName: "Administrator"
policyDocument: {
version: "2012-10-17"
statement: [{ effect: "Allow", action: "*", resource: "*" }]
}
}]
}
registryAccess := new iam.AccessKey {
serial: 1
status: "Active"
user: registryUser
}
balancer := new elasticloadbalancing.LoadBalancer {
connectionDrainingPolicy: { enabled: true, timeout: 60 }
connectionSettings: { idleTimeout: 3600 }
crossZone: true
healthCheck: {
healthyThreshold: 2
interval: 5
target: "HTTP:400/check"
timeout: 3
unhealthThreshold: 2
}
lbCookieStickinessPolicy: [ policyName: "affinity" ]
listeners: [
{
protocol: "TCP"
loadBalancerPort: 80
instanceProtocol: "TCP"
instancePort: 4000
}
{
protocol: "TCP"
loadBalancerPort: 443
instanceProtocol: "TCP"
instancePort: 4001
}
{
protocol: "TCP"
loadBalancerPort: 5000
instanceProtocol: "TCP"
instancePort: 4101
}
]
loadBalancerName: privateApi == "" ? undefined : "internal"
securityGroups: [ balancerSecurityGroup ]
subnets: privateApi == "" ? subnets : subnetPrivates
tags: [{ key: "GatewayAttachment", value: existingVpc == "" ? gatewayAttachment : "existing" }]
}
balancerSecurityGroup := new ec2.SecurityGroup {
groupDescription: context.stack.name + "-balancer"
securityGroupIngress: [
{
cidrIp: privateApi ? vpccidr : "0.0.0.0/0"
ipProtocol: "tcp"
fromPort: 80
toPort: 80
}
{
cidrIp: privateApi ? vpccidr : "0.0.0.0/0"
ipProtocol: "tcp"
fromPort: 443
toPort: 443
}
{
cidrIp: privateApi ? vpccidr : "0.0.0.0/0"
ipProtocol: "tcp"
fromPort: 5000
toPort: 5000
}
]
vpc: vpc
}
rackWeb := new ecs.Service {
cluster: cluster
deploymentConfiguration: {
minimumHealthyPercent: 100
maximumPercent: 200
}
desiredCount: 2
loadBalancers: [{
containerName: "web"
containerPort: 3000
loadBalancer: balancer
}]
role: serviceRole
taskDefinition: rackWebTasks
}
rackMonitor := new ecs.Service {
cluster: cluster
deploymentConfiguration: {
minimumHealthyPercent: 100
maximumPercent: 200
}
desiredCount: 1
taskDefinition: rackMonitorTasks
}
serviceRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "ServiceRole"
policyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
action: [
"elasticloadbalancing:Describe*"
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer"
"elasticloadbalancing:RegisterInstancesWithLoadBalancer"
"ec2:Describe*"
"ec2:AuthorizeSecurityGroupIngress"
]
resource: "*"
}]
}
}]
}
}
dynamoBuilds := new dynamodb.Table {
tableName: context.stack.name + "-builds"
attributeDefinitions: [
{ attributeName: "id", attributeType: "S" }
{ attributeName: "app", attributeType: "S" }
{ attributeName: "created", attributeType: "S" }
]
keySchema: [{ attributeName: "id", keyType: "HASH" }]
globalSecondaryIndexes: [{
indexName: "app.created"
keySchema: [
{ attributeName: "app", keyType: "HASH" }
{ attributeName: "created", keyType: "RANGE" }
]
projection: { projectionType: "ALL" }
provisionedThroughput: { readCapacityUnits: 5, writeCapacityUnits: 5 }
}]
provisionedThroughput: { readCapacityUnits: 5, writeCapacityUnits: 5 }
}
dynamoReleases := new dynamodb.Table {
tableName: context.stack.name + "-releases"
attributeDefinitions: [
{ attributeName: "id", attributeType: "S" }
{ attributeName: "app", attributeType: "S" }
{ attributeName: "created", attributeType: "S" }
]
keySchema: [{ attributeName: "id", keyType: "HASH" }]
globalSecondaryIndexes: [{
indexName: "app.created"
keySchema: [
{ attributeName: "app", keyType: "HASH" }
{ attributeName: "created", keyType: "RANGE" }
]
projection: { projectionType: "ALL" }
provisionedThroughput: { readCapacityUnits: 5, writeCapacityUnits: 5 }
}]
provisionedThroughput: { readCapacityUnits: 5, writeCapacityUnits: 5 }
}
if regionHasEFS {
volumeFilesystem := new efs.FileSystem {
fileSystemTags: [{ key: "Name", value: context.stack.name + "-shared-volumes" }]
}
volumeSecurity := new ec2.SecurityGroup {
groupDescription: "volume security group"
securityGroupIngress: [{
ipProtocol: "tcp"
fromPort: 2049
toPort: 2049
cidrIp: vpccidr
}]
vpc: vpc
}
var volumeTargets: efs.MountTarget[]
for i, subnet in (private ? subnetPrivates : subnets) {
append(volumeTargets, new efs.MountTarget {
fileSystem: volumeFilesystem
subnet: subnet
securityGroups: [ volumeSecurity ]
})
}
}
settings := new s3.Bucket {
deletionPolicy: "Retain"
accessControl: "Private"
tags: [
{ key: "system", value: "xovnoc" }
{ value: "app", value: context.stack.name }
]
}
rackBuildTasks := new ECSTaskDefinition {
name: context.stack.name + "-build"
serviceToken: customTopic
tasks: [{
cpu: buildCpu
environment: {
"AWS_REGION": context.region
"AWS_ACCESS": kernelAccess
"AWS_SECRET": kernelAccess.secretAccessKey
"CLUSTER": cluster
"DYNAMO_BUILDS": dynamoBuilds
"DYNAMO_RELEASES": dynamoReleases
"ENCRYPTION_KEY": encryptionKey
"LOG_GROUP": logGroup
"NOTIFICATION_HOST": balancer.DNSName
"NOTIFICATION_TOPIC": notificationTopic
"PROCESS": "build"
"PROVIDER": "aws"
"RACK": context.stack.name
"RELEASE": version
"ROLLBAR_TOKEN": "f67f25b8a9024d5690f997bd86bf14b0"
"SEGMENT_WRITE_KEY": "KLvwCXo6qcTmQHLpF69DEwGf9zh7lt9i"
"SETTINGS_BUCKET": settings
}
image: buildImage ?? "xovnoc/api:" + version
links: []
memory: buildMemory
name: "build"
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
}]
}
rackWebTasks := new ECSTaskDefinition {
name: context.stack.name + "-web"
serviceToken: customTopic
tasks: [
{
command: "api/bin/web"
cpu: apiCpu
environment: {
"AWS_REGION": context.region
"AWS_ACCESS": kernelAccess
"AWS_SECRET": kernelAccess.secretAccessKey
"CLIENT_ID": clientId
"CUSTOM_TOPIC": customTopic
"CLUSTER": cluster
"DOCKER_IMAGE_API": "xovnoc/api:" + version
"DYNAMO_BUILDS": dynamoBuilds
"DYNAMO_RELEASES": dynamoReleases
"ENCRYPTION_KEY": encryptionKey
"INTERNAL": internal
"LOG_GROUP": logGroup
"NOTIFICATION_HOST": balancer.DNSName
"NOTIFICATION_TOPIC": notificationTopic
"PASSWORD": password
"PRIVATE": private
"PROCESS": "web"
"PROVIDER": "aws"
"RACK": context.stack.name
"REGISTRY_HOST": balancer.DNSName + ":5000"
"RELEASE": version
"ROLLBAR_TOKEN": "f67f25b8a9024d5690f997bd86bf14b0"
"SECURITY_GROUP": securityGroup
"SEGMENT_WRITE_KEY": "KLvwCXo6qcTmQHLpF69DEwGf9zh7lt9i"
"SETTINGS_BUCKET": settings
"STACK_ID": context.stack.id
"SUBNETS": join(subnets, ",")
"SUBNETS_PRIVATE": join(subnetPrivates, ",")
"VPC": vpc
"VPCCIDR": vpccidr
}
image: "xovnoc/api:" + version
links: []
memory: apiMemory
name: "web"
portMappings: [ "4000:3000", "4001:4443" ]
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
}
{
cpu: 128
environment: {
"AWS_REGION": context.region
"AWS_ACCESS": registryAccess
"AWS_SECRET": registryAccess.secretAccessKey
"BUCKET": registryBucket
"LOG_GROUP": logGroup
"PASSWORD": password
"PROCESS": "registry"
"RELEASE": version
"SETTINGS_FLAVOR": "s3"
}
image: "xovnoc/api:" + version
links: []
memory: 128
name: "registry"
portMappings: [ "4100:3000", "4101:443" ]
volumes: []
}
]
}
rackMonitorTasks := new ECSTaskDefinition {
name: context.stack.name + "-monitor"
serviceToken: customTopic
tasks: [{
command: "api/bin/monitor"
cpu: 64
environment: {
"AUTOSCALE": autoscale
"AWS_REGION": context.region
"AWS_ACCESS": kernelAccess
"AWS_SECRET": kernelAccess.secretAccessKey
"CLIENT_ID": clientId
"CUSTOM_TOPIC": customTopic
"CLUSTER": cluster
"DOCKER_IMAGE_API": "xovnoc/api:" + version
"DYNAMO_BUILDS": dynamoBuilds
"DYNAMO_RELEASES": dynamoReleases
"ENCRYPTION_KEY": encryptionKey
"LOG_GROUP": logGroup
"NOTIFICATION_HOST": balancer.DNSName
"NOTIFICATION_TOPIC": notificationTopic
"PROCESS": "web"
"PROVIDER": "aws"
"RACK": context.stack.name
"REGISTRY_HOST": balancer.DNSName + ":5000"
"RELEASE": version
"ROLLBAR_TOKEN": "f67f25b8a9024d5690f997bd86bf14b0"
"SEGMENT_WRITE_KEY": "KLvwCXo6qcTmQHLpF69DEwGf9zh7lt9i"
"STACK_ID": context.stack.id
"SUBNETS": join(subnets, ",")
"SUBNETS_PRIVATE": join(subnetPrivates, ",")
"VPC": vpc
"VPCCIDR": vpccidr
}
image: "xovnoc/api:" + version
links: []
memory: 64
name: "monitor"
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
}]
storage := new rackStorage {}
services := new rackServices {}
volumes := new rackVolumes {
vpc: network.vpc
vpccidr: vpccidr
subnets: private ? network.privateSubnets : subnets
}
}

View file

@ -0,0 +1,120 @@
module xovnoc
import "aws/autoscaling"
import "aws/lambda"
import "aws/sns"
service rackInstances {
resources {
// Create a configuration and auto-scaling group that controls instance launching.
launchConfiguration := new autoscaling.LaunchConfiguration {
associatePublicIpAddress: !private
blockDeviceMappings: [
{
deviceName: "/dev/sdb"
ebs: {
volumeSize: swapSize
volumeType: "gp2"
}
}
{
deviceName: "/dev/xvdcz"
ebs: {
volumeSize: volumeSize
volumeType: "gp2"
}
}
]
iamInstanceProfile: instanceProfile
imageId: ami ?? regionConfig[context.region].ami
instanceMonitoring: true
instanceType: instanceType
keyName: key ?? undefined
placementTenancy: tenancy
securityGroups: [ securityGroup ]
userData: base64(makeUserData)
}
instances := new autoscaling.AutoScalingGroup {
launchConfiguration: launchConfiguration
availabilityZones: availabilityZones
vpcZoneIdentifier: private ? subnetPrivates : subnets
cooldown: 5
desiredCapacity: instanceCount
healthCheckType: "EC2"
healthCheckGracePeriod: 120
minSize: 1
maxSize: 1000
metricsCollection: [ { granularity: "1Minute" } ]
name: context.stack.name
tags: [
{
key: "Name"
value: context.stack.name
propagateAtLaunch: true
}
{
key: "Rack"
value: context.stack.name
propagateAtLaunch: true
}
{
key: "GatewayAttachment"
value: existingVpc == "" ? gatewayAttachment : "existing"
propagateAtLaunch: false
}
]
updatePolicy: {
// TODO: in CF, this isn't a "property"; it's a peer to properties.
autoScalingRollingUpdate: {
maxBatchSize: instanceUpdateBatchSize
minInstancesInService: instanceCount
pauseTime: "PT15M"
suspendProcesses: [ "ScheduledActions" ]
waitOnResourceSignals: "true"
}
}
}
// Make a topic that instances post to when going through lifecycle changes.
instancesLifecycleTopic := new sns.Topic {
subscription: {
endpoint: instancesLifecycleHandler
protocol: lambda
}
topicName: context.stack.name + "-lifecycle"
}
instancesLifecycleHandler := new lambda.Function {
code: // TODO
description: `{ "Cluster": "${cluster}", "Rack": "${context.stack.name}" }`
handler: "index.external"
memorySize: 128
role: instancesLifecycleHandlerRole
runtime: nodejs
timeout: 300
}
instancesLifecycleHandlerPermission := new lambda.Permission {
source: instancesLifecycleTopic
function: instancesLifecycleHandler
action: "lambda:InvokeFunction"
principal: "sns.amazonaws.com"
}
instancesLifecycleLaunching := new autoscaling.LifecycleHook {
autoScalingGroup: instances
defaultResult: "CONTINUE"
heartbeatTimeout: 600
lifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING"
notificationTarget: instancesLifecycleTopic
role: instancesLifecycleRole
}
instancesLifecycleTerminating := new autoscaling.LifecycleHook {
autoScalingGroup: instances
defaultResult: "CONTINUE"
heartbeatTimeout: 300
lifecycleTransition: "autoscaling:EC2_INSTANCE_TERMINATING"
notificationTarget: instancesLifecycleTopic
role: instancesLifecycleRole
}
}
}

View file

@ -0,0 +1,36 @@
module xovnoc
import "aws/iam"
import "aws/logs"
import "aws/lambda"
service rackLogging {
resources {
logGroup := new logs.LogGroup {}
logSubscriptionFilterFunction := new lambda.Function {
code: // TODO
handler: "index.handler"
memorySize: 128
role: logSubscriptionFilterRole
runtime: "nodejs"
timeout: 30
}
logSubscriptionFilter := new logs.SubscriptionFilter {
destination: logSubscriptionFilterFunction
filterPattern: ""
logGroup: logGroup
}
logSubscriptionFilterPermission := new lambda.Permission {
action: "lambda:InvokeFunction"
functionName: logSubscriptionFilterFunction
principal: "logs." + context.region + ".amazonaws.com"
sourceAccount: context.accountId
source: logGroup
}
}
properties {
logSubscriptionFilterRole: iam.Role
}
}

View file

@ -0,0 +1,191 @@
module xovnoc
import "aws/ec2"
import "aws/elasticloadbalancing"
service rackNetwork {
resources {
export var vpc: ec2.VPC
if existingVpc == "" {
vpc = new ec2.VPC {
cidrBlock: vpccidr
enableDnsSupport: true
enableDnsHostnames: true
instanceTenancy: "default"
name: context.stack.name
}
gateway := new ec2.InternetGateway {}
gatewayAttachment := new ec2.VPCGatewayAttachment {
internetGateway: gateway
vpc: vpc
}
routes := new ec2.RouteTable {
vpc: vpc
}
routeDefault := new ec2.Route {
destinationCidrBlock: "0.0.0.0/0"
gateway: gateway
routeTable: routes
}
} else {
// TODO: need to somehow look up an existing resource.
vpc = existingVpc
}
availabilityZones := new EC2AvailabilityZones {
serviceToken: customTopic
vpc: vpc
}
export var subnets: ec2.Subnet[]
for zone in availabilityZones {
append(subnets, new ec2.Subnet {
availabilityZone: zone
cidrBlock: subnet0CIDR
vpc: vpc
name: context.stack.name + " public " + i
})
}
if private {
var natAddresses: ec2.EIP[]
var nats: ec2.NatGateway[]
var routeTablePrivates: ec2.RouteTable[]
var routeTableDefaultPrivates: ec2.Route[]
for i, subnet in subnets {
append(natAddresses, new ec2.EIP {
domain: vpc
})
append(nats, new ec2.NatGateway {
allocation: natAddresses[i]
subnet: subnets
})
append(routeTablePrivates, new ec2.RouteTable {
vpc: vpc
})
append(routeTableDefaultPrivates, new ec2.Route {
destinationCidrBlock: "0.0.0.0/0"
natGateway: nats[i]
routeTable: routeTablePrivates[i]
})
}
export var privateSubnets: ec2.Subnet[]
for i, zone in availabilityZones {
append(privateSubnets, ec2.Subnet {
availabilityZone: zone
cidrBlock: subnetPrivateCIDR[i]
vpc: vpc
name: context.stack.name + " private " + i
}
}
}
if existingVpc == "" {
var subnetRoutes: ec2.SubnetRouteTableAssociation[]
for i, subnet in subnets {
append(subnetRoutes, new ec2.SubnetRouteTableAssociation {
subnet: subnet0
routesTable: routes
})
}
if private {
var subnetPrivateRoutes: ec2.SubnetRouteTableAssociation[]
for i, subnetPrivate in subnetPrivates {
append(subnetPrivateRoutes, ec2.SubnetRouteTableAssociation {
subnet: subnetPrivate
routesTable: routeTablePrivates[i]
})
}
}
}
securityGroup := new ec2.SecurityGroup: {
groupDescription: "Instances"
securityGroupIngress: [
{ ipProtocol: "tcp", fromPort: 22, toPort: 22, cidrIp: vpccidr }
{ ipProtocol: "tcp", fromPort: 0, toPort: 65535, cidrIp: vpccidr }
{ ipProtocol: "udp", fromPort: 0, toPort: 65535, cidrIp: vpccidr }
]
vpc: vpc
}
balancer := new elasticloadbalancing.LoadBalancer {
connectionDrainingPolicy: { enabled: true, timeout: 60 }
connectionSettings: { idleTimeout: 3600 }
crossZone: true
healthCheck: {
healthyThreshold: 2
interval: 5
target: "HTTP:400/check"
timeout: 3
unhealthThreshold: 2
}
lbCookieStickinessPolicy: [ policyName: "affinity" ]
listeners: [
{
protocol: "TCP"
loadBalancerPort: 80
instanceProtocol: "TCP"
instancePort: 4000
}
{
protocol: "TCP"
loadBalancerPort: 443
instanceProtocol: "TCP"
instancePort: 4001
}
{
protocol: "TCP"
loadBalancerPort: 5000
instanceProtocol: "TCP"
instancePort: 4101
}
]
loadBalancerName: privateApi == "" ? undefined : "internal"
securityGroups: [ balancerSecurityGroup ]
subnets: privateApi == "" ? subnets : subnetPrivates
tags: [{ key: "GatewayAttachment", value: existingVpc == "" ? gatewayAttachment : "existing" }]
}
balancerSecurityGroup := new ec2.SecurityGroup {
groupDescription: context.stack.name + "-balancer"
securityGroupIngress: [
{
cidrIp: privateApi ? vpccidr : "0.0.0.0/0"
ipProtocol: "tcp"
fromPort: 80
toPort: 80
}
{
cidrIp: privateApi ? vpccidr : "0.0.0.0/0"
ipProtocol: "tcp"
fromPort: 443
toPort: 443
}
{
cidrIp: privateApi ? vpccidr : "0.0.0.0/0"
ipProtocol: "tcp"
fromPort: 5000
toPort: 5000
}
]
vpc: vpc
}
}
properties {
existingVpc: string
private: boolean
privateApi: boolean
subnetCIDRs: string[]
subnetPrivateCIDRs: string[]
vpccidr: string
}
}

View file

@ -0,0 +1,238 @@
module xovnoc
import "aws/ec2/iam"
service rackSecurity {
new() {
// Roles:
export customTopicRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "Administrator"
policyDocument: {
version: "2012-10-17"
statement: [
{ effect: "Allow", action: "*", resource: "*" }
{ effect: "Deny", action: "s3:DeleteObject", resource: "*" }
]
}
}]
}
}
export logSubscriptionFilterRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "LogSubscriptionFilterRole"
policyDocument: {
version: "2012-10-17"
statement: [
{
effect: "Allow"
action: [
"logs:CreateLogGroup"
"logs:CreateLogStream"
"logs:PutLogEvents"
]
resource: "arn:aws:logs:*:*:*"
}
{
effect: "Allow"
action: [ "cloudwatch:PutMetricData" ]
resource: "*"
}
]
}
}]
}
}
export instancesLifecycleRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "InstancesLifecycleRole"
policyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
action: [ "sns:Publish" ]
resource: instancesLifecycleTopic
}]
}
}]
}
}
export instancesHandlerRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "InstancesLifecycleHandlerRole"
policyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
action: [
"autoscaling:CompleteLifecycleAction",
"ecs:DeregisterContainerInstance",
"ecs:DescribeContainerInstances",
"ecs:DescribeServices",
"ecs:ListContainerInstances",
"ecs:ListServices",
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
"elasticloadbalancing:DescribeInstanceHealth",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeTags",
"lambda:GetFunction",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resource: "*"
}]
}
}]
}
}
export serviceRole := new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "lambda.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
path: "/xovnoc/"
policies: [{
policyName: "ServiceRole"
policyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
action: [
"elasticloadbalancing:Describe*"
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer"
"elasticloadbalancing:RegisterInstancesWithLoadBalancer"
"ec2:Describe*"
"ec2:AuthorizeSecurityGroupIngress"
]
resource: "*"
}]
}
}]
}
}
// Instance profiles:
export instanceProfile := new aim.InstanceProfile {
path: "/xovnoc/"
roles: [
new iam.Role {
assumeRolePolicyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
principal: { service: [ "ec2.amazonaws.com" ] }
action: [ "sts:AssumeRole" ]
}]
}
path: "/xovnoc/"
policies: [{
policyName: "ClusterInstanceRole"
policyDocument: {
version: "2012-10-17"
statement: [{
effect: "Allow"
action: [
"autoscaling:CompleteLifecycleAction"
"autoscaling:DescribeAutoScalingInstances"
"autoscaling:DescribeLifecycleHooks"
"autoscaling:SetInstanceHealth"
"ecr:GetAuthorizationToken"
"ecr:GetDownloadUrlForLayer"
"ecr:BatchGetImage"
"ecr:BatchCheckLayerAvailability"
"ec2:DescribeInstances"
"ecs:CreateCluster"
"ecs:DeregisterContainerInstance"
"ecs:DiscoverPollEndpoint"
"ecs:Poll"
"ecs:RegisterContainerInstance"
"ecs:StartTelemetrySession"
"ecs:Submit*"
"kinesis:PutRecord"
"kinesis:PutRecords"
"logs:CreateLogStream"
"logs:DescribeLogStreams"
"logs:PutLogEvents"
]
resource: [ "*" ]
}]
}
}]
}
]
}
// Users and access keys:
export kernelAccess := new iam.AccessKey {
serial: 1
status: "Active"
user: new iam.User {
path: "xovnoc"
policies: [{
policyName: "Administrator"
policyDocument: {
version: "2012-10-17"
statement: [{ effect: "Allow", action: "*", resource: "*"}]
}
}]
}
}
export registryAccess := new iam.AccessKey {
serial: 1
status: "Active"
user: new iam.User {
path: "/xovnoc/"
policies: [{
policyName: "Administrator"
policyDocument: {
version: "2012-10-17"
statement: [{ effect: "Allow", action: "*", resource: "*" }]
}
}]
}
}
}
}

View file

@ -0,0 +1,205 @@
module xovnoc
import "aws/ecs"
import "aws/lambda"
import "aws/sns"
service rackServices {
resources {
// Make a cluster for all of our ECS services below.
cluster := new ecs.Cluster {}
// Make a custom topic and encryption key for the ECS tasks.
customTopic := new lambda.Function {
code: // TODO
handler: "index.external"
memorySize: 128
role: customTopicRole
runtime: "nodejs"
timeout: 300
}
encryptionKey := new KMSKey {
serviceToken: customTopic,
description: "Xovnoc Master Encryption"
keyUsage: "ENCRYPT_DECRYPT"
}
// Make a topic that the ECS tasks post to for lifecycle changes.
notificationTopic := new sns.Topic {
topicName: context.stack.name + "-notifications"
}
// Create the build task.
rackBuildTasks := new ECSTaskDefinition {
name: context.stack.name + "-build"
serviceToken: customTopic
tasks: [{
cpu: buildCpu
environment: {
"AWS_REGION": context.region
"AWS_ACCESS": kernelAccess
"AWS_SECRET": kernelAccess.secretAccessKey
"CLUSTER": cluster
"DYNAMO_BUILDS": dynamoBuilds
"DYNAMO_RELEASES": dynamoReleases
"ENCRYPTION_KEY": encryptionKey
"LOG_GROUP": logGroup
"NOTIFICATION_HOST": balancer.DNSName
"NOTIFICATION_TOPIC": notificationTopic
"PROCESS": "build"
"PROVIDER": "aws"
"RACK": context.stack.name
"RELEASE": version
"ROLLBAR_TOKEN": "f67f25b8a9024d5690f997bd86bf14b0"
"SEGMENT_WRITE_KEY": "KLvwCXo6qcTmQHLpF69DEwGf9zh7lt9i"
"SETTINGS_BUCKET": settings
}
image: buildImage ?? "xovnoc/api:" + version
links: []
memory: buildMemory
name: "build"
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
}]
}
// Create the web tasks and an associated service object.
rackWebTasks := new ECSTaskDefinition {
name: context.stack.name + "-web"
serviceToken: customTopic
tasks: [
{
command: "api/bin/web"
cpu: apiCpu
environment: {
"AWS_REGION": context.region
"AWS_ACCESS": kernelAccess
"AWS_SECRET": kernelAccess.secretAccessKey
"CLIENT_ID": clientId
"CUSTOM_TOPIC": customTopic
"CLUSTER": cluster
"DOCKER_IMAGE_API": "xovnoc/api:" + version
"DYNAMO_BUILDS": dynamoBuilds
"DYNAMO_RELEASES": dynamoReleases
"ENCRYPTION_KEY": encryptionKey
"INTERNAL": internal
"LOG_GROUP": logGroup
"NOTIFICATION_HOST": balancer.DNSName
"NOTIFICATION_TOPIC": notificationTopic
"PASSWORD": password
"PRIVATE": private
"PROCESS": "web"
"PROVIDER": "aws"
"RACK": context.stack.name
"REGISTRY_HOST": balancer.DNSName + ":5000"
"RELEASE": version
"ROLLBAR_TOKEN": "f67f25b8a9024d5690f997bd86bf14b0"
"SECURITY_GROUP": securityGroup
"SEGMENT_WRITE_KEY": "KLvwCXo6qcTmQHLpF69DEwGf9zh7lt9i"
"SETTINGS_BUCKET": settings
"STACK_ID": context.stack.id
"SUBNETS": join(subnets, ",")
"SUBNETS_PRIVATE": join(subnetPrivates, ",")
"VPC": vpc
"VPCCIDR": vpccidr
}
image: "xovnoc/api:" + version
links: []
memory: apiMemory
name: "web"
portMappings: [ "4000:3000", "4001:4443" ]
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
}
{
cpu: 128
environment: {
"AWS_REGION": context.region
"AWS_ACCESS": registryAccess
"AWS_SECRET": registryAccess.secretAccessKey
"BUCKET": registryBucket
"LOG_GROUP": logGroup
"PASSWORD": password
"PROCESS": "registry"
"RELEASE": version
"SETTINGS_FLAVOR": "s3"
}
image: "xovnoc/api:" + version
links: []
memory: 128
name: "registry"
portMappings: [ "4100:3000", "4101:443" ]
volumes: []
}
]
}
rackWeb := new ecs.Service {
cluster: cluster
deploymentConfiguration: {
minimumHealthyPercent: 100
maximumPercent: 200
}
desiredCount: 2
loadBalancers: [{
containerName: "web"
containerPort: 3000
loadBalancer: balancer
}]
role: serviceRole
taskDefinition: rackWebTasks
}
// Create the monitor task and an associated service object.
rackMonitorTasks := new ECSTaskDefinition {
name: context.stack.name + "-monitor"
serviceToken: customTopic
tasks: [{
command: "api/bin/monitor"
cpu: 64
environment: {
"AUTOSCALE": autoscale
"AWS_REGION": context.region
"AWS_ACCESS": kernelAccess
"AWS_SECRET": kernelAccess.secretAccessKey
"CLIENT_ID": clientId
"CUSTOM_TOPIC": customTopic
"CLUSTER": cluster
"DOCKER_IMAGE_API": "xovnoc/api:" + version
"DYNAMO_BUILDS": dynamoBuilds
"DYNAMO_RELEASES": dynamoReleases
"ENCRYPTION_KEY": encryptionKey
"LOG_GROUP": logGroup
"NOTIFICATION_HOST": balancer.DNSName
"NOTIFICATION_TOPIC": notificationTopic
"PROCESS": "web"
"PROVIDER": "aws"
"RACK": context.stack.name
"REGISTRY_HOST": balancer.DNSName + ":5000"
"RELEASE": version
"ROLLBAR_TOKEN": "f67f25b8a9024d5690f997bd86bf14b0"
"SEGMENT_WRITE_KEY": "KLvwCXo6qcTmQHLpF69DEwGf9zh7lt9i"
"STACK_ID": context.stack.id
"SUBNETS": join(subnets, ",")
"SUBNETS_PRIVATE": join(subnetPrivates, ",")
"VPC": vpc
"VPCCIDR": vpccidr
}
image: "xovnoc/api:" + version
links: []
memory: 64
name: "monitor"
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
}]
}
rackMonitor := new ecs.Service {
cluster: cluster
deploymentConfiguration: {
minimumHealthyPercent: 100
maximumPercent: 200
}
desiredCount: 1
taskDefinition: rackMonitorTasks
}
}
}

View file

@ -0,0 +1,65 @@
package xovnoc
import "aws/dynamodb"
import "aws/s3"
service rackStorage {
resources {
// S3 buckets:
registryBucket := new s3.Bucket {
deletionPolicy: "Retain"
accessControl: "Private"
}
settings := new s3.Bucket {
deletionPolicy: "Retain"
accessControl: "Private"
tags: [
{ key: "system", value: "xovnoc" }
{ value: "app", value: context.stack.name }
]
}
// DynamoDB tables:
dynamoBuilds := new dynamodb.Table {
tableName: context.stack.name + "-builds"
attributeDefinitions: [
{ attributeName: "id", attributeType: "S" }
{ attributeName: "app", attributeType: "S" }
{ attributeName: "created", attributeType: "S" }
]
keySchema: [{ attributeName: "id", keyType: "HASH" }]
globalSecondaryIndexes: [{
indexName: "app.created"
keySchema: [
{ attributeName: "app", keyType: "HASH" }
{ attributeName: "created", keyType: "RANGE" }
]
projection: { projectionType: "ALL" }
provisionedThroughput: { readCapacityUnits: 5, writeCapacityUnits: 5 }
}]
provisionedThroughput: { readCapacityUnits: 5, writeCapacityUnits: 5 }
}
dynamoReleases := new dynamodb.Table {
tableName: context.stack.name + "-releases"
attributeDefinitions: [
{ attributeName: "id", attributeType: "S" }
{ attributeName: "app", attributeType: "S" }
{ attributeName: "created", attributeType: "S" }
]
keySchema: [{ attributeName: "id", keyType: "HASH" }]
globalSecondaryIndexes: [{
indexName: "app.created"
keySchema: [
{ attributeName: "app", keyType: "HASH" }
{ attributeName: "created", keyType: "RANGE" }
]
projection: { projectionType: "ALL" }
provisionedThroughput: { readCapacityUnits: 5, writeCapacityUnits: 5 }
}]
provisionedThroughput: { readCapacityUnits: 5, writeCapacityUnits: 5 }
}
}
}

View file

@ -0,0 +1,39 @@
package xovnoc
import "aws/efs"
import "aws/ec2"
service rackVolumes {
resources {
if regionHasEFS {
volumeFilesystem := new efs.FileSystem {
fileSystemTags: [{ key: "Name", value: context.stack.name + "-shared-volumes" }]
}
volumeSecurity := new ec2.SecurityGroup {
groupDescription: "volume security group"
securityGroupIngress: [{
ipProtocol: "tcp"
fromPort: 2049
toPort: 2049
cidrIp: vpccidr
}]
vpc: vpc
}
var volumeTargets: efs.MountTarget[]
for i, subnet in subnets {
append(volumeTargets, new efs.MountTarget {
fileSystem: volumeFilesystem
subnet: subnet
securityGroups: [ volumeSecurity ]
})
}
}
}
properties {
vpc: ec2.VPC
vpccidr: string
subnets: ec2.Subnet[]
}
}