func (t *EmpireTemplate) addTaskDefinition(tmpl *troposphere.Template, app *scheduler.App, p *scheduler.Process) (troposphere.NamedResource, *ContainerDefinitionProperties) { key := processResourceName(p.Type) // The task definition that will be used to run the ECS task. taskDefinition := troposphere.NamedResource{ Name: fmt.Sprintf("%sTaskDefinition", key), } cd := t.ContainerDefinition(app, p) containerDefinition := cloudformationContainerDefinition(cd) var taskDefinitionProperties interface{} taskDefinitionType := taskDefinitionResourceType(app) if taskDefinitionType == "Custom::ECSTaskDefinition" { taskDefinition.Name = fmt.Sprintf("%sTD", key) processEnvironment := fmt.Sprintf("%sEnvironment", key) tmpl.Resources[processEnvironment] = troposphere.Resource{ Type: "Custom::ECSEnvironment", Properties: map[string]interface{}{ "ServiceToken": t.CustomResourcesTopic, "Environment": sortedEnvironment(p.Env), }, } containerDefinition.Environment = []interface{}{ Ref(appEnvironment), Ref(processEnvironment), } taskDefinitionProperties = &CustomTaskDefinitionProperties{ Volumes: []interface{}{}, ServiceToken: t.CustomResourcesTopic, Family: fmt.Sprintf("%s-%s", app.Name, p.Type), ContainerDefinitions: []*ContainerDefinitionProperties{ containerDefinition, }, } } else { containerDefinition.Environment = cd.Environment taskDefinitionProperties = &TaskDefinitionProperties{ Volumes: []interface{}{}, ContainerDefinitions: []*ContainerDefinitionProperties{ containerDefinition, }, } } taskDefinition.Resource = troposphere.Resource{ Type: taskDefinitionType, Properties: taskDefinitionProperties, } tmpl.AddResource(taskDefinition) return taskDefinition, containerDefinition }
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 }
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 }