浏览代码

Merge pull request #638 from docker/clean

Nicolas De loof 5 年之前
父节点
当前提交
3685cbbf8d
共有 13 个文件被更改,包括 611 次插入637 次删除
  1. 284 0
      ecs/awsResources.go
  2. 2 1
      ecs/backend.go
  3. 116 330
      ecs/cloudformation.go
  4. 13 17
      ecs/cloudformation_test.go
  5. 23 0
      ecs/compatibility.go
  6. 4 6
      ecs/convert.go
  7. 10 23
      ecs/ec2.go
  8. 5 7
      ecs/ps.go
  9. 19 24
      ecs/sdk.go
  10. 73 143
      ecs/testdata/simple/simple-cloudformation-conversion.golden
  11. 2 85
      ecs/up.go
  12. 59 0
      ecs/volumes.go
  13. 1 1
      ecs/x.go

+ 284 - 0
ecs/awsResources.go

@@ -0,0 +1,284 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package ecs
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/aws/aws-sdk-go/service/elbv2"
+	"github.com/awslabs/goformation/v4/cloudformation/ec2"
+	"github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
+
+	"github.com/awslabs/goformation/v4/cloudformation"
+	"github.com/awslabs/goformation/v4/cloudformation/ecs"
+	"github.com/compose-spec/compose-go/types"
+	"github.com/sirupsen/logrus"
+)
+
+// awsResources hold the AWS component being used or created to support services definition
+type awsResources struct {
+	vpc              string
+	subnets          []string
+	cluster          string
+	loadBalancer     string
+	loadBalancerType string
+	securityGroups   map[string]string
+}
+
+func (r *awsResources) serviceSecurityGroups(service types.ServiceConfig) []string {
+	var groups []string
+	for net := range service.Networks {
+		groups = append(groups, r.securityGroups[net])
+	}
+	return groups
+}
+
+func (r *awsResources) allSecurityGroups() []string {
+	var securityGroups []string
+	for _, r := range r.securityGroups {
+		securityGroups = append(securityGroups, r)
+	}
+	return securityGroups
+}
+
+// parse look into compose project for configured resource to use, and check they are valid
+func (b *ecsAPIService) parse(ctx context.Context, project *types.Project) (awsResources, error) {
+	r := awsResources{}
+	var err error
+	r.cluster, err = b.parseClusterExtension(ctx, project)
+	if err != nil {
+		return r, err
+	}
+	r.vpc, r.subnets, err = b.parseVPCExtension(ctx, project)
+	if err != nil {
+		return r, err
+	}
+	r.loadBalancer, r.loadBalancerType, err = b.parseLoadBalancerExtension(ctx, project)
+	if err != nil {
+		return r, err
+	}
+	r.securityGroups, err = b.parseSecurityGroupExtension(ctx, project)
+	if err != nil {
+		return r, err
+	}
+	return r, nil
+}
+
+func (b *ecsAPIService) parseClusterExtension(ctx context.Context, project *types.Project) (string, error) {
+	if x, ok := project.Extensions[extensionCluster]; ok {
+		cluster := x.(string)
+		ok, err := b.SDK.ClusterExists(ctx, cluster)
+		if err != nil {
+			return "", err
+		}
+		if !ok {
+			return "", fmt.Errorf("cluster does not exist: %s", cluster)
+		}
+		return cluster, nil
+	}
+	return "", nil
+}
+
+func (b *ecsAPIService) parseVPCExtension(ctx context.Context, project *types.Project) (string, []string, error) {
+	var vpc string
+	if x, ok := project.Extensions[extensionVPC]; ok {
+		vpc = x.(string)
+		err := b.SDK.CheckVPC(ctx, vpc)
+		if err != nil {
+			return "", nil, err
+		}
+
+	} else {
+		defaultVPC, err := b.SDK.GetDefaultVPC(ctx)
+		if err != nil {
+			return "", nil, err
+		}
+		vpc = defaultVPC
+	}
+
+	subNets, err := b.SDK.GetSubNets(ctx, vpc)
+	if err != nil {
+		return "", nil, err
+	}
+	if len(subNets) < 2 {
+		return "", nil, fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc)
+	}
+	return vpc, subNets, nil
+}
+
+func (b *ecsAPIService) parseLoadBalancerExtension(ctx context.Context, project *types.Project) (string, string, error) {
+	if x, ok := project.Extensions[extensionLoadBalancer]; ok {
+		loadBalancer := x.(string)
+		loadBalancerType, err := b.SDK.LoadBalancerType(ctx, loadBalancer)
+		if err != nil {
+			return "", "", err
+		}
+
+		required := getRequiredLoadBalancerType(project)
+		if loadBalancerType != required {
+			return "", "", fmt.Errorf("load balancer %s is of type %s, project require a %s", loadBalancer, loadBalancerType, required)
+		}
+
+		return loadBalancer, loadBalancerType, nil
+	}
+	return "", "", nil
+}
+
+func (b *ecsAPIService) parseSecurityGroupExtension(ctx context.Context, project *types.Project) (map[string]string, error) {
+	securityGroups := make(map[string]string, len(project.Networks))
+	for name, net := range project.Networks {
+		var sg string
+		if net.External.External {
+			sg = net.Name
+		}
+		if x, ok := net.Extensions[extensionSecurityGroup]; ok {
+			logrus.Warn("to use an existing security-group, use `network.external` and `network.name` in your compose file")
+			logrus.Debugf("Security Group for network %q set by user to %q", net.Name, x)
+			sg = x.(string)
+		}
+		exists, err := b.SDK.SecurityGroupExists(ctx, sg)
+		if err != nil {
+			return nil, err
+		}
+		if !exists {
+			return nil, fmt.Errorf("security group %s doesn't exist", sg)
+		}
+		securityGroups[name] = sg
+	}
+	return securityGroups, nil
+}
+
+// ensureResources create required resources in template if not yet defined
+func (b *ecsAPIService) ensureResources(resources *awsResources, project *types.Project, template *cloudformation.Template) {
+	b.ensureCluster(resources, project, template)
+	b.ensureNetworks(resources, project, template)
+	b.ensureLoadBalancer(resources, project, template)
+}
+
+func (b *ecsAPIService) ensureCluster(r *awsResources, project *types.Project, template *cloudformation.Template) {
+	if r.cluster != "" {
+		return
+	}
+	template.Resources["Cluster"] = &ecs.Cluster{
+		ClusterName: project.Name,
+		Tags:        projectTags(project),
+	}
+	r.cluster = cloudformation.Ref("Cluster")
+}
+
+func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project, template *cloudformation.Template) {
+	if r.securityGroups == nil {
+		r.securityGroups = make(map[string]string, len(project.Networks))
+	}
+	for name, net := range project.Networks {
+		securityGroup := networkResourceName(name)
+		template.Resources[securityGroup] = &ec2.SecurityGroup{
+			GroupDescription: fmt.Sprintf("%s Security Group for %s network", project.Name, name),
+			GroupName:        securityGroup,
+			VpcId:            r.vpc,
+			Tags:             networkTags(project, net),
+		}
+
+		ingress := securityGroup + "Ingress"
+		template.Resources[ingress] = &ec2.SecurityGroupIngress{
+			Description:           fmt.Sprintf("Allow communication within network %s", name),
+			IpProtocol:            allProtocols,
+			GroupId:               cloudformation.Ref(securityGroup),
+			SourceSecurityGroupId: cloudformation.Ref(securityGroup),
+		}
+
+		r.securityGroups[name] = cloudformation.Ref(securityGroup)
+	}
+}
+
+func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, project *types.Project, template *cloudformation.Template) {
+	if r.loadBalancer != "" {
+		return
+	}
+	if allServices(project.Services, func(it types.ServiceConfig) bool {
+		return len(it.Ports) == 0
+	}) {
+		logrus.Debug("Application does not expose any public port, so no need for a LoadBalancer")
+		return
+	}
+
+	balancerType := getRequiredLoadBalancerType(project)
+	template.Resources["LoadBalancer"] = &elasticloadbalancingv2.LoadBalancer{
+		Scheme:         elbv2.LoadBalancerSchemeEnumInternetFacing,
+		SecurityGroups: r.getLoadBalancerSecurityGroups(project),
+		Subnets:        r.subnets,
+		Tags:           projectTags(project),
+		Type:           balancerType,
+	}
+	r.loadBalancer = cloudformation.Ref("LoadBalancer")
+	r.loadBalancerType = balancerType
+}
+
+func (r *awsResources) getLoadBalancerSecurityGroups(project *types.Project) []string {
+	securityGroups := []string{}
+	for name, network := range project.Networks {
+		if !network.Internal {
+			securityGroups = append(securityGroups, r.securityGroups[name])
+		}
+	}
+	return securityGroups
+}
+
+func getRequiredLoadBalancerType(project *types.Project) string {
+	loadBalancerType := elbv2.LoadBalancerTypeEnumNetwork
+	if allServices(project.Services, func(it types.ServiceConfig) bool {
+		return allPorts(it.Ports, portIsHTTP)
+	}) {
+		loadBalancerType = elbv2.LoadBalancerTypeEnumApplication
+	}
+	return loadBalancerType
+}
+
+func portIsHTTP(it types.ServicePortConfig) bool {
+	if v, ok := it.Extensions[extensionProtocol]; ok {
+		protocol := v.(string)
+		return protocol == "http" || protocol == "https"
+	}
+	return it.Target == 80 || it.Target == 443
+}
+
+// predicate[types.ServiceConfig]
+type servicePredicate func(it types.ServiceConfig) bool
+
+// all[types.ServiceConfig]
+func allServices(services types.Services, p servicePredicate) bool {
+	for _, s := range services {
+		if !p(s) {
+			return false
+		}
+	}
+	return true
+}
+
+// predicate[types.ServicePortConfig]
+type portPredicate func(it types.ServicePortConfig) bool
+
+// all[types.ServicePortConfig]
+func allPorts(ports []types.ServicePortConfig, p portPredicate) bool {
+	for _, s := range ports {
+		if !p(s) {
+			return false
+		}
+	}
+	return true
+}

