diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index ff43850561651..8fdd1fd7b98cc 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -809,6 +809,25 @@ Here's an example: [example ECS pipeline for an application in a separate source code repository](test/integ.pipeline-ecs-separate-source.lit.ts) +#### Deploying to an existing ECS service across account and/or regions + +CodePipeline can deploy to an existing ECS service, even across accounts and/or regions. +This requires importing the ECS service in the pipeline stack using the service's full ARN, +which is then used to determine the correct account and region for the ECS Action. +Here's an example: + +[example pipeline deploying to an existing ECS service cross account and region](test/integ.ecs-pipeline-cross-region-account-existing.lit.ts) + +#### Deploying to a new ECS service across account and/or regions + +CodePipeline can also deploy to a new ECS service across accounts and/or regions. +This can be accomplished by using the `Stack.formatArn` method to save the full ARN of the service, +which can then be used to import the ECS service in the pipeline stack, +and the region and account of the ECS Action will be determined from that ARN. +Here's an example: + +[example pipeline deploying to a new ECS service cross account and region](test/integ.ecs-pipeline-cross-region-account-new.lit.ts) + ### AWS S3 Deployment To use an S3 Bucket as a deployment target in CodePipeline: diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs-pipeline-cross-region-account-helpers.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs-pipeline-cross-region-account-helpers.ts new file mode 100644 index 0000000000000..553d840217ff8 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs-pipeline-cross-region-account-helpers.ts @@ -0,0 +1,39 @@ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cpactions from '../lib'; + +/** + * This is our pipeline which has initial actions. + */ +export class EcsServiceCrossRegionAccountPipelineStack extends cdk.Stack { + public artifact: codepipeline.Artifact; + public pipeline: codepipeline.Pipeline; + + constructor(scope: Construct, id: string, props: cdk.StackProps) { + super(scope, id, props); + + const artifact = new codepipeline.Artifact('Artifact'); + this.artifact = artifact; + const bucket = new s3.Bucket(this, 'PipelineBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + const source = new cpactions.S3SourceAction({ + actionName: 'Source', + output: artifact, + bucket, + bucketKey: 'key', + }); + this.pipeline = new codepipeline.Pipeline(this, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [source], + }, + ], + }); + + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-existing.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-existing.lit.expected.json new file mode 100644 index 0000000000000..93bb93fe5a608 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-existing.lit.expected.json @@ -0,0 +1,1109 @@ +[ + { + "Resources": { + "PipelineBucketB967BD35": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineArtifactsBucketEncryptionKey01D58D69": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-existingecsservicepipelinestackpipelinef8304f40", + "TargetKeyId": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineArtifactsBucket22248F97": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineTestStagePrepareChangesCodePipelineActionRole96F79520", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineTestStageExecuteChangesCodePipelineActionRole92A91B69", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::existingecsservicepipelineplicationbucketaedc716a96aab89ec381" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::existingecsservicepipelineplicationbucketaedc716a96aab89ec381/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::service-account:role/deployrole" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "S3ObjectKey": "key" + }, + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "Artifact" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "TestStage-ecsStack", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineTestStagePrepareChangesRoleB43D8DC1", + "Arn" + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "changeset-TestStage-ecsStack", + "TemplatePath": "Artifact::ExistingEcsServicePipelineStackTestStageecsStack6FB39135.template.json" + }, + "InputArtifacts": [ + { + "Name": "Artifact" + } + ], + "Name": "PrepareChanges", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineTestStagePrepareChangesCodePipelineActionRole96F79520", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "TestStage-ecsStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "changeset-TestStage-ecsStack" + }, + "Name": "ExecuteChanges", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineTestStageExecuteChangesCodePipelineActionRole92A91B69", + "Arn" + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "ECS", + "Version": "1" + }, + "Configuration": { + "ClusterName": "cluster-name", + "ServiceName": "service-name" + }, + "InputArtifacts": [ + { + "Name": "Artifact" + } + ], + "Name": "ECS", + "Region": "service-region", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::service-account:role/deployrole" + ] + ] + }, + "RunOrder": 3 + } + ], + "Name": "TestStage" + } + ], + "ArtifactStores": [ + { + "ArtifactStore": { + "EncryptionKey": { + "Id": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:service-region:pipeline-account:alias/epipelintencryptionalias6398ac74ff79104bcf6e" + ] + ] + }, + "Type": "KMS" + }, + "Location": "existingecsservicepipelineplicationbucketaedc716a96aab89ec381", + "Type": "S3" + }, + "Region": "service-region" + }, + { + "ArtifactStore": { + "EncryptionKey": { + "Id": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + }, + "Type": "KMS" + }, + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + }, + "Region": "pipeline-region" + } + ] + }, + "DependsOn": [ + "PipelineRoleDefaultPolicyC7A05455", + "PipelineRoleD68726F7" + ] + }, + "PipelineSourceCodePipelineActionRoleC6F9E7F5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/key" + ] + ] + } + ] + }, + { + "Action": [ + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Decrypt" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925", + "Roles": [ + { + "Ref": "PipelineSourceCodePipelineActionRoleC6F9E7F5" + } + ] + } + }, + "PipelineTestStagePrepareChangesCodePipelineActionRole96F79520": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineTestStagePrepareChangesCodePipelineActionRoleDefaultPolicy815C349E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineTestStagePrepareChangesRoleB43D8DC1", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + { + "Action": [ + "cloudformation:CreateChangeSet", + "cloudformation:DeleteChangeSet", + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStacks" + ], + "Condition": { + "StringEqualsIfExists": { + "cloudformation:ChangeSetName": "changeset-TestStage-ecsStack" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:pipeline-region:pipeline-account:stack/TestStage-ecsStack/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineTestStagePrepareChangesCodePipelineActionRoleDefaultPolicy815C349E", + "Roles": [ + { + "Ref": "PipelineTestStagePrepareChangesCodePipelineActionRole96F79520" + } + ] + } + }, + "PipelineTestStagePrepareChangesRoleB43D8DC1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineTestStagePrepareChangesRoleDefaultPolicy56424C45": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + { + "Action": "*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineTestStagePrepareChangesRoleDefaultPolicy56424C45", + "Roles": [ + { + "Ref": "PipelineTestStagePrepareChangesRoleB43D8DC1" + } + ] + } + }, + "PipelineTestStageExecuteChangesCodePipelineActionRole92A91B69": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineTestStageExecuteChangesCodePipelineActionRoleDefaultPolicy30A1554D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStacks", + "cloudformation:ExecuteChangeSet" + ], + "Condition": { + "StringEqualsIfExists": { + "cloudformation:ChangeSetName": "changeset-TestStage-ecsStack" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:pipeline-region:pipeline-account:stack/TestStage-ecsStack/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineTestStageExecuteChangesCodePipelineActionRoleDefaultPolicy30A1554D", + "Roles": [ + { + "Ref": "PipelineTestStageExecuteChangesCodePipelineActionRole92A91B69" + } + ] + } + } + } + }, + { + "Resources": { + "CrossRegionCodePipelineReplicationBucketEncryptionKey70216490": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::service-account:role/deployrole" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CrossRegionCodePipelineReplicationBucketEncryptionAliasF1A0F37D": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/epipelintencryptionalias6398ac74ff79104bcf6e", + "TargetKeyId": { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketEncryptionKey70216490", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CrossRegionCodePipelineReplicationBucketFC3227F2": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:service-region:pipeline-account:alias/epipelintencryptionalias6398ac74ff79104bcf6e" + ] + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "BucketName": "existingecsservicepipelineplicationbucketaedc716a96aab89ec381", + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "CrossRegionCodePipelineReplicationBucketPolicyB7BA2BCA": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CrossRegionCodePipelineReplicationBucketFC3227F2" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketFC3227F2", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketFC3227F2", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::service-account:role/deployrole" + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketFC3227F2", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketFC3227F2", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + } + } + } +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-existing.lit.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-existing.lit.ts new file mode 100644 index 0000000000000..f72d164780219 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-existing.lit.ts @@ -0,0 +1,192 @@ +/// !cdk-integ * + +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cpactions from '../lib'; +import { EcsServiceCrossRegionAccountPipelineStack } from './ecs-pipeline-cross-region-account-helpers'; +/** + * This example demonstrates how to create a CodePipeline that deploys to an existing ECS Service across accounts and regions. + * This will not deploy because integ tests only run in one account. + * Updates to this require yarn integ --dry-run integ.ecs-pipeline-cross-region-account-existing.lit.js to generate the expected JSON file. + */ + +/// !show + +const app = new cdk.App(); + +/** + * Deploying to an existing ECS Service using CodePipeline across account(s) and/or region(s). + */ + + +/** + * This is the Stack which will import our existing ECS Service that uses + * the provided clusterName and serviceName. + */ +class ExistingEcsServiceStack extends cdk.Stack { + public readonly serviceArn: string; + public readonly deployRole: iam.IRole; + + constructor(scope: Construct, id: string, props: cdk.StackProps) { + super(scope, id, props); + + // import the existing VPC + const vpc = ec2.Vpc.fromLookup(this, 'VpcLookup', { + isDefault: false, + }); + const clusterName = 'cluster-name'; + const cluster = ecs.Cluster.fromClusterAttributes(this, 'Cluster', { + vpc, + securityGroups: [], + clusterName: clusterName, + }); + const service = ecs.FargateService.fromFargateServiceAttributes(this, 'FargateService', { + serviceName: 'service-name', + cluster: cluster, + }); + /** + * The default serviceArn doesn't include the cluster, as it isn't in the new format. + */ + this.serviceArn = this.formatArn({ + service: 'ecs', + resource: 'service', + resourceName: `${cluster.clusterName}/${service.serviceName}`, + }); + /** + * The deployRole is being looked up from an existing role. + * If you want to create a new role here you can do that however you will need this stack deployed + * before you add the ECSDeployAction to the pipeline. + * + * The role could be created in another pipeline or you could leverage the CDKBootstrap created role. + * + * To leverage the CDKBootstrap created role you would use something like this to define the resourceName. + * 'hnb659fds' is the default bootstrap qualifier if you leverage a different qualifer change that. + * const cdkBootstrapQualifier = 'hnb659fds'; + * const deployRoleName = `cdk-${cdkBootstrapQualifier}-deploy-role-${props.env!.account!}-${props.env!.region!}`; + * + * Ensure the role has permissions to deploy to ECS refer to. + * https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services + */ + const deployArn = this.formatArn({ + region: '', + service: 'iam', + resource: 'role', + resourceName: 'deployrole', + }); + this.deployRole = iam.Role.fromRoleArn(this, 'DeployRole', deployArn); + // Adding ECS Deploy Permissions to deployRole + this.deployRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: [ + 'ecs:DescribeServices', + 'ecs:DescribeTaskDefinition', + 'ecs:DescribeTasks', + 'ecs:ListTasks', + 'ecs:RegisterTaskDefinition', + 'ecs:UpdateService', + ], + resources: ['*'], + })); + + this.deployRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['iam:PassRole'], + resources: ['*'], + conditions: { + StringEqualsIfExists: { + 'iam:PassedToService': [ + 'ec2.amazonaws.com', + 'ecs-tasks.amazonaws.com', + ], + }, + }, + })); + } +} + +/** + * This is the Stage which does our import using {@link ExistingEcsServiceStack}. + */ +class ExistingEcsServiceStage extends cdk.Stage { + public readonly serviceArn: string; + public readonly deployRole: iam.IRole; + public readonly stack: cdk.Stack + + constructor(scope: Construct, id: string, props: cdk.StageProps) { + super(scope, id, props); + + const testStack = new ExistingEcsServiceStack(this, 'ecsStack', {}); + this.stack = testStack; + this.serviceArn = testStack.serviceArn; + this.deployRole = testStack.deployRole; + } +} + +/** + * This is our pipeline which will create an ECS Service and deploy + * to is using {@link EcsDeployAction} using our {@link NewEcsServiceStage} + */ +const pipelineStack = new EcsServiceCrossRegionAccountPipelineStack(app, 'ExistingEcsServicePipelineStack', { + env: { + region: 'pipeline-region', + account: 'pipeline-account', + }, +}); +const pipeline: codepipeline.Pipeline = pipelineStack.pipeline; +const artifact: codepipeline.Artifact = pipelineStack.artifact; + +const testStage = new ExistingEcsServiceStage(pipelineStack, 'TestStage', { + env: { + region: 'service-region', + account: 'service-account', + }, +}); +const stackName = testStage.stack.stackName; +const changeSetName = `changeset-${testStage.stack.stackName}`; +const stageActions = []; +stageActions.push(new cpactions.CloudFormationCreateReplaceChangeSetAction({ + actionName: 'PrepareChanges', + stackName: stackName, + changeSetName: changeSetName, + adminPermissions: true, + templatePath: artifact.atPath(testStage.stack.templateFile), + runOrder: 1, +})); +stageActions.push(new cpactions.CloudFormationExecuteChangeSetAction({ + actionName: 'ExecuteChanges', + stackName: stackName, + changeSetName: changeSetName, + runOrder: 2, +})); + +const testIStage = pipeline.addStage({ + stageName: testStage.stageName, + actions: stageActions, +}); +/** + * This imports the service so it can be used by the ECS Deploy Action. + * This must use the serviceArn so that the region is set correctly. + * The VPC doesn't actually matter and it doesn't get created. + * The construct ids will need to be unique. +*/ +const service = ecs.BaseService.fromServiceArnWithCluster(pipelineStack, 'FargateService', testStage.serviceArn); +/** + * It is highly recommended passing in the role from the stage/stack, if not a new role + * will be added to the pipeline action, will not exist in the account you are deploying to. + * + * Using input however imageFile could be used as well, based on your use case. + */ +const deployAction = new cpactions.EcsDeployAction({ + actionName: 'ECS', + service: service, + input: artifact, + role: testStage.deployRole, + runOrder: 3, +}); +testIStage.addAction(deployAction); + +/// !hide + +app.synth(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-new.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-new.lit.expected.json new file mode 100644 index 0000000000000..f763680b44113 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-new.lit.expected.json @@ -0,0 +1,1109 @@ +[ + { + "Resources": { + "PipelineBucketB967BD35": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineArtifactsBucketEncryptionKey01D58D69": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineArtifactsBucketEncryptionKeyAlias5C510EEE": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-newecsservicepipelinestackpipeline99b88d5b", + "TargetKeyId": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineArtifactsBucket22248F97": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PipelineArtifactsBucketPolicyD4F9712A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineTestStagePrepareChangesCodePipelineActionRole96F79520", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineTestStageExecuteChangesCodePipelineActionRole92A91B69", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::newecsservicepipelinestaceplicationbucketaedc716a1d343700eab8" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::newecsservicepipelinestaceplicationbucketaedc716a1d343700eab8/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::service-account:role/deployrole" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "S3ObjectKey": "key" + }, + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "Artifact" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "TestStage-ecsStack", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineTestStagePrepareChangesRoleB43D8DC1", + "Arn" + ] + }, + "ActionMode": "CHANGE_SET_REPLACE", + "ChangeSetName": "changeset-TestStage-ecsStack", + "TemplatePath": "Artifact::NewEcsServicePipelineStackTestStageecsStackE57BE78B.template.json" + }, + "InputArtifacts": [ + { + "Name": "Artifact" + } + ], + "Name": "PrepareChanges", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineTestStagePrepareChangesCodePipelineActionRole96F79520", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "TestStage-ecsStack", + "ActionMode": "CHANGE_SET_EXECUTE", + "ChangeSetName": "changeset-TestStage-ecsStack" + }, + "Name": "ExecuteChanges", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineTestStageExecuteChangesCodePipelineActionRole92A91B69", + "Arn" + ] + }, + "RunOrder": 2 + }, + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "ECS", + "Version": "1" + }, + "Configuration": { + "ClusterName": "teststage-ecsstackeecsstackcluster331c7d387248ac9d21ad", + "ServiceName": "teststage-ecsstackckfargateservicefb3c60b42c6b80ed8e7b" + }, + "InputArtifacts": [ + { + "Name": "Artifact" + } + ], + "Name": "ECS", + "Region": "service-region", + "RoleArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::service-account:role/deployrole" + ] + ] + }, + "RunOrder": 3 + } + ], + "Name": "TestStage" + } + ], + "ArtifactStores": [ + { + "ArtifactStore": { + "EncryptionKey": { + "Id": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:service-region:pipeline-account:alias/linestactencryptionalias6398ac74d0eaa87e2093" + ] + ] + }, + "Type": "KMS" + }, + "Location": "newecsservicepipelinestaceplicationbucketaedc716a1d343700eab8", + "Type": "S3" + }, + "Region": "service-region" + }, + { + "ArtifactStore": { + "EncryptionKey": { + "Id": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + }, + "Type": "KMS" + }, + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + }, + "Region": "pipeline-region" + } + ] + }, + "DependsOn": [ + "PipelineRoleDefaultPolicyC7A05455", + "PipelineRoleD68726F7" + ] + }, + "PipelineSourceCodePipelineActionRoleC6F9E7F5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/key" + ] + ] + } + ] + }, + { + "Action": [ + "s3:DeleteObject*", + "s3:PutObject", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Decrypt" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925", + "Roles": [ + { + "Ref": "PipelineSourceCodePipelineActionRoleC6F9E7F5" + } + ] + } + }, + "PipelineTestStagePrepareChangesCodePipelineActionRole96F79520": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineTestStagePrepareChangesCodePipelineActionRoleDefaultPolicy815C349E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineTestStagePrepareChangesRoleB43D8DC1", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + { + "Action": [ + "cloudformation:CreateChangeSet", + "cloudformation:DeleteChangeSet", + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStacks" + ], + "Condition": { + "StringEqualsIfExists": { + "cloudformation:ChangeSetName": "changeset-TestStage-ecsStack" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:pipeline-region:pipeline-account:stack/TestStage-ecsStack/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineTestStagePrepareChangesCodePipelineActionRoleDefaultPolicy815C349E", + "Roles": [ + { + "Ref": "PipelineTestStagePrepareChangesCodePipelineActionRole96F79520" + } + ] + } + }, + "PipelineTestStagePrepareChangesRoleB43D8DC1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineTestStagePrepareChangesRoleDefaultPolicy56424C45": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineArtifactsBucketEncryptionKey01D58D69", + "Arn" + ] + } + }, + { + "Action": "*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineTestStagePrepareChangesRoleDefaultPolicy56424C45", + "Roles": [ + { + "Ref": "PipelineTestStagePrepareChangesRoleB43D8DC1" + } + ] + } + }, + "PipelineTestStageExecuteChangesCodePipelineActionRole92A91B69": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineTestStageExecuteChangesCodePipelineActionRoleDefaultPolicy30A1554D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStacks", + "cloudformation:ExecuteChangeSet" + ], + "Condition": { + "StringEqualsIfExists": { + "cloudformation:ChangeSetName": "changeset-TestStage-ecsStack" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:pipeline-region:pipeline-account:stack/TestStage-ecsStack/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineTestStageExecuteChangesCodePipelineActionRoleDefaultPolicy30A1554D", + "Roles": [ + { + "Ref": "PipelineTestStageExecuteChangesCodePipelineActionRole92A91B69" + } + ] + } + } + } + }, + { + "Resources": { + "CrossRegionCodePipelineReplicationBucketEncryptionKey70216490": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::pipeline-account:root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::service-account:role/deployrole" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CrossRegionCodePipelineReplicationBucketEncryptionAliasF1A0F37D": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/linestactencryptionalias6398ac74d0eaa87e2093", + "TargetKeyId": { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketEncryptionKey70216490", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CrossRegionCodePipelineReplicationBucketFC3227F2": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:service-region:pipeline-account:alias/linestactencryptionalias6398ac74d0eaa87e2093" + ] + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "BucketName": "newecsservicepipelinestaceplicationbucketaedc716a1d343700eab8", + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "CrossRegionCodePipelineReplicationBucketPolicyB7BA2BCA": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CrossRegionCodePipelineReplicationBucketFC3227F2" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketFC3227F2", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketFC3227F2", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::service-account:role/deployrole" + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketFC3227F2", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CrossRegionCodePipelineReplicationBucketFC3227F2", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + } + } + } +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-new.lit.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-new.lit.ts new file mode 100644 index 0000000000000..44c1fa15b0d05 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.ecs-pipeline-cross-region-account-new.lit.ts @@ -0,0 +1,202 @@ +/// !cdk-integ * + +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as cpactions from '../lib'; +import { EcsServiceCrossRegionAccountPipelineStack } from './ecs-pipeline-cross-region-account-helpers'; + +/** + * This example demonstrates how to create a CodePipeline that deploys to a new ECS Service across + * accounts and regions. + * This will not deploy because integ tests only run in one account. + * Updates to this require yarn integ --dry-run integ.ecs-pipeline-cross-region-account-new.lit.js to generate the expected JSON file. + */ + +/// !show + +const app = new cdk.App(); + +/** + * This is the Stack which will create an ECS Service and gets deployed to. + */ +class NewEcsServiceStack extends cdk.Stack { + public readonly serviceArn: string; + public readonly deployRole: iam.IRole; + + constructor(scope: Construct, id: string, props: cdk.StackProps) { + super(scope, id, props); + + const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition'); + taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + /** + * We are creating the VPC here, if you have VPC already then look it up. + */ + const vpc = new ec2.Vpc(this, 'VPC'); + /** + * The clusterName must be defined. + */ + const cluster = new ecs.Cluster(this, 'Cluster', { + clusterName: cdk.PhysicalName.GENERATE_IF_NEEDED, + vpc, + }); + /** + * The serviceName must be defined. + */ + const service = new ecs.FargateService(this, 'FargateService', { + cluster, + taskDefinition, + serviceName: cdk.PhysicalName.GENERATE_IF_NEEDED, + }); + /** + * Defining the serviceArn using formatArn is required, leveraging service.serviceArn results in a token. + * The token causes in the ECS Deploy Action to not be able to determine region. + * Using the formatArn produces a ARN which only makes the resourceName as a token, which allows + * the ECS Deploy Action to determine the region and account you are deploying to. + * https://github.com/aws/aws-cdk/pull/18382 addresses the serviceArn not being a token. + */ + this.serviceArn = this.formatArn({ + service: 'ecs', + resource: 'service', + resourceName: `${cluster.clusterName}/${service.serviceName}`, + }); + /** + * The deployRole is being looked up from an existing role. + * If you want to create a new role here you can do that however you will need this stack deployed + * before you add the ECSDeployAction to the pipeline. + * + * The role could be created in another pipeline or you could leverage the CDKBootstrap created role. + * + * To leverage the CDKBootstrap created role you would use something like this to define the resourceName. + * 'hnb659fds' is the default bootstrap qualifier if you leverage a different qualifer change that. + * const cdkBootstrapQualifier = 'hnb659fds'; + * const deployRoleName = `cdk-${cdkBootstrapQualifier}-deploy-role-${props.env!.account!}-${props.env!.region!}`; + * + * Ensure the role has permissions to deploy to ECS refer to. + * https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services + */ + const deployArn = this.formatArn({ + region: '', + service: 'iam', + resource: 'role', + resourceName: 'deployrole', + }); + this.deployRole = iam.Role.fromRoleArn(this, 'DeployRole', deployArn); + // Adding ECS Deploy Permissions to deployRole + this.deployRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: [ + 'ecs:DescribeServices', + 'ecs:DescribeTaskDefinition', + 'ecs:DescribeTasks', + 'ecs:ListTasks', + 'ecs:RegisterTaskDefinition', + 'ecs:UpdateService', + ], + resources: ['*'], + })); + + this.deployRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['iam:PassRole'], + resources: ['*'], + conditions: { + StringEqualsIfExists: { + 'iam:PassedToService': [ + 'ec2.amazonaws.com', + 'ecs-tasks.amazonaws.com', + ], + }, + }, + })); + } +} + +/** + * This is the Stage which does our create using {@link NewEcsServiceStack}. + */ +class NewEcsServiceStage extends cdk.Stage { + public readonly serviceArn: string; + public readonly deployRole: iam.IRole; + public readonly stack: cdk.Stack; + + constructor(scope: Construct, id: string, props: cdk.StageProps) { + super(scope, id, props); + + const testStack = new NewEcsServiceStack(this, 'ecsStack', {}); + this.stack = testStack; + this.serviceArn = testStack.serviceArn; + this.deployRole = testStack.deployRole; + } +} + +/** + * This is our pipeline which will create an ECS Service and deploy + * to is using {@link EcsDeployAction} using our {@link NewEcsServiceStage} + */ +const pipelineStack = new EcsServiceCrossRegionAccountPipelineStack(app, 'NewEcsServicePipelineStack', { + env: { + region: 'pipeline-region', + account: 'pipeline-account', + }, +}); +const pipeline: codepipeline.Pipeline = pipelineStack.pipeline; +const artifact: codepipeline.Artifact = pipelineStack.artifact; + +const testStage = new NewEcsServiceStage(pipelineStack, 'TestStage', { + env: { + region: 'service-region', + account: 'service-account', + }, +}); +const stackName = testStage.stack.stackName; +const changeSetName = `changeset-${testStage.stack.stackName}`; +const stageActions = []; +stageActions.push(new cpactions.CloudFormationCreateReplaceChangeSetAction({ + actionName: 'PrepareChanges', + stackName: stackName, + changeSetName: changeSetName, + adminPermissions: true, + templatePath: artifact.atPath(testStage.stack.templateFile), + runOrder: 1, +})); +stageActions.push(new cpactions.CloudFormationExecuteChangeSetAction({ + actionName: 'ExecuteChanges', + stackName: stackName, + changeSetName: changeSetName, + runOrder: 2, +})); + +const testIStage = pipeline.addStage({ + stageName: testStage.stageName, + actions: stageActions, +}); + +/** + * This imports the service so it can be used by the ECS Deploy Action. + * This must use the serviceArn so that the region is set correctly. + * The VPC doesn't actually matter and it doesn't get created. + * The construct ids will need to be unique. + */ +const service = ecs.BaseService.fromServiceArnWithCluster(pipelineStack, 'FargateService', testStage.serviceArn); +/** + * It is highly recommended passing in the role from the stage/stack, if not a new role + * will be added to the pipeline action, will not exist in the account you are deploying to. + * + * Using input however imageFile could be used as well, based on your use case. + */ +const deployAction = new cpactions.EcsDeployAction({ + actionName: 'ECS', + service: service, + input: artifact, + role: testStage.deployRole, + runOrder: 3, +}); +testIStage.addAction(deployAction); + +/// !hide + +app.synth();