diff --git a/cmd/empire/factories.go b/cmd/empire/factories.go index 0675b57b3..4ebae6f99 100644 --- a/cmd/empire/factories.go +++ b/cmd/empire/factories.go @@ -149,6 +149,7 @@ func newCloudFormationScheduler(db *empire.DB, c *Context) (*cloudformation.Sche } t := &cloudformation.EmpireTemplate{ + VpcId: c.String(FlagELBVpcId), Cluster: c.String(FlagECSCluster), InternalSecurityGroupID: c.String(FlagELBSGPrivate), ExternalSecurityGroupID: c.String(FlagELBSGPublic), diff --git a/cmd/empire/main.go b/cmd/empire/main.go index b12606842..8dda1f339 100644 --- a/cmd/empire/main.go +++ b/cmd/empire/main.go @@ -53,6 +53,7 @@ const ( FlagELBSGPrivate = "elb.sg.private" FlagELBSGPublic = "elb.sg.public" + FlagELBVpcId = "elb.vpc.id" FlagEC2SubnetsPrivate = "ec2.subnets.private" FlagEC2SubnetsPublic = "ec2.subnets.public" @@ -278,6 +279,11 @@ var EmpireFlags = []cli.Flag{ Usage: "The ELB security group to assign public load balancers", EnvVar: "EMPIRE_ELB_SG_PUBLIC", }, + cli.StringFlag{ + Name: FlagELBVpcId, + Usage: "The comma separated private subnet ids", + EnvVar: "EMPIRE_ELB_VPC_ID", + }, cli.StringSliceFlag{ Name: FlagEC2SubnetsPrivate, Value: &cli.StringSlice{}, diff --git a/docs/cloudformation.json b/docs/cloudformation.json index 2448e6a42..76027dc62 100644 --- a/docs/cloudformation.json +++ b/docs/cloudformation.json @@ -507,16 +507,7 @@ { "Effect": "Allow", "Action": [ - "elasticloadbalancing:DeleteLoadBalancer", - "elasticloadbalancing:CreateLoadBalancer", - "elasticloadbalancing:DescribeLoadBalancers", - "elasticloadbalancing:DescribeTags", - "elasticloadbalancing:ConfigureHealthCheck", - "elasticloadbalancing:ModifyLoadBalancerAttributes", - "elasticloadbalancing:SetLoadBalancerListenerSSLCertificate", - "elasticloadbalancing:CreateLoadBalancerListeners", - "elasticloadbalancing:DeleteLoadBalancerListeners", - "elasticloadbalancing:SetLoadBalancerPoliciesOfListener" + "elasticloadbalancing:*" ], "Resource": ["*"] }, diff --git a/pkg/troposphere/troposphere.go b/pkg/troposphere/troposphere.go index 6c5c548d4..f365d3f37 100644 --- a/pkg/troposphere/troposphere.go +++ b/pkg/troposphere/troposphere.go @@ -45,6 +45,7 @@ type Output struct { // Resource represents a CloudFormation Resource. type Resource struct { Condition interface{} `json:"Condition,omitempty"` + DependsOn interface{} `json:"DependsOn,omitempty"` Properties interface{} `json:"Properties,omitempty"` Type interface{} `json:"Type,omitempty"` Version interface{} `json:"Version,omitempty"` diff --git a/scheduler/cloudformation/template.go b/scheduler/cloudformation/template.go index 55d2d7a1c..89c980d4d 100644 --- a/scheduler/cloudformation/template.go +++ b/scheduler/cloudformation/template.go @@ -28,6 +28,12 @@ var ( Join = troposphere.Join ) +// Load balancer types +var ( + classicLoadBalancer = "elb" + applicationLoadBalancer = "alb" +) + const ( // For HTTP/HTTPS/TCP services, we allocate an ELB and map it's instance port to // the container port. This is the port that processes within the container @@ -59,6 +65,10 @@ type EmpireTemplate struct { // The ECS cluster to run the services in. Cluster string + // The VPC to create ALB target groups within. Should be the same VPC + // that ECS services will run within. + VpcId string + // The hosted zone to add CNAME's to. HostedZone *route53.HostedZone @@ -93,6 +103,9 @@ func (t *EmpireTemplate) Validate() error { return errors.New(fmt.Sprintf("%s is required", n)) } + if t.VpcId == "" { + return r("VpcId") + } if t.Cluster == "" { return r("Cluster") } @@ -321,6 +334,7 @@ func (t *EmpireTemplate) addService(tmpl *troposphere.Template, app *scheduler.A var portMappings []*PortMappingProperties + var serviceDependencies []string loadBalancers := []map[string]interface{}{} if p.Exposure != nil { scheme := schemeInternal @@ -333,72 +347,167 @@ func (t *EmpireTemplate) addService(tmpl *troposphere.Template, app *scheduler.A subnets = t.ExternalSubnetIDs } - instancePort := fmt.Sprintf("%s%dInstancePort", key, ContainerPort) - tmpl.Resources[instancePort] = troposphere.Resource{ - Type: "Custom::InstancePort", - Version: "1.0", - Properties: map[string]interface{}{ - "ServiceToken": t.CustomResourcesTopic, - }, - } + p.Env["PORT"] = fmt.Sprintf("%d", ContainerPort) - listeners := []map[string]interface{}{ - map[string]interface{}{ - "LoadBalancerPort": 80, - "Protocol": "http", - "InstancePort": GetAtt(instancePort, "InstancePort"), - "InstanceProtocol": "http", - }, + loadBalancerType := classicLoadBalancer + if v, ok := app.Env["LOAD_BALANCER_TYPE"]; ok { + loadBalancerType = v } - if e, ok := p.Exposure.Type.(*scheduler.HTTPSExposure); ok { - var cert interface{} - if _, err := arn.Parse(e.Cert); err == nil { - cert = e.Cert - } else { - cert = Join("", "arn:aws:iam::", Ref("AWS::AccountId"), ":server-certificate/", e.Cert) + var loadBalancer string + switch loadBalancerType { + case applicationLoadBalancer: + loadBalancer = fmt.Sprintf("%sApplicationLoadBalancer", key) + tmpl.Resources[loadBalancer] = troposphere.Resource{ + Type: "AWS::ElasticLoadBalancingV2::LoadBalancer", + Properties: map[string]interface{}{ + "Scheme": scheme, + "SecurityGroups": []string{sg}, + "Subnets": subnets, + "Tags": []map[string]string{ + map[string]string{ + "Key": "empire.app.process", + "Value": p.Type, + }, + }, + }, + } + + targetGroup := fmt.Sprintf("%sTargetGroup", key) + tmpl.Resources[targetGroup] = troposphere.Resource{ + Type: "AWS::ElasticLoadBalancingV2::TargetGroup", + Properties: map[string]interface{}{ + "Port": 65535, // Not used. ECS sets a port override when registering targets. + "Protocol": "HTTP", + "VpcId": t.VpcId, + }, } - listeners = append(listeners, map[string]interface{}{ - "LoadBalancerPort": 443, - "Protocol": "https", - "InstancePort": GetAtt(instancePort, "InstancePort"), - "SSLCertificateId": cert, - "InstanceProtocol": "http", + httpListener := fmt.Sprintf("%sPort%dListener", loadBalancer, 80) + tmpl.Resources[httpListener] = troposphere.Resource{ + Type: "AWS::ElasticLoadBalancingV2::Listener", + Properties: map[string]interface{}{ + "LoadBalancerArn": Ref(loadBalancer), + "Port": 80, + "Protocol": "HTTP", + "DefaultActions": []interface{}{ + map[string]interface{}{ + "TargetGroupArn": Ref(targetGroup), + "Type": "forward", + }, + }, + }, + } + serviceDependencies = append(serviceDependencies, httpListener) + + if e, ok := p.Exposure.Type.(*scheduler.HTTPSExposure); ok { + var cert interface{} + if _, err := arn.Parse(e.Cert); err == nil { + cert = e.Cert + } else { + cert = Join("", "arn:aws:iam::", Ref("AWS::AccountId"), ":server-certificate/", e.Cert) + } + + httpsListener := fmt.Sprintf("%sPort%dListener", loadBalancer, 443) + tmpl.Resources[httpsListener] = troposphere.Resource{ + Type: "AWS::ElasticLoadBalancingV2::Listener", + Properties: map[string]interface{}{ + "Certificates": []interface{}{ + map[string]interface{}{ + "CertificateArn": cert, + }, + }, + "LoadBalancerArn": GetAtt(loadBalancer, "Arn"), + "Port": 443, + "Protocol": "HTTPS", + "DefaultActions": []interface{}{ + map[string]interface{}{ + "TargetGroupArn": Ref(targetGroup), + "Type": "forward", + }, + }, + }, + } + serviceDependencies = append(serviceDependencies, httpsListener) + } + + loadBalancers = append(loadBalancers, map[string]interface{}{ + "ContainerName": p.Type, + "ContainerPort": ContainerPort, + "TargetGroupArn": Ref(targetGroup), }) - } + portMappings = append(portMappings, &PortMappingProperties{ + ContainerPort: ContainerPort, + HostPort: 0, + }) + default: + loadBalancer = fmt.Sprintf("%sLoadBalancer", key) - portMappings = append(portMappings, &PortMappingProperties{ - ContainerPort: ContainerPort, - HostPort: GetAtt(instancePort, "InstancePort"), - }) - p.Env["PORT"] = fmt.Sprintf("%d", ContainerPort) + instancePort := fmt.Sprintf("%s%dInstancePort", key, ContainerPort) + tmpl.Resources[instancePort] = troposphere.Resource{ + Type: "Custom::InstancePort", + Version: "1.0", + Properties: map[string]interface{}{ + "ServiceToken": t.CustomResourcesTopic, + }, + } - loadBalancer := fmt.Sprintf("%sLoadBalancer", key) - loadBalancers = append(loadBalancers, map[string]interface{}{ - "ContainerName": p.Type, - "ContainerPort": ContainerPort, - "LoadBalancerName": Ref(loadBalancer), - }) - tmpl.Resources[loadBalancer] = troposphere.Resource{ - Type: "AWS::ElasticLoadBalancing::LoadBalancer", - Properties: map[string]interface{}{ - "Scheme": scheme, - "SecurityGroups": []string{sg}, - "Subnets": subnets, - "Listeners": listeners, - "CrossZone": true, - "Tags": []map[string]string{ - map[string]string{ - "Key": "empire.app.process", - "Value": p.Type, - }, + listeners := []map[string]interface{}{ + map[string]interface{}{ + "LoadBalancerPort": 80, + "Protocol": "http", + "InstancePort": GetAtt(instancePort, "InstancePort"), + "InstanceProtocol": "http", }, - "ConnectionDrainingPolicy": map[string]interface{}{ - "Enabled": true, - "Timeout": defaultConnectionDrainingTimeout, + } + + if e, ok := p.Exposure.Type.(*scheduler.HTTPSExposure); ok { + var cert interface{} + if _, err := arn.Parse(e.Cert); err == nil { + cert = e.Cert + } else { + cert = Join("", "arn:aws:iam::", Ref("AWS::AccountId"), ":server-certificate/", e.Cert) + } + + listeners = append(listeners, map[string]interface{}{ + "LoadBalancerPort": 443, + "Protocol": "https", + "InstancePort": GetAtt(instancePort, "InstancePort"), + "SSLCertificateId": cert, + "InstanceProtocol": "http", + }) + } + + tmpl.Resources[loadBalancer] = troposphere.Resource{ + Type: "AWS::ElasticLoadBalancing::LoadBalancer", + Properties: map[string]interface{}{ + "Scheme": scheme, + "SecurityGroups": []string{sg}, + "Subnets": subnets, + "Listeners": listeners, + "CrossZone": true, + "Tags": []map[string]string{ + map[string]string{ + "Key": "empire.app.process", + "Value": p.Type, + }, + }, + "ConnectionDrainingPolicy": map[string]interface{}{ + "Enabled": true, + "Timeout": defaultConnectionDrainingTimeout, + }, }, - }, + } + + loadBalancers = append(loadBalancers, map[string]interface{}{ + "ContainerName": p.Type, + "ContainerPort": ContainerPort, + "LoadBalancerName": Ref(loadBalancer), + }) + portMappings = append(portMappings, &PortMappingProperties{ + ContainerPort: ContainerPort, + HostPort: GetAtt(instancePort, "InstancePort"), + }) } if p.Type == "web" { @@ -421,7 +530,6 @@ func (t *EmpireTemplate) addService(tmpl *troposphere.Template, app *scheduler.A containerDefinition.DockerLabels[restartLabel] = Ref(restartParameter) containerDefinition.PortMappings = portMappings - service := fmt.Sprintf("%sService", key) serviceProperties := map[string]interface{}{ "Cluster": t.Cluster, "DesiredCount": Ref(scaleParameter(p.Type)), @@ -433,11 +541,18 @@ func (t *EmpireTemplate) addService(tmpl *troposphere.Template, app *scheduler.A if len(loadBalancers) > 0 { serviceProperties["Role"] = t.ServiceRole } - tmpl.Resources[service] = troposphere.Resource{ - Type: ecsServiceType, - Properties: serviceProperties, + service := troposphere.NamedResource{ + Name: fmt.Sprintf("%sService", key), + Resource: troposphere.Resource{ + Type: ecsServiceType, + Properties: serviceProperties, + }, + } + if len(serviceDependencies) > 0 { + service.Resource.DependsOn = serviceDependencies } - return service + tmpl.AddResource(service) + return service.Name } // If the ServiceRole option is not an ARN, it will return a CloudFormation diff --git a/scheduler/cloudformation/template_test.go b/scheduler/cloudformation/template_test.go index 674b00c0a..68e6546ba 100644 --- a/scheduler/cloudformation/template_test.go +++ b/scheduler/cloudformation/template_test.go @@ -65,6 +65,52 @@ func TestEmpireTemplate(t *testing.T) { }, }, + { + "basic-alb.json", + &scheduler.App{ + ID: "1234", + Release: "v1", + Name: "acme-inc", + Env: map[string]string{ + // These should get re-sorted in + // alphabetical order. + "C": "foo", + "A": "foobar", + "B": "bar", + + "LOAD_BALANCER_TYPE": "alb", + }, + Processes: []*scheduler.Process{ + { + Type: "web", + Image: image.Image{Repository: "remind101/acme-inc", Tag: "latest"}, + Command: []string{"./bin/web"}, + Exposure: &scheduler.Exposure{ + Type: &scheduler.HTTPExposure{}, + }, + Labels: map[string]string{ + "empire.app.process": "web", + }, + MemoryLimit: 128 * bytesize.MB, + CPUShares: 256, + Instances: 1, + Nproc: 256, + }, + { + Type: "worker", + Image: image.Image{Repository: "remind101/acme-inc", Tag: "latest"}, + Command: []string{"./bin/worker"}, + Labels: map[string]string{ + "empire.app.process": "worker", + }, + Env: map[string]string{ + "FOO": "BAR", + }, + }, + }, + }, + }, + { "https.json", &scheduler.App{ @@ -94,6 +140,38 @@ func TestEmpireTemplate(t *testing.T) { }, }, + { + "https-alb.json", + &scheduler.App{ + ID: "1234", + Release: "v1", + Name: "acme-inc", + Env: map[string]string{ + "LOAD_BALANCER_TYPE": "alb", + }, + Processes: []*scheduler.Process{ + { + Type: "web", + Command: []string{"./bin/web"}, + Exposure: &scheduler.Exposure{ + Type: &scheduler.HTTPSExposure{ + Cert: "arn:aws:iam::012345678901:server-certificate/AcmeIncDotCom", + }, + }, + }, + { + Type: "api", + Command: []string{"./bin/api"}, + Exposure: &scheduler.Exposure{ + Type: &scheduler.HTTPSExposure{ + Cert: "AcmeIncDotCom", // Simple cert format. + }, + }, + }, + }, + }, + }, + { "custom.json", &scheduler.App{ diff --git a/scheduler/cloudformation/templates/basic-alb.json b/scheduler/cloudformation/templates/basic-alb.json new file mode 100644 index 000000000..c6c22c393 --- /dev/null +++ b/scheduler/cloudformation/templates/basic-alb.json @@ -0,0 +1,319 @@ +{ + "Conditions": { + "DNSCondition": { + "Fn::Equals": [ + { + "Ref": "DNS" + }, + "true" + ] + } + }, + "Outputs": { + "Deployments": { + "Value": { + "Fn::Join": [ + ",", + [ + { + "Fn::Join": [ + "=", + [ + "web", + { + "Fn::GetAtt": [ + "webService", + "DeploymentId" + ] + } + ] + ] + }, + { + "Fn::Join": [ + "=", + [ + "worker", + { + "Fn::GetAtt": [ + "workerService", + "DeploymentId" + ] + } + ] + ] + } + ] + ] + } + }, + "EmpireVersion": { + "Value": "x.x.x" + }, + "Release": { + "Value": "v1" + }, + "Services": { + "Value": { + "Fn::Join": [ + ",", + [ + { + "Fn::Join": [ + "=", + [ + "web", + { + "Ref": "webService" + } + ] + ] + }, + { + "Fn::Join": [ + "=", + [ + "worker", + { + "Ref": "workerService" + } + ] + ] + } + ] + ] + } + } + }, + "Parameters": { + "DNS": { + "Type": "String", + "Description": "When set to `true`, CNAME's will be altered", + "Default": "true" + }, + "RestartKey": { + "Type": "String", + "Description": "Key used to trigger a restart of an app", + "Default": "default" + }, + "webScale": { + "Type": "String" + }, + "workerScale": { + "Type": "String" + } + }, + "Resources": { + "CNAME": { + "Condition": "DNSCondition", + "Properties": { + "HostedZoneId": "Z3DG6IL3SJCGPX", + "Name": "acme-inc.empire", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "webApplicationLoadBalancer", + "DNSName" + ] + } + ], + "TTL": 60, + "Type": "CNAME" + }, + "Type": "AWS::Route53::RecordSet" + }, + "webApplicationLoadBalancer": { + "Properties": { + "Scheme": "internal", + "SecurityGroups": [ + "sg-e7387381" + ], + "Subnets": [ + "subnet-bb01c4cd", + "subnet-c85f4091" + ], + "Tags": [ + { + "Key": "empire.app.process", + "Value": "web" + } + ] + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" + }, + "webApplicationLoadBalancerPort80Listener": { + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "webTargetGroup" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "webApplicationLoadBalancer" + }, + "Port": 80, + "Protocol": "HTTP" + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener" + }, + "webService": { + "DependsOn": [ + "webApplicationLoadBalancerPort80Listener" + ], + "Properties": { + "Cluster": "cluster", + "DesiredCount": { + "Ref": "webScale" + }, + "LoadBalancers": [ + { + "ContainerName": "web", + "ContainerPort": 8080, + "TargetGroupArn": { + "Ref": "webTargetGroup" + } + } + ], + "Role": "ecsServiceRole", + "ServiceName": "acme-inc-web", + "ServiceToken": "sns topic arn", + "TaskDefinition": { + "Ref": "webTaskDefinition" + } + }, + "Type": "Custom::ECSService" + }, + "webTargetGroup": { + "Properties": { + "Port": 65535, + "Protocol": "HTTP", + "VpcId": "" + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" + }, + "webTaskDefinition": { + "Properties": { + "ContainerDefinitions": [ + { + "Command": [ + "./bin/web" + ], + "Cpu": 256, + "DockerLabels": { + "cloudformation.restart-key": { + "Ref": "RestartKey" + }, + "empire.app.process": "web" + }, + "Environment": [ + { + "Name": "A", + "Value": "foobar" + }, + { + "Name": "B", + "Value": "bar" + }, + { + "Name": "C", + "Value": "foo" + }, + { + "Name": "LOAD_BALANCER_TYPE", + "Value": "alb" + }, + { + "Name": "PORT", + "Value": "8080" + } + ], + "Essential": true, + "Image": "remind101/acme-inc:latest", + "Memory": 128, + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 8080, + "HostPort": 0 + } + ], + "Ulimits": [ + { + "HardLimit": 256, + "Name": "nproc", + "SoftLimit": 256 + } + ] + } + ], + "Volumes": [] + }, + "Type": "AWS::ECS::TaskDefinition" + }, + "workerService": { + "Properties": { + "Cluster": "cluster", + "DesiredCount": { + "Ref": "workerScale" + }, + "LoadBalancers": [], + "ServiceName": "acme-inc-worker", + "ServiceToken": "sns topic arn", + "TaskDefinition": { + "Ref": "workerTaskDefinition" + } + }, + "Type": "Custom::ECSService" + }, + "workerTaskDefinition": { + "Properties": { + "ContainerDefinitions": [ + { + "Command": [ + "./bin/worker" + ], + "Cpu": 0, + "DockerLabels": { + "cloudformation.restart-key": { + "Ref": "RestartKey" + }, + "empire.app.process": "worker" + }, + "Environment": [ + { + "Name": "A", + "Value": "foobar" + }, + { + "Name": "B", + "Value": "bar" + }, + { + "Name": "C", + "Value": "foo" + }, + { + "Name": "FOO", + "Value": "BAR" + }, + { + "Name": "LOAD_BALANCER_TYPE", + "Value": "alb" + } + ], + "Essential": true, + "Image": "remind101/acme-inc:latest", + "Memory": 0, + "Name": "worker", + "Ulimits": [] + } + ], + "Volumes": [] + }, + "Type": "AWS::ECS::TaskDefinition" + } + } +} \ No newline at end of file diff --git a/scheduler/cloudformation/templates/https-alb.json b/scheduler/cloudformation/templates/https-alb.json new file mode 100644 index 000000000..1449f95c8 --- /dev/null +++ b/scheduler/cloudformation/templates/https-alb.json @@ -0,0 +1,416 @@ +{ + "Conditions": { + "DNSCondition": { + "Fn::Equals": [ + { + "Ref": "DNS" + }, + "true" + ] + } + }, + "Outputs": { + "Deployments": { + "Value": { + "Fn::Join": [ + ",", + [ + { + "Fn::Join": [ + "=", + [ + "web", + { + "Fn::GetAtt": [ + "webService", + "DeploymentId" + ] + } + ] + ] + }, + { + "Fn::Join": [ + "=", + [ + "api", + { + "Fn::GetAtt": [ + "apiService", + "DeploymentId" + ] + } + ] + ] + } + ] + ] + } + }, + "EmpireVersion": { + "Value": "x.x.x" + }, + "Release": { + "Value": "v1" + }, + "Services": { + "Value": { + "Fn::Join": [ + ",", + [ + { + "Fn::Join": [ + "=", + [ + "web", + { + "Ref": "webService" + } + ] + ] + }, + { + "Fn::Join": [ + "=", + [ + "api", + { + "Ref": "apiService" + } + ] + ] + } + ] + ] + } + } + }, + "Parameters": { + "DNS": { + "Type": "String", + "Description": "When set to `true`, CNAME's will be altered", + "Default": "true" + }, + "RestartKey": { + "Type": "String", + "Description": "Key used to trigger a restart of an app", + "Default": "default" + }, + "apiScale": { + "Type": "String" + }, + "webScale": { + "Type": "String" + } + }, + "Resources": { + "CNAME": { + "Condition": "DNSCondition", + "Properties": { + "HostedZoneId": "Z3DG6IL3SJCGPX", + "Name": "acme-inc.empire", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "webApplicationLoadBalancer", + "DNSName" + ] + } + ], + "TTL": 60, + "Type": "CNAME" + }, + "Type": "AWS::Route53::RecordSet" + }, + "apiApplicationLoadBalancer": { + "Properties": { + "Scheme": "internal", + "SecurityGroups": [ + "sg-e7387381" + ], + "Subnets": [ + "subnet-bb01c4cd", + "subnet-c85f4091" + ], + "Tags": [ + { + "Key": "empire.app.process", + "Value": "api" + } + ] + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" + }, + "apiApplicationLoadBalancerPort443Listener": { + "Properties": { + "Certificates": [ + { + "CertificateArn": { + "Fn::Join": [ + "", + [ + "arn:aws:iam::", + { + "Ref": "AWS::AccountId" + }, + ":server-certificate/", + "AcmeIncDotCom" + ] + ] + } + } + ], + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "apiTargetGroup" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Fn::GetAtt": [ + "apiApplicationLoadBalancer", + "Arn" + ] + }, + "Port": 443, + "Protocol": "HTTPS" + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener" + }, + "apiApplicationLoadBalancerPort80Listener": { + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "apiTargetGroup" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "apiApplicationLoadBalancer" + }, + "Port": 80, + "Protocol": "HTTP" + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener" + }, + "apiService": { + "DependsOn": [ + "apiApplicationLoadBalancerPort80Listener", + "apiApplicationLoadBalancerPort443Listener" + ], + "Properties": { + "Cluster": "cluster", + "DesiredCount": { + "Ref": "apiScale" + }, + "LoadBalancers": [ + { + "ContainerName": "api", + "ContainerPort": 8080, + "TargetGroupArn": { + "Ref": "apiTargetGroup" + } + } + ], + "Role": "ecsServiceRole", + "ServiceName": "acme-inc-api", + "ServiceToken": "sns topic arn", + "TaskDefinition": { + "Ref": "apiTaskDefinition" + } + }, + "Type": "Custom::ECSService" + }, + "apiTargetGroup": { + "Properties": { + "Port": 65535, + "Protocol": "HTTP", + "VpcId": "" + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" + }, + "apiTaskDefinition": { + "Properties": { + "ContainerDefinitions": [ + { + "Command": [ + "./bin/api" + ], + "Cpu": 0, + "DockerLabels": { + "cloudformation.restart-key": { + "Ref": "RestartKey" + } + }, + "Environment": [ + { + "Name": "LOAD_BALANCER_TYPE", + "Value": "alb" + }, + { + "Name": "PORT", + "Value": "8080" + } + ], + "Essential": true, + "Image": "", + "Memory": 0, + "Name": "api", + "PortMappings": [ + { + "ContainerPort": 8080, + "HostPort": 0 + } + ], + "Ulimits": [] + } + ], + "Volumes": [] + }, + "Type": "AWS::ECS::TaskDefinition" + }, + "webApplicationLoadBalancer": { + "Properties": { + "Scheme": "internal", + "SecurityGroups": [ + "sg-e7387381" + ], + "Subnets": [ + "subnet-bb01c4cd", + "subnet-c85f4091" + ], + "Tags": [ + { + "Key": "empire.app.process", + "Value": "web" + } + ] + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" + }, + "webApplicationLoadBalancerPort443Listener": { + "Properties": { + "Certificates": [ + { + "CertificateArn": "arn:aws:iam::012345678901:server-certificate/AcmeIncDotCom" + } + ], + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "webTargetGroup" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Fn::GetAtt": [ + "webApplicationLoadBalancer", + "Arn" + ] + }, + "Port": 443, + "Protocol": "HTTPS" + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener" + }, + "webApplicationLoadBalancerPort80Listener": { + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "webTargetGroup" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "webApplicationLoadBalancer" + }, + "Port": 80, + "Protocol": "HTTP" + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener" + }, + "webService": { + "DependsOn": [ + "webApplicationLoadBalancerPort80Listener", + "webApplicationLoadBalancerPort443Listener" + ], + "Properties": { + "Cluster": "cluster", + "DesiredCount": { + "Ref": "webScale" + }, + "LoadBalancers": [ + { + "ContainerName": "web", + "ContainerPort": 8080, + "TargetGroupArn": { + "Ref": "webTargetGroup" + } + } + ], + "Role": "ecsServiceRole", + "ServiceName": "acme-inc-web", + "ServiceToken": "sns topic arn", + "TaskDefinition": { + "Ref": "webTaskDefinition" + } + }, + "Type": "Custom::ECSService" + }, + "webTargetGroup": { + "Properties": { + "Port": 65535, + "Protocol": "HTTP", + "VpcId": "" + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" + }, + "webTaskDefinition": { + "Properties": { + "ContainerDefinitions": [ + { + "Command": [ + "./bin/web" + ], + "Cpu": 0, + "DockerLabels": { + "cloudformation.restart-key": { + "Ref": "RestartKey" + } + }, + "Environment": [ + { + "Name": "LOAD_BALANCER_TYPE", + "Value": "alb" + }, + { + "Name": "PORT", + "Value": "8080" + } + ], + "Essential": true, + "Image": "", + "Memory": 0, + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 8080, + "HostPort": 0 + } + ], + "Ulimits": [] + } + ], + "Volumes": [] + }, + "Type": "AWS::ECS::TaskDefinition" + } + } +} \ No newline at end of file diff --git a/server/cloudformation/ecs.go b/server/cloudformation/ecs.go index c18b8139e..aeb236925 100644 --- a/server/cloudformation/ecs.go +++ b/server/cloudformation/ecs.go @@ -47,6 +47,7 @@ type LoadBalancer struct { ContainerName *string ContainerPort *customresources.IntValue LoadBalancerName *string + TargetGroupArn *string } // ECSServiceProperties represents the properties for the Custom::ECSService @@ -133,6 +134,7 @@ func (p *ECSServiceResource) create(ctx context.Context, clientToken string, pro ContainerName: v.ContainerName, ContainerPort: v.ContainerPort.Value(), LoadBalancerName: v.LoadBalancerName, + TargetGroupArn: v.TargetGroupArn, }) }