diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index 4450229449fdc..34c4ed0939ba8 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -390,6 +390,33 @@ where you will define your Pipeline, and deploy the `lambdaStack` using a CloudFormation CodePipeline Action (see above for a complete example). +#### ECS + +CodePipeline can deploy an ECS service. +The deploy Action receives one input Artifact which contains the [image definition file]: + +```typescript +const deployStage = pipeline.addStage({ + name: 'Deploy', + actions: [ + new codepipeline_actions.EcsDeployAction({ + actionName: 'DeployAction', + service, + // if your file is called imagedefinitions.json, + // use the `inputArtifact` property, + // and leave out the `imageFile` property + inputArtifact: buildAction.outputArtifact, + // if your file name is _not_ imagedefinitions.json, + // use the `imageFile` property, + // and leave out the `inputArtifact` property + imageFile: buildAction.outputArtifact.atPath('imageDef.json'), + }), + ], +}); +``` + +[image definition file]: https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions + #### AWS S3 To use an S3 Bucket as a deployment target in CodePipeline: diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts new file mode 100644 index 0000000000000..7f90a63847a22 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts @@ -0,0 +1,104 @@ +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import ecs = require('@aws-cdk/aws-ecs'); +import iam = require('@aws-cdk/aws-iam'); + +/** + * Construction properties of {@link EcsDeployAction}. + */ +export interface EcsDeployActionProps extends codepipeline.CommonActionProps { + /** + * The input artifact that contains the JSON image definitions file to use for deployments. + * The JSON file is a list of objects, + * each with 2 keys: `name` is the name of the container in the Task Definition, + * and `imageUri` is the Docker image URI you want to update your service with. + * If you use this property, it's assumed the file is called 'imagedefinitions.json'. + * If your build uses a different file, leave this property empty, + * and use the `imageFile` property instead. + * + * @default - one of this property, or `imageFile`, is required + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions + */ + readonly inputArtifact?: codepipeline.Artifact; + + /** + * The name of the JSON image definitions file to use for deployments. + * The JSON file is a list of objects, + * each with 2 keys: `name` is the name of the container in the Task Definition, + * and `imageUri` is the Docker image URI you want to update your service with. + * Use this property if you want to use a different name for this file than the default 'imagedefinitions.json'. + * If you use this property, you don't need to specify the `inputArtifact` property. + * + * @default - one of this property, or `inputArtifact`, is required + * @see https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions + */ + readonly imageFile?: codepipeline.ArtifactPath; + + /** + * The ECS Service to deploy. + */ + readonly service: ecs.BaseService; +} + +/** + * CodePipeline Action to deploy an ECS Service. + */ +export class EcsDeployAction extends codepipeline.DeployAction { + constructor(props: EcsDeployActionProps) { + super({ + ...props, + inputArtifact: determineInputArtifact(props), + provider: 'ECS', + artifactBounds: { + minInputs: 1, + maxInputs: 1, + minOutputs: 0, + maxOutputs: 0, + }, + configuration: { + ClusterName: props.service.clusterName, + ServiceName: props.service.serviceName, + FileName: props.imageFile && props.imageFile.fileName, + }, + }); + } + + protected bind(info: codepipeline.ActionBind): void { + // permissions based on CodePipeline documentation: + // https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services + info.role.addToPolicy(new iam.PolicyStatement() + .addActions( + 'ecs:DescribeServices', + 'ecs:DescribeTaskDefinition', + 'ecs:DescribeTasks', + 'ecs:ListTasks', + 'ecs:RegisterTaskDefinition', + 'ecs:UpdateService', + ) + .addAllResources()); + + info.role.addToPolicy(new iam.PolicyStatement() + .addActions( + 'iam:PassRole', + ) + .addAllResources() + .addCondition('StringEqualsIfExists', { + 'iam:PassedToService': [ + 'ec2.amazonaws.com', + 'ecs-tasks.amazonaws.com', + ], + })); + } +} + +function determineInputArtifact(props: EcsDeployActionProps): codepipeline.Artifact { + if (props.imageFile && props.inputArtifact) { + throw new Error("Exactly one of 'inputArtifact' or 'imageFile' can be provided in the ECS deploy Action"); + } + if (props.imageFile) { + return props.imageFile.artifact; + } + if (props.inputArtifact) { + return props.inputArtifact; + } + throw new Error("Specifying one of 'inputArtifact' or 'imageFile' is required for the ECS deploy Action"); +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts index 6ae1d7f82ea89..6554cd00a64e4 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts @@ -4,6 +4,7 @@ export * from './codebuild/pipeline-actions'; export * from './codecommit/source-action'; export * from './codedeploy/server-deploy-action'; export * from './ecr/source-action'; +export * from './ecs/deploy-action'; export * from './github/source-action'; export * from './jenkins/jenkins-actions'; export * from './jenkins/jenkins-provider'; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index d40eb85a820a4..c487120f25f8d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -74,7 +74,9 @@ "@aws-cdk/aws-codecommit": "^0.28.0", "@aws-cdk/aws-codedeploy": "^0.28.0", "@aws-cdk/aws-codepipeline": "^0.28.0", + "@aws-cdk/aws-ec2": "^0.28.0", "@aws-cdk/aws-ecr": "^0.28.0", + "@aws-cdk/aws-ecs": "^0.28.0", "@aws-cdk/aws-events": "^0.28.0", "@aws-cdk/aws-iam": "^0.28.0", "@aws-cdk/aws-lambda": "^0.28.0", @@ -89,7 +91,9 @@ "@aws-cdk/aws-codecommit": "^0.28.0", "@aws-cdk/aws-codedeploy": "^0.28.0", "@aws-cdk/aws-codepipeline": "^0.28.0", + "@aws-cdk/aws-ec2": "^0.28.0", "@aws-cdk/aws-ecr": "^0.28.0", + "@aws-cdk/aws-ecs": "^0.28.0", "@aws-cdk/aws-events": "^0.28.0", "@aws-cdk/aws-iam": "^0.28.0", "@aws-cdk/aws-lambda": "^0.28.0", @@ -100,4 +104,4 @@ "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts new file mode 100644 index 0000000000000..f022ba18d139b --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts @@ -0,0 +1,85 @@ +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import ec2 = require('@aws-cdk/aws-ec2'); +import ecs = require('@aws-cdk/aws-ecs'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import cpactions = require('../../lib'); + +export = { + 'ECS deploy Action': { + 'throws an exception if neither inputArtifact nor imageFile were provided'(test: Test) { + const service = anyEcsService(); + + test.throws(() => { + new cpactions.EcsDeployAction({ + actionName: 'ECS', + service, + }); + }, /one of 'inputArtifact' or 'imageFile' is required/); + + test.done(); + }, + + 'can be created just by specifying the inputArtifact'(test: Test) { + const service = anyEcsService(); + const artifact = new codepipeline.Artifact('Artifact'); + + const action = new cpactions.EcsDeployAction({ + actionName: 'ECS', + service, + inputArtifact: artifact, + }); + + test.equal(action.configuration.FileName, undefined); + + test.done(); + }, + + 'can be created just by specifying the imageFile'(test: Test) { + const service = anyEcsService(); + const artifact = new codepipeline.Artifact('Artifact'); + + const action = new cpactions.EcsDeployAction({ + actionName: 'ECS', + service, + imageFile: artifact.atPath('imageFile.json'), + }); + + test.equal(action.configuration.FileName, 'imageFile.json'); + + test.done(); + }, + + 'throws an exception if both inputArtifact and imageFile were provided'(test: Test) { + const service = anyEcsService(); + const artifact = new codepipeline.Artifact('Artifact'); + + test.throws(() => { + new cpactions.EcsDeployAction({ + actionName: 'ECS', + service, + inputArtifact: artifact, + imageFile: artifact.atPath('file.json'), + }); + }, /one of 'inputArtifact' or 'imageFile' can be provided/); + + test.done(); + }, + }, +}; + +function anyEcsService(): ecs.FargateService { + const stack = new cdk.Stack(); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDefinition'); + taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { + vpc, + }); + return new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json new file mode 100644 index 0000000000000..231618e99c41e --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json @@ -0,0 +1,826 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/17", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/17", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "EcsCluster97242B84": { + "Type": "AWS::ECS::Cluster" + }, + "EcrRepoBB83A592": { + "Type": "AWS::ECR::Repository" + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ecs-tasks.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Links": [], + "LinuxParameters": { + "Capabilities": { + "Add": [], + "Drop": [] + }, + "Devices": [], + "Tmpfs": [] + }, + "MountPoints": [], + "Name": "Container", + "PortMappings": [], + "Ulimits": [], + "VolumesFrom": [] + } + ], + "Cpu": "256", + "Family": "awscdkcodepipelineecsdeployTaskDefCF95BCAC", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "Volumes": [] + } + }, + "FargateServiceAC2B3B85": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "TaskDef54694570" + }, + "Cluster": { + "Ref": "EcsCluster97242B84" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "LaunchType": "FARGATE", + "LoadBalancers": [], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup0A0E79CB", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + ] + } + }, + "ServiceRegistries": [] + } + }, + "FargateServiceSecurityGroup0A0E79CB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-codepipeline-ecs-deploy/FargateService/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + } + }, + "EcsProjectRoleE2F0E9D2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "codebuild.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "EcsProjectRoleDefaultPolicy1A8C91E0": { + "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": "EcsProject54EFDCA6" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "EcsProject54EFDCA6" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcrRepoBB83A592", + "Arn" + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcrRepoBB83A592", + "Arn" + ] + } + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsProjectRoleDefaultPolicy1A8C91E0", + "Roles": [ + { + "Ref": "EcsProjectRoleE2F0E9D2" + } + ] + } + }, + "EcsProject54EFDCA6": { + "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": [ + "EcrRepoBB83A592", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "EcrRepoBB83A592", + "Arn" + ] + } + ] + } + ] + }, + ".amazonaws.com/", + { + "Ref": "EcrRepoBB83A592" + } + ] + ] + } + } + ], + "Image": "aws/codebuild/docker:17.09.0", + "PrivilegedMode": true, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "EcsProjectRoleE2F0E9D2", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"pre_build\": {\n \"commands\": \"$(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)\"\n },\n \"build\": {\n \"commands\": \"docker build -t $REPOSITORY_URI:latest .\"\n },\n \"post_build\": {\n \"commands\": [\n \"docker push $REPOSITORY_URI:latest\",\n \"printf '[{ \\\"name\\\": \\\"Container\\\", \\\"imageUri\\\": \\\"%s\\\" }]' $REPOSITORY_URI:latest > imagedefinitions.json\"\n ]\n }\n },\n \"artifacts\": {\n \"files\": \"imagedefinitions.json\"\n }\n}", + "Type": "CODEPIPELINE" + } + } + }, + "MyPipelineRoleC0D47CA4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "codepipeline.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyPipelineRoleDefaultPolicy34F09EFA": { + "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": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuild", + "codebuild:StopBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsProject54EFDCA6", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:DescribeServices", + "ecs:DescribeTaskDefinition", + "ecs:DescribeTasks", + "ecs:ListTasks", + "ecs:RegisterTaskDefinition", + "ecs:UpdateService" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringEqualsIfExists": { + "iam:PassedToService": [ + "ec2.amazonaws.com", + "ecs-tasks.amazonaws.com" + ] + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyPipelineRoleDefaultPolicy34F09EFA", + "Roles": [ + { + "Ref": "MyPipelineRoleC0D47CA4" + } + ] + } + }, + "MyPipelineAED38ECF": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "MyPipelineRoleC0D47CA4", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "MyBucketF68F3FF0" + }, + "S3ObjectKey": "path/to/Dockerfile" + }, + "InputArtifacts": [], + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "EcsProject54EFDCA6" + } + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "CodeBuild", + "OutputArtifacts": [ + { + "Name": "Artifact_CodeBuild_awscdkcodepipelineecsdeployEcsProject77DC1B55" + } + ], + "RunOrder": 1 + } + ], + "Name": "Build" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "ECS", + "Version": "1" + }, + "Configuration": { + "ClusterName": { + "Ref": "EcsCluster97242B84" + }, + "ServiceName": { + "Fn::GetAtt": [ + "FargateServiceAC2B3B85", + "Name" + ] + } + }, + "InputArtifacts": [ + { + "Name": "Artifact_CodeBuild_awscdkcodepipelineecsdeployEcsProject77DC1B55" + } + ], + "Name": "DeployAction", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "Deploy" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "MyBucketF68F3FF0" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "MyPipelineRoleDefaultPolicy34F09EFA", + "MyPipelineRoleC0D47CA4" + ] + } + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.ts new file mode 100644 index 0000000000000..72539617325de --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.ts @@ -0,0 +1,107 @@ +import codebuild = require('@aws-cdk/aws-codebuild'); +import codepipeline = require('@aws-cdk/aws-codepipeline'); +import ec2 = require('@aws-cdk/aws-ec2'); +import ecr = require('@aws-cdk/aws-ecr'); +import ecs = require('@aws-cdk/aws-ecs'); +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import cpactions = require('../lib'); + +// tslint:disable:object-literal-key-quotes + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-ecs-deploy'); + +const vpc = new ec2.VpcNetwork(stack, 'VPC', { + maxAZs: 1, +}); +const cluster = new ecs.Cluster(stack, "EcsCluster", { + vpc, +}); +const repository = new ecr.Repository(stack, 'EcrRepo'); +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef'); +const containerName = 'Container'; +taskDefinition.addContainer(containerName, { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), +}); +const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, +}); + +const bucket = new s3.Bucket(stack, 'MyBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.Destroy, +}); +const sourceAction = new cpactions.S3SourceAction({ + actionName: 'Source', + outputArtifactName: 'SourceArtifact', + bucket, + bucketKey: 'path/to/Dockerfile', +}); + +const project = new codebuild.PipelineProject(stack, 'EcsProject', { + environment: { + buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_DOCKER_17_09_0, + privileged: true, + }, + buildSpec: { + version: '0.2', + phases: { + pre_build: { + commands: '$(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)', + }, + build: { + commands: 'docker build -t $REPOSITORY_URI:latest .', + }, + post_build: { + commands: [ + 'docker push $REPOSITORY_URI:latest', + `printf '[{ "name": "${containerName}", "imageUri": "%s" }]' $REPOSITORY_URI:latest > imagedefinitions.json`, + ], + }, + }, + artifacts: { + files: 'imagedefinitions.json', + }, + }, + environmentVariables: { + 'REPOSITORY_URI': { + value: repository.repositoryUri, + }, + }, +}); +// needed for `docker push` +repository.grantPullPush(project); +const buildAction = new cpactions.CodeBuildBuildAction({ + actionName: 'CodeBuild', + project, + inputArtifact: sourceAction.outputArtifact, +}); + +new codepipeline.Pipeline(stack, 'MyPipeline', { + artifactBucket: bucket, + stages: [ + { + name: 'Source', + actions: [sourceAction], + }, + { + name: 'Build', + actions: [buildAction], + }, + { + name: 'Deploy', + actions: [ + new cpactions.EcsDeployAction({ + actionName: 'DeployAction', + inputArtifact: buildAction.outputArtifact, + service, + }), + ], + }, + ], +}); + +app.run();