Repository: docker/compose-cli
Pull request #747 (2020-10-08 15:56:15)

Add support for Volume creation
**What I did**

- Check external filesystems exists on AWS on `up`
- Search for a previous filesystem associated with project's volume
- Create new filesystems for volume if needed
- Create TargetGroups within project's SecurityGroup to allow NFS mounts from services

Major change here is that we don't assume anymore EFS filesystem has TargetGroups we need to attach to by backing the adequate ingress rules. We create TargetGroups inside project's SecurityGroup (arbitrary traffic is already allowed inside project's SecurityGroup). Those can be deleted by `compose down` without the filesystem been impacted.

**usage**
```yaml
services:
  jenkins:
    image: jenkins/jenkins:lts
    volumes:
      - config:/var/jenkins_home
volumes:
  config: {}
```

First `compose up` will create a filesystem on EFS and attach as volume to the container
volume will be re-used by next `compose up` based on tags (project name + volume name)

#close https://github.com/docker/compose-cli/issues/737
#close https://github.com/docker/compose-cli/issues/738

/test-ecs

![image](https://user-images.githubusercontent.com/132757/95729085-9f1eb580-0c7c-11eb-90f7-df3ee807ae1b.png)



Patch:
diff --git a/api/compose/tags.go b/api/compose/tags.go
index 9e6263da..042a1409 100644
--- a/api/compose/tags.go
+++ b/api/compose/tags.go
@@ -23,4 +23,6 @@ const (
 	NetworkTag = "com.docker.compose.network"
 	// ServiceTag allow to track resource related to a compose service
 	ServiceTag = "com.docker.compose.service"
+	// VolumeTag allow to track resource related to a compose volume
+	VolumeTag = "com.docker.compose.volume"
 )
diff --git a/ecs/aws.go b/ecs/aws.go
index f1bdddc1..b5852017 100644
--- a/ecs/aws.go
+++ b/ecs/aws.go
@@ -68,9 +68,12 @@ type API interface {
 	GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error)
 	LoadBalancerType(ctx context.Context, arn string) (string, error)
 	GetLoadBalancerURL(ctx context.Context, arn string) (string, error)
-	WithVolumeSecurityGroups(ctx context.Context, id string, fn func(securityGroups []string) error) error
 	GetParameter(ctx context.Context, name string) (string, error)
 	SecurityGroupExists(ctx context.Context, sg string) (bool, error)
 	DeleteCapacityProvider(ctx context.Context, arn string) error
 	DeleteAutoscalingGroup(ctx context.Context, arn string) error
+	FileSystemExists(ctx context.Context, id string) (bool, error)
+	FindFileSystem(ctx context.Context, tags map[string]string) (string, error)
+	CreateFileSystem(ctx context.Context, tags map[string]string) (string, error)
+	DeleteFileSystem(ctx context.Context, id string) error
 }
diff --git a/ecs/awsResources.go b/ecs/awsResources.go
index 8f9912e5..af5a5537 100644
--- a/ecs/awsResources.go
+++ b/ecs/awsResources.go
@@ -20,13 +20,17 @@ 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/docker/compose-cli/api/compose"
+
+	"github.com/docker/compose-cli/errdefs"
 
+	"github.com/aws/aws-sdk-go/service/elbv2"
 	"github.com/awslabs/goformation/v4/cloudformation"
+	"github.com/awslabs/goformation/v4/cloudformation/ec2"
 	"github.com/awslabs/goformation/v4/cloudformation/ecs"
+	"github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
 	"github.com/compose-spec/compose-go/types"
+	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
 
@@ -38,6 +42,7 @@ type awsResources struct {
 	loadBalancer     string
 	loadBalancerType string
 	securityGroups   map[string]string
+	filesystems      map[string]string
 }
 
 func (r *awsResources) serviceSecurityGroups(service types.ServiceConfig) []string {
@@ -72,7 +77,11 @@ func (b *ecsAPIService) parse(ctx context.Context, project *types.Project) (awsR
 	if err != nil {
 		return r, err
 	}
-	r.securityGroups, err = b.parseSecurityGroupExtension(ctx, project)
+	r.securityGroups, err = b.parseExternalNetworks(ctx, project)
+	if err != nil {
+		return r, err
+	}
+	r.filesystems, err = b.parseExternalVolumes(ctx, project)
 	if err != nil {
 		return r, err
 	}
@@ -87,7 +96,7 @@ func (b *ecsAPIService) parseClusterExtension(ctx context.Context, project *type
 			return "", err
 		}
 		if !ok {
-			return "", fmt.Errorf("cluster does not exist: %s", cluster)
+			return "", errors.Wrapf(errdefs.ErrNotFound, "cluster %q does not exist", cluster)
 		}
 		return cluster, nil
 	}
@@ -139,30 +148,70 @@ func (b *ecsAPIService) parseLoadBalancerExtension(ctx context.Context, project
 	return "", "", nil
 }
 
-func (b *ecsAPIService) parseSecurityGroupExtension(ctx context.Context, project *types.Project) (map[string]string, error) {
+func (b *ecsAPIService) parseExternalNetworks(ctx context.Context, project *types.Project) (map[string]string, error) {
 	securityGroups := make(map[string]string, len(project.Networks))
 	for name, net := range project.Networks {
-		if !net.External.External {
-			continue
-		}
-		sg := net.Name
+		// FIXME remove this for G.A
 		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)
+			net.External.External = true
+			net.Name = x.(string)
+			project.Networks[name] = net
 		}
-		exists, err := b.aws.SecurityGroupExists(ctx, sg)
+
+		if !net.External.External {
+			continue
+		}
+		exists, err := b.aws.SecurityGroupExists(ctx, net.Name)
 		if err != nil {
 			return nil, err
 		}
 		if !exists {
-			return nil, fmt.Errorf("security group %s doesn't exist", sg)
+			return nil, errors.Wrapf(errdefs.ErrNotFound, "security group %q doesn't exist", net.Name)
 		}
-		securityGroups[name] = sg
+		securityGroups[name] = net.Name
 	}
 	return securityGroups, nil
 }
 
+func (b *ecsAPIService) parseExternalVolumes(ctx context.Context, project *types.Project) (map[string]string, error) {
+	filesystems := make(map[string]string, len(project.Volumes))
+	for name, vol := range project.Volumes {
+		if vol.External.External {
+			exists, err := b.aws.FileSystemExists(ctx, vol.Name)
+			if err != nil {
+				return nil, err
+			}
+			if !exists {
+				return nil, errors.Wrapf(errdefs.ErrNotFound, "EFS file system %q doesn't exist", vol.Name)
+			}
+			filesystems[name] = vol.Name
+			continue
+		}
+
+		logrus.Debugf("searching for existing filesystem as volume %q", name)
+		tags := map[string]string{
+			compose.ProjectTag: project.Name,
+			compose.VolumeTag:  name,
+		}
+		id, err := b.aws.FindFileSystem(ctx, tags)
+		if err != nil {
+			return nil, err
+		}
+		if id == "" {
+			tags["Name"] = fmt.Sprintf("%s_%s", project.Name, vol.Name)
+			logrus.Debug("no EFS filesystem found, create a fresh new one")
+			id, err = b.aws.CreateFileSystem(ctx, tags)
+			if err != nil {
+				return nil, err
+			}
+		}
+		filesystems[name] = id
+	}
+	return filesystems, 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)
@@ -186,11 +235,9 @@ func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project,
 		r.securityGroups = make(map[string]string, len(project.Networks))
 	}
 	for name, net := range project.Networks {
-		if net.External.External {
-			r.securityGroups[name] = net.Name
+		if _, ok := r.securityGroups[name]; ok {
 			continue
 		}
-
 		securityGroup := networkResourceName(name)
 		template.Resources[securityGroup] = &ec2.SecurityGroup{
 			GroupDescription: fmt.Sprintf("%s Security Group for %s network", project.Name, name),
diff --git a/ecs/aws_mock.go b/ecs/aws_mock.go
index da892759..504de0c5 100644
--- a/ecs/aws_mock.go
+++ b/ecs/aws_mock.go
@@ -110,6 +110,21 @@ func (mr *MockAPIMockRecorder) CreateCluster(arg0, arg1 interface{}) *gomock.Cal
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCluster", reflect.TypeOf((*MockAPI)(nil).CreateCluster), arg0, arg1)
 }
 
+// CreateFileSystem mocks base method
+func (m *MockAPI) CreateFileSystem(arg0 context.Context, arg1 map[string]string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CreateFileSystem", arg0, arg1)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// CreateFileSystem indicates an expected call of CreateFileSystem
+func (mr *MockAPIMockRecorder) CreateFileSystem(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFileSystem", reflect.TypeOf((*MockAPI)(nil).CreateFileSystem), arg0, arg1)
+}
+
 // CreateSecret mocks base method
 func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 secrets.Secret) (string, error) {
 	m.ctrl.T.Helper()
@@ -167,6 +182,20 @@ func (mr *MockAPIMockRecorder) DeleteCapacityProvider(arg0, arg1 interface{}) *g
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCapacityProvider", reflect.TypeOf((*MockAPI)(nil).DeleteCapacityProvider), arg0, arg1)
 }
 
+// DeleteFileSystem mocks base method
+func (m *MockAPI) DeleteFileSystem(arg0 context.Context, arg1 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DeleteFileSystem", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// DeleteFileSystem indicates an expected call of DeleteFileSystem
+func (mr *MockAPIMockRecorder) DeleteFileSystem(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFileSystem", reflect.TypeOf((*MockAPI)(nil).DeleteFileSystem), arg0, arg1)
+}
+
 // DeleteSecret mocks base method
 func (m *MockAPI) DeleteSecret(arg0 context.Context, arg1 string, arg2 bool) error {
 	m.ctrl.T.Helper()
@@ -225,6 +254,36 @@ func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0, arg1 interface{}) *gomo
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), arg0, arg1)
 }
 