+ 2 - 1
ecs/backend.go

@@ -73,10 +73,11 @@ func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) {
 		return nil, err
 	}
 
+	sdk := newSDK(sess)
 	return &ecsAPIService{
 		ctx:    ecsCtx,
 		Region: ecsCtx.Region,
-		SDK:    newSDK(sess),
+		SDK:    sdk,
 	}, nil
 }
 

+ 116 - 330
ecs/cloudformation.go

@@ -34,22 +34,21 @@ import (
 	"github.com/awslabs/goformation/v4/cloudformation/logs"
 	"github.com/awslabs/goformation/v4/cloudformation/secretsmanager"
 	cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
-	"github.com/compose-spec/compose-go/compatibility"
-	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/types"
-	"github.com/sirupsen/logrus"
-)
-
-const (
-	parameterClusterName     = "ParameterClusterName"
-	parameterVPCId           = "ParameterVPCId"
-	parameterSubnet1Id       = "ParameterSubnet1Id"
-	parameterSubnet2Id       = "ParameterSubnet2Id"
-	parameterLoadBalancerARN = "ParameterLoadBalancerARN"
 )
 
 func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
-	template, networks, err := b.convert(project)
+	err := b.checkCompatibility(project)
+	if err != nil {
+		return nil, err
+	}
+
+	resources, err := b.parse(ctx, project)
+	if err != nil {
+		return nil, err
+	}
+
+	template, err := b.convert(project, resources)
 	if err != nil {
 		return nil, err
 	}
@@ -58,43 +57,14 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]
 	// as "source security group" use an arbitrary network attached to service(s) who mounts target volume
 	for n, vol := range project.Volumes {
 		err := b.SDK.WithVolumeSecurityGroups(ctx, vol.Name, func(securityGroups []string) error {
-			target := securityGroups[0]
-			for _, s := range project.Services {
-				for _, v := range s.Volumes {
-					if v.Source != n {
-						continue
-					}
-					var source string
-					for net := range s.Networks {
-						network := project.Networks[net]
-						if ext, ok := network.Extensions[extensionSecurityGroup]; ok {
-							source = ext.(string)
-						} else {
-							source = networkResourceName(project, net)
-						}
-						break
-					}
-					name := fmt.Sprintf("%sNFSMount%s", s.Name, n)
-					template.Resources[name] = &ec2.SecurityGroupIngress{
-						Description:           fmt.Sprintf("Allow NFS mount for %s on %s", s.Name, n),
-						GroupId:               target,
-						SourceSecurityGroupId: cloudformation.Ref(source),
-						IpProtocol:            "tcp",
-						FromPort:              2049,
-						ToPort:                2049,
-					}
-					service := template.Resources[serviceResourceName(s.Name)].(*ecs.Service)
-					service.AWSCloudFormationDependsOn = append(service.AWSCloudFormationDependsOn, name)
-				}
-			}
-			return nil
+			return b.createNFSmountIngress(securityGroups, project, n, template)
 		})
 		if err != nil {
 			return nil, err
 		}
 	}
 
-	err = b.createCapacityProvider(ctx, project, networks, template)
+	err = b.createCapacityProvider(ctx, project, template, resources)
 	if err != nil {
 		return nil, err
 	}
@@ -103,103 +73,31 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]
 }
 
 // Convert a compose project into a CloudFormation template
-func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, map[string]string, error) { //nolint:gocyclo
-	var checker compatibility.Checker = &fargateCompatibilityChecker{
-		compatibility.AllowList{
-			Supported: compatibleComposeAttributes,
-		},
-	}
-	compatibility.Check(project, checker)
-	for _, err := range checker.Errors() {
-		if errdefs.IsIncompatibleError(err) {
-			logrus.Error(err.Error())
-		} else {
-			logrus.Warn(err.Error())
-		}
-	}
-	if !compatibility.IsCompatible(checker) {
-		return nil, nil, fmt.Errorf("compose file is incompatible with Amazon ECS")
-	}
-
+func (b *ecsAPIService) convert(project *types.Project, resources awsResources) (*cloudformation.Template, error) {
 	template := cloudformation.NewTemplate()
-	template.Description = "CloudFormation template created by Docker for deploying applications on Amazon ECS"
-	template.Parameters[parameterClusterName] = cloudformation.Parameter{
-		Type:        "String",
-		Description: "Name of the ECS cluster to deploy to (optional)",
-	}
-
-	template.Parameters[parameterVPCId] = cloudformation.Parameter{
-		Type:        "AWS::EC2::VPC::Id",
-		Description: "ID of the VPC",
-	}
-
-	/*
-		FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282
-		template.Parameters["SubnetIds"] = cloudformation.Parameter{
-			Type:        "List<AWS::EC2::Subnet::Id>",
-			Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC",
-		}
-	*/
-	template.Parameters[parameterSubnet1Id] = cloudformation.Parameter{
-		Type:        "AWS::EC2::Subnet::Id",
-		Description: "SubnetId, for Availability Zone 1 in the region in your VPC",
-	}
-	template.Parameters[parameterSubnet2Id] = cloudformation.Parameter{
-		Type:        "AWS::EC2::Subnet::Id",
-		Description: "SubnetId, for Availability Zone 2 in the region in your VPC",
-	}
-
-	template.Parameters[parameterLoadBalancerARN] = cloudformation.Parameter{
-		Type:        "String",
-		Description: "Name of the LoadBalancer to connect to (optional)",
-	}
-
-	template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(parameterClusterName))
-
-	cluster := createCluster(project, template)
-
-	networks := map[string]string{}
-	for _, net := range project.Networks {
-		networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(parameterVPCId), template)
-	}
+	b.ensureResources(&resources, project, template)
 
