diff --git a/internal/pkg/deploy/cloudformation/cloudformation_integration_test.go b/internal/pkg/deploy/cloudformation/cloudformation_integration_test.go index 3a651e86b8f..d4839b18979 100644 --- a/internal/pkg/deploy/cloudformation/cloudformation_integration_test.go +++ b/internal/pkg/deploy/cloudformation/cloudformation_integration_test.go @@ -1,5 +1,4 @@ //go:build integration -// +build integration // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/internal/pkg/deploy/cloudformation/env_test.go b/internal/pkg/deploy/cloudformation/env_test.go index d398460fdd7..fe97851f7a6 100644 --- a/internal/pkg/deploy/cloudformation/env_test.go +++ b/internal/pkg/deploy/cloudformation/env_test.go @@ -6,6 +6,7 @@ package cloudformation import ( "errors" "fmt" + "io" "testing" "github.com/aws/aws-sdk-go/aws" @@ -100,7 +101,10 @@ func TestCloudFormation_UpgradeEnvironment(t *testing.T) { }) }) s3 := mocks.NewMocks3Client(ctrl) - s3.EXPECT().Upload("mockbucket", "manual/templates/phonetool-test/6806328b58e482e354cfd1879fecc310113fcf1b95c6bdf058c486f440087847.yml", gomock.Any()).Return("url", nil) + s3.EXPECT().Upload("mockbucket", gomock.Any(), gomock.Any()).DoAndReturn(func(bucket, key string, data io.Reader) (string, error) { + require.Contains(t, key, "manual/templates/phonetool-test/") + return "url", nil + }) return &CloudFormation{ cfnClient: m, diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index 9c724456435..94e48058488 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" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/cloudformation" + "gopkg.in/yaml.v3" "github.com/aws/copilot-cli/internal/pkg/aws/s3" "github.com/aws/copilot-cli/internal/pkg/config" @@ -82,6 +83,14 @@ func (e *EnvStackConfig) Template() (string, error) { if err != nil { return "", err } + var mft string + if e.in.Mft != nil { + out, err := yaml.Marshal(e.in.Mft) + if err != nil { + return "", fmt.Errorf("marshal environment manifest to embed in template: %v", err) + } + mft = string(out) + } content, err := e.parser.ParseEnv(&template.EnvOpts{ AppName: e.in.App.Name, @@ -92,12 +101,13 @@ func (e *EnvStackConfig) Template() (string, error) { ArtifactBucketARN: e.in.ArtifactBucketARN, ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN, - ImportCertARNs: e.in.ImportCertARNs, + ImportCertARNs: e.importCertARNs(), VPCConfig: e.vpcConfig(), Telemetry: e.telemetryConfig(), Version: e.in.Version, LatestVersion: deploy.LatestEnvTemplateVersion, + Manifest: mft, }, template.WithFuncs(map[string]interface{}{ "inc": template.IncFunc, })) diff --git a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go new file mode 100644 index 00000000000..4ae53f93042 --- /dev/null +++ b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go @@ -0,0 +1,87 @@ +//go:build integration || localintegration + +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package stack_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "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" + "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack" + "github.com/stretchr/testify/require" +) + +func TestEnvStack_Template(t *testing.T) { + testCases := map[string]struct { + input *deploy.CreateEnvironmentInput + wantedFileName string + }{ + "generate template with embedded manifest file with container insights and imported certificates": { + 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. +`), &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", + }, + Mft: &mft, + } + }(), + wantedFileName: "template-with-imported-certs-observability.yml", + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // GIVEN + wanted, err := os.ReadFile(filepath.Join("testdata", "environments", tc.wantedFileName)) + require.NoError(t, err, "read wanted template") + wantedObj := make(map[any]any) + require.NoError(t, yaml.Unmarshal(wanted, wantedObj)) + + // WHEN + envStack := stack.NewEnvStackConfig(tc.input) + actual, err := envStack.Template() + require.NoError(t, err, "serialize template") + fmt.Println(actual) + actualObj := make(map[any]any) + require.NoError(t, yaml.Unmarshal([]byte(actual), actualObj)) + + // THEN + require.Equal(t, wantedObj, actualObj) + }) + } +} diff --git a/internal/pkg/deploy/cloudformation/stack/lb_web_service_integration_test.go b/internal/pkg/deploy/cloudformation/stack/lb_web_service_integration_test.go index cfcf0e9d2e3..c4985b27ca5 100644 --- a/internal/pkg/deploy/cloudformation/stack/lb_web_service_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/lb_web_service_integration_test.go @@ -1,5 +1,4 @@ //go:build integration || localintegration -// +build integration localintegration // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-observability.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-observability.yml new file mode 100644 index 00000000000..c37c9592dd1 --- /dev/null +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-observability.yml @@ -0,0 +1,830 @@ +# 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: + Version: v1.9.0 + Manifest: | + name: test + type: Environment + observability: {container_insights: true} + http: {public: {certificates: [cert-1, cert-2]}} + +Parameters: + AppName: + Type: String + EnvironmentName: + Type: String + ALBWorkloads: + 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] + ServiceDiscoveryEndpoint: + Type: String +Conditions: + CreateALB: + !Not [!Equals [ !Ref ALBWorkloads, "" ]] + DelegateDNS: + !Not [!Equals [ !Ref AppDNSName, "" ]] + ExportHTTPSListener: !And + - !Condition CreateALB + - !Equals [!Ref CreateHTTPSListener, true] + CreateEFS: + !Not [!Equals [ !Ref EFSWorkloads, ""]] + 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' + # 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' + EnvironmentSecurityGroupIngressFromPublicALB: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateALB + Properties: + Description: Ingress from the public ALB + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref PublicLoadBalancerSecurityGroup + EnvironmentSecurityGroupIngressFromSelf: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from other containers in the same security group + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + 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 + 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 + 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 + 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},${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 4a43d6263be..4889d74b540 100644 --- a/internal/pkg/manifest/env.go +++ b/internal/pkg/manifest/env.go @@ -21,19 +21,19 @@ type Environment struct { // EnvironmentConfig holds the configuration for an environment. type EnvironmentConfig struct { - Network environmentNetworkConfig `yaml:"network"` - Observability environmentObservability `yaml:"observability"` - HTTPConfig environmentHTTPConfig `yaml:"http"` + Network environmentNetworkConfig `yaml:"network,omitempty,flow"` + Observability environmentObservability `yaml:"observability,omitempty,flow"` + HTTPConfig environmentHTTPConfig `yaml:"http,omitempty,flow"` } type environmentNetworkConfig struct { - VPC environmentVPCConfig `yaml:"vpc"` + VPC environmentVPCConfig `yaml:"vpc,omitempty"` } type environmentVPCConfig struct { ID *string `yaml:"id"` CIDR *IPNet `yaml:"cidr"` - Subnets subnetsConfiguration `yaml:"subnets"` + Subnets subnetsConfiguration `yaml:"subnets,omitempty"` } func (v environmentVPCConfig) imported() bool { @@ -97,8 +97,8 @@ func (v environmentVPCConfig) ManagedVPC() *template.ManagedVPC { } type subnetsConfiguration struct { - Public []subnetConfiguration `yaml:"public"` - Private []subnetConfiguration `yaml:"private"` + Public []subnetConfiguration `yaml:"public,omitempty"` + Private []subnetConfiguration `yaml:"private,omitempty"` } type subnetConfiguration struct { @@ -108,7 +108,7 @@ type subnetConfiguration struct { } type environmentObservability struct { - ContainerInsights *bool `yaml:"container_insights"` + ContainerInsights *bool `yaml:"container_insights,omitempty"` } // IsEmpty returns true if there is no configuration to the environment's observability. @@ -117,9 +117,9 @@ func (o environmentObservability) IsEmpty() bool { } type environmentHTTPConfig struct { - Public publicHTTPConfig `yaml:"public"` + Public publicHTTPConfig `yaml:"public,omitempty"` } type publicHTTPConfig struct { - Certificates []string `yaml:"certificates"` + Certificates []string `yaml:"certificates,omitempty"` } diff --git a/internal/pkg/template/env.go b/internal/pkg/template/env.go index 7a4f43c1f72..3a4e24ef079 100644 --- a/internal/pkg/template/env.go +++ b/internal/pkg/template/env.go @@ -46,6 +46,7 @@ type EnvOpts struct { Telemetry *Telemetry LatestVersion string + Manifest string // Serialized manifest used to render the environment template. } type VPCConfig struct { diff --git a/internal/pkg/template/templates/environment/cf.yml b/internal/pkg/template/templates/environment/cf.yml index f782da54f2c..d6398e4d4bf 100644 --- a/internal/pkg/template/templates/environment/cf.yml +++ b/internal/pkg/template/templates/environment/cf.yml @@ -3,6 +3,10 @@ Description: CloudFormation environment template for infrastructure shared among Copilot workloads. Metadata: Version: {{ .LatestVersion }} +{{- if .Manifest }} + Manifest: | +{{indent 4 .Manifest}} +{{- end}} Parameters: AppName: Type: String diff --git a/internal/pkg/template/templates/environment/partials/lambdas.yml b/internal/pkg/template/templates/environment/partials/lambdas.yml index 09ab27bd689..79832f3aac7 100644 --- a/internal/pkg/template/templates/environment/partials/lambdas.yml +++ b/internal/pkg/template/templates/environment/partials/lambdas.yml @@ -1,4 +1,3 @@ -# DNS Delegation Resources CertificateValidationFunction: Type: AWS::Lambda::Function Condition: DelegateDNS