Skip to content

Commit

Permalink
feat(aws-ecs): add support for Event Targets (#1571)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rix0rrr authored Feb 4, 2019
1 parent 428a812 commit aa68db5
Show file tree
Hide file tree
Showing 14 changed files with 1,412 additions and 9 deletions.
9 changes: 9 additions & 0 deletions packages/@aws-cdk/aws-ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,15 @@ autoScalingGroup.scaleOnCpuUtilization('KeepCpuHalfwayLoaded', {
See the `@aws-cdk/aws-autoscaling` library for more autoscaling options
you can configure on your instances.

### Integration with CloudWatch Events

To start an ECS task on an EC2-backed Cluster, instantiate an
`Ec2TaskEventRuleTarget` instead of an `Ec2Service`:

[example of CloudWatch Events integration](test/ec2/integ.event-task.lit.ts)

> Note: it is currently not possible to start Fargate tasks in this way.
### Roadmap

- [ ] Service Discovery Integration
Expand Down
14 changes: 7 additions & 7 deletions packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ export class TaskDefinition extends cdk.Construct {
*/
public compatibility: Compatibility;

/**
* Execution role for this task definition
*
* May not exist, will be created as needed.
*/
public executionRole?: iam.IRole;

/**
* All containers
*/
Expand All @@ -143,13 +150,6 @@ export class TaskDefinition extends cdk.Construct {
*/
private readonly volumes: Volume[] = [];

/**
* Execution role for this task definition
*
* Will be created as needed.
*/
private executionRole?: iam.Role;

/**
* Placement constraints for task instances
*/
Expand Down
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export class Cluster extends cdk.Construct implements ICluster {
public export(): ClusterImportProps {
return {
clusterName: new cdk.Output(this, 'ClusterName', { value: this.clusterName }).makeImportValue().toString(),
clusterArn: this.clusterArn,
vpc: this.vpc.export(),
securityGroups: this.connections.securityGroups.map(sg => sg.export()),
hasEc2Capacity: this.hasEc2Capacity,
Expand Down Expand Up @@ -233,6 +234,11 @@ export interface ICluster extends cdk.IConstruct {
*/
readonly clusterName: string;

/**
* The ARN of this cluster
*/
readonly clusterArn: string;

/**
* VPC that the cluster instances are running in
*/
Expand Down Expand Up @@ -263,6 +269,13 @@ export interface ClusterImportProps {
*/
clusterName: string;

/**
* ARN of the cluster
*
* @default Derived from clusterName
*/
clusterArn?: string;

/**
* VPC that the cluster instances are running in
*/
Expand Down Expand Up @@ -290,6 +303,11 @@ class ImportedCluster extends cdk.Construct implements ICluster {
*/
public readonly clusterName: string;

/**
* ARN of the cluster
*/
public readonly clusterArn: string;

/**
* VPC that the cluster instances are running in
*/
Expand All @@ -311,6 +329,12 @@ class ImportedCluster extends cdk.Construct implements ICluster {
this.vpc = ec2.VpcNetwork.import(this, "vpc", props.vpc);
this.hasEc2Capacity = props.hasEc2Capacity !== false;

this.clusterArn = props.clusterArn !== undefined ? props.clusterArn : cdk.Stack.find(this).formatArn({
service: 'ecs',
resource: 'cluster',
resourceName: props.clusterName,
});

let i = 1;
for (const sgProps of props.securityGroups) {
this.connections.addSecurityGroup(ec2.SecurityGroup.import(this, `SecurityGroup${i}`, sgProps));
Expand Down
101 changes: 101 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/ec2/ec2-event-rule-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import events = require ('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import { TaskDefinition } from '../base/task-definition';
import { ICluster } from '../cluster';
import { isEc2Compatible } from '../util';

/**
* Properties to define an EC2 Event Task
*/
export interface Ec2EventRuleTargetProps {
/**
* Cluster where service will be deployed
*/
cluster: ICluster;

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

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

/**
* Start a service on an EC2 cluster
*/
export class Ec2EventRuleTarget extends cdk.Construct implements events.IEventRuleTarget {
private readonly cluster: ICluster;
private readonly taskDefinition: TaskDefinition;
private readonly taskCount: number;

constructor(scope: cdk.Construct, id: string, props: Ec2EventRuleTargetProps) {
super(scope, id);

if (!isEc2Compatible(props.taskDefinition.compatibility)) {
throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2');
}

this.cluster = props.cluster;
this.taskDefinition = props.taskDefinition;
this.taskCount = props.taskCount !== undefined ? props.taskCount : 1;
}

/**
* Allows using containers as target of CloudWatch events
*/
public asEventRuleTarget(_ruleArn: string, _ruleUniqueId: string): events.EventRuleTargetProps {
const role = this.eventsRole;

role.addToPolicy(new iam.PolicyStatement()
.addAction('ecs:RunTask')
.addResource(this.taskDefinition.taskDefinitionArn)
.addCondition('ArnEquals', { "ecs:cluster": this.cluster.clusterArn }));

return {
id: this.node.id,
arn: this.cluster.clusterArn,
roleArn: role.roleArn,
ecsParameters: {
taskCount: this.taskCount,
taskDefinitionArn: this.taskDefinition.taskDefinitionArn
}
};
}

/**
* Create or get the IAM Role used to start this Task Definition.
*
* We create it under the TaskDefinition object so that if we have multiple EventTargets
* they can reuse the same role.
*/
public get eventsRole(): iam.IRole {
let role = this.taskDefinition.node.tryFindChild('EventsRole') as iam.IRole;
if (role === undefined) {
role = new iam.Role(this.taskDefinition, 'EventsRole', {
assumedBy: new iam.ServicePrincipal('events.amazonaws.com')
});
}

return role;
}

/**
* Prepare the Event Rule Target
*/
protected prepare() {
// 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).
//
// It never needs permissions to the Task Role.
if (this.taskDefinition.executionRole !== undefined) {
this.taskDefinition.taskRole.grantPassRole(this.eventsRole);
}
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ecs/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './cluster';

export * from './ec2/ec2-service';
export * from './ec2/ec2-task-definition';
export * from './ec2/ec2-event-rule-target';

export * from './fargate/fargate-service';
export * from './fargate/fargate-task-definition';
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-ecs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@aws-cdk/aws-cloudwatch": "^0.22.0",
"@aws-cdk/aws-ec2": "^0.22.0",
"@aws-cdk/aws-ecr": "^0.22.0",
"@aws-cdk/aws-events": "^0.22.0",
"@aws-cdk/aws-elasticloadbalancing": "^0.22.0",
"@aws-cdk/aws-elasticloadbalancingv2": "^0.22.0",
"@aws-cdk/aws-iam": "^0.22.0",
Expand All @@ -90,6 +91,7 @@
"@aws-cdk/aws-cloudwatch": "^0.22.0",
"@aws-cdk/aws-ec2": "^0.22.0",
"@aws-cdk/aws-ecr": "^0.22.0",
"@aws-cdk/aws-events": "^0.22.0",
"@aws-cdk/aws-elasticloadbalancing": "^0.22.0",
"@aws-cdk/aws-elasticloadbalancingv2": "^0.22.0",
"@aws-cdk/aws-iam": "^0.22.0",
Expand Down
Loading

0 comments on commit aa68db5

Please sign in to comment.