-	for i, s := range project.Secrets {
-		if s.External.External {
-			continue
-		}
-		secret, err := ioutil.ReadFile(s.File)
+	for name, secret := range project.Secrets {
+		err := b.createSecret(project, name, secret, template)
 		if err != nil {
-			return nil, nil, err
-		}
-
-		name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name))
-		template.Resources[name] = &secretsmanager.Secret{
-			Description:  "",
-			SecretString: string(secret),
-			Tags:         projectTags(project),
+			return nil, err
 		}
-		s.Name = cloudformation.Ref(name)
-		project.Secrets[i] = s
 	}
 
-	createLogGroup(project, template)
+	b.createLogGroup(project, template)
 
 	// Private DNS namespace will allow DNS name for the services to be <service>.<project>.local
-	createCloudMap(project, template)
-
-	loadBalancerARN := createLoadBalancer(project, template)
+	b.createCloudMap(project, template, resources.vpc)
 
 	for _, service := range project.Services {
+		taskExecutionRole := b.createTaskExecutionRole(project, service, template)
+		taskRole := b.createTaskRole(service, template)
 
-		definition, err := convert(project, service)
+		definition, err := b.createTaskExecution(project, service)
 		if err != nil {
-			return nil, nil, err
+			return nil, err
 		}
-
-		taskExecutionRole := createTaskExecutionRole(service, definition, template)
 		definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole)
-
-		taskRole := createTaskRole(service, template)
 		if taskRole != "" {
 			definition.TaskRoleArn = cloudformation.Ref(taskRole)
 		}
@@ -208,34 +106,30 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
 		template.Resources[taskDefinition] = definition
 
 		var healthCheck *cloudmap.Service_HealthCheckConfig
+		serviceRegistry := b.createServiceRegistry(service, template, healthCheck)
 
-		serviceRegistry := createServiceRegistry(service, template, healthCheck)
-
-		serviceSecurityGroups := []string{}
-		for net := range service.Networks {
-			serviceSecurityGroups = append(serviceSecurityGroups, networks[net])
-		}
+		var (
+			dependsOn []string
+			serviceLB []ecs.Service_LoadBalancer
+		)
+		for _, port := range service.Ports {
+			for net := range service.Networks {
+				b.createIngress(service, net, port, template, resources)
+			}
 
-		dependsOn := []string{}
-		serviceLB := []ecs.Service_LoadBalancer{}
-		if len(service.Ports) > 0 {
-			for _, port := range service.Ports {
-				protocol := strings.ToUpper(port.Protocol)
-				if getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication {
-					// we don't set Https as a certificate must be specified for HTTPS listeners
-					protocol = elbv2.ProtocolEnumHttp
-				}
-				if loadBalancerARN != "" {
-					targetGroupName := createTargetGroup(project, service, port, template, protocol)
-					listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol)
-					dependsOn = append(dependsOn, listenerName)
-					serviceLB = append(serviceLB, ecs.Service_LoadBalancer{
-						ContainerName:  service.Name,
-						ContainerPort:  int(port.Target),
-						TargetGroupArn: cloudformation.Ref(targetGroupName),
-					})
-				}
+			protocol := strings.ToUpper(port.Protocol)
+			if resources.loadBalancerType == elbv2.LoadBalancerTypeEnumApplication {
+				// we don't set Https as a certificate must be specified for HTTPS listeners
+				protocol = elbv2.ProtocolEnumHttp
 			}
+			targetGroupName := b.createTargetGroup(project, service, port, template, protocol, resources.vpc)
+			listenerName := b.createListener(service, port, template, targetGroupName, resources.loadBalancer, protocol)
+			dependsOn = append(dependsOn, listenerName)
+			serviceLB = append(serviceLB, ecs.Service_LoadBalancer{
+				ContainerName:  service.Name,
+				ContainerPort:  int(port.Target),
+				TargetGroupArn: cloudformation.Ref(targetGroupName),
+			})
 		}
 
 		desiredCount := 1
@@ -249,7 +143,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
 
 		minPercent, maxPercent, err := computeRollingUpdateLimits(service)
 		if err != nil {
-			return nil, nil, err
+			return nil, err
 		}
 
 		assignPublicIP := ecsapi.AssignPublicIpEnabled
@@ -263,7 +157,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
 
 		template.Resources[serviceResourceName(service.Name)] = &ecs.Service{
 			AWSCloudFormationDependsOn: dependsOn,
-			Cluster:                    cluster,
+			Cluster:                    resources.cluster,
 			DesiredCount:               desiredCount,
 			DeploymentController: &ecs.Service_DeploymentController{
 				Type: ecsapi.DeploymentControllerTypeEcs,
@@ -278,11 +172,8 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
 			NetworkConfiguration: &ecs.Service_NetworkConfiguration{
 				AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
 					AssignPublicIp: assignPublicIP,
-					SecurityGroups: serviceSecurityGroups,
-					Subnets: []string{
-						cloudformation.Ref(parameterSubnet1Id),
-						cloudformation.Ref(parameterSubnet2Id),
-					},
+					SecurityGroups: resources.serviceSecurityGroups(service),
+					Subnets:        resources.subnets,
 				},
 			},
 			PlatformVersion:    platformVersion,
@@ -293,10 +184,48 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
 			TaskDefinition:     cloudformation.Ref(normalizeResourceName(taskDefinition)),
 		}
 	}
-	return template, networks, nil
+	return template, nil
+}
+
+const allProtocols = "-1"
+
+func (b *ecsAPIService) createIngress(service types.ServiceConfig, net string, port types.ServicePortConfig, template *cloudformation.Template, resources awsResources) {
+	protocol := strings.ToUpper(port.Protocol)
+	if protocol == "" {
+		protocol = allProtocols
+	}
+	ingress := fmt.Sprintf("%s%dIngress", normalizeResourceName(net), port.Target)
+	template.Resources[ingress] = &ec2.SecurityGroupIngress{
+		CidrIp:      "0.0.0.0/0",
+		Description: fmt.Sprintf("%s:%d/%s on %s nextwork", service.Name, port.Target, port.Protocol, net),
+		GroupId:     resources.securityGroups[net],
+		FromPort:    int(port.Target),
+		IpProtocol:  protocol,
+		ToPort:      int(port.Target),
+	}
 }
 
