Skip to content

Commit

Permalink
feat(codepipeline): add ECS deploy Action.
Browse files Browse the repository at this point in the history
Fixes #1386
  • Loading branch information
aweiher authored and skinny85 committed Apr 8, 2019
1 parent 4247966 commit 657411f
Show file tree
Hide file tree
Showing 7 changed files with 1,155 additions and 1 deletion.
27 changes: 27 additions & 0 deletions packages/@aws-cdk/aws-codepipeline-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
104 changes: 104 additions & 0 deletions packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts
Original file line number Diff line number Diff line change
@@ -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");
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 5 additions & 1 deletion packages/@aws-cdk/aws-codepipeline-actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -100,4 +104,4 @@
"engines": {
"node": ">= 8.10.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -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,
});
}
Loading

0 comments on commit 657411f

Please sign in to comment.