Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(event-targets): add support for fargate/awsvpc tasks #2707

Merged
merged 3 commits into from
Jun 3, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface ContainerOverride {
/**
* Name of the container inside the task definition
*/
readonly containerName: string;
readonly name: string;
jogold marked this conversation as resolved.
Show resolved Hide resolved

/**
* Command to run inside the container
Expand Down Expand Up @@ -55,4 +55,4 @@ export interface TaskEnvironmentVariable {
* Exactly one of `value` and `valuePath` must be specified.
*/
readonly value: string;
}
}
166 changes: 166 additions & 0 deletions packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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 input = { containerOverrides: this.props.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', {
onUpdate: { // We don't need an onDelete here because the target will be owned by CF anyway
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is confusing to me. Why no onCreate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onCreate defaults to onUpdate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to say I have mixed feelings about this implementation... but I guess this is the point of the CDK :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which implementation, AwsCustomResource or this ECS target?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the fact that it is using a custom resource to achieve its goal.

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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"
}
}
}
Loading