// DescribeTaskDefinition will use the task definition from cache if provided // with a task definition ARN. func (c *cachingECSClient) DescribeTaskDefinition(input *ecs.DescribeTaskDefinitionInput) (*ecs.DescribeTaskDefinitionOutput, error) { if _, err := arn.Parse(*input.TaskDefinition); err != nil { return c.ecsClient.DescribeTaskDefinition(input) } if v, ok := c.taskDefinitions.Get(*input.TaskDefinition); ok { return &ecs.DescribeTaskDefinitionOutput{ TaskDefinition: v.(*ecs.TaskDefinition), }, nil } resp, err := c.ecsClient.DescribeTaskDefinition(input) if err != nil { return resp, err } c.taskDefinitions.Set(*resp.TaskDefinition.TaskDefinitionArn, resp.TaskDefinition, 0) return resp, err }
// If the ServiceRole option is not an ARN, it will return a CloudFormation // expression that expands the ServiceRole to an ARN. func (t *EmpireTemplate) serviceRoleArn() interface{} { if _, err := arn.Parse(t.ServiceRole); err == nil { return t.ServiceRole } return Join("", "arn:aws:iam::", Ref("AWS::AccountId"), ":role/", t.ServiceRole) }
func (t *EmpireTemplate) addService(tmpl *troposphere.Template, app *scheduler.App, p *scheduler.Process) (serviceName string) { key := processResourceName(p.Type) // The standard AWS::ECS::Service resource's default behavior is to wait // for services to stabilize when you update them. While this is a // sensible default for CloudFormation, the overall behavior when // applied to Empire is not a great experience, because updates will // lock up the stack. // // Setting this option makes the stack use a Custom::ECSService // resources intead, which does not wait for the service to stabilize // after updating. ecsServiceType := "Custom::ECSService" var portMappings []*PortMappingProperties var serviceDependencies []string loadBalancers := []map[string]interface{}{} if p.Exposure != nil { scheme := schemeInternal sg := t.InternalSecurityGroupID subnets := t.InternalSubnetIDs if p.Exposure.External { scheme = schemeExternal sg = t.ExternalSecurityGroupID subnets = t.ExternalSubnetIDs } p.Env["PORT"] = fmt.Sprintf("%d", ContainerPort) loadBalancerType := classicLoadBalancer if v, ok := app.Env["LOAD_BALANCER_TYPE"]; ok { loadBalancerType = v } 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, }, } 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) 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, }, } listeners := []map[string]interface{}{ map[string]interface{}{ "LoadBalancerPort": 80, "Protocol": "http", "InstancePort": GetAtt(instancePort, "InstancePort"), "InstanceProtocol": "http", }, } 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" { tmpl.Resources["CNAME"] = troposphere.Resource{ Type: "AWS::Route53::RecordSet", Condition: "DNSCondition", Properties: map[string]interface{}{ "HostedZoneId": *t.HostedZone.Id, "Name": fmt.Sprintf("%s.%s", app.Name, *t.HostedZone.Name), "Type": "CNAME", "TTL": defaultCNAMETTL, "ResourceRecords": []interface{}{GetAtt(loadBalancer, "DNSName")}, }, } } } taskDefinition, containerDefinition := t.addTaskDefinition(tmpl, app, p) containerDefinition.DockerLabels[restartLabel] = Ref(restartParameter) containerDefinition.PortMappings = portMappings serviceProperties := map[string]interface{}{ "Cluster": t.Cluster, "DesiredCount": Ref(scaleParameter(p.Type)), "LoadBalancers": loadBalancers, "TaskDefinition": Ref(taskDefinition), "ServiceName": fmt.Sprintf("%s-%s", app.Name, p.Type), "ServiceToken": t.CustomResourcesTopic, } if len(loadBalancers) > 0 { serviceProperties["Role"] = t.ServiceRole } service := troposphere.NamedResource{ Name: fmt.Sprintf("%sService", key), Resource: troposphere.Resource{ Type: ecsServiceType, Properties: serviceProperties, }, } if len(serviceDependencies) > 0 { service.Resource.DependsOn = serviceDependencies } tmpl.AddResource(service) return service.Name }
// Build builds a Go representation of a CloudFormation template for the app. func (t *EmpireTemplate) Build(app *scheduler.App) (interface{}, error) { parameters := map[string]interface{}{ "DNS": map[string]string{ "Type": "String", "Description": "When set to `true`, CNAME's will be altered", "Default": "true", }, restartParameter: map[string]string{ "Type": "String", }, } conditions := map[string]interface{}{ "DNSCondition": map[string]interface{}{ "Fn::Equals": []interface{}{ map[string]string{ "Ref": "DNS", }, "true", }, }, } resources := map[string]interface{}{} outputs := map[string]interface{}{ "Release": map[string]interface{}{ "Value": app.Release, }, "EmpireVersion": map[string]interface{}{ "Value": empire.Version, }, } serviceMappings := []map[string]interface{}{} // The standard AWS::ECS::Service resource's default behavior is to wait // for services to stabilize when you update them. While this is a // sensible default for CloudFormation, the overall behavior when // applied to Empire is not a great experience, because updates will // lock up the stack. // // Setting this option makes the stack use a Custom::ECSService // resources intead, which does not wait for the service to stabilize // after updating. ecsServiceType := "Custom::ECSService" if app.Env["ECS_SERVICE"] == "standard" { ecsServiceType = "AWS::ECS::Service" } for _, p := range app.Processes { cd := t.ContainerDefinition(app, p) key := processResourceName(p.Type) parameters[scaleParameter(p.Type)] = map[string]string{ "Type": "String", } portMappings := []map[string]interface{}{} loadBalancers := []map[string]interface{}{} if p.Exposure != nil { scheme := schemeInternal sg := t.InternalSecurityGroupID subnets := t.InternalSubnetIDs if p.Exposure.External { scheme = schemeExternal sg = t.ExternalSecurityGroupID subnets = t.ExternalSubnetIDs } instancePort := fmt.Sprintf("%s%dInstancePort", key, ContainerPort) resources[instancePort] = map[string]interface{}{ "Type": "Custom::InstancePort", "Version": "1.0", "Properties": map[string]interface{}{ "ServiceToken": t.CustomResourcesTopic, }, } listeners := []map[string]interface{}{ map[string]interface{}{ "LoadBalancerPort": 80, "Protocol": "http", "InstancePort": map[string][]string{ "Fn::GetAtt": []string{ instancePort, "InstancePort", }, }, "InstanceProtocol": "http", }, } 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 = map[string]interface{}{ "Fn::Join": []interface{}{ "", []interface{}{"arn:aws:iam::", map[string]string{"Ref": "AWS::AccountId"}, ":server-certificate/", e.Cert}, }, } } listeners = append(listeners, map[string]interface{}{ "LoadBalancerPort": 443, "Protocol": "https", "InstancePort": map[string][]string{ "Fn::GetAtt": []string{ instancePort, "InstancePort", }, }, "SSLCertificateId": cert, "InstanceProtocol": "http", }) } portMappings = append(portMappings, map[string]interface{}{ "ContainerPort": ContainerPort, "HostPort": map[string][]string{ "Fn::GetAtt": []string{ instancePort, "InstancePort", }, }, }) cd.Environment = append(cd.Environment, &ecs.KeyValuePair{ Name: aws.String("PORT"), Value: aws.String(fmt.Sprintf("%d", ContainerPort)), }) loadBalancer := fmt.Sprintf("%sLoadBalancer", key) loadBalancers = append(loadBalancers, map[string]interface{}{ "ContainerName": p.Type, "ContainerPort": ContainerPort, "LoadBalancerName": map[string]string{ "Ref": loadBalancer, }, }) resources[loadBalancer] = map[string]interface{}{ "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, }, }, } if p.Type == "web" { resources["CNAME"] = map[string]interface{}{ "Type": "AWS::Route53::RecordSet", "Condition": "DNSCondition", "Properties": map[string]interface{}{ "HostedZoneId": *t.HostedZone.Id, "Name": fmt.Sprintf("%s.%s", app.Name, *t.HostedZone.Name), "Type": "CNAME", "TTL": defaultCNAMETTL, "ResourceRecords": []map[string][]string{ map[string][]string{ "Fn::GetAtt": []string{loadBalancer, "DNSName"}, }, }, }, } } } labels := map[string]interface{}{} for k, v := range cd.DockerLabels { labels[k] = v } labels["cloudformation.restart-key"] = map[string]string{"Ref": restartParameter} taskDefinition := fmt.Sprintf("%sTaskDefinition", key) containerDefinition := map[string]interface{}{ "Name": *cd.Name, "Command": cd.Command, "Cpu": *cd.Cpu, "Image": *cd.Image, "Essential": *cd.Essential, "Memory": *cd.Memory, "Environment": cd.Environment, "PortMappings": portMappings, "DockerLabels": labels, "Ulimits": cd.Ulimits, } if cd.LogConfiguration != nil { containerDefinition["LogConfiguration"] = cd.LogConfiguration } resources[taskDefinition] = map[string]interface{}{ "Type": "AWS::ECS::TaskDefinition", "Properties": map[string]interface{}{ "ContainerDefinitions": []interface{}{ containerDefinition, }, "Volumes": []interface{}{}, }, } service := fmt.Sprintf("%s", key) serviceProperties := map[string]interface{}{ "Cluster": t.Cluster, "DesiredCount": map[string]string{ "Ref": scaleParameter(p.Type), }, "LoadBalancers": loadBalancers, "TaskDefinition": map[string]string{ "Ref": taskDefinition, }, } if ecsServiceType == "Custom::ECSService" { // It's not possible to change the type of a resource, // so we have to change the name of the service resource // to something different (just append "Service"). service = fmt.Sprintf("%sService", service) serviceProperties["ServiceName"] = fmt.Sprintf("%s-%s", app.Name, p.Type) serviceProperties["ServiceToken"] = t.CustomResourcesTopic } if len(loadBalancers) > 0 { serviceProperties["Role"] = t.ServiceRole } serviceMappings = append(serviceMappings, map[string]interface{}{ "Fn::Join": []interface{}{ "=", []interface{}{p.Type, map[string]string{"Ref": service}}, }, }) resources[service] = map[string]interface{}{ "Type": ecsServiceType, "Properties": serviceProperties, } } outputs[servicesOutput] = map[string]interface{}{ "Value": map[string]interface{}{ "Fn::Join": []interface{}{ ",", serviceMappings, }, }, } return map[string]interface{}{ "Parameters": parameters, "Conditions": conditions, "Resources": resources, "Outputs": outputs, }, nil }
func (t *EmpireTemplate) addService(tmpl *troposphere.Template, app *scheduler.App, p *scheduler.Process, stackTags []*cloudformation.Tag) (serviceName string, err error) { key := processResourceName(p.Type) // Process specific tags to apply to resources. tags := tagsFromLabels(p.Labels) // The standard AWS::ECS::Service resource's default behavior is to wait // for services to stabilize when you update them. While this is a // sensible default for CloudFormation, the overall behavior when // applied to Empire is not a great experience, because updates will // lock up the stack. // // Setting this option makes the stack use a Custom::ECSService // resources intead, which does not wait for the service to stabilize // after updating. ecsServiceType := "Custom::ECSService" var portMappings []*PortMappingProperties var serviceDependencies []string loadBalancers := []map[string]interface{}{} if p.Exposure != nil { scheme := schemeInternal sg := t.InternalSecurityGroupID subnets := t.InternalSubnetIDs if p.Exposure.External { scheme = schemeExternal sg = t.ExternalSecurityGroupID subnets = t.ExternalSubnetIDs } loadBalancerType := loadBalancerType(app, p) var ( loadBalancer troposphere.NamedResource canonicalHostedZoneId interface{} ) switch loadBalancerType { case applicationLoadBalancer: loadBalancer = troposphere.NamedResource{ Name: fmt.Sprintf("%sApplicationLoadBalancer", key), Resource: troposphere.Resource{ Type: "AWS::ElasticLoadBalancingV2::LoadBalancer", Properties: map[string]interface{}{ "Scheme": scheme, "SecurityGroups": []string{sg}, "Subnets": subnets, "Tags": append(stackTags, tags...), }, }, } canonicalHostedZoneId = GetAtt(loadBalancer, "CanonicalHostedZoneID") tmpl.AddResource(loadBalancer) 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, "Tags": append(stackTags, tags...), }, } // Add a port mapping for each unique container port. containerPorts := make(map[int]bool) for _, port := range p.Exposure.Ports { if ok := containerPorts[port.Container]; !ok { containerPorts[port.Container] = true portMappings = append(portMappings, &PortMappingProperties{ ContainerPort: port.Container, HostPort: 0, }) } } // Unlike ELB, ALB can only route to a single container // port, when dynamic ports are used. Thus, we have to // ensure that all of the defined ports map to the same // container port. // // ELB can route to multiple container ports, because a // listener can directly point to a container port, // through an instance port: // // Listener Port => Instance Port => Container Port if len(containerPorts) > 1 { err = fmt.Errorf("AWS Application Load Balancers can only map listeners to a single container port. %d unique container ports were defined: [%s]", len(p.Exposure.Ports), fmtPorts(p.Exposure.Ports)) return } // Add a listener for each port. for _, port := range p.Exposure.Ports { listener := troposphere.NamedResource{ Name: fmt.Sprintf("%sPort%dListener", loadBalancer.Name, port.Host), } switch e := port.Protocol.(type) { case *scheduler.HTTP: listener.Resource = troposphere.Resource{ Type: "AWS::ElasticLoadBalancingV2::Listener", Properties: map[string]interface{}{ "LoadBalancerArn": Ref(loadBalancer), "Port": port.Host, "Protocol": "HTTP", "DefaultActions": []interface{}{ map[string]interface{}{ "TargetGroupArn": Ref(targetGroup), "Type": "forward", }, }, }, } case *scheduler.HTTPS: 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) } listener.Resource = troposphere.Resource{ Type: "AWS::ElasticLoadBalancingV2::Listener", Properties: map[string]interface{}{ "Certificates": []interface{}{ map[string]interface{}{ "CertificateArn": cert, }, }, "LoadBalancerArn": Ref(loadBalancer), "Port": port.Host, "Protocol": "HTTPS", "DefaultActions": []interface{}{ map[string]interface{}{ "TargetGroupArn": Ref(targetGroup), "Type": "forward", }, }, }, } default: err = fmt.Errorf("%s listeners are not supported with AWS Application Load Balancing", e.Protocol()) return } tmpl.AddResource(listener) serviceDependencies = append(serviceDependencies, listener.Name) } loadBalancers = append(loadBalancers, map[string]interface{}{ "ContainerName": p.Type, "ContainerPort": p.Exposure.Ports[0].Container, "TargetGroupArn": Ref(targetGroup), }) default: loadBalancer = troposphere.NamedResource{ Name: fmt.Sprintf("%sLoadBalancer", key), } canonicalHostedZoneId = GetAtt(loadBalancer, "CanonicalHostedZoneNameID") listeners := []map[string]interface{}{} // Add a port mapping for each unique container port. instancePorts := make(map[int]troposphere.NamedResource) for _, port := range p.Exposure.Ports { if _, ok := instancePorts[port.Container]; !ok { instancePort := troposphere.NamedResource{ Name: fmt.Sprintf("%s%dInstancePort", key, port.Container), Resource: troposphere.Resource{ Type: "Custom::InstancePort", Version: "1.0", Properties: map[string]interface{}{ "ServiceToken": t.CustomResourcesTopic, }, }, } portMappings = append(portMappings, &PortMappingProperties{ ContainerPort: port.Container, HostPort: GetAtt(instancePort, "InstancePort"), }) tmpl.AddResource(instancePort) instancePorts[port.Container] = instancePort } } for _, port := range p.Exposure.Ports { instancePort := instancePorts[port.Container] switch e := port.Protocol.(type) { case *scheduler.TCP: listeners = append(listeners, map[string]interface{}{ "LoadBalancerPort": port.Host, "Protocol": "tcp", "InstancePort": GetAtt(instancePort, "InstancePort"), "InstanceProtocol": "tcp", }) case *scheduler.SSL: 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": port.Host, "Protocol": "ssl", "InstancePort": GetAtt(instancePort, "InstancePort"), "SSLCertificateId": cert, "InstanceProtocol": "tcp", }) case *scheduler.HTTP: listeners = append(listeners, map[string]interface{}{ "LoadBalancerPort": port.Host, "Protocol": "http", "InstancePort": GetAtt(instancePort, "InstancePort"), "InstanceProtocol": "http", }) case *scheduler.HTTPS: 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": port.Host, "Protocol": "https", "InstancePort": GetAtt(instancePort, "InstancePort"), "SSLCertificateId": cert, "InstanceProtocol": "http", }) } } loadBalancer.Resource = troposphere.Resource{ Type: "AWS::ElasticLoadBalancing::LoadBalancer", Properties: map[string]interface{}{ "Scheme": scheme, "SecurityGroups": []string{sg}, "Subnets": subnets, "Listeners": listeners, "CrossZone": true, "Tags": tags, "ConnectionDrainingPolicy": map[string]interface{}{ "Enabled": true, "Timeout": defaultConnectionDrainingTimeout, }, }, } tmpl.AddResource(loadBalancer) loadBalancers = append(loadBalancers, map[string]interface{}{ "ContainerName": p.Type, "ContainerPort": p.Exposure.Ports[0].Container, "LoadBalancerName": Ref(loadBalancer), }) } alias := troposphere.NamedResource{ Name: fmt.Sprintf("%sAlias", key), Resource: troposphere.Resource{ Type: "AWS::Route53::RecordSet", Condition: "DNSCondition", Properties: map[string]interface{}{ "HostedZoneId": *t.HostedZone.Id, "Name": fmt.Sprintf("%s.%s.%s", p.Type, app.Name, *t.HostedZone.Name), "Type": "A", "AliasTarget": map[string]interface{}{ "DNSName": GetAtt(loadBalancer, "DNSName"), "EvaluateTargetHealth": "true", "HostedZoneId": canonicalHostedZoneId, }, }, }, } tmpl.AddResource(alias) // DEPRECATED: This was used in the world where only the "web" // process could be exposed. if p.Type == "web" { tmpl.Resources["CNAME"] = troposphere.Resource{ Type: "AWS::Route53::RecordSet", Condition: "DNSCondition", Properties: map[string]interface{}{ "HostedZoneId": *t.HostedZone.Id, "Name": fmt.Sprintf("%s.%s", app.Name, *t.HostedZone.Name), "Type": "CNAME", "TTL": defaultCNAMETTL, "ResourceRecords": []interface{}{GetAtt(loadBalancer, "DNSName")}, }, } } } taskDefinition, containerDefinition := t.addTaskDefinition(tmpl, app, p) containerDefinition.DockerLabels[restartLabel] = Ref(restartParameter) containerDefinition.PortMappings = portMappings serviceProperties := map[string]interface{}{ "Cluster": t.Cluster, "DesiredCount": Ref(scaleParameter(p.Type)), "LoadBalancers": loadBalancers, "TaskDefinition": Ref(taskDefinition), "ServiceName": fmt.Sprintf("%s-%s", app.Name, p.Type), "ServiceToken": t.CustomResourcesTopic, } if len(loadBalancers) > 0 { serviceProperties["Role"] = t.ServiceRole } service := troposphere.NamedResource{ Name: fmt.Sprintf("%sService", key), Resource: troposphere.Resource{ Type: ecsServiceType, Properties: serviceProperties, }, } if len(serviceDependencies) > 0 { service.Resource.DependsOn = serviceDependencies } tmpl.AddResource(service) return service.Name, nil }