diff --git a/ecs/awsResources.go b/ecs/awsResources.go index 7abb7cf94..97d79ee96 100644 --- a/ecs/awsResources.go +++ b/ecs/awsResources.go @@ -51,6 +51,9 @@ func (r *awsResources) serviceSecurityGroups(service types.ServiceConfig) []stri for net := range service.Networks { groups = append(groups, r.securityGroups[net]) } + if len(service.Ports) > 0 { + groups = append(groups, r.securityGroups[serviceIngressSecGroupName(service.Name, false)]) + } return groups } @@ -330,6 +333,7 @@ func (b *ecsAPIService) ensureCluster(r *awsResources, project *types.Project, t func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project, template *cloudformation.Template) { if r.securityGroups == nil { + // TODO NITZ change the size hint? r.securityGroups = make(map[string]string, len(project.Networks)) } for name, net := range project.Networks { @@ -353,6 +357,27 @@ func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project, r.securityGroups[name] = cloudformation.Ref(securityGroup) } + + for _, service := range project.Services { + if len(service.Ports) == 0 { + continue + } + lbSecurityGroup := serviceIngressSecGroupName(service.Name, true) + template.Resources[lbSecurityGroup] = &ec2.SecurityGroup{ + GroupDescription: fmt.Sprintf("%s Security Group for service %s ingress, LB side", project.Name, service.Name), + VpcId: r.vpc, + Tags: serviceTags(project, service), + } + r.securityGroups[lbSecurityGroup] = cloudformation.Ref(lbSecurityGroup) + + serviceSecurityGroup := serviceIngressSecGroupName(service.Name, false) + template.Resources[serviceSecurityGroup] = &ec2.SecurityGroup{ + GroupDescription: fmt.Sprintf("%s Security Group for service %s ingress, Service side", project.Name, service.Name), + VpcId: r.vpc, + Tags: serviceTags(project, service), + } + r.securityGroups[serviceSecurityGroup] = cloudformation.Ref(serviceSecurityGroup) + } } func (b *ecsAPIService) ensureVolumes(r *awsResources, project *types.Project, template *cloudformation.Template) error { @@ -465,9 +490,9 @@ func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, project *types.Proje 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]) + for _, service := range project.Services { + if len(service.Ports) > 0 { + securityGroups = append(securityGroups, r.securityGroups[serviceIngressSecGroupName(service.Name, true)]) } } return securityGroups diff --git a/ecs/cloudformation.go b/ecs/cloudformation.go index 8b673510b..88cc70478 100644 --- a/ecs/cloudformation.go +++ b/ecs/cloudformation.go @@ -197,11 +197,15 @@ func (b *ecsAPIService) createService(project *types.Project, service types.Serv dependsOn []string serviceLB []ecs.Service_LoadBalancer ) - for _, port := range service.Ports { - for net := range service.Networks { - b.createIngress(service, net, port, template, resources) - } + for _, port := range service.Ports { + lbSecurityGroupName := serviceIngressSecGroupName(service.Name, true) + serviceSecurityGroupName := serviceIngressSecGroupName(service.Name, false) + // outside to ingress + b.createIngress(service, lbSecurityGroupName, port, template, resources) + // ingress to service + b.createCrossGroupIngress(service, lbSecurityGroupName, serviceSecurityGroupName, port, template, resources) + // TODO attach new secgroup to this service protocol := strings.ToUpper(port.Protocol) if resources.loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { // we don't set Https as a certificate must be specified for HTTPS listeners @@ -293,6 +297,22 @@ func (b *ecsAPIService) createIngress(service types.ServiceConfig, net string, p } } +func (b *ecsAPIService) createCrossGroupIngress(service types.ServiceConfig, source string, dest string, port types.ServicePortConfig, template *cloudformation.Template, resources awsResources) { + protocol := strings.ToUpper(port.Protocol) + if protocol == "" { + protocol = allProtocols + } + ingress := fmt.Sprintf("%s%d%sIngress", normalizeResourceName(source), port.Target, normalizeResourceName(dest)) + template.Resources[ingress] = &ec2.SecurityGroupIngress{ + SourceSecurityGroupId: resources.securityGroups[source], + Description: fmt.Sprintf("LB connectivity on %d", port.Target), + GroupId: resources.securityGroups[dest], + FromPort: int(port.Target), + IpProtocol: protocol, + ToPort: int(port.Target), + } +} + func (b *ecsAPIService) createSecret(project *types.Project, name string, s types.SecretConfig, template *cloudformation.Template) error { if s.External.External { return nil @@ -534,6 +554,16 @@ func networkResourceName(network string) string { return fmt.Sprintf("%sNetwork", normalizeResourceName(network)) } +func serviceIngressSecGroupName(service string, isLBSide bool) string { + var header string + if isLBSide { + header = "LB" + } else { + header = "Service" + } + return fmt.Sprintf("%s%sIngressSecurityGroup", normalizeResourceName(service), header) +} + func serviceResourceName(service string) string { return fmt.Sprintf("%sService", normalizeResourceName(service)) } diff --git a/ecs/cloudformation_test.go b/ecs/cloudformation_test.go index a957a5bc7..5f086d20b 100644 --- a/ecs/cloudformation_test.go +++ b/ecs/cloudformation_test.go @@ -51,6 +51,17 @@ func TestSimpleConvert(t *testing.T) { golden.Assert(t, result, expected) } +func TestSlightlyComplexConvert(t *testing.T) { + bytes, err := ioutil.ReadFile("testdata/input/slightly-complex-service.yaml") + assert.NilError(t, err) + template := convertYaml(t, string(bytes), nil, useDefaultVPC) + resultAsJSON, err := marshall(template, "yaml") + assert.NilError(t, err) + result := fmt.Sprintf("%s\n", string(resultAsJSON)) + expected := "slightly-complex-cloudformation-conversion.golden" + golden.Assert(t, result, expected) +} + func TestLogging(t *testing.T) { template := convertYaml(t, ` services: diff --git a/ecs/testdata/input/slightly-complex-service.yaml b/ecs/testdata/input/slightly-complex-service.yaml new file mode 100644 index 000000000..23ff9728b --- /dev/null +++ b/ecs/testdata/input/slightly-complex-service.yaml @@ -0,0 +1,8 @@ +services: + entrance: + image: nginx + ports: + - "80:80" + + sensitive: + image: httpd diff --git a/ecs/testdata/simple-cloudformation-conversion.golden b/ecs/testdata/simple-cloudformation-conversion.golden index 7f82eb5f0..daa007407 100644 --- a/ecs/testdata/simple-cloudformation-conversion.golden +++ b/ecs/testdata/simple-cloudformation-conversion.golden @@ -13,16 +13,6 @@ Resources: - Key: com.docker.compose.project Value: TestSimpleConvert Type: AWS::ECS::Cluster - Default80Ingress: - Properties: - CidrIp: 0.0.0.0/0 - Description: simple:80/tcp on default network - FromPort: 80 - GroupId: - Ref: DefaultNetwork - IpProtocol: TCP - ToPort: 80 - Type: AWS::EC2::SecurityGroupIngress DefaultNetwork: Properties: GroupDescription: TestSimpleConvert Security Group for default network @@ -46,7 +36,7 @@ Resources: Properties: Scheme: internet-facing SecurityGroups: - - Ref: DefaultNetwork + - Ref: SimpleLBIngressSecurityGroup Subnets: - subnet1 - subnet2 @@ -59,6 +49,38 @@ Resources: Properties: LogGroupName: /docker-compose/TestSimpleConvert Type: AWS::Logs::LogGroup + SimpleLBIngressSecurityGroup: + Properties: + GroupDescription: TestSimpleConvert Security Group for service simple ingress, + LB side + Tags: + - Key: com.docker.compose.project + Value: TestSimpleConvert + - Key: com.docker.compose.service + Value: simple + VpcId: vpc-123 + Type: AWS::EC2::SecurityGroup + SimpleLBIngressSecurityGroup80Ingress: + Properties: + CidrIp: 0.0.0.0/0 + Description: simple:80/tcp on SimpleLBIngressSecurityGroup network + FromPort: 80 + GroupId: + Ref: SimpleLBIngressSecurityGroup + IpProtocol: TCP + ToPort: 80 + Type: AWS::EC2::SecurityGroupIngress + SimpleLBIngressSecurityGroup80SimpleServiceIngressSecurityGroupIngress: + Properties: + Description: LB connectivity on 80 + FromPort: 80 + GroupId: + Ref: SimpleServiceIngressSecurityGroup + IpProtocol: TCP + SourceSecurityGroupId: + Ref: SimpleLBIngressSecurityGroup + ToPort: 80 + Type: AWS::EC2::SecurityGroupIngress SimpleService: DependsOn: - SimpleTCP80Listener @@ -84,6 +106,7 @@ Resources: AssignPublicIp: ENABLED SecurityGroups: - Ref: DefaultNetwork + - Ref: SimpleServiceIngressSecurityGroup Subnets: - subnet1 - subnet2 @@ -117,6 +140,17 @@ Resources: NamespaceId: Ref: CloudMap Type: AWS::ServiceDiscovery::Service + SimpleServiceIngressSecurityGroup: + Properties: + GroupDescription: TestSimpleConvert Security Group for service simple ingress, + Service side + Tags: + - Key: com.docker.compose.project + Value: TestSimpleConvert + - Key: com.docker.compose.service + Value: simple + VpcId: vpc-123 + Type: AWS::EC2::SecurityGroup SimpleTCP80Listener: Properties: DefaultActions: diff --git a/ecs/testdata/slightly-complex-cloudformation-conversion.golden b/ecs/testdata/slightly-complex-cloudformation-conversion.golden new file mode 100644 index 000000000..57aab2444 --- /dev/null +++ b/ecs/testdata/slightly-complex-cloudformation-conversion.golden @@ -0,0 +1,354 @@ +AWSTemplateFormatVersion: 2010-09-09 +Resources: + CloudMap: + Properties: + Description: Service Map for Docker Compose project TestSlightlyComplexConvert + Name: TestSlightlyComplexConvert.local + Vpc: vpc-123 + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Cluster: + Properties: + ClusterName: TestSlightlyComplexConvert + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + Type: AWS::ECS::Cluster + DefaultNetwork: + Properties: + GroupDescription: TestSlightlyComplexConvert Security Group for default network + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + - Key: com.docker.compose.network + Value: TestSlightlyComplexConvert_default + VpcId: vpc-123 + Type: AWS::EC2::SecurityGroup + DefaultNetworkIngress: + Properties: + Description: Allow communication within network default + GroupId: + Ref: DefaultNetwork + IpProtocol: "-1" + SourceSecurityGroupId: + Ref: DefaultNetwork + Type: AWS::EC2::SecurityGroupIngress + EntranceLBIngressSecurityGroup: + Properties: + GroupDescription: TestSlightlyComplexConvert Security Group for service entrance + ingress, LB side + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + - Key: com.docker.compose.service + Value: entrance + VpcId: vpc-123 + Type: AWS::EC2::SecurityGroup + EntranceLBIngressSecurityGroup80EntranceServiceIngressSecurityGroupIngress: + Properties: + Description: LB connectivity on 80 + FromPort: 80 + GroupId: + Ref: EntranceServiceIngressSecurityGroup + IpProtocol: TCP + SourceSecurityGroupId: + Ref: EntranceLBIngressSecurityGroup + ToPort: 80 + Type: AWS::EC2::SecurityGroupIngress + EntranceLBIngressSecurityGroup80Ingress: + Properties: + CidrIp: 0.0.0.0/0 + Description: entrance:80/tcp on EntranceLBIngressSecurityGroup network + FromPort: 80 + GroupId: + Ref: EntranceLBIngressSecurityGroup + IpProtocol: TCP + ToPort: 80 + Type: AWS::EC2::SecurityGroupIngress + EntranceService: + DependsOn: + - EntranceTCP80Listener + Properties: + Cluster: + Fn::GetAtt: + - Cluster + - Arn + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 100 + DeploymentController: + Type: ECS + DesiredCount: 1 + LaunchType: FARGATE + LoadBalancers: + - ContainerName: entrance + ContainerPort: 80 + TargetGroupArn: + Ref: EntranceTCP80TargetGroup + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - Ref: DefaultNetwork + - Ref: EntranceServiceIngressSecurityGroup + Subnets: + - subnet1 + - subnet2 + PlatformVersion: 1.4.0 + PropagateTags: SERVICE + SchedulingStrategy: REPLICA + ServiceRegistries: + - RegistryArn: + Fn::GetAtt: + - EntranceServiceDiscoveryEntry + - Arn + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + - Key: com.docker.compose.service + Value: entrance + TaskDefinition: + Ref: EntranceTaskDefinition + Type: AWS::ECS::Service + EntranceServiceDiscoveryEntry: + Properties: + Description: '"entrance" service discovery entry in Cloud Map' + DnsConfig: + DnsRecords: + - TTL: 60 + Type: A + RoutingPolicy: MULTIVALUE + HealthCheckCustomConfig: + FailureThreshold: 1 + Name: entrance + NamespaceId: + Ref: CloudMap + Type: AWS::ServiceDiscovery::Service + EntranceServiceIngressSecurityGroup: + Properties: + GroupDescription: TestSlightlyComplexConvert Security Group for service entrance + ingress, Service side + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + - Key: com.docker.compose.service + Value: entrance + VpcId: vpc-123 + Type: AWS::EC2::SecurityGroup + EntranceTCP80Listener: + Properties: + DefaultActions: + - ForwardConfig: + TargetGroups: + - TargetGroupArn: + Ref: EntranceTCP80TargetGroup + Type: forward + LoadBalancerArn: + Ref: LoadBalancer + Port: 80 + Protocol: HTTP + Type: AWS::ElasticLoadBalancingV2::Listener + EntranceTCP80TargetGroup: + Properties: + Port: 80 + Protocol: HTTP + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + TargetType: ip + VpcId: vpc-123 + Type: AWS::ElasticLoadBalancingV2::TargetGroup + EntranceTaskDefinition: + Properties: + ContainerDefinitions: + - Command: + - .compute.internal + - TestSlightlyComplexConvert.local + Essential: false + Image: docker/ecs-searchdomain-sidecar:1.0 + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: + Ref: LogGroup + awslogs-region: + Ref: AWS::Region + awslogs-stream-prefix: TestSlightlyComplexConvert + Name: Entrance_ResolvConf_InitContainer + - DependsOn: + - Condition: SUCCESS + ContainerName: Entrance_ResolvConf_InitContainer + Essential: true + Image: nginx + LinuxParameters: {} + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: + Ref: LogGroup + awslogs-region: + Ref: AWS::Region + awslogs-stream-prefix: TestSlightlyComplexConvert + Name: entrance + PortMappings: + - ContainerPort: 80 + HostPort: 80 + Protocol: tcp + Cpu: "256" + ExecutionRoleArn: + Ref: EntranceTaskExecutionRole + Family: TestSlightlyComplexConvert-entrance + Memory: "512" + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + Type: AWS::ECS::TaskDefinition + EntranceTaskExecutionRole: + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: + - sts:AssumeRole + Condition: {} + Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Version: 2012-10-17 + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + - Key: com.docker.compose.service + Value: entrance + Type: AWS::IAM::Role + LoadBalancer: + Properties: + Scheme: internet-facing + SecurityGroups: + - Ref: EntranceLBIngressSecurityGroup + Subnets: + - subnet1 + - subnet2 + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + Type: application + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + LogGroup: + Properties: + LogGroupName: /docker-compose/TestSlightlyComplexConvert + Type: AWS::Logs::LogGroup + SensitiveService: + Properties: + Cluster: + Fn::GetAtt: + - Cluster + - Arn + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 100 + DeploymentController: + Type: ECS + DesiredCount: 1 + LaunchType: FARGATE + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - Ref: DefaultNetwork + Subnets: + - subnet1 + - subnet2 + PlatformVersion: 1.4.0 + PropagateTags: SERVICE + SchedulingStrategy: REPLICA + ServiceRegistries: + - RegistryArn: + Fn::GetAtt: + - SensitiveServiceDiscoveryEntry + - Arn + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + - Key: com.docker.compose.service + Value: sensitive + TaskDefinition: + Ref: SensitiveTaskDefinition + Type: AWS::ECS::Service + SensitiveServiceDiscoveryEntry: + Properties: + Description: '"sensitive" service discovery entry in Cloud Map' + DnsConfig: + DnsRecords: + - TTL: 60 + Type: A + RoutingPolicy: MULTIVALUE + HealthCheckCustomConfig: + FailureThreshold: 1 + Name: sensitive + NamespaceId: + Ref: CloudMap + Type: AWS::ServiceDiscovery::Service + SensitiveTaskDefinition: + Properties: + ContainerDefinitions: + - Command: + - .compute.internal + - TestSlightlyComplexConvert.local + Essential: false + Image: docker/ecs-searchdomain-sidecar:1.0 + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: + Ref: LogGroup + awslogs-region: + Ref: AWS::Region + awslogs-stream-prefix: TestSlightlyComplexConvert + Name: Sensitive_ResolvConf_InitContainer + - DependsOn: + - Condition: SUCCESS + ContainerName: Sensitive_ResolvConf_InitContainer + Essential: true + Image: httpd + LinuxParameters: {} + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: + Ref: LogGroup + awslogs-region: + Ref: AWS::Region + awslogs-stream-prefix: TestSlightlyComplexConvert + Name: sensitive + Cpu: "256" + ExecutionRoleArn: + Ref: SensitiveTaskExecutionRole + Family: TestSlightlyComplexConvert-sensitive + Memory: "512" + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + Type: AWS::ECS::TaskDefinition + SensitiveTaskExecutionRole: + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: + - sts:AssumeRole + Condition: {} + Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Version: 2012-10-17 + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly + Tags: + - Key: com.docker.compose.project + Value: TestSlightlyComplexConvert + - Key: com.docker.compose.service + Value: sensitive + Type: AWS::IAM::Role +