diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index ac6db2b9fab00..e64ce8b9d349e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -515,7 +515,7 @@ for more details about using CloudFormation in CodePipeline. #### Actions defined by this package -This package defines the following actions: +This package contains the following CloudFormation actions: * **CloudFormationCreateUpdateStackAction** - Deploy a CloudFormation template directly from the pipeline. The indicated stack is created, or updated if it already exists. If the stack is in a failure state, deployment will fail (unless `replaceOnFailure` @@ -657,6 +657,18 @@ const deployStage = pipeline.addStage({ [image definition file]: https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions +#### Deploying ECS applications stored in a separate source code repository + +The idiomatic CDK way of deploying an ECS application is to have your Dockerfiles and your CDK code in the same source code repository, +leveraging [Docker Assets])(https://docs.aws.amazon.com/cdk/latest/guide/assets.html#assets_types_docker), +and use the [CDK Pipelines module](https://docs.aws.amazon.com/cdk/api/latest/docs/pipelines-readme.html). + +However, if you want to deploy a Docker application whose source code is kept in a separate version control repository than the CDK code, +you can use the `TagParameterContainerImage` class from the ECS module. +Here's an example: + +[example ECS pipeline for an application in a separate source code repository](test/integ.pipeline-ecs-separate-source.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/integ.pipeline-ecs-separate-source.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.expected.json new file mode 100644 index 0000000000000..4576cb2ca969c --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.expected.json @@ -0,0 +1,1906 @@ +[ + { + "Resources": { + "EcsDeployRepositoryE7A569C0": { + "Type": "AWS::ECR::Repository", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AppCodeDockerImageBuildAndPushProjectRole991CF4D7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "AppCodeDockerImageBuildAndPushProjectRoleDefaultPolicyDDF56E3F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "AppCodeDockerImageBuildAndPushProject00DD6671" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "AppCodeDockerImageBuildAndPushProject00DD6671" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":report-group/", + { + "Ref": "AppCodeDockerImageBuildAndPushProject00DD6671" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsDeployRepositoryE7A569C0", + "Arn" + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsDeployRepositoryE7A569C0", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AppCodeDockerImageBuildAndPushProjectRoleDefaultPolicyDDF56E3F", + "Roles": [ + { + "Ref": "AppCodeDockerImageBuildAndPushProjectRole991CF4D7" + } + ] + } + }, + "AppCodeDockerImageBuildAndPushProject00DD6671": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "EnvironmentVariables": [ + { + "Name": "REPOSITORY_URI", + "Type": "PLAINTEXT", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "EcsDeployRepositoryE7A569C0", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "EcsDeployRepositoryE7A569C0", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "EcsDeployRepositoryE7A569C0" + } + ] + ] + } + } + ], + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": true, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "AppCodeDockerImageBuildAndPushProjectRole991CF4D7", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"$(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)\",\n \"docker build -t $REPOSITORY_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION .\"\n ]\n },\n \"post_build\": {\n \"commands\": [\n \"docker push $REPOSITORY_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION\",\n \"export imageTag=$CODEBUILD_RESOLVED_SOURCE_VERSION\"\n ]\n }\n },\n \"env\": {\n \"exported-variables\": [\n \"imageTag\"\n ]\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3" + } + }, + "CdkCodeBuildProjectRole6830A58A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CdkCodeBuildProjectRoleDefaultPolicyA531D3BE": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "CdkCodeBuildProject98C8CAB8" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "CdkCodeBuildProject98C8CAB8" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":report-group/", + { + "Ref": "CdkCodeBuildProject98C8CAB8" + }, + "-*" + ] + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CdkCodeBuildProjectRoleDefaultPolicyA531D3BE", + "Roles": [ + { + "Ref": "CdkCodeBuildProjectRole6830A58A" + } + ] + } + }, + "CdkCodeBuildProject98C8CAB8": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "CdkCodeBuildProjectRole6830A58A", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"install\": {\n \"commands\": [\n \"npm install\"\n ]\n },\n \"build\": {\n \"commands\": [\n \"npx cdk synth --verbose\"\n ]\n }\n },\n \"artifacts\": {\n \"base-directory\": \"cdk.out\",\n \"files\": \"**/*\"\n }\n}", + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3" + } + }, + "ArtifactBucket7410C9EF": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AppCodeSourceRepository9F7363A1": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "AppCodeSourceRepository" + } + }, + "AppCodeSourceRepositoryawscdkpipelineecsseparatesourcesCodePipelineDeployingEcsApplicationF8E9E764EventRuleA9D5E83B": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.codecommit" + ], + "resources": [ + { + "Fn::GetAtt": [ + "AppCodeSourceRepository9F7363A1", + "Arn" + ] + } + ], + "detail-type": [ + "CodeCommit Repository State Change" + ], + "detail": { + "event": [ + "referenceCreated", + "referenceUpdated" + ], + "referenceName": [ + "master" + ] + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "CodePipelineDeployingEcsApplication81EB9383" + } + ] + ] + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationEventsRoleEEEA39E5", + "Arn" + ] + } + } + ] + } + }, + "CdkCodeSourceRepositoryF10B9DC6": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "CdkCodeSourceRepository" + } + }, + "CdkCodeSourceRepositoryawscdkpipelineecsseparatesourcesCodePipelineDeployingEcsApplicationF8E9E764EventRule94D5EE4B": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.codecommit" + ], + "resources": [ + { + "Fn::GetAtt": [ + "CdkCodeSourceRepositoryF10B9DC6", + "Arn" + ] + } + ], + "detail-type": [ + "CodeCommit Repository State Change" + ], + "detail": { + "event": [ + "referenceCreated", + "referenceUpdated" + ], + "referenceName": [ + "master" + ] + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "CodePipelineDeployingEcsApplication81EB9383" + } + ] + ] + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationEventsRoleEEEA39E5", + "Arn" + ] + } + } + ] + } + }, + "CodePipelineDeployingEcsApplicationRole138CDC17": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineDeployingEcsApplicationRoleDefaultPolicyDBDC1339": { + "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": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationSourceAppCodeSourceCodePipelineActionRole6D88B36F", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationSourceCdkCodeSourceCodePipelineActionRoleA1E3A5E9", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationBuildAppCodeDockerImageBuildAndPushCodePipelineActionRole9B025737", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationBuildCdkCodeBuildAndSynthCodePipelineActionRole54094521", + "Arn" + ] + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationDeployCFNDeployCodePipelineActionRoleC97FFCE2", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineDeployingEcsApplicationRoleDefaultPolicyDBDC1339", + "Roles": [ + { + "Ref": "CodePipelineDeployingEcsApplicationRole138CDC17" + } + ] + } + }, + "CodePipelineDeployingEcsApplication81EB9383": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationRole138CDC17", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "AppCodeSourceRepository9F7363A1", + "Name" + ] + }, + "BranchName": "master", + "PollForSourceChanges": false + }, + "Name": "AppCodeSource", + "OutputArtifacts": [ + { + "Name": "Artifact_Source_AppCodeSource" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationSourceAppCodeSourceCodePipelineActionRole6D88B36F", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "CdkCodeSourceRepositoryF10B9DC6", + "Name" + ] + }, + "BranchName": "master", + "PollForSourceChanges": false + }, + "Name": "CdkCodeSource", + "OutputArtifacts": [ + { + "Name": "Artifact_Source_CdkCodeSource" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationSourceCdkCodeSourceCodePipelineActionRoleA1E3A5E9", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "AppCodeDockerImageBuildAndPushProject00DD6671" + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_Source_AppCodeSource" + } + ], + "Name": "AppCodeDockerImageBuildAndPush", + "Namespace": "Build_AppCodeDockerImageBuildAndPush_NS", + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationBuildAppCodeDockerImageBuildAndPushCodePipelineActionRole9B025737", + "Arn" + ] + }, + "RunOrder": 1 + }, + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "CdkCodeBuildProject98C8CAB8" + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_Source_CdkCodeSource" + } + ], + "Name": "CdkCodeBuildAndSynth", + "OutputArtifacts": [ + { + "Name": "Artifact_Build_CdkCodeBuildAndSynth" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationBuildCdkCodeBuildAndSynthCodePipelineActionRole54094521", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "CloudFormation", + "Version": "1" + }, + "Configuration": { + "StackName": "SampleEcsStackDeployedFromCodePipeline", + "Capabilities": "CAPABILITY_NAMED_IAM", + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationDeployCFNDeployRole71BFA647", + "Arn" + ] + }, + "ParameterOverrides": "{\"TaskDefinitionAppContainerImageTagParam6DBCD720\":\"#{Build_AppCodeDockerImageBuildAndPush_NS.imageTag}\"}", + "ActionMode": "CREATE_UPDATE", + "TemplatePath": "Artifact_Build_CdkCodeBuildAndSynth::EcsStackDeployedInPipeline.template.json" + }, + "InputArtifacts": [ + { + "Name": "Artifact_Build_CdkCodeBuildAndSynth" + } + ], + "Name": "CFN_Deploy", + "RoleArn": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationDeployCFNDeployCodePipelineActionRoleC97FFCE2", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Deploy" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "ArtifactBucket7410C9EF" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "CodePipelineDeployingEcsApplicationRoleDefaultPolicyDBDC1339", + "CodePipelineDeployingEcsApplicationRole138CDC17" + ] + }, + "CodePipelineDeployingEcsApplicationSourceAppCodeSourceCodePipelineActionRole6D88B36F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineDeployingEcsApplicationSourceAppCodeSourceCodePipelineActionRoleDefaultPolicy9DFF92D6": { + "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": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "AppCodeSourceRepository9F7363A1", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineDeployingEcsApplicationSourceAppCodeSourceCodePipelineActionRoleDefaultPolicy9DFF92D6", + "Roles": [ + { + "Ref": "CodePipelineDeployingEcsApplicationSourceAppCodeSourceCodePipelineActionRole6D88B36F" + } + ] + } + }, + "CodePipelineDeployingEcsApplicationSourceCdkCodeSourceCodePipelineActionRoleA1E3A5E9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineDeployingEcsApplicationSourceCdkCodeSourceCodePipelineActionRoleDefaultPolicyB5DFA55D": { + "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": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CdkCodeSourceRepositoryF10B9DC6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineDeployingEcsApplicationSourceCdkCodeSourceCodePipelineActionRoleDefaultPolicyB5DFA55D", + "Roles": [ + { + "Ref": "CodePipelineDeployingEcsApplicationSourceCdkCodeSourceCodePipelineActionRoleA1E3A5E9" + } + ] + } + }, + "CodePipelineDeployingEcsApplicationEventsRoleEEEA39E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineDeployingEcsApplicationEventsRoleDefaultPolicy19AFD1BD": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "codepipeline:StartPipelineExecution", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "CodePipelineDeployingEcsApplication81EB9383" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineDeployingEcsApplicationEventsRoleDefaultPolicy19AFD1BD", + "Roles": [ + { + "Ref": "CodePipelineDeployingEcsApplicationEventsRoleEEEA39E5" + } + ] + } + }, + "CodePipelineDeployingEcsApplicationBuildAppCodeDockerImageBuildAndPushCodePipelineActionRole9B025737": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineDeployingEcsApplicationBuildAppCodeDockerImageBuildAndPushCodePipelineActionRoleDefaultPolicyE8804DE5": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "AppCodeDockerImageBuildAndPushProject00DD6671", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineDeployingEcsApplicationBuildAppCodeDockerImageBuildAndPushCodePipelineActionRoleDefaultPolicyE8804DE5", + "Roles": [ + { + "Ref": "CodePipelineDeployingEcsApplicationBuildAppCodeDockerImageBuildAndPushCodePipelineActionRole9B025737" + } + ] + } + }, + "CodePipelineDeployingEcsApplicationBuildCdkCodeBuildAndSynthCodePipelineActionRole54094521": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineDeployingEcsApplicationBuildCdkCodeBuildAndSynthCodePipelineActionRoleDefaultPolicy5BE54B75": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CdkCodeBuildProject98C8CAB8", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineDeployingEcsApplicationBuildCdkCodeBuildAndSynthCodePipelineActionRoleDefaultPolicy5BE54B75", + "Roles": [ + { + "Ref": "CodePipelineDeployingEcsApplicationBuildCdkCodeBuildAndSynthCodePipelineActionRole54094521" + } + ] + } + }, + "CodePipelineDeployingEcsApplicationDeployCFNDeployCodePipelineActionRoleC97FFCE2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineDeployingEcsApplicationDeployCFNDeployCodePipelineActionRoleDefaultPolicy39F9A0A0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CodePipelineDeployingEcsApplicationDeployCFNDeployRole71BFA647", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "cloudformation:CreateStack", + "cloudformation:DescribeStack*", + "cloudformation:GetStackPolicy", + "cloudformation:GetTemplate*", + "cloudformation:SetStackPolicy", + "cloudformation:UpdateStack", + "cloudformation:ValidateTemplate" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudformation:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":stack/SampleEcsStackDeployedFromCodePipeline/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineDeployingEcsApplicationDeployCFNDeployCodePipelineActionRoleDefaultPolicy39F9A0A0", + "Roles": [ + { + "Ref": "CodePipelineDeployingEcsApplicationDeployCFNDeployCodePipelineActionRoleC97FFCE2" + } + ] + } + }, + "CodePipelineDeployingEcsApplicationDeployCFNDeployRole71BFA647": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "cloudformation.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "CodePipelineDeployingEcsApplicationDeployCFNDeployRoleDefaultPolicy859D7B9F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ArtifactBucket7410C9EF", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CodePipelineDeployingEcsApplicationDeployCFNDeployRoleDefaultPolicy859D7B9F", + "Roles": [ + { + "Ref": "CodePipelineDeployingEcsApplicationDeployCFNDeployRole71BFA647" + } + ] + } + } + }, + "Outputs": { + "ExportsOutputFnGetAttEcsDeployRepositoryE7A569C0ArnCCACE9DD": { + "Value": { + "Fn::GetAtt": [ + "EcsDeployRepositoryE7A569C0", + "Arn" + ] + }, + "Export": { + "Name": "aws-cdk-pipeline-ecs-separate-sources:ExportsOutputFnGetAttEcsDeployRepositoryE7A569C0ArnCCACE9DD" + } + }, + "ExportsOutputRefEcsDeployRepositoryE7A569C04EC3EB5E": { + "Value": { + "Ref": "EcsDeployRepositoryE7A569C0" + }, + "Export": { + "Name": "aws-cdk-pipeline-ecs-separate-sources:ExportsOutputRefEcsDeployRepositoryE7A569C04EC3EB5E" + } + } + } + }, + { + "Resources": { + "TaskDefinitionTaskRoleFD40A61D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefinitionB36D86D9": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::ImportValue": "aws-cdk-pipeline-ecs-separate-sources:ExportsOutputFnGetAttEcsDeployRepositoryE7A569C0ArnCCACE9DD" + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::ImportValue": "aws-cdk-pipeline-ecs-separate-sources:ExportsOutputFnGetAttEcsDeployRepositoryE7A569C0ArnCCACE9DD" + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Fn::ImportValue": "aws-cdk-pipeline-ecs-separate-sources:ExportsOutputRefEcsDeployRepositoryE7A569C04EC3EB5E" + }, + ":", + { + "Ref": "TaskDefinitionAppContainerImageTagParam6DBCD720" + } + ] + ] + }, + "Name": "AppContainer" + } + ], + "Cpu": "1024", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "TaskDefinitionExecutionRole8D61C2FB", + "Arn" + ] + }, + "Family": "EcsStackDeployedInPipelineTaskDefinition3105C51D", + "Memory": "2048", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefinitionTaskRoleFD40A61D", + "Arn" + ] + } + } + }, + "TaskDefinitionExecutionRole8D61C2FB": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefinitionExecutionRoleDefaultPolicy1F3406F5": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::ImportValue": "aws-cdk-pipeline-ecs-separate-sources:ExportsOutputFnGetAttEcsDeployRepositoryE7A569C0ArnCCACE9DD" + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefinitionExecutionRoleDefaultPolicy1F3406F5", + "Roles": [ + { + "Ref": "TaskDefinitionExecutionRole8D61C2FB" + } + ] + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "EcsStackDeployedInPipeline/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "EcsStackDeployedInPipeline/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "EcsStackDeployedInPipeline/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "EcsStackDeployedInPipeline/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "EcsStackDeployedInPipeline/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "EcsStackDeployedInPipeline/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "EcsStackDeployedInPipeline/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "EcsStackDeployedInPipeline/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "ClusterEB0386A7": { + "Type": "AWS::ECS::Cluster" + }, + "EcsService81FC6EF6": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "ClusterEB0386A7" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EcsServiceSecurityGroup8FDFD52F", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + ] + } + }, + "TaskDefinition": { + "Ref": "TaskDefinitionB36D86D9" + } + } + }, + "EcsServiceSecurityGroup8FDFD52F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "EcsStackDeployedInPipeline/EcsService/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + } + }, + "Parameters": { + "TaskDefinitionAppContainerImageTagParam6DBCD720": { + "Type": "String" + } + } + } +] diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.ts new file mode 100644 index 0000000000000..ddf3a3a5cb93a --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-separate-source.lit.ts @@ -0,0 +1,221 @@ +/// !cdk-integ * + +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecr from '@aws-cdk/aws-ecr'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as codepipeline_actions from '../lib'; + +/** + * This example demonstrates how to create a CodePipeline that deploys an ECS Service + * from a different source repository than the source repository of your CDK code. + * If your application code and your CDK code are in the same repository, + * use the CDK Pipelines module instead of this method. + */ + +/// !show + +/** + * These are the construction properties for {@link EcsAppStack}. + * They extend the standard Stack properties, + * but also require providing the ContainerImage that the service will use. + * That Image will be provided from the Stack containing the CodePipeline. + */ +export interface EcsAppStackProps extends cdk.StackProps { + readonly image: ecs.ContainerImage; +} + +/** + * This is the Stack containing a simple ECS Service that uses the provided ContainerImage. + */ +export class EcsAppStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: EcsAppStackProps) { + super(scope, id, props); + + const taskDefinition = new ecs.TaskDefinition(this, 'TaskDefinition', { + compatibility: ecs.Compatibility.FARGATE, + cpu: '1024', + memoryMiB: '2048', + }); + taskDefinition.addContainer('AppContainer', { + image: props.image, + }); + new ecs.FargateService(this, 'EcsService', { + taskDefinition, + cluster: new ecs.Cluster(this, 'Cluster', { + vpc: new ec2.Vpc(this, 'Vpc', { + maxAzs: 1, + }), + }), + }); + } +} + +/** + * This is the Stack containing the CodePipeline definition that deploys an ECS Service. + */ +export class PipelineStack extends cdk.Stack { + public readonly tagParameterContainerImage: ecs.TagParameterContainerImage; + + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + /* ********** ECS part **************** */ + + // this is the ECR repository where the built Docker image will be pushed + const appEcrRepo = new ecr.Repository(this, 'EcsDeployRepository'); + // the build that creates the Docker image, and pushes it to the ECR repo + const appCodeDockerBuild = new codebuild.PipelineProject(this, 'AppCodeDockerImageBuildAndPushProject', { + environment: { + // we need to run Docker + privileged: true, + }, + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + build: { + commands: [ + // login to ECR first + '$(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)', + // if your application needs any build steps, they would be invoked here + + // build the image, and tag it with the commit hash + // (CODEBUILD_RESOLVED_SOURCE_VERSION is a special environment variable available in CodeBuild) + 'docker build -t $REPOSITORY_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION .', + ], + }, + post_build: { + commands: [ + // push the built image into the ECR repository + 'docker push $REPOSITORY_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION', + // save the declared tag as an environment variable, + // that is then exported below in the 'exported-variables' section as a CodePipeline Variable + 'export imageTag=$CODEBUILD_RESOLVED_SOURCE_VERSION', + ], + }, + }, + env: { + // save the imageTag environment variable as a CodePipeline Variable + 'exported-variables': [ + 'imageTag', + ], + }, + }), + environmentVariables: { + REPOSITORY_URI: { + value: appEcrRepo.repositoryUri, + }, + }, + }); + // needed for `docker push` + appEcrRepo.grantPullPush(appCodeDockerBuild); + // create the ContainerImage used for the ECS application Stack + this.tagParameterContainerImage = new ecs.TagParameterContainerImage(appEcrRepo); + + const cdkCodeBuild = new codebuild.PipelineProject(this, 'CdkCodeBuildProject', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + phases: { + install: { + commands: [ + 'npm install', + ], + }, + build: { + commands: [ + // synthesize the CDK code for the ECS application Stack + 'npx cdk synth --verbose', + ], + }, + }, + artifacts: { + // store the entire Cloud Assembly as the output artifact + 'base-directory': 'cdk.out', + 'files': '**/*', + }, + }), + }); + + /* ********** Pipeline part **************** */ + + const appCodeSourceOutput = new codepipeline.Artifact(); + const cdkCodeSourceOutput = new codepipeline.Artifact(); + const cdkCodeBuildOutput = new codepipeline.Artifact(); + const appCodeBuildAction = new codepipeline_actions.CodeBuildAction({ + actionName: 'AppCodeDockerImageBuildAndPush', + project: appCodeDockerBuild, + input: appCodeSourceOutput, + }); + new codepipeline.Pipeline(this, 'CodePipelineDeployingEcsApplication', { + artifactBucket: new s3.Bucket(this, 'ArtifactBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }), + stages: [ + { + stageName: 'Source', + actions: [ + // this is the Action that takes the source of your application code + new codepipeline_actions.CodeCommitSourceAction({ + actionName: 'AppCodeSource', + repository: new codecommit.Repository(this, 'AppCodeSourceRepository', { repositoryName: 'AppCodeSourceRepository' }), + output: appCodeSourceOutput, + }), + // this is the Action that takes the source of your CDK code + // (which would probably include this Pipeline code as well) + new codepipeline_actions.CodeCommitSourceAction({ + actionName: 'CdkCodeSource', + repository: new codecommit.Repository(this, 'CdkCodeSourceRepository', { repositoryName: 'CdkCodeSourceRepository' }), + output: cdkCodeSourceOutput, + }), + ], + }, + { + stageName: 'Build', + actions: [ + appCodeBuildAction, + new codepipeline_actions.CodeBuildAction({ + actionName: 'CdkCodeBuildAndSynth', + project: cdkCodeBuild, + input: cdkCodeSourceOutput, + outputs: [cdkCodeBuildOutput], + }), + ], + }, + { + stageName: 'Deploy', + actions: [ + new codepipeline_actions.CloudFormationCreateUpdateStackAction({ + actionName: 'CFN_Deploy', + stackName: 'SampleEcsStackDeployedFromCodePipeline', + // this name has to be the same name as used below in the CDK code for the application Stack + templatePath: cdkCodeBuildOutput.atPath('EcsStackDeployedInPipeline.template.json'), + adminPermissions: true, + parameterOverrides: { + // read the tag pushed to the ECR repository from the CodePipeline Variable saved by the application build step, + // and pass it as the CloudFormation Parameter for the tag + [this.tagParameterContainerImage.tagParameterName]: appCodeBuildAction.variable('imageTag'), + }, + }), + ], + }, + ], + }); + } +} + +const app = new cdk.App(); + +// the CodePipeline Stack needs to be created first +const pipelineStack = new PipelineStack(app, 'aws-cdk-pipeline-ecs-separate-sources'); +// we supply the image to the ECS application Stack from the CodePipeline Stack +new EcsAppStack(app, 'EcsStackDeployedInPipeline', { + image: pipelineStack.tagParameterContainerImage, +}); +/// !hide + +app.synth(); diff --git a/packages/@aws-cdk/aws-ecs/lib/images/tag-parameter-container-image.ts b/packages/@aws-cdk/aws-ecs/lib/images/tag-parameter-container-image.ts new file mode 100644 index 0000000000000..de738d3c562e3 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/images/tag-parameter-container-image.ts @@ -0,0 +1,48 @@ +import * as ecr from '@aws-cdk/aws-ecr'; +import * as cdk from '@aws-cdk/core'; +import { ContainerDefinition } from '../container-definition'; +import { ContainerImage, ContainerImageConfig } from '../container-image'; + +/** + * A special type of {@link ContainerImage} that uses an ECR repository for the image, + * but a CloudFormation Parameter for the tag of the image in that repository. + * This allows providing this tag through the Parameter at deploy time, + * for example in a CodePipeline that pushes a new tag of the image to the repository during a build step, + * and then provides that new tag through the CloudFormation Parameter in the deploy step. + * + * @see #tagParameterName + */ +export class TagParameterContainerImage extends ContainerImage { + private readonly repository: ecr.IRepository; + private imageTagParameter?: cdk.CfnParameter; + + public constructor(repository: ecr.IRepository) { + super(); + this.repository = repository; + } + + public bind(scope: cdk.Construct, containerDefinition: ContainerDefinition): ContainerImageConfig { + this.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole()); + const imageTagParameter = new cdk.CfnParameter(scope, 'ImageTagParam'); + this.imageTagParameter = imageTagParameter; + return { + imageName: this.repository.repositoryUriForTag(imageTagParameter.valueAsString), + }; + } + + /** + * Returns the name of the CloudFormation Parameter that represents the tag of the image + * in the ECR repository. + */ + public get tagParameterName(): string { + return cdk.Lazy.string({ + produce: () => { + if (this.imageTagParameter) { + return this.imageTagParameter.logicalId; + } else { + throw new Error('TagParameterContainerImage must be used in a container definition when using tagParameterName'); + } + }, + }); + } +} diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 6c6ebd10bd3c6..7b16d7be07827 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -20,6 +20,7 @@ export * from './linux-parameters'; export * from './images/asset-image'; export * from './images/repository'; export * from './images/ecr'; +export * from './images/tag-parameter-container-image'; export * from './log-drivers/aws-log-driver'; export * from './log-drivers/base-log-driver'; diff --git a/packages/@aws-cdk/aws-ecs/test/images/test.tag-parameter-container-image.ts b/packages/@aws-cdk/aws-ecs/test/images/test.tag-parameter-container-image.ts new file mode 100644 index 0000000000000..2a96c471b1417 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/images/test.tag-parameter-container-image.ts @@ -0,0 +1,25 @@ +import { SynthUtils } from '@aws-cdk/assert'; +import * as ecr from '@aws-cdk/aws-ecr'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as ecs from '../../lib'; + +export = { + 'TagParameter container image': { + 'throws an error when tagParameterName() is used without binding the image'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const repository = new ecr.Repository(stack, 'Repository'); + const tagParameterContainerImage = new ecs.TagParameterContainerImage(repository); + new cdk.CfnOutput(stack, 'Output', { + value: tagParameterContainerImage.tagParameterName, + }); + + test.throws(() => { + SynthUtils.synthesize(stack); + }, /TagParameterContainerImage must be used in a container definition when using tagParameterName/); + + test.done(); + }, + }, +};