-func createLogGroup(project *types.Project, template *cloudformation.Template) {
+func (b *ecsAPIService) createSecret(project *types.Project, name string, s types.SecretConfig, template *cloudformation.Template) error {
+	if s.External.External {
+		return nil
+	}
+	sensitiveData, err := ioutil.ReadFile(s.File)
+	if err != nil {
+		return err
+	}
+
+	resource := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name))
+	template.Resources[resource] = &secretsmanager.Secret{
+		Description:  fmt.Sprintf("Secret %s", s.Name),
+		SecretString: string(sensitiveData),
+		Tags:         projectTags(project),
+	}
+	s.Name = cloudformation.Ref(resource)
+	project.Secrets[name] = s
+	return nil
+}
+
+func (b *ecsAPIService) createLogGroup(project *types.Project, template *cloudformation.Template) {
 	retention := 0
 	if v, ok := project.Extensions[extensionRetention]; ok {
 		retention = v.(int)
@@ -348,74 +277,9 @@ func computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) {
 	return minPercent, maxPercent, nil
 }
 
-func getLoadBalancerType(project *types.Project) string {
-	for _, service := range project.Services {
-		for _, port := range service.Ports {
-			protocol := port.Protocol
-			v, ok := port.Extensions[extensionProtocol]
-			if ok {
-				protocol = v.(string)
-			}
-			if protocol == "http" || protocol == "https" {
-				continue
-			}
-			if port.Published != 80 && port.Published != 443 {
-				return elbv2.LoadBalancerTypeEnumNetwork
-			}
-		}
-	}
-	return elbv2.LoadBalancerTypeEnumApplication
-}
-
-func getLoadBalancerSecurityGroups(project *types.Project, template *cloudformation.Template) []string {
-	securityGroups := []string{}
-	for _, network := range project.Networks {
-		if !network.Internal {
-			net := convertNetwork(project, network, cloudformation.Ref(parameterVPCId), template)
-			securityGroups = append(securityGroups, net)
-		}
-	}
-	return uniqueStrings(securityGroups)
-}
-
-func createLoadBalancer(project *types.Project, template *cloudformation.Template) string {
-	ports := 0
-	for _, service := range project.Services {
-		ports += len(service.Ports)
-	}
-	if ports == 0 {
-		// Project do not expose any port (batch jobs?)
-		// So no need to create a PortPublisher
-		return ""
-	}
-
-	// load balancer names are limited to 32 characters total
-	loadBalancerName := fmt.Sprintf("%.32s", fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)))
-	// Create PortPublisher if `ParameterLoadBalancerName` is not set
-	template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(parameterLoadBalancerARN))
-
-	loadBalancerType := getLoadBalancerType(project)
-	securityGroups := []string{}
-	if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication {
-		securityGroups = getLoadBalancerSecurityGroups(project, template)
-	}
-
-	template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{
-		Name:           loadBalancerName,
-		Scheme:         elbv2.LoadBalancerSchemeEnumInternetFacing,
-		SecurityGroups: securityGroups,
-		Subnets: []string{
-			cloudformation.Ref(parameterSubnet1Id),
-			cloudformation.Ref(parameterSubnet2Id),
-		},
-		Tags:                       projectTags(project),
-		Type:                       loadBalancerType,
-		AWSCloudFormationCondition: "CreateLoadBalancer",
-	}
-	return cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(parameterLoadBalancerARN))
-}
-
-func createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string {
+func (b *ecsAPIService) createListener(service types.ServiceConfig, port types.ServicePortConfig,
+	template *cloudformation.Template,
+	targetGroupName string, loadBalancerARN string, protocol string) string {
 	listenerName := fmt.Sprintf(
 		"%s%s%dListener",
 		normalizeResourceName(service.Name),
@@ -444,7 +308,7 @@ func createListener(service types.ServiceConfig, port types.ServicePortConfig, t
 	return listenerName
 }
 
-func createTargetGroup(project *types.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string {
+func (b *ecsAPIService) createTargetGroup(project *types.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string, vpc string) string {
 	targetGroupName := fmt.Sprintf(
 		"%s%s%dTargetGroup",
 		normalizeResourceName(service.Name),
@@ -457,12 +321,12 @@ func createTargetGroup(project *types.Project, service types.ServiceConfig, port
 		Protocol:           protocol,
 		Tags:               projectTags(project),
 		TargetType:         elbv2.TargetTypeEnumIp,
-		VpcId:              cloudformation.Ref(parameterVPCId),
+		VpcId:              vpc,
 	}
 	return targetGroupName
 }
 
-func createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry {
+func (b *ecsAPIService) createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry {
 	serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name))
 	serviceRegistry := ecs.Service_ServiceRegistry{
 		RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"),
@@ -489,9 +353,9 @@ func createServiceRegistry(service types.ServiceConfig, template *cloudformation
 	return serviceRegistry
 }
 
-func createTaskExecutionRole(service types.ServiceConfig, definition *ecs.TaskDefinition, template *cloudformation.Template) string {
+func (b *ecsAPIService) createTaskExecutionRole(project *types.Project, service types.ServiceConfig, template *cloudformation.Template) string {
 	taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name))
-	policies := createPolicies(service, definition)
+	policies := b.createPolicies(project, service)
 	template.Resources[taskExecutionRole] = &iam.Role{
 		AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument,
 		Policies:                 policies,
@@ -503,7 +367,7 @@ func createTaskExecutionRole(service types.ServiceConfig, definition *ecs.TaskDe
 	return taskExecutionRole
 }
 
-func createTaskRole(service types.ServiceConfig, template *cloudformation.Template) string {
+func (b *ecsAPIService) createTaskRole(service types.ServiceConfig, template *cloudformation.Template) string {
 	taskRole := fmt.Sprintf("%sTaskRole", normalizeResourceName(service.Name))
 	rolePolicies := []iam.Role_Policy{}
 	if roles, ok := service.Extensions[extensionRole]; ok {
@@ -528,99 +392,21 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa
 	return taskRole
 }
 
-func createCluster(project *types.Project, template *cloudformation.Template) string {
-	template.Resources["Cluster"] = &ecs.Cluster{
-		ClusterName:                project.Name,
-		Tags:                       projectTags(project),
-		AWSCloudFormationCondition: "CreateCluster",
-	}
-	cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(parameterClusterName))
-	return cluster
-}
-
-func createCloudMap(project *types.Project, template *cloudformation.Template) {
+func (b *ecsAPIService) createCloudMap(project *types.Project, template *cloudformation.Template, vpc string) {
 	template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{
 		Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name),
 		Name:        fmt.Sprintf("%s.local", project.Name),
-		Vpc:         cloudformation.Ref(parameterVPCId),
+		Vpc:         vpc,
 	}
 }
 
-func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string {
-	if net.External.External {
-		return net.Name
-	}
-	if sg, ok := net.Extensions[extensionSecurityGroup]; ok {
-		logrus.Warn("to use an existing security-group, set `network.external` and `network.name` in your compose file")
-		logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg)
-		return sg.(string)
-	}
-
-	var ingresses []ec2.SecurityGroup_Ingress
-	if !net.Internal {
-		for _, service := range project.Services {
-			if _, ok := service.Networks[net.Name]; ok {
-				for _, port := range service.Ports {
-					protocol := strings.ToUpper(port.Protocol)
-					if protocol == "" {
-						protocol = "-1"
-					}
-					ingresses = append(ingresses, ec2.SecurityGroup_Ingress{
-						CidrIp:      "0.0.0.0/0",
-						Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol),
-						FromPort:    int(port.Target),
-						IpProtocol:  protocol,
-						ToPort:      int(port.Target),
-					})
-				}
-			}
-		}
-	}
-
-	securityGroup := networkResourceName(project, net.Name)
-	template.Resources[securityGroup] = &ec2.SecurityGroup{
-		GroupDescription:     fmt.Sprintf("%s %s Security Group", project.Name, net.Name),
-		GroupName:            securityGroup,
-		SecurityGroupIngress: ingresses,
-		VpcId:                vpc,
-		Tags:                 networkTags(project, net),
+func (b *ecsAPIService) createPolicies(project *types.Project, service types.ServiceConfig) []iam.Role_Policy {
+	var arns []string
+	if value, ok := service.Extensions[extensionPullCredentials]; ok {
+		arns = append(arns, value.(string))
 	}
-
-	ingress := securityGroup + "Ingress"
-	template.Resources[ingress] = &ec2.SecurityGroupIngress{
-		Description:           fmt.Sprintf("Allow communication within network %s", net.Name),
-		IpProtocol:            "-1", // all protocols
-		GroupId:               cloudformation.Ref(securityGroup),
-		SourceSecurityGroupId: cloudformation.Ref(securityGroup),
-	}
-
-	return cloudformation.Ref(securityGroup)
-}
-
-func networkResourceName(project *types.Project, network string) string {
-	return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network))
-}
-
-func serviceResourceName(service string) string {
-	return fmt.Sprintf("%sService", normalizeResourceName(service))
-}
-
-func normalizeResourceName(s string) string {
-	return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, ""))
-}
-
-func createPolicies(service types.ServiceConfig, taskDef *ecs.TaskDefinition) []iam.Role_Policy {
-	arns := []string{}
-	for _, container := range taskDef.ContainerDefinitions {
-		if container.RepositoryCredentials != nil {
-			arns = append(arns, container.RepositoryCredentials.CredentialsParameter)
-		}
-		if len(container.Secrets) > 0 {
-			for _, s := range container.Secrets {
-				arns = append(arns, s.ValueFrom)
-			}
-		}
-
+	for _, secret := range service.Secrets {
+		arns = append(arns, project.Secrets[secret.Source].Name)
 	}
 	if len(arns) > 0 {
 		return []iam.Role_Policy{
@@ -641,14 +427,14 @@ func createPolicies(service types.ServiceConfig, taskDef *ecs.TaskDefinition) []
 	return nil
 }
 
-func uniqueStrings(items []string) []string {
-	keys := make(map[string]bool)
-	unique := []string{}
-	for _, item := range items {
-		if _, val := keys[item]; !val {
-			keys[item] = true
-			unique = append(unique, item)
-		}
-	}
-	return unique
+func networkResourceName(network string) string {
+	return fmt.Sprintf("%sNetwork", normalizeResourceName(network))
+}
+
+func serviceResourceName(service string) string {
+	return fmt.Sprintf("%sService", normalizeResourceName(service))
+}
+
+func normalizeResourceName(s string) string {
+	return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, ""))
 }

+ 13 - 17
ecs/cloudformation_test.go

@@ -170,13 +170,13 @@ networks:
   back-tier:
     internal: true
 `)
-	assert.Check(t, template.Resources["TestPublicNetwork"] != nil)
-	assert.Check(t, template.Resources["TestBacktierNetwork"] != nil)
-	assert.Check(t, template.Resources["TestBacktierNetworkIngress"] != nil)
-	i := template.Resources["TestPublicNetworkIngress"]
+	assert.Check(t, template.Resources["FronttierNetwork"] != nil)
+	assert.Check(t, template.Resources["BacktierNetwork"] != nil)
+	assert.Check(t, template.Resources["BacktierNetworkIngress"] != nil)
+	i := template.Resources["FronttierNetworkIngress"]
 	assert.Check(t, i != nil)
 	ingress := *i.(*ec2.SecurityGroupIngress)
-	assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("TestPublicNetwork"))
+	assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("FronttierNetwork"))
 
 }
 
@@ -187,13 +187,6 @@ func TestLoadBalancerTypeApplication(t *testing.T) {
     image: nginx
     ports:
       - 80:80
-`,
-		`services:
-  test:
-    image: nginx
-    ports:
-      - target: 8080
-        protocol: http
 `,
 		`services:
   test:
@@ -205,7 +198,7 @@ func TestLoadBalancerTypeApplication(t *testing.T) {
 	}
 	for _, y := range cases {
 		template := convertYaml(t, y)
-		lb := template.Resources["TestLoadBalancer"]
+		lb := template.Resources["LoadBalancer"]
 		assert.Check(t, lb != nil)
 		loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
 		assert.Check(t, len(loadBalancer.Name) <= 32)
@@ -328,7 +321,7 @@ services:
           memory: 2043248M
 `)
 	backend := &ecsAPIService{}
-	_, _, err := backend.convert(model)
+	_, err := backend.convert(model, awsResources{})
 	assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate")
 }
 
@@ -341,7 +334,7 @@ services:
       - 80:80
       - 88:88
 `)
-	lb := template.Resources["TestLoadBalancer"]
+	lb := template.Resources["LoadBalancer"]
 	assert.Check(t, lb != nil)
 	loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
 	assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork)
@@ -412,7 +405,10 @@ services:
 
 func convertResultAsString(t *testing.T, project *types.Project) string {
 	backend := &ecsAPIService{}
-	template, _, err := backend.convert(project)
+	template, err := backend.convert(project, awsResources{
+		vpc:     "vpcID",
+		subnets: []string{"subnet1", "subnet2"},
+	})
 	assert.NilError(t, err)
 	resultAsJSON, err := marshall(template)
 	assert.NilError(t, err)
@@ -432,7 +428,7 @@ func load(t *testing.T, paths ...string) *types.Project {
 func convertYaml(t *testing.T, yaml string) *cloudformation.Template {
 	project := loadConfig(t, yaml)
 	backend := &ecsAPIService{}
-	template, _, err := backend.convert(project)
+	template, err := backend.convert(project, awsResources{})
 	assert.NilError(t, err)
 	return template
 }

+ 23 - 0
ecs/compatibility.go

@@ -17,10 +17,33 @@
 package ecs
 
 import (
+	"fmt"
+
 	"github.com/compose-spec/compose-go/compatibility"
+	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/types"
+	"github.com/sirupsen/logrus"
 )
 
+func (b *ecsAPIService) checkCompatibility(project *types.Project) error {
+	var checker compatibility.Checker = &fargateCompatibilityChecker{
+		compatibility.AllowList{
+			Supported: compatibleComposeAttributes,
+		},
+	}
+	compatibility.Check(project, checker)
+	for _, err := range checker.Errors() {
+		if errdefs.IsIncompatibleError(err) {
+			return err
+		}
+		logrus.Warn(err.Error())
+	}
+	if !compatibility.IsCompatible(checker) {
+		return fmt.Errorf("compose file is incompatible with Amazon ECS")
+	}
+	return nil
+}
+
 type fargateCompatibilityChecker struct {
 	compatibility.AllowList
 }

+ 4 - 6
ecs/convert.go

@@ -27,6 +27,7 @@ import (
 	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
+
 	ecsapi "github.com/aws/aws-sdk-go/service/ecs"
 	"github.com/awslabs/goformation/v4/cloudformation"
 	"github.com/awslabs/goformation/v4/cloudformation/ecs"
@@ -39,7 +40,7 @@ import (
 
 const secretsInitContainerImage = "docker/ecs-secrets-sidecar"
 
-func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) {
+func (b *ecsAPIService) createTaskExecution(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) {
 	cpu, mem, err := toLimits(service)
 	if err != nil {
 		return nil, err
@@ -532,11 +533,8 @@ func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry {
 }
 
 func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials {
-	// extract registry and namespace string from image name
-	for key, value := range service.Extensions {
-		if key == extensionPullCredentials {
-			return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)}
-		}
+	if value, ok := service.Extensions[extensionPullCredentials]; ok {
+		return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)}
 	}
 	return nil
 }

+ 10 - 23
ecs/ec2.go

@@ -28,7 +28,7 @@ import (
 	"github.com/compose-spec/compose-go/types"
 )
 
-func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *types.Project, networks map[string]string, template *cloudformation.Template) error {
+func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *types.Project, template *cloudformation.Template, resources awsResources) error {
 	var ec2 bool
 	for _, s := range project.Services {
 		if requireEC2(s) {
@@ -51,11 +51,6 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
 		return err
 	}
 
-	var securityGroups []string
-	for _, r := range networks {
-		securityGroups = append(securityGroups, r)
-	}
-
 	template.Resources["CapacityProvider"] = &ecs.CapacityProvider{
 		AutoScalingGroupProvider: &ecs.CapacityProvider_AutoScalingGroupProvider{
 			AutoScalingGroupArn: cloudformation.Ref("AutoscalingGroup"),
@@ -63,36 +58,29 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
 				TargetCapacity: 100,
 			},
 		},
-		Tags:                       projectTags(project),
-		AWSCloudFormationCondition: "CreateCluster",
+		Tags: projectTags(project),
 	}
 
 	template.Resources["AutoscalingGroup"] = &autoscaling.AutoScalingGroup{
 		LaunchConfigurationName: cloudformation.Ref("LaunchConfiguration"),
 		MaxSize:                 "10", //TODO
 		MinSize:                 "1",
-		VPCZoneIdentifier: []string{
-			cloudformation.Ref(parameterSubnet1Id),
-			cloudformation.Ref(parameterSubnet2Id),
-		},
-		AWSCloudFormationCondition: "CreateCluster",
+		VPCZoneIdentifier:       resources.subnets,
 	}
 
 	userData := base64.StdEncoding.EncodeToString([]byte(
 		fmt.Sprintf("#!/bin/bash\necho ECS_CLUSTER=%s >> /etc/ecs/ecs.config", project.Name)))
 
 	template.Resources["LaunchConfiguration"] = &autoscaling.LaunchConfiguration{
-		ImageId:                    ami,
-		InstanceType:               machineType,
-		SecurityGroups:             securityGroups,
-		IamInstanceProfile:         cloudformation.Ref("EC2InstanceProfile"),
-		UserData:                   userData,
-		AWSCloudFormationCondition: "CreateCluster",
+		ImageId:            ami,
+		InstanceType:       machineType,
+		SecurityGroups:     resources.allSecurityGroups(),
+		IamInstanceProfile: cloudformation.Ref("EC2InstanceProfile"),
+		UserData:           userData,
 	}
 
 	template.Resources["EC2InstanceProfile"] = &iam.InstanceProfile{
-		Roles:                      []string{cloudformation.Ref("EC2InstanceRole")},
-		AWSCloudFormationCondition: "CreateCluster",
+		Roles: []string{cloudformation.Ref("EC2InstanceRole")},
 	}
 
 	template.Resources["EC2InstanceRole"] = &iam.Role{
@@ -100,8 +88,7 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
 		ManagedPolicyArns: []string{
 			ecsEC2InstanceRole,
 		},
-		Tags:                       projectTags(project),
-		AWSCloudFormationCondition: "CreateCluster",
+		Tags: projectTags(project),
 	}
 
 	cluster := template.Resources["Cluster"].(*ecs.Cluster)

+ 5 - 7
ecs/ps.go

@@ -25,18 +25,15 @@ import (
 )
 
 func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
-	parameters, err := b.SDK.ListStackParameters(ctx, project)
-	if err != nil {
-		return nil, err
-	}
-	cluster := parameters[parameterClusterName]
-
 	resources, err := b.SDK.ListStackResources(ctx, project)
 	if err != nil {
 		return nil, err
 	}
 
-	servicesARN := []string{}
+	var (
+		cluster     = project
+		servicesARN []string
+	)
 	for _, r := range resources {
 		switch r.Type {
 		case "AWS::ECS::Service":
@@ -45,6 +42,7 @@ func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.Servi
 			cluster = r.ARN
 		}
 	}
+
 	if len(servicesARN) == 0 {
 		return nil, nil
 	}

+ 19 - 24
ecs/sdk.go

@@ -196,22 +196,13 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
 	return len(stacks.Stacks) > 0, nil
 }
 
-func (s sdk) CreateStack(ctx context.Context, name string, template []byte, parameters map[string]string) error {
+func (s sdk) CreateStack(ctx context.Context, name string, template []byte) error {
 	logrus.Debug("Create CloudFormation stack")
 
-	param := []*cloudformation.Parameter{}
-	for name, value := range parameters {
-		param = append(param, &cloudformation.Parameter{
-			ParameterKey:   aws.String(name),
-			ParameterValue: aws.String(value),
-		})
-	}
-
 	_, err := s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
 		OnFailure:        aws.String("DELETE"),
 		StackName:        aws.String(name),
 		TemplateBody:     aws.String(string(template)),
-		Parameters:       param,
 		TimeoutInMinutes: nil,
 		Capabilities: []*string{
 			aws.String(cloudformation.CapabilityCapabilityIam),
@@ -226,24 +217,15 @@ func (s sdk) CreateStack(ctx context.Context, name string, template []byte, para
 	return err
 }
 
-func (s sdk) CreateChangeSet(ctx context.Context, name string, template []byte, parameters map[string]string) (string, error) {
+func (s sdk) CreateChangeSet(ctx context.Context, name string, template []byte) (string, error) {
 	logrus.Debug("Create CloudFormation Changeset")
 
-	param := []*cloudformation.Parameter{}
-	for name := range parameters {
-		param = append(param, &cloudformation.Parameter{
-			ParameterKey:     aws.String(name),
-			UsePreviousValue: aws.Bool(true),
-		})
-	}
-
 	update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05"))
 	changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{
 		ChangeSetName: aws.String(update),
 		ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
 		StackName:     aws.String(name),
 		TemplateBody:  aws.String(string(template)),
-		Parameters:    param,
 		Capabilities: []*string{
 			aws.String(cloudformation.CapabilityCapabilityIam),
 		},
@@ -647,15 +629,18 @@ func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string
 	return publicIPs, nil
 }
 
-func (s sdk) LoadBalancerExists(ctx context.Context, arn string) (bool, error) {
-	logrus.Debug("CheckRequirements if PortPublisher exists: ", arn)
+func (s sdk) LoadBalancerType(ctx context.Context, arn string) (string, error) {
+	logrus.Debug("Check if LoadBalancer exists: ", arn)
 	lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
 		LoadBalancerArns: []*string{aws.String(arn)},
 	})
 	if err != nil {
-		return false, err
+		return "", err
 	}
-	return len(lbs.LoadBalancers) > 0, nil
+	if len(lbs.LoadBalancers) == 0 {
+		return "", fmt.Errorf("load balancer does not exist: %s", arn)
+	}
+	return aws.StringValue(lbs.LoadBalancers[0].Type), nil
 }
 
 func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
@@ -719,3 +704,13 @@ func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
 
 	return ami.ImageID, nil
 }
+
+func (s sdk) SecurityGroupExists(ctx context.Context, sg string) (bool, error) {
+	desc, err := s.EC2.DescribeSecurityGroupsWithContext(ctx, &ec2.DescribeSecurityGroupsInput{
+		GroupIds: aws.StringSlice([]string{sg}),
+	})
+	if err != nil {
+		return false, err
+	}
+	return len(desc.SecurityGroups) > 0, nil
+}

+ 73 - 143
ecs/testdata/simple/simple-cloudformation-conversion.golden

@@ -1,59 +1,15 @@
 {
   "AWSTemplateFormatVersion": "2010-09-09",
-  "Conditions": {
-    "CreateCluster": {
-      "Fn::Equals": [
-        "",
-        {
-          "Ref": "ParameterClusterName"
-        }
-      ]
-    },
-    "CreateLoadBalancer": {
-      "Fn::Equals": [
-        "",
-        {
-          "Ref": "ParameterLoadBalancerARN"
-        }
-      ]
-    }
-  },
-  "Description": "CloudFormation template created by Docker for deploying applications on Amazon ECS",
-  "Parameters": {
-    "ParameterClusterName": {
-      "Description": "Name of the ECS cluster to deploy to (optional)",
-      "Type": "String"
-    },
-    "ParameterLoadBalancerARN": {
-      "Description": "Name of the LoadBalancer to connect to (optional)",
-      "Type": "String"
-    },
-    "ParameterSubnet1Id": {
-      "Description": "SubnetId, for Availability Zone 1 in the region in your VPC",
-      "Type": "AWS::EC2::Subnet::Id"
-    },
-    "ParameterSubnet2Id": {
-      "Description": "SubnetId, for Availability Zone 2 in the region in your VPC",
-      "Type": "AWS::EC2::Subnet::Id"
-    },
-    "ParameterVPCId": {
-      "Description": "ID of the VPC",
-      "Type": "AWS::EC2::VPC::Id"
-    }
-  },
   "Resources": {
     "CloudMap": {
       "Properties": {
         "Description": "Service Map for Docker Compose project TestSimpleConvert",
         "Name": "TestSimpleConvert.local",
-        "Vpc": {
-          "Ref": "ParameterVPCId"
-        }
+        "Vpc": "vpcID"
       },
       "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace"
     },
     "Cluster": {
-      "Condition": "CreateCluster",
       "Properties": {
         "ClusterName": "TestSimpleConvert",
         "Tags": [
@@ -65,6 +21,72 @@
       },
       "Type": "AWS::ECS::Cluster"
     },
+    "Default80Ingress": {
+      "Properties": {
+        "CidrIp": "0.0.0.0/0",
+        "Description": "simple:80/tcp on default nextwork",
+        "FromPort": 80,
+        "GroupId": {
+          "Ref": "DefaultNetwork"
+        },
+        "IpProtocol": "TCP",
+        "ToPort": 80
+      },
+      "Type": "AWS::EC2::SecurityGroupIngress"
+    },
+    "DefaultNetwork": {
+      "Properties": {
+        "GroupDescription": "TestSimpleConvert Security Group for default network",
+        "GroupName": "DefaultNetwork",
+        "Tags": [
+          {
+            "Key": "com.docker.compose.project",
+            "Value": "TestSimpleConvert"
+          },
+          {
+            "Key": "com.docker.compose.network",
+            "Value": "default"
+          }
+        ],
+        "VpcId": "vpcID"
+      },
+      "Type": "AWS::EC2::SecurityGroup"
+    },
+    "DefaultNetworkIngress": {
+      "Properties": {
+        "Description": "Allow communication within network default",
+        "GroupId": {
+          "Ref": "DefaultNetwork"
+        },
+        "IpProtocol": "-1",
+        "SourceSecurityGroupId": {
+          "Ref": "DefaultNetwork"
+        }
+      },
+      "Type": "AWS::EC2::SecurityGroupIngress"
+    },
+    "LoadBalancer": {
+      "Properties": {
+        "Scheme": "internet-facing",
+        "SecurityGroups": [
+          {
+            "Ref": "DefaultNetwork"
+          }
+        ],
+        "Subnets": [
+          "subnet1",
+          "subnet2"
+        ],
+        "Tags": [
+          {
+            "Key": "com.docker.compose.project",
+            "Value": "TestSimpleConvert"
+          }
+        ],
+        "Type": "application"
+      },
+      "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer"
+    },
     "LogGroup": {
       "Properties": {
         "LogGroupName": "/docker-compose/TestSimpleConvert"
@@ -77,15 +99,7 @@
       ],
       "Properties": {
         "Cluster": {
-          "Fn::If": [
-            "CreateCluster",
-            {
-              "Ref": "Cluster"
-            },
-            {
-              "Ref": "ParameterClusterName"
-            }
-          ]
+          "Ref": "Cluster"
         },
         "DeploymentConfiguration": {
           "MaximumPercent": 200,
@@ -110,16 +124,12 @@
             "AssignPublicIp": "ENABLED",
             "SecurityGroups": [
               {
-                "Ref": "TestSimpleConvertDefaultNetwork"
+                "Ref": "DefaultNetwork"
               }
             ],
             "Subnets": [
-              {
-                "Ref": "ParameterSubnet1Id"
-              },
-              {
-                "Ref": "ParameterSubnet2Id"
-              }
+              "subnet1",
+              "subnet2"
             ]
           }
         },
@@ -191,15 +201,7 @@
           }
         ],
         "LoadBalancerArn": {
-          "Fn::If": [
-            "CreateLoadBalancer",
-            {
-              "Ref": "TestSimpleConvertLoadBalancer"
-            },
-            {
-              "Ref": "ParameterLoadBalancerARN"
-            }
-          ]
+          "Ref": "LoadBalancer"
         },
         "Port": 80,
         "Protocol": "HTTP"
@@ -217,9 +219,7 @@
           }
         ],
         "TargetType": "ip",
-        "VpcId": {
-          "Ref": "ParameterVPCId"
-        }
+        "VpcId": "vpcID"
       },
       "Type": "AWS::ElasticLoadBalancingV2::TargetGroup"
     },
@@ -304,76 +304,6 @@
         ]
       },
       "Type": "AWS::IAM::Role"
-    },
-    "TestSimpleConvertDefaultNetwork": {
-      "Properties": {
-        "GroupDescription": "TestSimpleConvert default Security Group",
-        "GroupName": "TestSimpleConvertDefaultNetwork",
-        "SecurityGroupIngress": [
-          {
-            "CidrIp": "0.0.0.0/0",
-            "Description": "simple:80/tcp",
-            "FromPort": 80,
-            "IpProtocol": "TCP",
-            "ToPort": 80
-          }
-        ],
-        "Tags": [
-          {
-            "Key": "com.docker.compose.project",
-            "Value": "TestSimpleConvert"
-          },
-          {
-            "Key": "com.docker.compose.network",
-            "Value": "default"
-          }
-        ],
-        "VpcId": {
-          "Ref": "ParameterVPCId"
-        }
-      },
-      "Type": "AWS::EC2::SecurityGroup"
-    },
-    "TestSimpleConvertDefaultNetworkIngress": {
-      "Properties": {
-        "Description": "Allow communication within network default",
-        "GroupId": {
-          "Ref": "TestSimpleConvertDefaultNetwork"
-        },
-        "IpProtocol": "-1",
-        "SourceSecurityGroupId": {
-          "Ref": "TestSimpleConvertDefaultNetwork"
-        }
-      },
-      "Type": "AWS::EC2::SecurityGroupIngress"
-    },
-    "TestSimpleConvertLoadBalancer": {
-      "Condition": "CreateLoadBalancer",
-      "Properties": {
-        "Name": "TestSimpleConvertLoadBalancer",
-        "Scheme": "internet-facing",
-        "SecurityGroups": [
-          {
-            "Ref": "TestSimpleConvertDefaultNetwork"
-          }
-        ],
-        "Subnets": [
-          {
-            "Ref": "ParameterSubnet1Id"
-          },
-          {
-            "Ref": "ParameterSubnet2Id"
-          }
-        ],
-        "Tags": [
-          {
-            "Key": "com.docker.compose.project",
-            "Value": "TestSimpleConvert"
-          }
-        ],
-        "Type": "application"
-      },
-      "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer"
     }
   }
 }

+ 2 - 85
ecs/up.go

@@ -32,42 +32,11 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error {
 		return err
 	}
 
-	cluster, err := b.GetCluster(ctx, project)
-	if err != nil {
-		return err
-	}
-
 	template, err := b.Convert(ctx, project)
 	if err != nil {
 		return err
 	}
 
-	vpc, err := b.GetVPC(ctx, project)
-	if err != nil {
-		return err
-	}
-
-	subNets, err := b.SDK.GetSubNets(ctx, vpc)
-	if err != nil {
-		return err
-	}
-	if len(subNets) < 2 {
-		return fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc)
-	}
-
-	lb, err := b.GetLoadBalancer(ctx, project)
-	if err != nil {
-		return err
-	}
-
-	parameters := map[string]string{
-		parameterClusterName:     cluster,
-		parameterVPCId:           vpc,
-		parameterSubnet1Id:       subNets[0],
-		parameterSubnet2Id:       subNets[1],
-		parameterLoadBalancerARN: lb,
-	}
-
 	update, err := b.SDK.StackExists(ctx, project.Name)
 	if err != nil {
 		return err
@@ -75,7 +44,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error {
 	operation := stackCreate
 	if update {
 		operation = stackUpdate
-		changeset, err := b.SDK.CreateChangeSet(ctx, project.Name, template, parameters)
+		changeset, err := b.SDK.CreateChangeSet(ctx, project.Name, template)
 		if err != nil {
 			return err
 		}
@@ -84,7 +53,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error {
 			return err
 		}
 	} else {
-		err = b.SDK.CreateStack(ctx, project.Name, template, parameters)
+		err = b.SDK.CreateStack(ctx, project.Name, template)
 		if err != nil {
 			return err
 		}
@@ -101,55 +70,3 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error {
 	err = b.WaitStackCompletion(ctx, project.Name, operation)
 	return err
 }
-
-func (b ecsAPIService) GetVPC(ctx context.Context, project *types.Project) (string, error) {
-	var vpcID string
-	//check compose file for custom VPC selected
-	if vpc, ok := project.Extensions[extensionVPC]; ok {
-		vpcID = vpc.(string)
-	} else {
-		defaultVPC, err := b.SDK.GetDefaultVPC(ctx)
-		if err != nil {
-			return "", err
-		}
-		vpcID = defaultVPC
-	}
-
-	err := b.SDK.CheckVPC(ctx, vpcID)
-	if err != nil {
-		return "", err
-	}
-	return vpcID, nil
-}
-
-func (b ecsAPIService) GetLoadBalancer(ctx context.Context, project *types.Project) (string, error) {
-	//check compose file for custom VPC selected
-	if ext, ok := project.Extensions[extensionLB]; ok {
-		lb := ext.(string)
-		ok, err := b.SDK.LoadBalancerExists(ctx, lb)
-		if err != nil {
-			return "", err
-		}
-		if !ok {
-			return "", fmt.Errorf("load balancer does not exist: %s", lb)
-		}
-		return lb, nil
-	}
-	return "", nil
-}
-
-func (b ecsAPIService) GetCluster(ctx context.Context, project *types.Project) (string, error) {
-	//check compose file for custom VPC selected
-	if ext, ok := project.Extensions[extensionCluster]; ok {
-		cluster := ext.(string)
-		ok, err := b.SDK.ClusterExists(ctx, cluster)
-		if err != nil {
-			return "", err
-		}
-		if !ok {
-			return "", fmt.Errorf("cluster does not exist: %s", cluster)
-		}
-		return cluster, nil
-	}
-	return "", nil
-}

+ 59 - 0
ecs/volumes.go

@@ -0,0 +1,59 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package ecs
+
+import (
+	"fmt"
+
+	"github.com/awslabs/goformation/v4/cloudformation"
+	"github.com/awslabs/goformation/v4/cloudformation/ec2"
+	"github.com/awslabs/goformation/v4/cloudformation/ecs"
+	"github.com/compose-spec/compose-go/types"
+)
+
+func (b *ecsAPIService) createNFSmountIngress(securityGroups []string, project *types.Project, n string, template *cloudformation.Template) error {
+	target := securityGroups[0]
+	for _, s := range project.Services {
+		for _, v := range s.Volumes {
+			if v.Source != n {
+				continue
+			}
+			var source string
+			for net := range s.Networks {
+				network := project.Networks[net]
+				if ext, ok := network.Extensions[extensionSecurityGroup]; ok {
+					source = ext.(string)
+				} else {
+					source = networkResourceName(net)
+				}
+				break
+			}
+			name := fmt.Sprintf("%sNFSMount%s", s.Name, n)
+			template.Resources[name] = &ec2.SecurityGroupIngress{
+				Description:           fmt.Sprintf("Allow NFS mount for %s on %s", s.Name, n),
+				GroupId:               target,
+				SourceSecurityGroupId: cloudformation.Ref(source),
+				IpProtocol:            "tcp",
+				FromPort:              2049,
+				ToPort:                2049,
+			}
+			service := template.Resources[serviceResourceName(s.Name)].(*ecs.Service)
+			service.AWSCloudFormationDependsOn = append(service.AWSCloudFormationDependsOn, name)
+		}
+	}
+	return nil
+}

+ 1 - 1
ecs/x.go

@@ -20,7 +20,7 @@ const (
 	extensionSecurityGroup   = "x-aws-securitygroup"
 	extensionVPC             = "x-aws-vpc"
 	extensionPullCredentials = "x-aws-pull_credentials"
-	extensionLB              = "x-aws-loadbalancer"
+	extensionLoadBalancer    = "x-aws-loadbalancer"
 	extensionProtocol        = "x-aws-protocol"
 	extensionCluster         = "x-aws-cluster"
 	extensionKeys            = "x-aws-keys"