Example #1
0
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
}
Example #2
0
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
}
Example #3
0
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
}