From 7913f6a00231fd6b027bc2b16cc6915b7db96b25 Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Tue, 12 Jul 2022 11:12:57 -0700 Subject: [PATCH 01/10] chore(manifest): add security_group to env manifest --- .../pkg/deploy/cloudformation/stack/env.go | 41 +- .../stack/env_integration_test.go | 49 + .../deploy/cloudformation/stack/env_test.go | 1 + .../template-with-custom-security-group.yml | 1011 +++++++++++++++++ internal/pkg/manifest/env.go | 10 +- internal/pkg/template/env.go | 12 +- .../pkg/template/templates/environment/cf.yml | 16 + 7 files changed, 1129 insertions(+), 11 deletions(-) create mode 100644 internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index 652882a933c..ed57b285d86 100644 --- a/internal/pkg/deploy/cloudformation/stack/env.go +++ b/internal/pkg/deploy/cloudformation/stack/env.go @@ -5,11 +5,12 @@ package stack import ( "fmt" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/aws/copilot-cli/internal/pkg/manifest" "gopkg.in/yaml.v3" + "strings" "github.com/aws/copilot-cli/internal/pkg/aws/s3" "github.com/aws/copilot-cli/internal/pkg/config" @@ -98,6 +99,12 @@ func (e *EnvStackConfig) Template() (string, error) { } mft = string(out) } + + securityGroupConfig, err := getSecurityGroupConfig(e.in.Mft) + if err != nil { + return "", err + } + content, err := e.parser.ParseEnv(&template.EnvOpts{ AppName: e.in.App.Name, EnvName: e.in.Name, @@ -114,10 +121,10 @@ func (e *EnvStackConfig) Template() (string, error) { CustomInternalALBSubnets: e.internalALBSubnets(), AllowVPCIngress: e.in.AllowVPCIngress, // TODO(jwh): fetch AllowVPCIngress from Manifest or SSM. Telemetry: e.telemetryConfig(), - - Version: e.in.Version, - LatestVersion: deploy.LatestEnvTemplateVersion, - Manifest: mft, + SecurityGroupConfig: securityGroupConfig, + Version: e.in.Version, + LatestVersion: deploy.LatestEnvTemplateVersion, + Manifest: mft, }, template.WithFuncs(map[string]interface{}{ "inc": template.IncFunc, "fmtSlice": template.FmtSliceFunc, @@ -128,6 +135,30 @@ func (e *EnvStackConfig) Template() (string, error) { return content.String(), nil } +func getSecurityGroupConfig(mft *manifest.Environment) (*template.SecurityGroupConfig, error) { + var ingress string + if mft != nil && !mft.Network.VPC.SecurityGroupConfig.Ingress.IsZero() { + out, err := yaml.Marshal(mft.Network.VPC.SecurityGroupConfig.Ingress) + if err != nil { + return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group from environment manifest to embed in template: %v", err) + } + ingress = strings.TrimSpace(string(out)) + } + + var egress string + if mft != nil && !mft.Network.VPC.SecurityGroupConfig.Egress.IsZero() { + out, err := yaml.Marshal(mft.Network.VPC.SecurityGroupConfig.Egress) + if err != nil { + return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group from environment manifest to embed in template: %v", err) + } + egress = strings.TrimSpace(string(out)) + } + return &template.SecurityGroupConfig{ + Ingress: ingress, + Egress: egress, + }, nil +} + func (e *EnvStackConfig) vpcConfig() template.VPCConfig { return template.VPCConfig{ Imported: e.importVPC(), diff --git a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go index 4af22fda307..4680cbeb979 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go @@ -63,6 +63,55 @@ observability: }(), wantedFileName: "template-with-imported-certs-observability.yml", }, + + "generate template with embedded manifest file with custom security groups added by te customer": { + input: func() *deploy.CreateEnvironmentInput { + var mft manifest.Environment + err := yaml.Unmarshal([]byte(` +name: test +type: Environment +# Create the public ALB with certificates attached. +# All these comments should be deleted. +http: + public: + certificates: + - cert-1 + - cert-2 +observability: + container_insights: true # Enable container insights. +network: + vpc: + security_group: + ingress: + ip_protocol: tcp + from_port: 0 + to_port: 65535 + egress: + ip_protocol: tcp + from_port: 0 + to_port: 65535 +`), &mft) + require.NoError(t, err) + return &deploy.CreateEnvironmentInput{ + Version: "1.x", + App: deploy.AppInformation{ + AccountPrincipalARN: "arn:aws:iam::000000000:root", + Name: "demo", + }, + Name: "test", + ArtifactBucketARN: "arn:aws:s3:::mockbucket", + ArtifactBucketKeyARN: "arn:aws:kms:us-west-2:000000000:key/1234abcd-12ab-34cd-56ef-1234567890ab", + CustomResourcesURLs: map[string]string{ + template.DNSCertValidatorFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/dns-cert-validator", + template.DNSDelegationFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/dns-delegation", + template.CustomDomainFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/custom-domain", + }, + AllowVPCIngress: true, + Mft: &mft, + } + }(), + wantedFileName: "template-with-custom-security-group.yml", + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { diff --git a/internal/pkg/deploy/cloudformation/stack/env_test.go b/internal/pkg/deploy/cloudformation/stack/env_test.go index 1affcfcae27..0643edc4dc1 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_test.go @@ -58,6 +58,7 @@ func TestEnv_Template(t *testing.T) { Key: "mockkey4", }, }, + SecurityGroupConfig: &template.SecurityGroupConfig{}, }, data) return &template.Content{Buffer: bytes.NewBufferString("mockTemplate")}, nil }) diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml new file mode 100644 index 00000000000..b0452c2261f --- /dev/null +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml @@ -0,0 +1,1011 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +Description: CloudFormation environment template for infrastructure shared among Copilot workloads. +Metadata: + Manifest: | + name: test + type: Environment + network: {vpc: {id: null, cidr: null, security_group: {ingress: {ip_protocol: tcp, from_port: 0, to_port: 65535}, egress: {ip_protocol: tcp, from_port: 0, to_port: 65535}}}} + observability: {container_insights: true} + http: {public: {certificates: [cert-1, cert-2]}} + +Parameters: + AppName: + Type: String + EnvironmentName: + Type: String + ALBWorkloads: + Type: String + InternalALBWorkloads: + Type: String + EFSWorkloads: + Type: String + NATWorkloads: + Type: String + ToolsAccountPrincipalARN: + Type: String + AppDNSName: + Type: String + AppDNSDelegationRole: + Type: String + Aliases: + Type: String + CreateHTTPSListener: + Type: String + AllowedValues: [true, false] + CreateInternalHTTPSListener: + Type: String + AllowedValues: [true, false] + ServiceDiscoveryEndpoint: + Type: String +Conditions: + CreateALB: + !Not [!Equals [ !Ref ALBWorkloads, "" ]] + DelegateDNS: + !Not [!Equals [ !Ref AppDNSName, "" ]] + ExportHTTPSListener: !And + - !Condition CreateALB + - !Equals [!Ref CreateHTTPSListener, true] + ExportInternalHTTPSListener: !And + - !Condition CreateInternalALB + - !Equals [ !Ref CreateInternalHTTPSListener, true] + CreateEFS: + !Not [!Equals [ !Ref EFSWorkloads, ""]] + CreateInternalALB: + !Not [!Equals [ !Ref InternalALBWorkloads, ""]] + CreateNATGateways: + !Not [!Equals [ !Ref NATWorkloads, ""]] + HasAliases: + !Not [!Equals [ !Ref Aliases, "" ]] +Resources: + VPC: + Metadata: + 'aws:copilot:description': 'A Virtual Private Cloud to control networking of your AWS resources' + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + InstanceTenancy: default + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}' + + PublicRouteTable: + Metadata: + 'aws:copilot:description': "A custom route table that directs network traffic for the public subnets" + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}' + + DefaultPublicRoute: + Type: AWS::EC2::Route + DependsOn: InternetGatewayAttachment + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + + InternetGateway: + Metadata: + 'aws:copilot:description': 'An Internet Gateway to connect to the public internet' + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}' + + InternetGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + InternetGatewayId: !Ref InternetGateway + VpcId: !Ref VPC + PublicSubnet1: + Metadata: + 'aws:copilot:description': 'Public subnet 1 for resources that can access the internet' + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.0.0/24 + VpcId: !Ref VPC + AvailabilityZone: !Select [ 0, !GetAZs '' ] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-pub0' + PublicSubnet2: + Metadata: + 'aws:copilot:description': 'Public subnet 2 for resources that can access the internet' + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.1.0/24 + VpcId: !Ref VPC + AvailabilityZone: !Select [ 1, !GetAZs '' ] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-pub1' + PrivateSubnet1: + Metadata: + 'aws:copilot:description': 'Private subnet 1 for resources with no internet access' + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.2.0/24 + VpcId: !Ref VPC + AvailabilityZone: !Select [ 0, !GetAZs '' ] + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-priv0' + PrivateSubnet2: + Metadata: + 'aws:copilot:description': 'Private subnet 2 for resources with no internet access' + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.3.0/24 + VpcId: !Ref VPC + AvailabilityZone: !Select [ 1, !GetAZs '' ] + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-priv1' + PublicSubnet1RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnet1 + PublicSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnet2 + + NatGateway1Attachment: + Type: AWS::EC2::EIP + Condition: CreateNATGateways + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + NatGateway1: + Metadata: + 'aws:copilot:description': 'NAT Gateway 1 enabling workloads placed in private subnet 1 to reach the internet' + Type: AWS::EC2::NatGateway + Condition: CreateNATGateways + Properties: + AllocationId: !GetAtt NatGateway1Attachment.AllocationId + SubnetId: !Ref PublicSubnet1 + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-0' + PrivateRouteTable1: + Type: AWS::EC2::RouteTable + Condition: CreateNATGateways + Properties: + VpcId: !Ref 'VPC' + PrivateRoute1: + Type: AWS::EC2::Route + Condition: CreateNATGateways + Properties: + RouteTableId: !Ref PrivateRouteTable1 + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway1 + PrivateRouteTable1Association: + Type: AWS::EC2::SubnetRouteTableAssociation + Condition: CreateNATGateways + Properties: + RouteTableId: !Ref PrivateRouteTable1 + SubnetId: !Ref PrivateSubnet1 + NatGateway2Attachment: + Type: AWS::EC2::EIP + Condition: CreateNATGateways + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + NatGateway2: + Metadata: + 'aws:copilot:description': 'NAT Gateway 2 enabling workloads placed in private subnet 2 to reach the internet' + Type: AWS::EC2::NatGateway + Condition: CreateNATGateways + Properties: + AllocationId: !GetAtt NatGateway2Attachment.AllocationId + SubnetId: !Ref PublicSubnet2 + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-1' + PrivateRouteTable2: + Type: AWS::EC2::RouteTable + Condition: CreateNATGateways + Properties: + VpcId: !Ref 'VPC' + PrivateRoute2: + Type: AWS::EC2::Route + Condition: CreateNATGateways + Properties: + RouteTableId: !Ref PrivateRouteTable2 + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway2 + PrivateRouteTable2Association: + Type: AWS::EC2::SubnetRouteTableAssociation + Condition: CreateNATGateways + Properties: + RouteTableId: !Ref PrivateRouteTable2 + SubnetId: !Ref PrivateSubnet2 + # Creates a service discovery namespace with the form provided in the parameter. + # For new environments after 1.5.0, this is "env.app.local". For upgraded environments from + # before 1.5.0, this is app.local. + ServiceDiscoveryNamespace: + Metadata: + 'aws:copilot:description': 'A private DNS namespace for discovering services within the environment' + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Properties: + Name: !Ref ServiceDiscoveryEndpoint + Vpc: !Ref VPC + Cluster: + Metadata: + 'aws:copilot:description': 'An ECS cluster to group your services' + Type: AWS::ECS::Cluster + Properties: + CapacityProviders: ['FARGATE', 'FARGATE_SPOT'] + Configuration: + ExecuteCommandConfiguration: + Logging: DEFAULT + ClusterSettings: + - Name: containerInsights + Value: enabled + PublicLoadBalancerSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your load balancer allowing HTTP and HTTPS traffic' + Condition: CreateALB + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to the public facing load balancer + SecurityGroupIngress: + - CidrIp: 0.0.0.0/0 + Description: Allow from anyone on port 80 + FromPort: 80 + IpProtocol: tcp + ToPort: 80 + - CidrIp: 0.0.0.0/0 + Description: Allow from anyone on port 443 + FromPort: 443 + IpProtocol: tcp + ToPort: 443 + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-lb' + InternalLoadBalancerSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your internal load balancer allowing HTTP traffic from within the VPC' + Condition: CreateInternalALB + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to the internal load balancer + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-internal-lb' + # Only accept requests coming from the public ALB or other containers in the same security group. + EnvironmentSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group to allow your containers to talk to each other' + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: !Join ['', [!Ref AppName, '-', !Ref EnvironmentName, EnvironmentSecurityGroup]] + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-env' + CustomEnvSecurityGroupIngressRule: + Metadata: + 'aws:copilot:description': 'A custom inbound rule defined by the customer' + Type: AWS::EC2::SecurityGroupIngress + Properties: + ip_protocol: tcp + from_port: 0 + to_port: 65535 + CustomEnvSecurityGroupEgressRule: + Metadata: + 'aws:copilot:description': 'A custom outbound rule defined by the customer' + Type: AWS::EC2::SecurityGroupEgress + Properties: + ip_protocol: tcp + from_port: 0 + to_port: 65535 + EnvironmentSecurityGroupIngressFromPublicALB: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateALB + Properties: + Description: Ingress from the public ALB + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref PublicLoadBalancerSecurityGroup + EnvironmentSecurityGroupIngressFromInternalALB: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateInternalALB + Properties: + Description: Ingress from the internal ALB + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref InternalLoadBalancerSecurityGroup + EnvironmentSecurityGroupIngressFromSelf: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from other containers in the same security group + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + InternalALBIngressFromEnvironmentSecurityGroup: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateInternalALB + Properties: + Description: Ingress from the env security group + GroupId: !Ref InternalLoadBalancerSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + InternalLoadBalancerSecurityGroupIngressFromHttp: + Metadata: + 'aws:copilot:description': 'An inbound rule to the internal load balancer security group for port 80 within the VPC' + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateInternalALB + Properties: + Description: Allow from within the VPC on port 80 + CidrIp: 0.0.0.0/0 + FromPort: 80 + ToPort: 80 + IpProtocol: tcp + GroupId: !Ref InternalLoadBalancerSecurityGroup + InternalLoadBalancerSecurityGroupIngressFromHttps: + Metadata: + 'aws:copilot:description': 'An inbound rule to the internal load balancer security group for port 443 within the VPC' + Type: AWS::EC2::SecurityGroupIngress + Condition: ExportInternalHTTPSListener + Properties: + Description: Allow from within the VPC on port 443 + CidrIp: 0.0.0.0/0 + FromPort: 443 + ToPort: 443 + IpProtocol: tcp + GroupId: !Ref InternalLoadBalancerSecurityGroup + PublicLoadBalancer: + Metadata: + 'aws:copilot:description': 'An Application Load Balancer to distribute public traffic to your services' + Condition: CreateALB + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internet-facing + SecurityGroups: [ !GetAtt PublicLoadBalancerSecurityGroup.GroupId ] + Subnets: [ !Ref PublicSubnet1, !Ref PublicSubnet2, ] + Type: application + # Assign a dummy target group that with no real services as targets, so that we can create + # the listeners for the services. + DefaultHTTPTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Condition: CreateALB + Properties: + # Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds. + HealthCheckIntervalSeconds: 10 # Default is 30. + HealthyThresholdCount: 2 # Default is 5. + HealthCheckTimeoutSeconds: 5 + Port: 80 + Protocol: HTTP + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 # Default is 300. + TargetType: ip + VpcId: !Ref VPC + HTTPListener: + Metadata: + 'aws:copilot:description': 'A load balancer listener to route HTTP traffic' + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: CreateALB + Properties: + DefaultActions: + - TargetGroupArn: !Ref DefaultHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref PublicLoadBalancer + Port: 80 + Protocol: HTTP + HTTPSListener: + Metadata: + 'aws:copilot:description': 'A load balancer listener to route HTTPS traffic' + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: ExportHTTPSListener + Properties: + Certificates: + - CertificateArn: cert-1 + DefaultActions: + - TargetGroupArn: !Ref DefaultHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref PublicLoadBalancer + Port: 443 + Protocol: HTTPS + HTTPSImportCertificate2: + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Condition: ExportHTTPSListener + Properties: + ListenerArn: !Ref HTTPSListener + Certificates: + - CertificateArn: cert-2 + InternalLoadBalancer: + Metadata: + 'aws:copilot:description': 'An internal Application Load Balancer to distribute private traffic from within the VPC to your services' + Condition: CreateInternalALB + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internal + SecurityGroups: [ !GetAtt InternalLoadBalancerSecurityGroup.GroupId ] + Subnets: [ !Ref PrivateSubnet1, !Ref PrivateSubnet2, ] + Type: application + DefaultInternalHTTPTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Condition: CreateInternalALB + Properties: + # Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds. + HealthCheckIntervalSeconds: 10 # Default is 30. + HealthyThresholdCount: 2 # Default is 5. + HealthCheckTimeoutSeconds: 5 + Port: 80 + Protocol: HTTP + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 # Default is 300. + TargetType: ip + VpcId: !Ref VPC + InternalHTTPListener: + Metadata: + 'aws:copilot:description': 'An internal load balancer listener to route HTTP traffic' + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: CreateInternalALB + Properties: + DefaultActions: + - TargetGroupArn: !Ref DefaultInternalHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref InternalLoadBalancer + Port: 80 + Protocol: HTTP + InternalHTTPSListener: + Metadata: + 'aws:copilot:description': 'An internal load balancer listener to route HTTPS traffic' + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: ExportInternalHTTPSListener + Properties: + DefaultActions: + - TargetGroupArn: !Ref DefaultInternalHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref InternalLoadBalancer + Port: 443 + Protocol: HTTPS + InternalWorkloadsHostedZone: + Metadata: + 'aws:copilot:description': 'A hosted zone named test.demo.internal for backends behind a private load balancer' + Condition: CreateInternalALB + Type: AWS::Route53::HostedZone + Properties: + Name: !Sub ${EnvironmentName}.${AppName}.internal + VPCs: + - VPCId: !Ref VPC + VPCRegion: !Ref AWS::Region + FileSystem: + Condition: CreateEFS + Type: AWS::EFS::FileSystem + Metadata: + 'aws:copilot:description': 'An EFS filesystem for persistent task storage' + Properties: + BackupPolicy: + Status: ENABLED + Encrypted: true + FileSystemPolicy: + Version: 2012-10-17 + Id: CopilotEFSPolicy + Statement: + - Sid: AllowIAMFromTaggedRoles + Effect: Allow + Principal: + AWS: '*' + Action: + - elasticfilesystem:ClientWrite + - elasticfilesystem:ClientMount + Condition: + Bool: + 'elasticfilesystem:AccessedViaMountTarget': true + StringEquals: + 'iam:ResourceTag/copilot-application': !Sub '${AppName}' + 'iam:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' + - Sid: DenyUnencryptedAccess + Effect: Deny + Principal: '*' + Action: 'elasticfilesystem:*' + Condition: + Bool: + 'aws:SecureTransport': false + LifecyclePolicies: + - TransitionToIA: AFTER_30_DAYS + PerformanceMode: generalPurpose + ThroughputMode: bursting + EFSSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group to allow your containers to talk to EFS storage' + Type: AWS::EC2::SecurityGroup + Condition: CreateEFS + Properties: + GroupDescription: !Join ['', [!Ref AppName, '-', !Ref EnvironmentName, EFSSecurityGroup]] + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-efs' + EFSSecurityGroupIngressFromEnvironment: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateEFS + Properties: + Description: Ingress from containers in the Environment Security Group. + GroupId: !Ref EFSSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + MountTarget1: + Type: AWS::EFS::MountTarget + Condition: CreateEFS + Properties: + FileSystemId: !Ref FileSystem + SubnetId: !Ref PrivateSubnet1 + SecurityGroups: + - !Ref EFSSecurityGroup + MountTarget2: + Type: AWS::EFS::MountTarget + Condition: CreateEFS + Properties: + FileSystemId: !Ref FileSystem + SubnetId: !Ref PrivateSubnet2 + SecurityGroups: + - !Ref EFSSecurityGroup + # The CloudformationExecutionRole definition must be immediately followed with DeletionPolicy: Retain. + # See #1533. + CloudformationExecutionRole: + Metadata: + 'aws:copilot:description': 'An IAM Role for AWS CloudFormation to manage resources' + DeletionPolicy: Retain + Type: AWS::IAM::Role + DependsOn: VPC + Properties: + RoleName: !Sub ${AWS::StackName}-CFNExecutionRole + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - 'cloudformation.amazonaws.com' + - 'lambda.amazonaws.com' + Action: sts:AssumeRole + Path: / + Policies: + - PolicyName: executeCfn + # This policy is more permissive than the managed PowerUserAccess + # since it allows arbitrary role creation, which is needed for the + # ECS task role specified by the customers. + PolicyDocument: + Version: '2012-10-17' + Statement: + - + Effect: Allow + NotAction: + - 'organizations:*' + - 'account:*' + Resource: '*' + - + Effect: Allow + Action: + - 'organizations:DescribeOrganization' + - 'account:ListRegions' + Resource: '*' + + EnvironmentManagerRole: + Metadata: + 'aws:copilot:description': 'An IAM Role to describe resources in your environment' + DeletionPolicy: Retain + Type: AWS::IAM::Role + DependsOn: CloudformationExecutionRole + Properties: + RoleName: !Sub ${AWS::StackName}-EnvManagerRole + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Sub ${ToolsAccountPrincipalARN} + Action: sts:AssumeRole + Path: / + Policies: + - PolicyName: root + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: ImportedCertificates + Effect: Allow + Action: [ + acm:DescribeCertificate + ] + Resource: + - "cert-1" + - "cert-2" + - Sid: CloudwatchLogs + Effect: Allow + Action: [ + "logs:GetLogRecord", + "logs:GetQueryResults", + "logs:StartQuery", + "logs:GetLogEvents", + "logs:DescribeLogStreams", + "logs:StopQuery", + "logs:TestMetricFilter", + "logs:FilterLogEvents", + "logs:GetLogGroupFields", + "logs:GetLogDelivery" + ] + Resource: "*" + - Sid: Cloudwatch + Effect: Allow + Action: [ + "cloudwatch:DescribeAlarms" + ] + Resource: "*" + - Sid: ECS + Effect: Allow + Action: [ + "ecs:ListAttributes", + "ecs:ListTasks", + "ecs:DescribeServices", + "ecs:DescribeTaskSets", + "ecs:ListContainerInstances", + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks", + "ecs:DescribeClusters", + "ecs:UpdateService", + "ecs:PutAttributes", + "ecs:StartTelemetrySession", + "ecs:StartTask", + "ecs:StopTask", + "ecs:ListServices", + "ecs:ListTaskDefinitionFamilies", + "ecs:DescribeTaskDefinition", + "ecs:ListTaskDefinitions", + "ecs:ListClusters", + "ecs:RunTask" + ] + Resource: "*" + - Sid: ExecuteCommand + Effect: Allow + Action: [ + "ecs:ExecuteCommand" + ] + Resource: "*" + Condition: + StringEquals: + 'aws:ResourceTag/copilot-application': !Sub '${AppName}' + 'aws:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' + - Sid: CloudFormation + Effect: Allow + Action: [ + "cloudformation:CancelUpdateStack", + "cloudformation:CreateChangeSet", + "cloudformation:CreateStack", + "cloudformation:DeleteChangeSet", + "cloudformation:DeleteStack", + "cloudformation:Describe*", + "cloudformation:DetectStackDrift", + "cloudformation:DetectStackResourceDrift", + "cloudformation:ExecuteChangeSet", + "cloudformation:GetTemplate", + "cloudformation:GetTemplateSummary", + "cloudformation:UpdateStack", + "cloudformation:UpdateTerminationProtection" + ] + Resource: "*" + - Sid: GetAndPassCopilotRoles + Effect: Allow + Action: [ + "iam:GetRole", + "iam:PassRole" + ] + Resource: "*" + Condition: + StringEquals: + 'iam:ResourceTag/copilot-application': !Sub '${AppName}' + 'iam:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' + - Sid: ECR + Effect: Allow + Action: [ + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:CompleteLayerUpload", + "ecr:DescribeImages", + "ecr:DescribeRepositories", + "ecr:GetDownloadUrlForLayer", + "ecr:InitiateLayerUpload", + "ecr:ListImages", + "ecr:ListTagsForResource", + "ecr:PutImage", + "ecr:UploadLayerPart", + "ecr:GetAuthorizationToken" + ] + Resource: "*" + - Sid: ResourceGroups + Effect: Allow + Action: [ + "resource-groups:GetGroup", + "resource-groups:GetGroupQuery", + "resource-groups:GetTags", + "resource-groups:ListGroupResources", + "resource-groups:ListGroups", + "resource-groups:SearchResources" + ] + Resource: "*" + - Sid: SSM + Effect: Allow + Action: [ + "ssm:DeleteParameter", + "ssm:DeleteParameters", + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:GetParametersByPath" + ] + Resource: "*" + - Sid: SSMSecret + Effect: Allow + Action: [ + "ssm:PutParameter", + "ssm:AddTagsToResource" + ] + Resource: + - !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/copilot/${AppName}/${EnvironmentName}/secrets/*' + - Sid: ELBv2 + Effect: Allow + Action: [ + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeSSLPolicies", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeTargetGroupAttributes", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeTags", + "elasticloadbalancing:DescribeTargetHealth", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeRules" + ] + Resource: "*" + - Sid: BuiltArtifactAccess + Effect: Allow + Action: [ + "s3:ListBucketByTags", + "s3:GetLifecycleConfiguration", + "s3:GetBucketTagging", + "s3:GetInventoryConfiguration", + "s3:GetObjectVersionTagging", + "s3:ListBucketVersions", + "s3:GetBucketLogging", + "s3:ListBucket", + "s3:GetAccelerateConfiguration", + "s3:GetBucketPolicy", + "s3:GetObjectVersionTorrent", + "s3:GetObjectAcl", + "s3:GetEncryptionConfiguration", + "s3:GetBucketRequestPayment", + "s3:GetObjectVersionAcl", + "s3:GetObjectTagging", + "s3:GetMetricsConfiguration", + "s3:HeadBucket", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketPolicyStatus", + "s3:ListBucketMultipartUploads", + "s3:GetBucketWebsite", + "s3:ListJobs", + "s3:GetBucketVersioning", + "s3:GetBucketAcl", + "s3:GetBucketNotification", + "s3:GetReplicationConfiguration", + "s3:ListMultipartUploadParts", + "s3:GetObject", + "s3:GetObjectTorrent", + "s3:GetAccountPublicAccessBlock", + "s3:ListAllMyBuckets", + "s3:DescribeJob", + "s3:GetBucketCORS", + "s3:GetAnalyticsConfiguration", + "s3:GetObjectVersionForReplication", + "s3:GetBucketLocation", + "s3:GetObjectVersion", + "kms:Decrypt" + ] + Resource: "*" + - Sid: PutObjectsToArtifactBucket + Effect: Allow + Action: + - s3:PutObject + - s3:PutObjectAcl + Resource: + - arn:aws:s3:::mockbucket + - arn:aws:s3:::mockbucket/* + - Sid: EncryptObjectsInArtifactBucket + Effect: Allow + Action: + - kms:GenerateDataKey + Resource: arn:aws:kms:us-west-2:000000000:key/1234abcd-12ab-34cd-56ef-1234567890ab + - Sid: EC2 + Effect: Allow + Action: [ + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeRouteTables" + ] + Resource: "*" + - Sid: AppRunner + Effect: Allow + Action: [ + "apprunner:DescribeService", + "apprunner:ListOperations", + "apprunner:ListServices", + "apprunner:PauseService", + "apprunner:ResumeService", + "apprunner:StartDeployment", + "apprunner:DescribeObservabilityConfiguration" + ] + Resource: "*" + - Sid: Tags + Effect: Allow + Action: [ + "tag:GetResources" + ] + Resource: "*" + - Sid: ApplicationAutoscaling + Effect: Allow + Action: [ + "application-autoscaling:DescribeScalingPolicies" + ] + Resource: "*" + - Sid: DeleteRoles + Effect: Allow + Action: [ + "iam:DeleteRole", + "iam:ListRolePolicies", + "iam:DeleteRolePolicy" + ] + Resource: + - !GetAtt CloudformationExecutionRole.Arn + - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AWS::StackName}-EnvManagerRole" + - Sid: DeleteEnvStack + Effect: Allow + Action: + - 'cloudformation:DescribeStacks' + - 'cloudformation:DeleteStack' + Resource: + - !Sub 'arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*' + +Outputs: + VpcId: + Value: !Ref VPC + Export: + Name: !Sub ${AWS::StackName}-VpcId + PublicSubnets: + Value: !Join [ ',', [ !Ref PublicSubnet1, !Ref PublicSubnet2, ] ] + Export: + Name: !Sub ${AWS::StackName}-PublicSubnets + PrivateSubnets: + Value: !Join [ ',', [ !Ref PrivateSubnet1, !Ref PrivateSubnet2, ] ] + Export: + Name: !Sub ${AWS::StackName}-PrivateSubnets + InternetGatewayID: + Value: !Ref InternetGateway + Export: + Name: !Sub ${AWS::StackName}-InternetGatewayID + PublicRouteTableID: + Value: !Ref PublicRouteTable + Export: + Name: !Sub ${AWS::StackName}-PublicRouteTableID + PrivateRouteTableIDs: + Condition: CreateNATGateways + Value: !Join [ ',', [ !Ref PrivateRouteTable1, !Ref PrivateRouteTable2, ] ] + Export: + Name: !Sub ${AWS::StackName}-PrivateRouteTableIDs + ServiceDiscoveryNamespaceID: + Value: !GetAtt ServiceDiscoveryNamespace.Id + Export: + Name: !Sub ${AWS::StackName}-ServiceDiscoveryNamespaceID + EnvironmentSecurityGroup: + Value: !Ref EnvironmentSecurityGroup + Export: + Name: !Sub ${AWS::StackName}-EnvironmentSecurityGroup + PublicLoadBalancerDNSName: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.DNSName + Export: + Name: !Sub ${AWS::StackName}-PublicLoadBalancerDNS + PublicLoadBalancerFullName: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.LoadBalancerFullName + Export: + Name: !Sub ${AWS::StackName}-PublicLoadBalancerFullName + PublicLoadBalancerHostedZone: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.CanonicalHostedZoneID + Export: + Name: !Sub ${AWS::StackName}-CanonicalHostedZoneID + HTTPListenerArn: + Condition: CreateALB + Value: !Ref HTTPListener + Export: + Name: !Sub ${AWS::StackName}-HTTPListenerArn + HTTPSListenerArn: + Condition: ExportHTTPSListener + Value: !Ref HTTPSListener + Export: + Name: !Sub ${AWS::StackName}-HTTPSListenerArn + DefaultHTTPTargetGroupArn: + Condition: CreateALB + Value: !Ref DefaultHTTPTargetGroup + Export: + Name: !Sub ${AWS::StackName}-DefaultHTTPTargetGroup + InternalLoadBalancerDNSName: + Condition: CreateInternalALB + Value: !GetAtt InternalLoadBalancer.DNSName + Export: + Name: !Sub ${AWS::StackName}-InternalLoadBalancerDNS + InternalLoadBalancerFullName: + Condition: CreateInternalALB + Value: !GetAtt InternalLoadBalancer.LoadBalancerFullName + Export: + Name: !Sub ${AWS::StackName}-InternalLoadBalancerFullName + InternalLoadBalancerHostedZone: + Condition: CreateInternalALB + Value: !GetAtt InternalLoadBalancer.CanonicalHostedZoneID + Export: + Name: !Sub ${AWS::StackName}-InternalLoadBalancerCanonicalHostedZoneID + InternalWorkloadsHostedZone: + Condition: CreateInternalALB + Value: !GetAtt InternalWorkloadsHostedZone.Id + Export: + Name: !Sub ${AWS::StackName}-InternalWorkloadsHostedZoneID + InternalWorkloadsHostedZoneName: + Condition: CreateInternalALB + Value: !Sub ${EnvironmentName}.${AppName}.internal + Export: + Name: !Sub ${AWS::StackName}-InternalWorkloadsHostedZoneName + InternalHTTPListenerArn: + Condition: CreateInternalALB + Value: !Ref InternalHTTPListener + Export: + Name: !Sub ${AWS::StackName}-InternalHTTPListenerArn + InternalHTTPSListenerArn: + Condition: ExportInternalHTTPSListener + Value: !Ref InternalHTTPSListener + Export: + Name: !Sub ${AWS::StackName}-InternalHTTPSListenerArn + InternalLoadBalancerSecurityGroup: + Condition: CreateInternalALB + Value: !Ref InternalLoadBalancerSecurityGroup + Export: + Name: !Sub ${AWS::StackName}-InternalLoadBalancerSecurityGroup + ClusterId: + Value: !Ref Cluster + Export: + Name: !Sub ${AWS::StackName}-ClusterId + EnvironmentManagerRoleARN: + Value: !GetAtt EnvironmentManagerRole.Arn + Description: The role to be assumed by the ecs-cli to manage environments. + Export: + Name: !Sub ${AWS::StackName}-EnvironmentManagerRoleARN + CFNExecutionRoleARN: + Value: !GetAtt CloudformationExecutionRole.Arn + Description: The role to be assumed by the Cloudformation service when it deploys application infrastructure. + Export: + Name: !Sub ${AWS::StackName}-CFNExecutionRoleARN + EnabledFeatures: + Value: !Sub '${ALBWorkloads},${InternalALBWorkloads},${EFSWorkloads},${NATWorkloads}' + Description: Required output to force the stack to update if mutating feature params, like ALBWorkloads, does not change the template. + ManagedFileSystemID: + Condition: CreateEFS + Value: !Ref FileSystem + Description: The ID of the Copilot-managed EFS filesystem. + Export: + Name: !Sub ${AWS::StackName}-FilesystemID diff --git a/internal/pkg/manifest/env.go b/internal/pkg/manifest/env.go index 32e223813b4..abf9830b275 100644 --- a/internal/pkg/manifest/env.go +++ b/internal/pkg/manifest/env.go @@ -64,9 +64,13 @@ type environmentNetworkConfig struct { } type environmentVPCConfig struct { - ID *string `yaml:"id"` - CIDR *IPNet `yaml:"cidr"` - Subnets subnetsConfiguration `yaml:"subnets,omitempty"` + ID *string `yaml:"id"` + CIDR *IPNet `yaml:"cidr"` + Subnets subnetsConfiguration `yaml:"subnets,omitempty"` + SecurityGroupConfig struct { + Ingress yaml.Node `yaml:"ingress"` + Egress yaml.Node `yaml:"egress"` + } `yaml:"security_group"` } func (cfg *environmentVPCConfig) loadVPCConfig(env *config.CustomizeEnv) { diff --git a/internal/pkg/template/env.go b/internal/pkg/template/env.go index 525eb09153c..4373e38cd0d 100644 --- a/internal/pkg/template/env.go +++ b/internal/pkg/template/env.go @@ -91,9 +91,9 @@ type EnvOpts struct { CustomInternalALBSubnets []string AllowVPCIngress bool Telemetry *Telemetry - - LatestVersion string - Manifest string // Serialized manifest used to render the environment template. + SecurityGroupConfig *SecurityGroupConfig + LatestVersion string + Manifest string // Serialized manifest used to render the environment template. } type VPCConfig struct { @@ -101,6 +101,12 @@ type VPCConfig struct { Managed ManagedVPC } +// SecurityGroupConfig holds the fields to import security group config +type SecurityGroupConfig struct { + Ingress string + Egress string +} + // ImportVPC holds the fields to import VPC resources. type ImportVPC struct { ID string diff --git a/internal/pkg/template/templates/environment/cf.yml b/internal/pkg/template/templates/environment/cf.yml index 75e5558045b..7cefa8a713a 100644 --- a/internal/pkg/template/templates/environment/cf.yml +++ b/internal/pkg/template/templates/environment/cf.yml @@ -148,6 +148,22 @@ Resources: Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-env' +{{- if and .SecurityGroupConfig .SecurityGroupConfig.Ingress }} + CustomEnvSecurityGroupIngressRule: + Metadata: + 'aws:copilot:description': 'A custom inbound rule defined by the customer' + Type: AWS::EC2::SecurityGroupIngress + Properties: +{{.SecurityGroupConfig.Ingress | indent 6}} +{{- end }} +{{- if and .SecurityGroupConfig .SecurityGroupConfig.Egress }} + CustomEnvSecurityGroupEgressRule: + Metadata: + 'aws:copilot:description': 'A custom outbound rule defined by the customer' + Type: AWS::EC2::SecurityGroupEgress + Properties: +{{.SecurityGroupConfig.Egress | indent 6}} +{{- end }} EnvironmentSecurityGroupIngressFromPublicALB: Type: AWS::EC2::SecurityGroupIngress Condition: CreateALB From d7a218d01734b3c22b3eaeb3a5a8c8484a7a2ec3 Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Tue, 12 Jul 2022 13:14:02 -0700 Subject: [PATCH 02/10] resolve merge conflicts --- .../pkg/deploy/cloudformation/stack/env.go | 312 ++++++++++-------- .../stack/env_integration_test.go | 48 ++- .../template-with-custom-security-group.yml | 11 +- internal/pkg/template/env.go | 47 ++- 4 files changed, 270 insertions(+), 148 deletions(-) diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index ed57b285d86..895f36aafc5 100644 --- a/internal/pkg/deploy/cloudformation/stack/env.go +++ b/internal/pkg/deploy/cloudformation/stack/env.go @@ -9,11 +9,12 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/copilot-cli/internal/pkg/manifest" + + "github.com/aws/copilot-cli/internal/pkg/config" + "gopkg.in/yaml.v3" "strings" - "github.com/aws/copilot-cli/internal/pkg/aws/s3" - "github.com/aws/copilot-cli/internal/pkg/config" "github.com/aws/copilot-cli/internal/pkg/deploy" "github.com/aws/copilot-cli/internal/pkg/template" ) @@ -21,6 +22,7 @@ import ( type envReadParser interface { template.ReadParser ParseEnv(data *template.EnvOpts, options ...template.ParseOption) (*template.Content, error) + ParseEnvBootstrap(data *template.EnvOpts, options ...template.ParseOption) (*template.Content, error) } // EnvStackConfig is for providing all the values to set up an @@ -63,8 +65,7 @@ var ( DefaultPrivateSubnetCIDRs = []string{"10.0.2.0/24", "10.0.3.0/24"} ) -// NewEnvStackConfig sets up a struct which can provide values to CloudFormation for -// spinning up an environment. +// NewEnvStackConfig sets up a struct that provides values to CloudFormation for deploying an environment. func NewEnvStackConfig(input *deploy.CreateEnvironmentInput) *EnvStackConfig { return &EnvStackConfig{ in: input, @@ -74,23 +75,11 @@ func NewEnvStackConfig(input *deploy.CreateEnvironmentInput) *EnvStackConfig { // Template returns the environment CloudFormation template. func (e *EnvStackConfig) Template() (string, error) { - crs, err := convertCustomResources(e.in.LambdaURLs) + crs, err := convertCustomResources(e.in.CustomResourcesURLs) if err != nil { return "", err } - bucket, dnsCertValidator, err := s3.ParseURL(e.in.CustomResourcesURLs[template.DNSCertValidatorFileName]) - if err != nil { - return "", err - } - _, dnsDelegation, err := s3.ParseURL(e.in.CustomResourcesURLs[template.DNSDelegationFileName]) - if err != nil { - return "", err - } - _, customDomain, err := s3.ParseURL(e.in.CustomResourcesURLs[template.CustomDomainFileName]) - if err != nil { - return "", err - } var mft string if e.in.Mft != nil { out, err := yaml.Marshal(e.in.Mft) @@ -109,10 +98,6 @@ func (e *EnvStackConfig) Template() (string, error) { AppName: e.in.App.Name, EnvName: e.in.Name, CustomResources: crs, - DNSCertValidatorLambda: dnsCertValidator, - DNSDelegationLambda: dnsDelegation, - CustomDomainLambda: customDomain, - ScriptBucketName: bucket, ArtifactBucketARN: e.in.ArtifactBucketARN, ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN, PublicImportedCertARNs: e.importPublicCertARNs(), @@ -122,9 +107,11 @@ func (e *EnvStackConfig) Template() (string, error) { AllowVPCIngress: e.in.AllowVPCIngress, // TODO(jwh): fetch AllowVPCIngress from Manifest or SSM. Telemetry: e.telemetryConfig(), SecurityGroupConfig: securityGroupConfig, - Version: e.in.Version, - LatestVersion: deploy.LatestEnvTemplateVersion, - Manifest: mft, + CDNConfig: e.cdnConfig(), + + Version: e.in.Version, + LatestVersion: deploy.LatestEnvTemplateVersion, + SerializedManifest: mft, }, template.WithFuncs(map[string]interface{}{ "inc": template.IncFunc, "fmtSlice": template.FmtSliceFunc, @@ -159,118 +146,14 @@ func getSecurityGroupConfig(mft *manifest.Environment) (*template.SecurityGroupC }, nil } -func (e *EnvStackConfig) vpcConfig() template.VPCConfig { - return template.VPCConfig{ - Imported: e.importVPC(), - Managed: e.managedVPC(), - } -} - -func (e *EnvStackConfig) importVPC() *template.ImportVPC { - // If a manifest is present, it is the only place we look at. - if e.in.Mft != nil { - return e.in.Mft.Network.VPC.ImportedVPC() - } - - // Fallthrough to SSM config. - if e.in.ImportVPCConfig == nil { - return nil - } - return &template.ImportVPC{ - ID: e.in.ImportVPCConfig.ID, - PublicSubnetIDs: e.in.ImportVPCConfig.PublicSubnetIDs, - PrivateSubnetIDs: e.in.ImportVPCConfig.PrivateSubnetIDs, - } -} - -func (e *EnvStackConfig) managedVPC() template.ManagedVPC { - defaultManagedVPC := template.ManagedVPC{ - CIDR: DefaultVPCCIDR, - PublicSubnetCIDRs: DefaultPublicSubnetCIDRs, - PrivateSubnetCIDRs: DefaultPrivateSubnetCIDRs, - } - // If a manifest is present, it is the only place we look at. - if e.in.Mft != nil { - if v := e.in.Mft.Network.VPC.ManagedVPC(); v != nil { - return *v - } - return defaultManagedVPC - } - - // Fallthrough to SSM config. - if e.in.AdjustVPCConfig == nil { - return defaultManagedVPC - } - return template.ManagedVPC{ - CIDR: e.in.AdjustVPCConfig.CIDR, - AZs: e.in.AdjustVPCConfig.AZs, - PublicSubnetCIDRs: e.in.AdjustVPCConfig.PublicSubnetCIDRs, - PrivateSubnetCIDRs: e.in.AdjustVPCConfig.PrivateSubnetCIDRs, - } -} - -func (e *EnvStackConfig) telemetryConfig() *template.Telemetry { - // If a manifest is present, it is the only place we look at. - if e.in.Mft != nil { - return &template.Telemetry{ - EnableContainerInsights: aws.BoolValue(e.in.Mft.Observability.ContainerInsights), - } - } - - // Fallthrough to SSM config. - if e.in.Telemetry == nil { - // For environments before Copilot v1.14.0, `Telemetry` is nil. - return nil - } - return &template.Telemetry{ - // For environments after v1.14.0, and v1.20.0, `Telemetry` is never nil, - // and `EnableContainerInsights` is either true or false. - EnableContainerInsights: e.in.Telemetry.EnableContainerInsights, - } -} - -func (e *EnvStackConfig) importPublicCertARNs() []string { - // If a manifest is present, it is the only place we look at. - if e.in.Mft != nil { - return e.in.Mft.HTTPConfig.Public.Certificates - } - // Fallthrough to SSM config. - if e.in.ImportVPCConfig != nil && len(e.in.ImportVPCConfig.PublicSubnetIDs) == 0 { - return nil - } - return e.in.ImportCertARNs -} - -func (e *EnvStackConfig) importPrivateCertARNs() []string { - // If a manifest is present, it is the only place we look at. - if e.in.Mft != nil { - return e.in.Mft.HTTPConfig.Private.Certificates - } - // Fallthrough to SSM config. - if e.in.ImportVPCConfig != nil && len(e.in.ImportVPCConfig.PublicSubnetIDs) == 0 { - return e.in.ImportCertARNs - } - return nil -} - -func (e *EnvStackConfig) internalALBSubnets() []string { - // If a manifest is present, it is the only place we look. - if e.in.Mft != nil { - return e.in.Mft.HTTPConfig.Private.InternalALBSubnets - } - // Fallthrough to SSM config. - return e.in.InternalALBSubnets -} - // Parameters returns the parameters to be passed into an environment CloudFormation template. func (e *EnvStackConfig) Parameters() ([]*cloudformation.Parameter, error) { httpsListener := "false" - if len(e.in.ImportCertARNs) != 0 || e.in.App.Domain != "" { + if len(e.importPublicCertARNs()) != 0 || e.in.App.Domain != "" { httpsListener = "true" } internalHTTPSListener := "false" - if len(e.in.ImportCertARNs) != 0 && e.in.ImportVPCConfig != nil && - len(e.in.ImportVPCConfig.PublicSubnetIDs) == 0 { + if len(e.importPrivateCertARNs()) != 0 { internalHTTPSListener = "true" } return []*cloudformation.Parameter{ @@ -349,9 +232,67 @@ func (e *EnvStackConfig) StackName() string { return NameForEnv(e.in.App.Name, e.in.Name) } +// NewBootstrapEnvStackConfig sets up a BootstrapEnvStackConfig struct. +func NewBootstrapEnvStackConfig(input *deploy.CreateEnvironmentInput) *BootstrapEnvStackConfig { + return &BootstrapEnvStackConfig{ + in: input, + parser: template.New(), + } +} + +// BootstrapEnvStackConfig contains information for creating a stack bootstrapping environment resources. +type BootstrapEnvStackConfig EnvStackConfig + +// Template returns the CloudFormation template to bootstrap environment resources. +func (e *BootstrapEnvStackConfig) Template() (string, error) { + content, err := e.parser.ParseEnvBootstrap(&template.EnvOpts{ + ArtifactBucketARN: e.in.ArtifactBucketARN, + ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN, + }) + if err != nil { + return "", err + } + return content.String(), nil +} + +// Parameters returns the parameters to be passed into the bootstrap stack's CloudFormation template. +func (e *BootstrapEnvStackConfig) Parameters() ([]*cloudformation.Parameter, error) { + return []*cloudformation.Parameter{ + { + ParameterKey: aws.String(envParamAppNameKey), + ParameterValue: aws.String(e.in.App.Name), + }, + { + ParameterKey: aws.String(envParamEnvNameKey), + ParameterValue: aws.String(e.in.Name), + }, + { + ParameterKey: aws.String(envParamToolsAccountPrincipalKey), + ParameterValue: aws.String(e.in.App.AccountPrincipalARN), + }, + }, nil +} + +// SerializedParameters returns the CloudFormation stack's parameters serialized +// to a YAML document annotated with comments for readability to users. +func (e *BootstrapEnvStackConfig) SerializedParameters() (string, error) { + // No-op for now. + return "", nil +} + +// Tags returns the tags that should be applied to the bootstrap CloudFormation stack. +func (e *BootstrapEnvStackConfig) Tags() []*cloudformation.Tag { + return (*EnvStackConfig)(e).Tags() +} + +// StackName returns the name of the CloudFormation stack (based on the app and env names). +func (e *BootstrapEnvStackConfig) StackName() string { + return (*EnvStackConfig)(e).StackName() +} + // ToEnv inspects an environment cloudformation stack and constructs an environment -// struct out of it (including resources like ECR Repo) -func (e *EnvStackConfig) ToEnv(stack *cloudformation.Stack) (*config.Environment, error) { +// struct out of it (including resources like ECR Repo). +func (e *BootstrapEnvStackConfig) ToEnv(stack *cloudformation.Stack) (*config.Environment, error) { stackARN, err := arn.Parse(*stack.StackId) if err != nil { return nil, fmt.Errorf("couldn't extract region and account from stack ID %s: %w", *stack.StackId, err) @@ -371,3 +312,110 @@ func (e *EnvStackConfig) ToEnv(stack *cloudformation.Stack) (*config.Environment ExecutionRoleARN: stackOutputs[envOutputCFNExecutionRoleARN], }, nil } + +func (e *EnvStackConfig) cdnConfig() *template.CDNConfig { + return nil // no-op - return &template.CDNConfig{} when feature is ready +} + +func (e *EnvStackConfig) vpcConfig() template.VPCConfig { + return template.VPCConfig{ + Imported: e.importVPC(), + Managed: e.managedVPC(), + } +} + +func (e *EnvStackConfig) importVPC() *template.ImportVPC { + // If a manifest is present, it is the only place we look at. + if e.in.Mft != nil { + return e.in.Mft.Network.VPC.ImportedVPC() + } + + // Fallthrough to SSM config. + if e.in.ImportVPCConfig == nil { + return nil + } + return &template.ImportVPC{ + ID: e.in.ImportVPCConfig.ID, + PublicSubnetIDs: e.in.ImportVPCConfig.PublicSubnetIDs, + PrivateSubnetIDs: e.in.ImportVPCConfig.PrivateSubnetIDs, + } +} + +func (e *EnvStackConfig) managedVPC() template.ManagedVPC { + defaultManagedVPC := template.ManagedVPC{ + CIDR: DefaultVPCCIDR, + PublicSubnetCIDRs: DefaultPublicSubnetCIDRs, + PrivateSubnetCIDRs: DefaultPrivateSubnetCIDRs, + } + // If a manifest is present, it is the only place we look at. + if e.in.Mft != nil { + if v := e.in.Mft.Network.VPC.ManagedVPC(); v != nil { + return *v + } + return defaultManagedVPC + } + + // Fallthrough to SSM config. + if e.in.AdjustVPCConfig == nil { + return defaultManagedVPC + } + return template.ManagedVPC{ + CIDR: e.in.AdjustVPCConfig.CIDR, + AZs: e.in.AdjustVPCConfig.AZs, + PublicSubnetCIDRs: e.in.AdjustVPCConfig.PublicSubnetCIDRs, + PrivateSubnetCIDRs: e.in.AdjustVPCConfig.PrivateSubnetCIDRs, + } +} + +func (e *EnvStackConfig) telemetryConfig() *template.Telemetry { + // If a manifest is present, it is the only place we look at. + if e.in.Mft != nil { + return &template.Telemetry{ + EnableContainerInsights: aws.BoolValue(e.in.Mft.Observability.ContainerInsights), + } + } + + // Fallthrough to SSM config. + if e.in.Telemetry == nil { + // For environments before Copilot v1.14.0, `Telemetry` is nil. + return nil + } + return &template.Telemetry{ + // For environments after v1.14.0, and v1.20.0, `Telemetry` is never nil, + // and `EnableContainerInsights` is either true or false. + EnableContainerInsights: e.in.Telemetry.EnableContainerInsights, + } +} + +func (e *EnvStackConfig) importPublicCertARNs() []string { + // If a manifest is present, it is the only place we look at. + if e.in.Mft != nil { + return e.in.Mft.HTTPConfig.Public.Certificates + } + // Fallthrough to SSM config. + if e.in.ImportVPCConfig != nil && len(e.in.ImportVPCConfig.PublicSubnetIDs) == 0 { + return nil + } + return e.in.ImportCertARNs +} + +func (e *EnvStackConfig) importPrivateCertARNs() []string { + // If a manifest is present, it is the only place we look at. + if e.in.Mft != nil { + return e.in.Mft.HTTPConfig.Private.Certificates + } + // Fallthrough to SSM config. + if e.in.ImportVPCConfig != nil && len(e.in.ImportVPCConfig.PublicSubnetIDs) == 0 { + return e.in.ImportCertARNs + } + return nil +} + +func (e *EnvStackConfig) internalALBSubnets() []string { + // If a manifest is present, it is the only place we look. + if e.in.Mft != nil { + return e.in.Mft.HTTPConfig.Private.InternalALBSubnets + } + // Fallthrough to SSM config. + return e.in.InternalALBSubnets +} diff --git a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go index 4680cbeb979..1749cb2b6c2 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go @@ -12,8 +12,6 @@ import ( "github.com/aws/copilot-cli/internal/pkg/manifest" - "github.com/aws/copilot-cli/internal/pkg/template" - "gopkg.in/yaml.v3" "github.com/aws/copilot-cli/internal/pkg/deploy" @@ -53,9 +51,9 @@ observability: ArtifactBucketARN: "arn:aws:s3:::mockbucket", ArtifactBucketKeyARN: "arn:aws:kms:us-west-2:000000000:key/1234abcd-12ab-34cd-56ef-1234567890ab", CustomResourcesURLs: map[string]string{ - template.DNSCertValidatorFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/dns-cert-validator", - template.DNSDelegationFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/dns-delegation", - template.CustomDomainFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/custom-domain", + "CertificateValidationFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/dns-cert-validator", + "DNSDelegationFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/dns-delegation", + "CustomDomainFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/custom-domain", }, AllowVPCIngress: true, Mft: &mft, @@ -89,8 +87,8 @@ network: egress: ip_protocol: tcp from_port: 0 - to_port: 65535 -`), &mft) + to_port: 65535`), &mft) + require.NoError(t, err) return &deploy.CreateEnvironmentInput{ Version: "1.x", @@ -102,16 +100,46 @@ network: ArtifactBucketARN: "arn:aws:s3:::mockbucket", ArtifactBucketKeyARN: "arn:aws:kms:us-west-2:000000000:key/1234abcd-12ab-34cd-56ef-1234567890ab", CustomResourcesURLs: map[string]string{ - template.DNSCertValidatorFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/dns-cert-validator", - template.DNSDelegationFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/dns-delegation", - template.CustomDomainFileName: "https://mockbucket.s3-us-west-2.amazonaws.com/custom-domain", + "CertificateValidationFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/dns-cert-validator", + "DNSDelegationFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/dns-delegation", + "CustomDomainFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/custom-domain", }, AllowVPCIngress: true, Mft: &mft, } }(), + wantedFileName: "template-with-custom-security-group.yml", }, + "generate template with custom resources": { + + input: func() *deploy.CreateEnvironmentInput { + var mft manifest.Environment + err := yaml.Unmarshal([]byte(` +name: test +type: Environment +`), &mft) + require.NoError(t, err) + return &deploy.CreateEnvironmentInput{ + Version: "1.x", + App: deploy.AppInformation{ + AccountPrincipalARN: "arn:aws:iam::000000000:root", + Name: "demo", + }, + Name: "test", + ArtifactBucketARN: "arn:aws:s3:::mockbucket", + ArtifactBucketKeyARN: "arn:aws:kms:us-west-2:000000000:key/1234abcd-12ab-34cd-56ef-1234567890ab", + CustomResourcesURLs: map[string]string{ + "CertificateValidationFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/dns-cert-validator", + "DNSDelegationFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/dns-delegation", + "CustomDomainFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/custom-domain", + }, + AllowVPCIngress: true, + Mft: &mft, + } + }(), + wantedFileName: "template-with-basic-manifest.yml", + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml index b0452c2261f..0eb76d0d326 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml @@ -498,7 +498,7 @@ Resources: Status: ENABLED Encrypted: true FileSystemPolicy: - Version: 2012-10-17 + Version: '2012-10-17' Id: CopilotEFSPolicy Statement: - Sid: AllowIAMFromTaggedRoles @@ -567,7 +567,6 @@ Resources: 'aws:copilot:description': 'An IAM Role for AWS CloudFormation to manage resources' DeletionPolicy: Retain Type: AWS::IAM::Role - DependsOn: VPC Properties: RoleName: !Sub ${AWS::StackName}-CFNExecutionRole AssumeRolePolicyDocument: @@ -685,6 +684,12 @@ Resources: StringEquals: 'aws:ResourceTag/copilot-application': !Sub '${AppName}' 'aws:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' + - Sid: StartStateMachine + Effect: Allow + Action: + - "states:StartExecution" + Resource: + - !Sub "arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${AppName}-${EnvironmentName}-*" - Sid: CloudFormation Effect: Allow Action: [ @@ -1008,4 +1013,4 @@ Outputs: Value: !Ref FileSystem Description: The ID of the Copilot-managed EFS filesystem. Export: - Name: !Sub ${AWS::StackName}-FilesystemID + Name: !Sub ${AWS::StackName}-FilesystemID \ No newline at end of file diff --git a/internal/pkg/template/env.go b/internal/pkg/template/env.go index 4373e38cd0d..dd7c28a03f5 100644 --- a/internal/pkg/template/env.go +++ b/internal/pkg/template/env.go @@ -9,11 +9,12 @@ import ( ) const ( - envCFTemplatePath = "environment/cf.yml" - fmtEnvCFSubTemplatePath = "environment/partials/%s.yml" + envCFTemplatePath = "environment/cf.yml" + fmtEnvCFSubTemplatePath = "environment/partials/%s.yml" + envBootstrapCFTemplatePath = "environment/bootstrap-cf.yml" ) -// Latest available env-controller managed feature names. +// Available env-controller managed feature names. const ( ALBFeatureName = "ALBWorkloads" EFSFeatureName = "EFSWorkloads" @@ -58,6 +59,7 @@ func LeastVersionForFeature(feature string) string { var ( // Template names under "environment/partials/". envCFSubTemplateNames = []string{ + "cdn-resources", "cfn-execution-role", "custom-resources", "custom-resources-role", @@ -65,6 +67,16 @@ var ( "lambdas", "vpc-resources", "nat-gateways", + "bootstrap-resources", + } +) + +var ( + // Template names under "environment/partials/". + bootstrapEnvSubTemplateName = []string{ + "cfn-execution-role", + "environment-manager-role", + "bootstrap-resources", } ) @@ -94,8 +106,14 @@ type EnvOpts struct { SecurityGroupConfig *SecurityGroupConfig LatestVersion string Manifest string // Serialized manifest used to render the environment template. + + CDNConfig *CDNConfig // If nil, no cdn is to be used + SerializedManifest string // Serialized manifest used to render the environment template. } +// CDNConfig represents a Content Delivery Network deployed by CloudFront. +type CDNConfig struct{} + type VPCConfig struct { Imported *ImportVPC // If not-nil, use the imported VPC resources instead of the Managed VPC. Managed ManagedVPC @@ -149,3 +167,26 @@ func (t *Template) ParseEnv(data *EnvOpts, options ...ParseOption) (*Content, er } return &Content{buf}, nil } + +// ParseEnvBootstrap parses the CloudFormation template that bootstrap IAM resources with the specified data object and returns its content. +func (t *Template) ParseEnvBootstrap(data *EnvOpts, options ...ParseOption) (*Content, error) { + tpl, err := t.parse("base", envBootstrapCFTemplatePath, options...) + if err != nil { + return nil, err + } + for _, templateName := range bootstrapEnvSubTemplateName { + nestedTpl, err := t.parse(templateName, fmt.Sprintf(fmtEnvCFSubTemplatePath, templateName), options...) + if err != nil { + return nil, err + } + _, err = tpl.AddParseTree(templateName, nestedTpl.Tree) + if err != nil { + return nil, fmt.Errorf("add parse tree of %s to base template: %w", templateName, err) + } + } + buf := &bytes.Buffer{} + if err := tpl.Execute(buf, data); err != nil { + return nil, fmt.Errorf("execute environment template with data %v: %w", data, err) + } + return &Content{buf}, nil +} From 6869f6999f70256e6119a0bf29f8394a5a27e6e3 Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Tue, 12 Jul 2022 13:35:06 -0700 Subject: [PATCH 03/10] resolve static check error --- internal/pkg/template/env.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/pkg/template/env.go b/internal/pkg/template/env.go index 5c8fba30e22..dd7c28a03f5 100644 --- a/internal/pkg/template/env.go +++ b/internal/pkg/template/env.go @@ -107,8 +107,8 @@ type EnvOpts struct { LatestVersion string Manifest string // Serialized manifest used to render the environment template. - CDNConfig *CDNConfig // If nil, no cdn is to be used - SerializedManifest string // Serialized manifest used to render the environment template. + CDNConfig *CDNConfig // If nil, no cdn is to be used + SerializedManifest string // Serialized manifest used to render the environment template. } // CDNConfig represents a Content Delivery Network deployed by CloudFront. @@ -153,7 +153,7 @@ func (t *Template) ParseEnv(data *EnvOpts, options ...ParseOption) (*Content, er } for _, templateName := range envCFSubTemplateNames { nestedTpl, err := t.parse(templateName, fmt.Sprintf(fmtEnvCFSubTemplatePath, templateName), options...) -https://github.com/aws/copilot-cli/pull/3749/conflict?name=internal%252Fpkg%252Fdeploy%252Fcloudformation%252Fstack%252Fenv_integration_test.go&ancestor_oid=4af22fda307f52cacb90040c9cdc16f07b7d6c06&base_oid=1749cb2b6c200a86a5e0a1b05bb2cf8805f81a08&head_oid=ace0a8055b962d7a4833f7e1cd7877ab5d4268bd if err != nil { + if err != nil { return nil, err } _, err = tpl.AddParseTree(templateName, nestedTpl.Tree) From b0cfe544c3713af7bedf329ba7ff9101353237e5 Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Wed, 13 Jul 2022 12:05:42 -0700 Subject: [PATCH 04/10] Address feedback --- internal/pkg/deploy/cloudformation/stack/env.go | 4 ++-- .../cloudformation/stack/env_integration_test.go | 6 +++--- internal/pkg/manifest/env.go | 10 ++++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index 3cf72eca396..9c29c6032ad 100644 --- a/internal/pkg/deploy/cloudformation/stack/env.go +++ b/internal/pkg/deploy/cloudformation/stack/env.go @@ -126,7 +126,7 @@ func getSecurityGroupConfig(mft *manifest.Environment) (*template.SecurityGroupC if mft != nil && !mft.Network.VPC.SecurityGroupConfig.Ingress.IsZero() { out, err := yaml.Marshal(mft.Network.VPC.SecurityGroupConfig.Ingress) if err != nil { - return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group from environment manifest to embed in template: %v", err) + return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group ingress from environment manifest to embed in template: %v", err) } ingress = strings.TrimSpace(string(out)) } @@ -135,7 +135,7 @@ func getSecurityGroupConfig(mft *manifest.Environment) (*template.SecurityGroupC if mft != nil && !mft.Network.VPC.SecurityGroupConfig.Egress.IsZero() { out, err := yaml.Marshal(mft.Network.VPC.SecurityGroupConfig.Egress) if err != nil { - return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group from environment manifest to embed in template: %v", err) + return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group egress from environment manifest to embed in template: %v", err) } egress = strings.TrimSpace(string(out)) } diff --git a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go index b28ad4406fd..b8b531e1ea7 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go @@ -61,8 +61,8 @@ observability: }(), wantedFileName: "template-with-imported-certs-observability.yml", }, - - "generate template with embedded manifest file with custom security groups added by te customer": { + + "generate template with embedded manifest file with custom security groups rules added by te customer": { input: func() *deploy.CreateEnvironmentInput { var mft manifest.Environment err := yaml.Unmarshal([]byte(` @@ -111,7 +111,7 @@ network: wantedFileName: "template-with-custom-security-group.yml", }, - + "generate template with custom resources": { input: func() *deploy.CreateEnvironmentInput { var mft manifest.Environment diff --git a/internal/pkg/manifest/env.go b/internal/pkg/manifest/env.go index f5e33c786e2..b398dd6cebe 100644 --- a/internal/pkg/manifest/env.go +++ b/internal/pkg/manifest/env.go @@ -98,10 +98,12 @@ type environmentVPCConfig struct { ID *string `yaml:"id"` CIDR *IPNet `yaml:"cidr"` Subnets subnetsConfiguration `yaml:"subnets,omitempty"` - SecurityGroupConfig struct { - Ingress yaml.Node `yaml:"ingress"` - Egress yaml.Node `yaml:"egress"` - } `yaml:"security_group"` + SecurityGroupConfig SecurityGroupConfig `yaml:"security_group"` +} + +type SecurityGroupConfig struct { + Ingress yaml.Node `yaml:"ingress"` + Egress yaml.Node `yaml:"egress"` } type environmentCDNConfig struct { From 32a21f3020ab0a7bf37560268154ff063ae172d6 Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Wed, 13 Jul 2022 12:42:55 -0700 Subject: [PATCH 05/10] nit --- internal/pkg/deploy/cloudformation/stack/env.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index 9c29c6032ad..a4440800f40 100644 --- a/internal/pkg/deploy/cloudformation/stack/env.go +++ b/internal/pkg/deploy/cloudformation/stack/env.go @@ -126,7 +126,7 @@ func getSecurityGroupConfig(mft *manifest.Environment) (*template.SecurityGroupC if mft != nil && !mft.Network.VPC.SecurityGroupConfig.Ingress.IsZero() { out, err := yaml.Marshal(mft.Network.VPC.SecurityGroupConfig.Ingress) if err != nil { - return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group ingress from environment manifest to embed in template: %v", err) + return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group ingress from environment manifest to embed in template: %w", err) } ingress = strings.TrimSpace(string(out)) } @@ -135,7 +135,7 @@ func getSecurityGroupConfig(mft *manifest.Environment) (*template.SecurityGroupC if mft != nil && !mft.Network.VPC.SecurityGroupConfig.Egress.IsZero() { out, err := yaml.Marshal(mft.Network.VPC.SecurityGroupConfig.Egress) if err != nil { - return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group egress from environment manifest to embed in template: %v", err) + return &template.SecurityGroupConfig{}, fmt.Errorf("marshal security group egress from environment manifest to embed in template: %w", err) } egress = strings.TrimSpace(string(out)) } From f3bdd6f0a4c0bc2ae08ceaa63fd70941e4e5568f Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Thu, 14 Jul 2022 11:18:03 -0700 Subject: [PATCH 06/10] typo --- .../pkg/deploy/cloudformation/stack/env_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go index b8b531e1ea7..bd3a73336c0 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go @@ -62,7 +62,7 @@ observability: wantedFileName: "template-with-imported-certs-observability.yml", }, - "generate template with embedded manifest file with custom security groups rules added by te customer": { + "generate template with embedded manifest file with custom security groups rules added by the customer": { input: func() *deploy.CreateEnvironmentInput { var mft manifest.Environment err := yaml.Unmarshal([]byte(` From ade35e58d450e2eba80e999b0388a8ce11ab6729 Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Thu, 14 Jul 2022 15:24:17 -0700 Subject: [PATCH 07/10] fix integ test --- .../pkg/deploy/cloudformation/stack/env.go | 1 + .../stack/env_integration_test.go | 13 ++++----- .../template-with-custom-security-group.yml | 29 ++++++++++++++----- internal/pkg/template/env.go | 8 ++--- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index cd726f5a012..e988fa9b178 100644 --- a/internal/pkg/deploy/cloudformation/stack/env.go +++ b/internal/pkg/deploy/cloudformation/stack/env.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/copilot-cli/internal/pkg/manifest" + "gopkg.in/yaml.v3" "strings" "github.com/aws/copilot-cli/internal/pkg/config" diff --git a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go index 05c69a642bd..199ebd0ede0 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go @@ -64,19 +64,16 @@ observability: "generate template with embedded manifest file with custom security groups rules added by the customer": { input: func() *deploy.CreateEnvironmentInput { - var mft manifest.Environment - err := yaml.Unmarshal([]byte(` -name: test + rawMft := `name: test type: Environment # Create the public ALB with certificates attached. -# All these comments should be deleted. http: public: certificates: - cert-1 - cert-2 observability: - container_insights: true # Enable container insights. + container_insights: true # Enable container insights. network: vpc: security_group: @@ -87,8 +84,9 @@ network: egress: ip_protocol: tcp from_port: 0 - to_port: 65535`), &mft) - + to_port: 65535` + var mft manifest.Environment + err := yaml.Unmarshal([]byte(rawMft), &mft) require.NoError(t, err) return &deploy.CreateEnvironmentInput{ Version: "1.x", @@ -106,6 +104,7 @@ network: }, AllowVPCIngress: true, Mft: &mft, + RawMft: []byte(rawMft), } }(), diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml index 0eb76d0d326..107c7f66314 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml @@ -2,13 +2,28 @@ # SPDX-License-Identifier: MIT-0 Description: CloudFormation environment template for infrastructure shared among Copilot workloads. Metadata: - Manifest: | - name: test - type: Environment - network: {vpc: {id: null, cidr: null, security_group: {ingress: {ip_protocol: tcp, from_port: 0, to_port: 65535}, egress: {ip_protocol: tcp, from_port: 0, to_port: 65535}}}} - observability: {container_insights: true} - http: {public: {certificates: [cert-1, cert-2]}} - + Manifest: | + name: test + type: Environment + # Create the public ALB with certificates attached. + http: + public: + certificates: + - cert-1 + - cert-2 + observability: + container_insights: true # Enable container insights. + network: + vpc: + security_group: + ingress: + ip_protocol: tcp + from_port: 0 + to_port: 65535 + egress: + ip_protocol: tcp + from_port: 0 + to_port: 65535 Parameters: AppName: Type: String diff --git a/internal/pkg/template/env.go b/internal/pkg/template/env.go index dd7c28a03f5..a33605ba387 100644 --- a/internal/pkg/template/env.go +++ b/internal/pkg/template/env.go @@ -104,11 +104,11 @@ type EnvOpts struct { AllowVPCIngress bool Telemetry *Telemetry SecurityGroupConfig *SecurityGroupConfig - LatestVersion string - Manifest string // Serialized manifest used to render the environment template. - CDNConfig *CDNConfig // If nil, no cdn is to be used - SerializedManifest string // Serialized manifest used to render the environment template. + CDNConfig *CDNConfig // If nil, no cdn is to be used + + LatestVersion string + SerializedManifest string // Serialized manifest used to render the environment template. } // CDNConfig represents a Content Delivery Network deployed by CloudFront. From 5170593a085dccef9dcec9d6f26cc4335f774f81 Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Thu, 14 Jul 2022 15:50:25 -0700 Subject: [PATCH 08/10] added removed part during merge conflicts --- internal/pkg/manifest/env.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/pkg/manifest/env.go b/internal/pkg/manifest/env.go index e0018647c9b..0345029429e 100644 --- a/internal/pkg/manifest/env.go +++ b/internal/pkg/manifest/env.go @@ -95,8 +95,8 @@ type environmentNetworkConfig struct { } type environmentVPCConfig struct { - ID *string `yaml:"id"` - CIDR *IPNet `yaml:"cidr"` + ID *string `yaml:"id,omitempty"` + CIDR *IPNet `yaml:"cidr,omitempty"` Subnets subnetsConfiguration `yaml:"subnets,omitempty"` SecurityGroupConfig SecurityGroupConfig `yaml:"security_group"` } From 7572d72c30c5308477c2f7d95e651725317e7bec Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Thu, 14 Jul 2022 17:59:53 -0700 Subject: [PATCH 09/10] remodified the code to incorporate multiple ingress and egress rules --- .../stack/env_integration_test.go | 15 ++++--- .../template-with-custom-security-group.yml | 42 +++++++++---------- internal/pkg/manifest/env.go | 4 +- .../pkg/template/templates/environment/cf.yml | 16 ++----- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go index 199ebd0ede0..7531430e7d3 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go @@ -78,13 +78,16 @@ network: vpc: security_group: ingress: - ip_protocol: tcp - from_port: 0 - to_port: 65535 + - IpProtocol: tcp + FromPort: 0 + ToPort: 65535 + - IpProtocol: tcp + FromPort: 1 + ToPort: 6 egress: - ip_protocol: tcp - from_port: 0 - to_port: 65535` + - IpProtocol: tcp + FromPort: 0 + ToPort: 65535` var mft manifest.Environment err := yaml.Unmarshal([]byte(rawMft), &mft) require.NoError(t, err) diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml index 107c7f66314..f3a02e64f01 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml @@ -17,13 +17,16 @@ Metadata: vpc: security_group: ingress: - ip_protocol: tcp - from_port: 0 - to_port: 65535 + - IpProtocol: tcp + FromPort: 0 + ToPort: 65535 + - IpProtocol: tcp + FromPort: 1 + ToPort: 6 egress: - ip_protocol: tcp - from_port: 0 - to_port: 65535 + - IpProtocol: tcp + FromPort: 0 + ToPort: 65535 Parameters: AppName: Type: String @@ -313,22 +316,17 @@ Resources: Tags: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-env' - CustomEnvSecurityGroupIngressRule: - Metadata: - 'aws:copilot:description': 'A custom inbound rule defined by the customer' - Type: AWS::EC2::SecurityGroupIngress - Properties: - ip_protocol: tcp - from_port: 0 - to_port: 65535 - CustomEnvSecurityGroupEgressRule: - Metadata: - 'aws:copilot:description': 'A custom outbound rule defined by the customer' - Type: AWS::EC2::SecurityGroupEgress - Properties: - ip_protocol: tcp - from_port: 0 - to_port: 65535 + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 0 + ToPort: 65535 + - IpProtocol: tcp + FromPort: 1 + ToPort: 6 + SecurityGroupEgress: + - IpProtocol: tcp + FromPort: 0 + ToPort: 65535 EnvironmentSecurityGroupIngressFromPublicALB: Type: AWS::EC2::SecurityGroupIngress Condition: CreateALB diff --git a/internal/pkg/manifest/env.go b/internal/pkg/manifest/env.go index 0345029429e..8a4e4cd226f 100644 --- a/internal/pkg/manifest/env.go +++ b/internal/pkg/manifest/env.go @@ -98,10 +98,10 @@ type environmentVPCConfig struct { ID *string `yaml:"id,omitempty"` CIDR *IPNet `yaml:"cidr,omitempty"` Subnets subnetsConfiguration `yaml:"subnets,omitempty"` - SecurityGroupConfig SecurityGroupConfig `yaml:"security_group"` + SecurityGroupConfig securityGroupConfig `yaml:"security_group"` } -type SecurityGroupConfig struct { +type securityGroupConfig struct { Ingress yaml.Node `yaml:"ingress"` Egress yaml.Node `yaml:"egress"` } diff --git a/internal/pkg/template/templates/environment/cf.yml b/internal/pkg/template/templates/environment/cf.yml index f8abef8f573..5f722c01891 100644 --- a/internal/pkg/template/templates/environment/cf.yml +++ b/internal/pkg/template/templates/environment/cf.yml @@ -153,20 +153,12 @@ Resources: - Key: Name Value: !Sub 'copilot-${AppName}-${EnvironmentName}-env' {{- if and .SecurityGroupConfig .SecurityGroupConfig.Ingress }} - CustomEnvSecurityGroupIngressRule: - Metadata: - 'aws:copilot:description': 'A custom inbound rule defined by the customer' - Type: AWS::EC2::SecurityGroupIngress - Properties: -{{.SecurityGroupConfig.Ingress | indent 6}} + SecurityGroupIngress: +{{.SecurityGroupConfig.Ingress | indent 8}} {{- end }} {{- if and .SecurityGroupConfig .SecurityGroupConfig.Egress }} - CustomEnvSecurityGroupEgressRule: - Metadata: - 'aws:copilot:description': 'A custom outbound rule defined by the customer' - Type: AWS::EC2::SecurityGroupEgress - Properties: -{{.SecurityGroupConfig.Egress | indent 6}} + SecurityGroupEgress: +{{.SecurityGroupConfig.Egress | indent 8}} {{- end }} EnvironmentSecurityGroupIngressFromPublicALB: Type: AWS::EC2::SecurityGroupIngress From 961b91caca5ca5ca97ebe1aa6c276586e93b7cf1 Mon Sep 17 00:00:00 2001 From: Parag Bhingre Date: Fri, 22 Jul 2022 14:49:50 -0700 Subject: [PATCH 10/10] fix merge conflict and integ test --- .../pkg/deploy/cloudformation/stack/env.go | 1 + .../template-with-custom-security-group.yml | 86 +++++++------------ 2 files changed, 34 insertions(+), 53 deletions(-) diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index 81150e4ecb7..abce4a26051 100644 --- a/internal/pkg/deploy/cloudformation/stack/env.go +++ b/internal/pkg/deploy/cloudformation/stack/env.go @@ -94,6 +94,7 @@ func (e *EnvStackConfig) Template() (string, error) { securityGroupConfig, err := getSecurityGroupConfig(e.in.Mft) if err != nil { return "", err + } forceUpdateID := e.lastForceUpdateID if e.in.ForceUpdate { id, err := uuid.NewRandom() diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml index f3a02e64f01..6348910e056 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml @@ -2,31 +2,31 @@ # SPDX-License-Identifier: MIT-0 Description: CloudFormation environment template for infrastructure shared among Copilot workloads. Metadata: - Manifest: | - name: test - type: Environment - # Create the public ALB with certificates attached. - http: - public: - certificates: - - cert-1 - - cert-2 - observability: - container_insights: true # Enable container insights. - network: - vpc: - security_group: - ingress: - - IpProtocol: tcp - FromPort: 0 - ToPort: 65535 - - IpProtocol: tcp - FromPort: 1 - ToPort: 6 - egress: - - IpProtocol: tcp - FromPort: 0 - ToPort: 65535 + Manifest: | + name: test + type: Environment + # Create the public ALB with certificates attached. + http: + public: + certificates: + - cert-1 + - cert-2 + observability: + container_insights: true # Enable container insights. + network: + vpc: + security_group: + ingress: + - IpProtocol: tcp + FromPort: 0 + ToPort: 65535 + - IpProtocol: tcp + FromPort: 1 + ToPort: 6 + egress: + - IpProtocol: tcp + FromPort: 0 + ToPort: 65535 Parameters: AppName: Type: String @@ -73,8 +73,9 @@ Conditions: !Not [!Equals [ !Ref InternalALBWorkloads, ""]] CreateNATGateways: !Not [!Equals [ !Ref NATWorkloads, ""]] - HasAliases: - !Not [!Equals [ !Ref Aliases, "" ]] + ManagedAliases: !And + - !Condition DelegateDNS + - !Not [!Equals [ !Ref Aliases, "" ]] Resources: VPC: Metadata: @@ -358,30 +359,6 @@ Resources: GroupId: !Ref InternalLoadBalancerSecurityGroup IpProtocol: -1 SourceSecurityGroupId: !Ref EnvironmentSecurityGroup - InternalLoadBalancerSecurityGroupIngressFromHttp: - Metadata: - 'aws:copilot:description': 'An inbound rule to the internal load balancer security group for port 80 within the VPC' - Type: AWS::EC2::SecurityGroupIngress - Condition: CreateInternalALB - Properties: - Description: Allow from within the VPC on port 80 - CidrIp: 0.0.0.0/0 - FromPort: 80 - ToPort: 80 - IpProtocol: tcp - GroupId: !Ref InternalLoadBalancerSecurityGroup - InternalLoadBalancerSecurityGroupIngressFromHttps: - Metadata: - 'aws:copilot:description': 'An inbound rule to the internal load balancer security group for port 443 within the VPC' - Type: AWS::EC2::SecurityGroupIngress - Condition: ExportInternalHTTPSListener - Properties: - Description: Allow from within the VPC on port 443 - CidrIp: 0.0.0.0/0 - FromPort: 443 - ToPort: 443 - IpProtocol: tcp - GroupId: !Ref InternalLoadBalancerSecurityGroup PublicLoadBalancer: Metadata: 'aws:copilot:description': 'An Application Load Balancer to distribute public traffic to your services' @@ -1019,11 +996,14 @@ Outputs: Export: Name: !Sub ${AWS::StackName}-CFNExecutionRoleARN EnabledFeatures: - Value: !Sub '${ALBWorkloads},${InternalALBWorkloads},${EFSWorkloads},${NATWorkloads}' + Value: !Sub '${ALBWorkloads},${InternalALBWorkloads},${EFSWorkloads},${NATWorkloads},${Aliases}' Description: Required output to force the stack to update if mutating feature params, like ALBWorkloads, does not change the template. ManagedFileSystemID: Condition: CreateEFS Value: !Ref FileSystem Description: The ID of the Copilot-managed EFS filesystem. Export: - Name: !Sub ${AWS::StackName}-FilesystemID \ No newline at end of file + Name: !Sub ${AWS::StackName}-FilesystemID + LastForceDeployID: + Value: "" + Description: Optionally force the template to update when no immediate resource change is present. \ No newline at end of file