Skip to content

Commit d46b814

Browse files
aweiherskinny85
authored andcommitted
feat(codepipeline): add ECS deploy Action. (#2050)
Fixes #1386
1 parent bbb3f34 commit d46b814

File tree

7 files changed

+1155
-1
lines changed

7 files changed

+1155
-1
lines changed

packages/@aws-cdk/aws-codepipeline-actions/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,33 @@ where you will define your Pipeline,
390390
and deploy the `lambdaStack` using a CloudFormation CodePipeline Action
391391
(see above for a complete example).
392392
393+
#### ECS
394+
395+
CodePipeline can deploy an ECS service.
396+
The deploy Action receives one input Artifact which contains the [image definition file]:
397+
398+
```typescript
399+
const deployStage = pipeline.addStage({
400+
name: 'Deploy',
401+
actions: [
402+
new codepipeline_actions.EcsDeployAction({
403+
actionName: 'DeployAction',
404+
service,
405+
// if your file is called imagedefinitions.json,
406+
// use the `inputArtifact` property,
407+
// and leave out the `imageFile` property
408+
inputArtifact: buildAction.outputArtifact,
409+
// if your file name is _not_ imagedefinitions.json,
410+
// use the `imageFile` property,
411+
// and leave out the `inputArtifact` property
412+
imageFile: buildAction.outputArtifact.atPath('imageDef.json'),
413+
}),
414+
],
415+
});
416+
```
417+
418+
[image definition file]: https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions
419+
393420
#### AWS S3
394421
395422
To use an S3 Bucket as a deployment target in CodePipeline:
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import codepipeline = require('@aws-cdk/aws-codepipeline');
2+
import ecs = require('@aws-cdk/aws-ecs');
3+
import iam = require('@aws-cdk/aws-iam');
4+
5+
/**
6+
* Construction properties of {@link EcsDeployAction}.
7+
*/
8+
export interface EcsDeployActionProps extends codepipeline.CommonActionProps {
9+
/**
10+
* The input artifact that contains the JSON image definitions file to use for deployments.
11+
* The JSON file is a list of objects,
12+
* each with 2 keys: `name` is the name of the container in the Task Definition,
13+
* and `imageUri` is the Docker image URI you want to update your service with.
14+
* If you use this property, it's assumed the file is called 'imagedefinitions.json'.
15+
* If your build uses a different file, leave this property empty,
16+
* and use the `imageFile` property instead.
17+
*
18+
* @default - one of this property, or `imageFile`, is required
19+
* @see https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions
20+
*/
21+
readonly inputArtifact?: codepipeline.Artifact;
22+
23+
/**
24+
* The name of the JSON image definitions file to use for deployments.
25+
* The JSON file is a list of objects,
26+
* each with 2 keys: `name` is the name of the container in the Task Definition,
27+
* and `imageUri` is the Docker image URI you want to update your service with.
28+
* Use this property if you want to use a different name for this file than the default 'imagedefinitions.json'.
29+
* If you use this property, you don't need to specify the `inputArtifact` property.
30+
*
31+
* @default - one of this property, or `inputArtifact`, is required
32+
* @see https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions
33+
*/
34+
readonly imageFile?: codepipeline.ArtifactPath;
35+
36+
/**
37+
* The ECS Service to deploy.
38+
*/
39+
readonly service: ecs.BaseService;
40+
}
41+
42+
/**
43+
* CodePipeline Action to deploy an ECS Service.
44+
*/
45+
export class EcsDeployAction extends codepipeline.DeployAction {
46+
constructor(props: EcsDeployActionProps) {
47+
super({
48+
...props,
49+
inputArtifact: determineInputArtifact(props),
50+
provider: 'ECS',
51+
artifactBounds: {
52+
minInputs: 1,
53+
maxInputs: 1,
54+
minOutputs: 0,
55+
maxOutputs: 0,
56+
},
57+
configuration: {
58+
ClusterName: props.service.clusterName,
59+
ServiceName: props.service.serviceName,
60+
FileName: props.imageFile && props.imageFile.fileName,
61+
},
62+
});
63+
}
64+
65+
protected bind(info: codepipeline.ActionBind): void {
66+
// permissions based on CodePipeline documentation:
67+
// https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services
68+
info.role.addToPolicy(new iam.PolicyStatement()
69+
.addActions(
70+
'ecs:DescribeServices',
71+
'ecs:DescribeTaskDefinition',
72+
'ecs:DescribeTasks',
73+
'ecs:ListTasks',
74+
'ecs:RegisterTaskDefinition',
75+
'ecs:UpdateService',
76+
)
77+
.addAllResources());
78+
79+
info.role.addToPolicy(new iam.PolicyStatement()
80+
.addActions(
81+
'iam:PassRole',
82+
)
83+
.addAllResources()
84+
.addCondition('StringEqualsIfExists', {
85+
'iam:PassedToService': [
86+
'ec2.amazonaws.com',
87+
'ecs-tasks.amazonaws.com',
88+
],
89+
}));
90+
}
91+
}
92+
93+
function determineInputArtifact(props: EcsDeployActionProps): codepipeline.Artifact {
94+
if (props.imageFile && props.inputArtifact) {
95+
throw new Error("Exactly one of 'inputArtifact' or 'imageFile' can be provided in the ECS deploy Action");
96+
}
97+
if (props.imageFile) {
98+
return props.imageFile.artifact;
99+
}
100+
if (props.inputArtifact) {
101+
return props.inputArtifact;
102+
}
103+
throw new Error("Specifying one of 'inputArtifact' or 'imageFile' is required for the ECS deploy Action");
104+
}

packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './codebuild/pipeline-actions';
44
export * from './codecommit/source-action';
55
export * from './codedeploy/server-deploy-action';
66
export * from './ecr/source-action';
7+
export * from './ecs/deploy-action';
78
export * from './github/source-action';
89
export * from './jenkins/jenkins-actions';
910
export * from './jenkins/jenkins-provider';

packages/@aws-cdk/aws-codepipeline-actions/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@
7474
"@aws-cdk/aws-codecommit": "^0.28.0",
7575
"@aws-cdk/aws-codedeploy": "^0.28.0",
7676
"@aws-cdk/aws-codepipeline": "^0.28.0",
77+
"@aws-cdk/aws-ec2": "^0.28.0",
7778
"@aws-cdk/aws-ecr": "^0.28.0",
79+
"@aws-cdk/aws-ecs": "^0.28.0",
7880
"@aws-cdk/aws-events": "^0.28.0",
7981
"@aws-cdk/aws-iam": "^0.28.0",
8082
"@aws-cdk/aws-lambda": "^0.28.0",
@@ -89,7 +91,9 @@
8991
"@aws-cdk/aws-codecommit": "^0.28.0",
9092
"@aws-cdk/aws-codedeploy": "^0.28.0",
9193
"@aws-cdk/aws-codepipeline": "^0.28.0",
94+
"@aws-cdk/aws-ec2": "^0.28.0",
9295
"@aws-cdk/aws-ecr": "^0.28.0",
96+
"@aws-cdk/aws-ecs": "^0.28.0",
9397
"@aws-cdk/aws-events": "^0.28.0",
9498
"@aws-cdk/aws-iam": "^0.28.0",
9599
"@aws-cdk/aws-lambda": "^0.28.0",
@@ -100,4 +104,4 @@
100104
"engines": {
101105
"node": ">= 8.10.0"
102106
}
103-
}
107+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import codepipeline = require('@aws-cdk/aws-codepipeline');
2+
import ec2 = require('@aws-cdk/aws-ec2');
3+
import ecs = require('@aws-cdk/aws-ecs');
4+
import cdk = require('@aws-cdk/cdk');
5+
import { Test } from 'nodeunit';
6+
import cpactions = require('../../lib');
7+
8+
export = {
9+
'ECS deploy Action': {
10+
'throws an exception if neither inputArtifact nor imageFile were provided'(test: Test) {
11+
const service = anyEcsService();
12+
13+
test.throws(() => {
14+
new cpactions.EcsDeployAction({
15+
actionName: 'ECS',
16+
service,
17+
});
18+
}, /one of 'inputArtifact' or 'imageFile' is required/);
19+
20+
test.done();
21+
},
22+
23+
'can be created just by specifying the inputArtifact'(test: Test) {
24+
const service = anyEcsService();
25+
const artifact = new codepipeline.Artifact('Artifact');
26+
27+
const action = new cpactions.EcsDeployAction({
28+
actionName: 'ECS',
29+
service,
30+
inputArtifact: artifact,
31+
});
32+
33+
test.equal(action.configuration.FileName, undefined);
34+
35+
test.done();
36+
},
37+
38+
'can be created just by specifying the imageFile'(test: Test) {
39+
const service = anyEcsService();
40+
const artifact = new codepipeline.Artifact('Artifact');
41+
42+
const action = new cpactions.EcsDeployAction({
43+
actionName: 'ECS',
44+
service,
45+
imageFile: artifact.atPath('imageFile.json'),
46+
});
47+
48+
test.equal(action.configuration.FileName, 'imageFile.json');
49+
50+
test.done();
51+
},
52+
53+
'throws an exception if both inputArtifact and imageFile were provided'(test: Test) {
54+
const service = anyEcsService();
55+
const artifact = new codepipeline.Artifact('Artifact');
56+
57+
test.throws(() => {
58+
new cpactions.EcsDeployAction({
59+
actionName: 'ECS',
60+
service,
61+
inputArtifact: artifact,
62+
imageFile: artifact.atPath('file.json'),
63+
});
64+
}, /one of 'inputArtifact' or 'imageFile' can be provided/);
65+
66+
test.done();
67+
},
68+
},
69+
};
70+
71+
function anyEcsService(): ecs.FargateService {
72+
const stack = new cdk.Stack();
73+
const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDefinition');
74+
taskDefinition.addContainer('MainContainer', {
75+
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
76+
});
77+
const vpc = new ec2.VpcNetwork(stack, 'VPC');
78+
const cluster = new ecs.Cluster(stack, 'Cluster', {
79+
vpc,
80+
});
81+
return new ecs.FargateService(stack, 'FargateService', {
82+
cluster,
83+
taskDefinition,
84+
});
85+
}

0 commit comments

Comments
 (0)