+// FileSystemExists mocks base method
+func (m *MockAPI) FileSystemExists(arg0 context.Context, arg1 string) (bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "FileSystemExists", arg0, arg1)
+	ret0, _ := ret[0].(bool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// FileSystemExists indicates an expected call of FileSystemExists
+func (mr *MockAPIMockRecorder) FileSystemExists(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileSystemExists", reflect.TypeOf((*MockAPI)(nil).FileSystemExists), arg0, arg1)
+}
+
+// FindFileSystem mocks base method
+func (m *MockAPI) FindFileSystem(arg0 context.Context, arg1 map[string]string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "FindFileSystem", arg0, arg1)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// FindFileSystem indicates an expected call of FindFileSystem
+func (mr *MockAPIMockRecorder) FindFileSystem(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindFileSystem", reflect.TypeOf((*MockAPI)(nil).FindFileSystem), arg0, arg1)
+}
+
 // GetDefaultVPC mocks base method
 func (m *MockAPI) GetDefaultVPC(arg0 context.Context) (string, error) {
 	m.ctrl.T.Helper()
@@ -587,20 +646,6 @@ func (mr *MockAPIMockRecorder) WaitStackComplete(arg0, arg1, arg2 interface{}) *
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitStackComplete", reflect.TypeOf((*MockAPI)(nil).WaitStackComplete), arg0, arg1, arg2)
 }
 
-// WithVolumeSecurityGroups mocks base method
-func (m *MockAPI) WithVolumeSecurityGroups(arg0 context.Context, arg1 string, arg2 func([]string) error) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "WithVolumeSecurityGroups", arg0, arg1, arg2)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// WithVolumeSecurityGroups indicates an expected call of WithVolumeSecurityGroups
-func (mr *MockAPIMockRecorder) WithVolumeSecurityGroups(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithVolumeSecurityGroups", reflect.TypeOf((*MockAPI)(nil).WithVolumeSecurityGroups), arg0, arg1, arg2)
-}
-
 // getURLWithPortMapping mocks base method
 func (m *MockAPI) getURLWithPortMapping(arg0 context.Context, arg1 []string) ([]compose.PortPublisher, error) {
 	m.ctrl.T.Helper()
diff --git a/ecs/cloudformation.go b/ecs/cloudformation.go
index b6b7c39a..ec518ded 100644
--- a/ecs/cloudformation.go
+++ b/ecs/cloudformation.go
@@ -72,6 +72,8 @@ func (b *ecsAPIService) convert(ctx context.Context, project *types.Project) (*c
 	// Private DNS namespace will allow DNS name for the services to be <service>.<project>.local
 	b.createCloudMap(project, template, resources.vpc)
 
+	b.createNFSMountTarget(project, resources, template)
+
 	for _, service := range project.Services {
 		err := b.createService(project, service, template, resources)
 		if err != nil {
@@ -81,17 +83,6 @@ func (b *ecsAPIService) convert(ctx context.Context, project *types.Project) (*c
 		b.createAutoscalingPolicy(project, resources, template, service)
 	}
 
-	// Create a NFS inbound rule on each mount target for volumes
-	// as "source security group" use an arbitrary network attached to service(s) who mounts target volume
-	for n, vol := range project.Volumes {
-		err := b.aws.WithVolumeSecurityGroups(ctx, vol.Name, func(securityGroups []string) error {
-			return b.createNFSmountIngress(securityGroups, project, n, template)
-		})
-		if err != nil {
-			return nil, err
-		}
-	}
-
 	err = b.createCapacityProvider(ctx, project, template, resources)
 	if err != nil {
 		return nil, err
@@ -104,7 +95,7 @@ func (b *ecsAPIService) createService(project *types.Project, service types.Serv
 	taskExecutionRole := b.createTaskExecutionRole(project, service, template)
 	taskRole := b.createTaskRole(project, service, template)
 
-	definition, err := b.createTaskDefinition(project, service)
+	definition, err := b.createTaskDefinition(project, service, resources)
 	if err != nil {
 		return err
 	}
@@ -152,6 +143,10 @@ func (b *ecsAPIService) createService(project *types.Project, service types.Serv
 		dependsOn = append(dependsOn, serviceResourceName(dependency))
 	}
 
+	for _, s := range service.Volumes {
+		dependsOn = append(dependsOn, b.mountTargets(s.Source, resources)...)
+	}
+
 	minPercent, maxPercent, err := computeRollingUpdateLimits(service)
 	if err != nil {
 		return err
@@ -326,12 +321,11 @@ func (b *ecsAPIService) createTargetGroup(project *types.Project, service types.
 		port.Published,
 	)
 	template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{
-		HealthCheckEnabled: false,
-		Port:               int(port.Target),
-		Protocol:           protocol,
-		Tags:               projectTags(project),
-		TargetType:         elbv2.TargetTypeEnumIp,
-		VpcId:              vpc,
+		Port:       int(port.Target),
+		Protocol:   protocol,
+		Tags:       projectTags(project),
+		TargetType: elbv2.TargetTypeEnumIp,
+		VpcId:      vpc,
 	}
 	return targetGroupName
 }
diff --git a/ecs/compatibility.go b/ecs/compatibility.go
index 23a07ed9..339ff0e4 100644
--- a/ecs/compatibility.go
+++ b/ecs/compatibility.go
@@ -97,7 +97,9 @@ var compatibleComposeAttributes = []string{
 	"secrets.file",
 	"volumes",
 	"volumes.external",
+	"volumes.name",
 	"networks.external",
+	"networks.name",
 }
 
 func (c *fargateCompatibilityChecker) CheckImage(service *types.ServiceConfig) {
@@ -133,9 +135,3 @@ func (c *fargateCompatibilityChecker) CheckLoggingDriver(config *types.LoggingCo
 		c.Unsupported("services.logging.driver %s is not supported", config.Driver)
 	}
 }
-
-func (c *fargateCompatibilityChecker) CheckVolumeConfigExternal(config *types.VolumeConfig) {
-	if !config.External.External {
-		c.Unsupported("non-external volumes are not supported")
-	}
-}
diff --git a/ecs/convert.go b/ecs/convert.go
index 8537a0a1..436e0bb3 100644
--- a/ecs/convert.go
+++ b/ecs/convert.go
@@ -39,7 +39,7 @@ import (
 const secretsInitContainerImage = "docker/ecs-secrets-sidecar"
 const searchDomainInitContainerImage = "docker/ecs-searchdomain-sidecar"
 
-func (b *ecsAPIService) createTaskDefinition(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) {
+func (b *ecsAPIService) createTaskDefinition(project *types.Project, service types.ServiceConfig, resources awsResources) (*ecs.TaskDefinition, error) {
 	cpu, mem, err := toLimits(service)
 	if err != nil {
 		return nil, err
@@ -81,11 +81,11 @@ func (b *ecsAPIService) createTaskDefinition(project *types.Project, service typ
 	}
 
 	for _, v := range service.Volumes {
-		source := project.Volumes[v.Source]
+		volume := project.Volumes[v.Source]
 		volumes = append(volumes, ecs.TaskDefinition_Volume{
 			EFSVolumeConfiguration: &ecs.TaskDefinition_EFSVolumeConfiguration{
-				FilesystemId:  source.Name,
-				RootDirectory: source.DriverOpts["root_directory"],
+				FilesystemId:  resources.filesystems[v.Source],
+				RootDirectory: volume.DriverOpts["root_directory"],
 			},
 			Name: v.Source,
 		})
@@ -100,7 +100,6 @@ func (b *ecsAPIService) createTaskDefinition(project *types.Project, service typ
 	if err != nil {
 		return nil, err
 	}
-
 	var reservations *types.Resource
 	if service.Deploy != nil && service.Deploy.Resources.Reservations != nil {
 		reservations = service.Deploy.Resources.Reservations
diff --git a/ecs/sdk.go b/ecs/sdk.go
index a6abb9e1..81cd7189 100644
--- a/ecs/sdk.go
+++ b/ecs/sdk.go
@@ -787,28 +787,6 @@ func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error)
 	return dnsName, nil
 }
 
-func (s sdk) WithVolumeSecurityGroups(ctx context.Context, id string, fn func(securityGroups []string) error) error {
-	mounts, err := s.EFS.DescribeMountTargetsWithContext(ctx, &efs.DescribeMountTargetsInput{
-		FileSystemId: aws.String(id),
-	})
-	if err != nil {
-		return err
-	}
-	for _, mount := range mounts.MountTargets {
-		groups, err := s.EFS.DescribeMountTargetSecurityGroupsWithContext(ctx, &efs.DescribeMountTargetSecurityGroupsInput{
-			MountTargetId: mount.MountTargetId,
-		})
-		if err != nil {
-			return err
-		}
-		err = fn(aws.StringValueSlice(groups.SecurityGroups))
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
 func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
 	parameter, err := s.SSM.GetParameterWithContext(ctx, &ssm.GetParameterInput{
 		Name: aws.String(name),
@@ -858,3 +836,74 @@ func (s sdk) DeleteAutoscalingGroup(ctx context.Context, arn string) error {
 	})
 	return err
 }
+
+func (s sdk) FileSystemExists(ctx context.Context, id string) (bool, error) {
+	desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
+		FileSystemId: aws.String(id),
+	})
+	if err != nil {
+		return false, err
+	}
+	return len(desc.FileSystems) > 0, nil
+}
+
+func (s sdk) FindFileSystem(ctx context.Context, tags map[string]string) (string, error) {
+	var token *string
+	for {
+		desc, err := s.EFS.DescribeFileSystemsWithContext(ctx, &efs.DescribeFileSystemsInput{
+			Marker: token,
+		})
+		if err != nil {
+			return "", err
+		}
+		for _, filesystem := range desc.FileSystems {
+			if containsAll(filesystem.Tags, tags) {
+				return aws.StringValue(filesystem.FileSystemId), nil
+			}
+		}
+		if desc.NextMarker == token {
+			return "", nil
+		}
+		token = desc.NextMarker
+	}
+}
+
+func containsAll(tags []*efs.Tag, required map[string]string) bool {
+TAGS:
+	for key, value := range required {
+		for _, t := range tags {
+			if aws.StringValue(t.Key) == key && aws.StringValue(t.Value) == value {
+				continue TAGS
+			}
+		}
+		return false
+	}
+	return true
+}
+
+func (s sdk) CreateFileSystem(ctx context.Context, tags map[string]string) (string, error) {
+	var efsTags []*efs.Tag
+	for k, v := range tags {
+		efsTags = append(efsTags, &efs.Tag{
+			Key:   aws.String(k),
+			Value: aws.String(v),
+		})
+	}
+	res, err := s.EFS.CreateFileSystemWithContext(ctx, &efs.CreateFileSystemInput{
+		Encrypted: aws.Bool(true),
+		Tags:      efsTags,
+	})
+	if err != nil {
+		return "", err
+	}
+	id := aws.StringValue(res.FileSystemId)
+	logrus.Debugf("Created file system %q", id)
+	return id, nil
+}
+
+func (s sdk) DeleteFileSystem(ctx context.Context, id string) error {
+	_, err := s.EFS.DeleteFileSystemWithContext(ctx, &efs.DeleteFileSystemInput{
+		FileSystemId: aws.String(id),
+	})
+	return err
+}
diff --git a/ecs/volumes.go b/ecs/volumes.go
index c17d5e07..a5d4ea3a 100644
--- a/ecs/volumes.go
+++ b/ecs/volumes.go
@@ -20,40 +20,27 @@ import (
 	"fmt"
 
 	"github.com/awslabs/goformation/v4/cloudformation"
-	"github.com/awslabs/goformation/v4/cloudformation/ec2"
-	"github.com/awslabs/goformation/v4/cloudformation/ecs"
+	"github.com/awslabs/goformation/v4/cloudformation/efs"
 	"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
+func (b *ecsAPIService) createNFSMountTarget(project *types.Project, resources awsResources, template *cloudformation.Template) {
+	for volume := range project.Volumes {
+		for _, subnet := range resources.subnets {
+			name := fmt.Sprintf("%sNFSMountTargetOn%s", normalizeResourceName(volume), normalizeResourceName(subnet))
+			template.Resources[name] = &efs.MountTarget{
+				FileSystemId:   resources.filesystems[volume],
+				SecurityGroups: resources.allSecurityGroups(),
+				SubnetId:       subnet,
 			}
-			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", normalizeResourceName(s.Name), normalizeResourceName(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
+}
+
+func (b *ecsAPIService) mountTargets(volume string, resources awsResources) []string {
+	var refs []string
+	for _, subnet := range resources.subnets {
+		refs = append(refs, fmt.Sprintf("%sNFSMountTargetOn%s", normalizeResourceName(volume), normalizeResourceName(subnet)))
+	}
+	return refs
 }
diff --git a/go.mod b/go.mod
index b158e585..55680f36 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,6 @@ go 1.15
 // we need to create a new release tag for docker/distribution
 replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20200708230824-53e18a9d9bfe
 
-replace github.com/awslabs/goformation/v4 => github.com/ndeloof/goformation/v4 v4.8.1-0.20200827081523-b7a7ac375adf
-
 require (
 	github.com/AlecAivazis/survey/v2 v2.1.1
 	github.com/Azure/azure-sdk-for-go v43.3.0+incompatible
@@ -21,10 +19,10 @@ require (
 	github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
 	github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5
 	github.com/Microsoft/hcsshim v0.8.9 // indirect
-	github.com/aws/aws-sdk-go v1.34.8
-	github.com/awslabs/goformation/v4 v4.14.0
+	github.com/aws/aws-sdk-go v1.35.7
+	github.com/awslabs/goformation/v4 v4.15.2
 	github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129
-	github.com/compose-spec/compose-go v0.0.0-20200907084823-057e1edc5b6f
+	github.com/compose-spec/compose-go v0.0.0-20201005072614-3b6106793209
 	github.com/containerd/console v1.0.0
 	github.com/containerd/containerd v1.3.5 // indirect
 	github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8
@@ -53,7 +51,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/tsdb v0.7.1
 	github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b
-	github.com/sirupsen/logrus v1.6.0
+	github.com/sirupsen/logrus v1.7.0
 	github.com/smartystreets/goconvey v1.6.4 // indirect
 	github.com/spf13/cobra v1.0.0
 	github.com/spf13/pflag v1.0.5
@@ -63,6 +61,7 @@ require (
 	golang.org/x/net v0.0.0-20200822124328-c89045814202
 	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
 	golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
+	golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 // indirect
 	google.golang.org/grpc v1.32.0
 	google.golang.org/protobuf v1.25.0
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
diff --git a/go.sum b/go.sum
index 65884f2e..80df99bb 100644
--- a/go.sum
+++ b/go.sum
@@ -96,8 +96,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
-github.com/aws/aws-sdk-go v1.34.8 h1:GDfVeXG8XQDbpOeAj7415F8qCQZwvY/k/fj+HBqUnBA=
-github.com/aws/aws-sdk-go v1.34.8/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.35.7 h1:FHMhVhyc/9jljgFAcGkQDYjpC9btM0B8VfkLBfctdNE=
+github.com/aws/aws-sdk-go v1.35.7/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
+github.com/awslabs/goformation/v4 v4.15.2 h1:sRfSdC1FnSBhsrz5G0XZZxapEtmJSlkNpnFQJf8ylfs=
+github.com/awslabs/goformation/v4 v4.15.2/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -120,8 +122,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/compose-spec/compose-go v0.0.0-20200907084823-057e1edc5b6f h1:YsU3/17YA/skXpCQbRcrzWJxslWZ2lmvQK0bRiCyC38=
-github.com/compose-spec/compose-go v0.0.0-20200907084823-057e1edc5b6f/go.mod h1:voTGL1mRFcKRaFbi1lXGlR1YffS/9YD1jnVl4N/rYzw=
+github.com/compose-spec/compose-go v0.0.0-20201005072614-3b6106793209 h1:PLZiS7hjkiAqZYBRAEq3tbGlhCh6/R14dO1ahwbEIBg=
+github.com/compose-spec/compose-go v0.0.0-20201005072614-3b6106793209/go.mod h1:rNXXqhdClEljsNb6QDIOqTQaRfigwTgGZZM6Zpr3LeY=
 github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f h1:tSNMc+rJDfmYntojat8lljbt1mgKNpTxUZJsSzJ9Y1s=
 github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
 github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
@@ -193,8 +195,6 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
-github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
@@ -296,8 +296,10 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
-github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
@@ -314,8 +316,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -364,8 +364,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
-github.com/ndeloof/goformation/v4 v4.8.1-0.20200827081523-b7a7ac375adf h1:jdmD8L6TKRZpa7B4qUmjiWRBMkgbfUF/7pi/Kgba5lA=
-github.com/ndeloof/goformation/v4 v4.8.1-0.20200827081523-b7a7ac375adf/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg=
 github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@@ -429,8 +427,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -459,7 +457,6 @@ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -616,6 +613,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -633,6 +631,8 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM=
+golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=


Test patch:
diff --git a/ecs/cloudformation_test.go b/ecs/cloudformation_test.go
index fd22374b..23a089a8 100644
--- a/ecs/cloudformation_test.go
+++ b/ecs/cloudformation_test.go
@@ -23,6 +23,8 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/awslabs/goformation/v4/cloudformation/efs"
+
 	"github.com/golang/mock/gomock"
 
 	"github.com/docker/compose-cli/api/compose"
@@ -336,7 +338,7 @@ services:
 	assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork)
 }
 
-func TestUseCustomNetwork(t *testing.T) {
+func TestUseExternalNetwork(t *testing.T) {
 	template := convertYaml(t, `
 services:
   test:
@@ -355,6 +357,67 @@ networks:
 	assert.Check(t, s.NetworkConfiguration.AwsvpcConfiguration.SecurityGroups[0] == "sg-123abc") //nolint:staticcheck
 }
 
+func testVolume(t *testing.T, yaml string, fn ...func(m *MockAPIMockRecorder)) {
+	template := convertYaml(t, yaml, fn...)
+
+	s := template.Resources["DbdataNFSMountTargetOnSubnet1"].(*efs.MountTarget)
+	assert.Check(t, s != nil)
+	assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
+
+	s = template.Resources["DbdataNFSMountTargetOnSubnet2"].(*efs.MountTarget)
+	assert.Check(t, s != nil)
+	assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
+}
+
+func TestUseExternalVolume(t *testing.T) {
+	testVolume(t, `
+services:
+  test:
+    image: nginx
+volumes:
+  db-data:
+    external: true
+    name: fs-123abc
+`, useDefaultVPC, func(m *MockAPIMockRecorder) {
+		m.FileSystemExists(gomock.Any(), "fs-123abc").Return(true, nil)
+	})
+}
+
+func TestCreateVolume(t *testing.T) {
+	testVolume(t, `
+services:
+  test:
+    image: nginx
+volumes:
+  db-data: {}
+`, useDefaultVPC, func(m *MockAPIMockRecorder) {
+		m.FindFileSystem(gomock.Any(), map[string]string{
+			compose.ProjectTag: t.Name(),
+			compose.VolumeTag:  "db-data",
+		}).Return("", nil)
+		m.CreateFileSystem(gomock.Any(), map[string]string{
+			compose.ProjectTag: t.Name(),
+			compose.VolumeTag:  "db-data",
+			"Name":             fmt.Sprintf("%s_%s", t.Name(), "db-data"),
+		}).Return("fs-123abc", nil)
+	})
+}
+
+func TestReusePreviousVolume(t *testing.T) {
+	testVolume(t, `
+services:
+  test:
+    image: nginx
+volumes:
+  db-data: {}
+`, useDefaultVPC, func(m *MockAPIMockRecorder) {
+		m.FindFileSystem(gomock.Any(), map[string]string{
+			compose.ProjectTag: t.Name(),
+			compose.VolumeTag:  "db-data",
+		}).Return("fs-123abc", nil)
+	})
+}
+
 func TestServiceMapping(t *testing.T) {
 	template := convertYaml(t, `
 services:
diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go
index 665d7e63..7b30712e 100644
--- a/tests/aci-e2e/e2e-aci_test.go
+++ b/tests/aci-e2e/e2e-aci_test.go
@@ -770,7 +770,6 @@ func TestUpUpdate(t *testing.T) {
 	})
 }
 
-/*
 func TestRunEnvVars(t *testing.T) {
 	c := NewParallelE2eCLI(t, binDir)
 	_, _, _ = setupTestResourceGroup(t, c)
@@ -811,12 +810,11 @@ func TestRunEnvVars(t *testing.T) {
 			if strings.Contains(res.Stdout(), "Giving user user1 access to schema mytestdb") {
 				return poll.Success()
 			}
-			return poll.Continue("waiting for DB container to be up\n%s", res.Combined())
+			return poll.Continue("waiting for DB container to be up")
 		}
 		poll.WaitOn(t, check, poll.WithDelay(5*time.Second), poll.WithTimeout(90*time.Second))
 	})
 }
-*/
 
 func setupTestResourceGroup(t *testing.T, c *E2eCLI) (string, string, string) {
 	startTime := strconv.Itoa(int(time.Now().Unix()))