Skip to content

Commit

Permalink
feat(event-targets): add support for fargate/awsvpc tasks (#2707)
Browse files Browse the repository at this point in the history
The target is "enhanced" using an AwsCustomResource calling `CloudWatchEvents.putTargets`.

Fix wrong reference to container name in `containerOverrides` (must be `name`).

BREAKING CHANGE: `targets.EcsEc2Task` renamed to `targets.EcsTask`
  • Loading branch information
jogold authored and rix0rrr committed Jun 3, 2019
1 parent ae4a04f commit 2754dde
Show file tree
Hide file tree
Showing 13 changed files with 1,508 additions and 191 deletions.
Expand Up @@ -98,7 +98,7 @@ export class ScheduledEc2Task extends cdk.Construct {
});

// Use Ec2TaskEventRuleTarget as the target of the EventRule
const eventRuleTarget = new eventsTargets.EcsEc2Task( {
const eventRuleTarget = new eventsTargets.EcsTask( {
cluster: props.cluster,
taskDefinition,
taskCount: props.desiredTaskCount
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ecs/README.md
Expand Up @@ -287,7 +287,7 @@ you can configure on your instances.
## Integration with CloudWatch Events

To start an Amazon ECS task on an Amazon EC2-backed Cluster, instantiate an
`@aws-cdk/aws-events-targets.EcsEc2Task` instead of an `Ec2Service`:
`@aws-cdk/aws-events-targets.EcsTask` instead of an `Ec2Service`:

```ts
import targets = require('@aws-cdk/aws-events-targets');
Expand Down
125 changes: 0 additions & 125 deletions packages/@aws-cdk/aws-events-targets/lib/ecs-ec2-task.ts

This file was deleted.

Expand Up @@ -55,4 +55,4 @@ export interface TaskEnvironmentVariable {
* Exactly one of `value` and `valuePath` must be specified.
*/
readonly value: string;
}
}
170 changes: 170 additions & 0 deletions packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts
@@ -0,0 +1,170 @@
import cloudformation = require('@aws-cdk/aws-cloudformation');
import ec2 = require('@aws-cdk/aws-ec2');
import ecs = require('@aws-cdk/aws-ecs');
import events = require ('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import { ContainerOverride } from './ecs-task-properties';
import { singletonEventRole } from './util';

/**
* Properties to define an ECS Event Task
*/
export interface EcsTaskProps {
/**
* Cluster where service will be deployed
*/
readonly cluster: ecs.ICluster;

/**
* Task Definition of the task that should be started
*/
readonly taskDefinition: ecs.TaskDefinition;

/**
* How many tasks should be started when this event is triggered
*
* @default 1
*/
readonly taskCount?: number;

/**
* Container setting overrides
*
* Key is the name of the container to override, value is the
* values you want to override.
*/
readonly containerOverrides?: ContainerOverride[];

/**
* In what subnets to place the task's ENIs
*
* (Only applicable in case the TaskDefinition is configured for AwsVpc networking)
*
* @default Private subnets
*/
readonly subnetSelection?: ec2.SubnetSelection;

/**
* Existing security group to use for the task's ENIs
*
* (Only applicable in case the TaskDefinition is configured for AwsVpc networking)
*
* @default A new security group is created
*/
readonly securityGroup?: ec2.ISecurityGroup;
}

/**
* Start a task on an ECS cluster
*/
export class EcsTask implements events.IRuleTarget {
public readonly securityGroup?: ec2.ISecurityGroup;
private readonly cluster: ecs.ICluster;
private readonly taskDefinition: ecs.TaskDefinition;
private readonly taskCount: number;

constructor(private readonly props: EcsTaskProps) {
this.cluster = props.cluster;
this.taskDefinition = props.taskDefinition;
this.taskCount = props.taskCount !== undefined ? props.taskCount : 1;

if (this.taskDefinition.networkMode === ecs.NetworkMode.AwsVpc) {
this.securityGroup = props.securityGroup || new ec2.SecurityGroup(this.taskDefinition, 'SecurityGroup', { vpc: this.props.cluster.vpc });
}
}

/**
* Allows using tasks as target of CloudWatch events
*/
public bind(rule: events.IRule): events.RuleTargetProperties {
const policyStatements = [new iam.PolicyStatement()
.addAction('ecs:RunTask')
.addResource(this.taskDefinition.taskDefinitionArn)
.addCondition('ArnEquals', { "ecs:cluster": this.cluster.clusterArn })
];

// If it so happens that a Task Execution Role was created for the TaskDefinition,
// then the CloudWatch Events Role must have permissions to pass it (otherwise it doesn't).
if (this.taskDefinition.executionRole !== undefined) {
policyStatements.push(new iam.PolicyStatement()
.addAction('iam:PassRole')
.addResource(this.taskDefinition.executionRole.roleArn));
}

// For Fargate task we need permission to pass the task role.
if (this.taskDefinition.isFargateCompatible) {
policyStatements.push(new iam.PolicyStatement()
.addAction('iam:PassRole')
.addResource(this.taskDefinition.taskRole.roleArn));
}

const id = this.taskDefinition.node.id + '-on-' + this.cluster.node.id;
const arn = this.cluster.clusterArn;
const role = singletonEventRole(this.taskDefinition, policyStatements);
const containerOverrides = this.props.containerOverrides && this.props.containerOverrides
.map(({ containerName, ...overrides }) => ({ name: containerName, ...overrides }));
const input = { containerOverrides };
const taskCount = this.taskCount;
const taskDefinitionArn = this.taskDefinition.taskDefinitionArn;

// Use a custom resource to "enhance" the target with network configuration
// when using awsvpc network mode.
if (this.taskDefinition.networkMode === ecs.NetworkMode.AwsVpc) {
const subnetSelection = this.props.subnetSelection || { subnetType: ec2.SubnetType.Private };
const assignPublicIp = subnetSelection.subnetType === ec2.SubnetType.Private ? 'DISABLED' : 'ENABLED';

new cloudformation.AwsCustomResource(this.taskDefinition, 'PutTargets', {
// `onCreate´ defaults to `onUpdate` and we don't need an `onDelete` here
// because the rule/target will be owned by CF anyway.
onUpdate: {
service: 'CloudWatchEvents',
apiVersion: '2015-10-07',
action: 'putTargets',
parameters: {
Rule: this.taskDefinition.node.stack.parseArn(rule.ruleArn).resourceName,
Targets: [
{
Arn: arn,
Id: id,
EcsParameters: {
TaskDefinitionArn: taskDefinitionArn,
LaunchType: this.taskDefinition.isEc2Compatible ? 'EC2' : 'FARGATE',
NetworkConfiguration: {
awsvpcConfiguration: {
Subnets: this.props.cluster.vpc.selectSubnets(subnetSelection).subnetIds,
AssignPublicIp: assignPublicIp,
SecurityGroups: this.securityGroup && [this.securityGroup.securityGroupId],
}
},
TaskCount: taskCount,
},
Input: JSON.stringify(input),
RoleArn: role.roleArn
}
]
},
physicalResourceId: id,
},
policyStatements: [ // Cannot use automatic policy statements because we need iam:PassRole
new iam.PolicyStatement()
.addAction('events:PutTargets')
.addResource(rule.ruleArn),
new iam.PolicyStatement()
.addAction('iam:PassRole')
.addResource(role.roleArn)
]
});
}

return {
id,
arn,
role,
ecsParameters: {
taskCount,
taskDefinitionArn
},
input: events.RuleTargetInput.fromObject(input)
};
}
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-events-targets/lib/index.ts
Expand Up @@ -3,5 +3,5 @@ export * from './sns';
export * from './codebuild';
export * from './lambda';
export * from './ecs-task-properties';
export * from './ecs-ec2-task';
export * from './ecs-task';
export * from './state-machine';
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-events-targets/package.json
Expand Up @@ -80,6 +80,7 @@
"pkglint": "^0.33.0"
},
"dependencies": {
"@aws-cdk/aws-cloudformation": "^0.33.0",
"@aws-cdk/aws-codebuild": "^0.33.0",
"@aws-cdk/aws-codepipeline": "^0.33.0",
"@aws-cdk/aws-ec2": "^0.33.0",
Expand All @@ -93,6 +94,7 @@
},
"homepage": "https://github.com/awslabs/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-cloudformation": "^0.33.0",
"@aws-cdk/aws-codebuild": "^0.33.0",
"@aws-cdk/aws-codepipeline": "^0.33.0",
"@aws-cdk/aws-ec2": "^0.33.0",
Expand All @@ -107,4 +109,4 @@
"engines": {
"node": ">= 8.10.0"
}
}
}

0 comments on commit 2754dde

Please sign in to comment.