Skip to content

Commit aa68db5

Browse files
authored
feat(aws-ecs): add support for Event Targets (#1571)
EC2 task definitions can now be used as CloudWatch event targets. ALSO IN THIS COMMIT * Improve hash calculation of Docker images. * Add `grantPassRole()` method to iam.Role Fixes #1370.
1 parent 428a812 commit aa68db5

File tree

14 files changed

+1412
-9
lines changed

14 files changed

+1412
-9
lines changed

packages/@aws-cdk/aws-ecs/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,15 @@ autoScalingGroup.scaleOnCpuUtilization('KeepCpuHalfwayLoaded', {
268268
See the `@aws-cdk/aws-autoscaling` library for more autoscaling options
269269
you can configure on your instances.
270270

271+
### Integration with CloudWatch Events
272+
273+
To start an ECS task on an EC2-backed Cluster, instantiate an
274+
`Ec2TaskEventRuleTarget` instead of an `Ec2Service`:
275+
276+
[example of CloudWatch Events integration](test/ec2/integ.event-task.lit.ts)
277+
278+
> Note: it is currently not possible to start Fargate tasks in this way.
279+
271280
### Roadmap
272281

273282
- [ ] Service Discovery Integration

packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ export class TaskDefinition extends cdk.Construct {
133133
*/
134134
public compatibility: Compatibility;
135135

136+
/**
137+
* Execution role for this task definition
138+
*
139+
* May not exist, will be created as needed.
140+
*/
141+
public executionRole?: iam.IRole;
142+
136143
/**
137144
* All containers
138145
*/
@@ -143,13 +150,6 @@ export class TaskDefinition extends cdk.Construct {
143150
*/
144151
private readonly volumes: Volume[] = [];
145152

146-
/**
147-
* Execution role for this task definition
148-
*
149-
* Will be created as needed.
150-
*/
151-
private executionRole?: iam.Role;
152-
153153
/**
154154
* Placement constraints for task instances
155155
*/

packages/@aws-cdk/aws-ecs/lib/cluster.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export class Cluster extends cdk.Construct implements ICluster {
147147
public export(): ClusterImportProps {
148148
return {
149149
clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(),
150+
clusterArn: this.clusterArn,
150151
vpc: this.vpc.export(),
151152
securityGroups: this.connections.securityGroups.map(sg => sg.export()),
152153
hasEc2Capacity: this.hasEc2Capacity,
@@ -233,6 +234,11 @@ export interface ICluster extends cdk.IConstruct {
233234
*/
234235
readonly clusterName: string;
235236

237+
/**
238+
* The ARN of this cluster
239+
*/
240+
readonly clusterArn: string;
241+
236242
/**
237243
* VPC that the cluster instances are running in
238244
*/
@@ -263,6 +269,13 @@ export interface ClusterImportProps {
263269
*/
264270
clusterName: string;
265271

272+
/**
273+
* ARN of the cluster
274+
*
275+
* @default Derived from clusterName
276+
*/
277+
clusterArn?: string;
278+
266279
/**
267280
* VPC that the cluster instances are running in
268281
*/
@@ -290,6 +303,11 @@ class ImportedCluster extends cdk.Construct implements ICluster {
290303
*/
291304
public readonly clusterName: string;
292305

306+
/**
307+
* ARN of the cluster
308+
*/
309+
public readonly clusterArn: string;
310+
293311
/**
294312
* VPC that the cluster instances are running in
295313
*/
@@ -311,6 +329,12 @@ class ImportedCluster extends cdk.Construct implements ICluster {
311329
this.vpc = ec2.VpcNetwork.import(this, "vpc", props.vpc);
312330
this.hasEc2Capacity = props.hasEc2Capacity !== false;
313331

332+
this.clusterArn = props.clusterArn !== undefined ? props.clusterArn : cdk.Stack.find(this).formatArn({
333+
service: 'ecs',
334+
resource: 'cluster',
335+
resourceName: props.clusterName,
336+
});
337+
314338
let i = 1;
315339
for (const sgProps of props.securityGroups) {
316340
this.connections.addSecurityGroup(ec2.SecurityGroup.import(this, `SecurityGroup${i}`, sgProps));
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import events = require ('@aws-cdk/aws-events');
2+
import iam = require('@aws-cdk/aws-iam');
3+
import cdk = require('@aws-cdk/cdk');
4+
import { TaskDefinition } from '../base/task-definition';
5+
import { ICluster } from '../cluster';
6+
import { isEc2Compatible } from '../util';
7+
8+
/**
9+
* Properties to define an EC2 Event Task
10+
*/
11+
export interface Ec2EventRuleTargetProps {
12+
/**
13+
* Cluster where service will be deployed
14+
*/
15+
cluster: ICluster;
16+
17+
/**
18+
* Task Definition of the task that should be started
19+
*/
20+
taskDefinition: TaskDefinition;
21+
22+
/**
23+
* How many tasks should be started when this event is triggered
24+
*
25+
* @default 1
26+
*/
27+
taskCount?: number;
28+
}
29+
30+
/**
31+
* Start a service on an EC2 cluster
32+
*/
33+
export class Ec2EventRuleTarget extends cdk.Construct implements events.IEventRuleTarget {
34+
private readonly cluster: ICluster;
35+
private readonly taskDefinition: TaskDefinition;
36+
private readonly taskCount: number;
37+
38+
constructor(scope: cdk.Construct, id: string, props: Ec2EventRuleTargetProps) {
39+
super(scope, id);
40+
41+
if (!isEc2Compatible(props.taskDefinition.compatibility)) {
42+
throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2');
43+
}
44+
45+
this.cluster = props.cluster;
46+
this.taskDefinition = props.taskDefinition;
47+
this.taskCount = props.taskCount !== undefined ? props.taskCount : 1;
48+
}
49+
50+
/**
51+
* Allows using containers as target of CloudWatch events
52+
*/
53+
public asEventRuleTarget(_ruleArn: string, _ruleUniqueId: string): events.EventRuleTargetProps {
54+
const role = this.eventsRole;
55+
56+
role.addToPolicy(new iam.PolicyStatement()
57+
.addAction('ecs:RunTask')
58+
.addResource(this.taskDefinition.taskDefinitionArn)
59+
.addCondition('ArnEquals', { "ecs:cluster": this.cluster.clusterArn }));
60+
61+
return {
62+
id: this.node.id,
63+
arn: this.cluster.clusterArn,
64+
roleArn: role.roleArn,
65+
ecsParameters: {
66+
taskCount: this.taskCount,
67+
taskDefinitionArn: this.taskDefinition.taskDefinitionArn
68+
}
69+
};
70+
}
71+
72+
/**
73+
* Create or get the IAM Role used to start this Task Definition.
74+
*
75+
* We create it under the TaskDefinition object so that if we have multiple EventTargets
76+
* they can reuse the same role.
77+
*/
78+
public get eventsRole(): iam.IRole {
79+
let role = this.taskDefinition.node.tryFindChild('EventsRole') as iam.IRole;
80+
if (role === undefined) {
81+
role = new iam.Role(this.taskDefinition, 'EventsRole', {
82+
assumedBy: new iam.ServicePrincipal('events.amazonaws.com')
83+
});
84+
}
85+
86+
return role;
87+
}
88+
89+
/**
90+
* Prepare the Event Rule Target
91+
*/
92+
protected prepare() {
93+
// If it so happens that a Task Execution Role was created for the TaskDefinition,
94+
// then the CloudWatch Events Role must have permissions to pass it (otherwise it doesn't).
95+
//
96+
// It never needs permissions to the Task Role.
97+
if (this.taskDefinition.executionRole !== undefined) {
98+
this.taskDefinition.taskRole.grantPassRole(this.eventsRole);
99+
}
100+
}
101+
}

packages/@aws-cdk/aws-ecs/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './cluster';
88

99
export * from './ec2/ec2-service';
1010
export * from './ec2/ec2-task-definition';
11+
export * from './ec2/ec2-event-rule-target';
1112

1213
export * from './fargate/fargate-service';
1314
export * from './fargate/fargate-task-definition';

packages/@aws-cdk/aws-ecs/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"@aws-cdk/aws-cloudwatch": "^0.22.0",
7272
"@aws-cdk/aws-ec2": "^0.22.0",
7373
"@aws-cdk/aws-ecr": "^0.22.0",
74+
"@aws-cdk/aws-events": "^0.22.0",
7475
"@aws-cdk/aws-elasticloadbalancing": "^0.22.0",
7576
"@aws-cdk/aws-elasticloadbalancingv2": "^0.22.0",
7677
"@aws-cdk/aws-iam": "^0.22.0",
@@ -90,6 +91,7 @@
9091
"@aws-cdk/aws-cloudwatch": "^0.22.0",
9192
"@aws-cdk/aws-ec2": "^0.22.0",
9293
"@aws-cdk/aws-ecr": "^0.22.0",
94+
"@aws-cdk/aws-events": "^0.22.0",
9395
"@aws-cdk/aws-elasticloadbalancing": "^0.22.0",
9496
"@aws-cdk/aws-elasticloadbalancingv2": "^0.22.0",
9597
"@aws-cdk/aws-iam": "^0.22.0",

0 commit comments

Comments
 (0)