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(events-targets): Add tagging for ECS tasks triggered by an event #23838

Merged
merged 16 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
38 changes: 34 additions & 4 deletions packages/@aws-cdk/aws-events-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ declare const logGroup: logs.LogGroup;
declare const rule: events.Rule;

rule.addTarget(new targets.CloudWatchLogGroup(logGroup, {
logEvent: targets.LogGroupTargetInput({
timestamp: events.EventField.from('$.time'),
message: events.EventField.from('$.detail-type'),
logEvent: targets.LogGroupTargetInput.fromObject({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Heads up to reviewers: these updates that aren't specifically related to ECS were required because they would not compile when running Rosetta.

timestamp: events.EventField.time,
message: events.EventField.detailType,
}),
}));
```
Expand All @@ -119,7 +119,7 @@ declare const logGroup: logs.LogGroup;
declare const rule: events.Rule;

rule.addTarget(new targets.CloudWatchLogGroup(logGroup, {
logEvent: targets.LogGroupTargetInput({
logEvent: targets.LogGroupTargetInput.fromObject({
message: JSON.stringify({
CustomField: 'CustomValue',
}),
Expand Down Expand Up @@ -345,3 +345,33 @@ rule.addTarget(new targets.EventBus(
),
));
```

## Run an ECS Task

Use the `EcsTask` target to run an ECS Task.

The code snippet below creates a scheduled event rule that will run the task described in `taskDefinition` every hour.

```ts
import * as ecs from "@aws-cdk/aws-ecs"
declare const cluster: ecs.ICluster
declare const taskDefinition: ecs.TaskDefinition

const rule = new events.Rule(this, 'Rule', {
schedule: events.Schedule.rate(cdk.Duration.hours(1)),
});

rule.addTarget(
new targets.EcsTask( {
cluster: cluster,
taskDefinition: taskDefinition,
propagateTags: true,
tagList: [
{
key: 'my-tag',
value: 'my-tag-value',
},
],
})
);
```
39 changes: 38 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ import { Construct } from 'constructs';
import { ContainerOverride } from './ecs-task-properties';
import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util';

/**
* Tag
*/
export interface Tag {

/**
* key to e tagged
*/
readonly key: string;
/**
* additional value
*/
readonly value: string;
}
/**
* Properties to define an ECS Event Task
*/
Expand Down Expand Up @@ -81,6 +95,20 @@ export interface EcsTaskProps extends TargetBaseProps {
* @default - ECS will set the Fargate platform version to 'LATEST'
*/
readonly platformVersion?: ecs.FargatePlatformVersion;

/**
* Specifies whether to propagate the tags from the task definition to the task. If no value is specified, the tags are not propagated.
*
* @default - Tags will not be propagated
*/
readonly propagateTags?: boolean

/**
* The metadata that you apply to the task to help you categorize and organize them. Each tag consists of a key and an optional value, both of which you define.
*
* @default - No tags are applied to the task
*/
readonly tagList?: Tag[]
}

/**
Expand Down Expand Up @@ -108,6 +136,8 @@ export class EcsTask implements events.IRuleTarget {
private readonly taskCount: number;
private readonly role: iam.IRole;
private readonly platformVersion?: ecs.FargatePlatformVersion;
private readonly propagateTags?: ecs.PropagatedTagSource;
private readonly tagList?: Tag[]

constructor(private readonly props: EcsTaskProps) {
if (props.securityGroup !== undefined && props.securityGroups !== undefined) {
Expand All @@ -118,12 +148,17 @@ export class EcsTask implements events.IRuleTarget {
this.taskDefinition = props.taskDefinition;
this.taskCount = props.taskCount ?? 1;
this.platformVersion = props.platformVersion;
this.propagateTags = props.propagateTags === true ? ecs.PropagatedTagSource.TASK_DEFINITION : undefined ;
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 giving me pause. If CloudFormation expects something other than a true or false here, I think we're potentially setting ourselves up for breaking changes if they ever add more allowed values here.

Copy link
Contributor Author

@rdbatch rdbatch Jan 26, 2023

Choose a reason for hiding this comment

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

I can see how this could become problematic even though this is the only allowed value today. I can update this so it takes a value of type ecs.PropagatedTagSource and then validates that you passed the correct value with some helpful messaging. That way if a new source is ever added, the validation can be removed or modified accordingly and existing code won't break.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated!


this.role = props.role ?? singletonEventRole(this.taskDefinition);
for (const stmt of this.createEventRolePolicyStatements()) {
this.role.addToPrincipalPolicy(stmt);
}

if (props.tagList) {
this.tagList = props.tagList;
}

// Security groups are only configurable with the "awsvpc" network mode.
if (this.taskDefinition.networkMode !== ecs.NetworkMode.AWS_VPC) {
if (props.securityGroup !== undefined || props.securityGroups !== undefined) {
Expand Down Expand Up @@ -159,11 +194,13 @@ export class EcsTask implements events.IRuleTarget {
const input = { containerOverrides };
const taskCount = this.taskCount;
const taskDefinitionArn = this.taskDefinition.taskDefinitionArn;
const propagateTags = this.propagateTags;
const tagList = this.tagList;

const subnetSelection = this.props.subnetSelection || { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS };
const assignPublicIp = subnetSelection.subnetType === ec2.SubnetType.PUBLIC ? 'ENABLED' : 'DISABLED';

const baseEcsParameters = { taskCount, taskDefinitionArn };
const baseEcsParameters = { taskCount, taskDefinitionArn, propagateTags, tagList };

const ecsParameters: events.CfnRule.EcsParametersProperty = this.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC
? {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Template } from '@aws-cdk/assertions';
import { Match, Template } from '@aws-cdk/assertions';
import * as autoscaling from '@aws-cdk/aws-autoscaling';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecs from '@aws-cdk/aws-ecs';
Expand Down Expand Up @@ -778,3 +778,83 @@ test('uses the specific fargate platform version', () => {
],
});
});

test('sets the propagate tags flag', () => {
// GIVEN
const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef');
taskDefinition.addContainer('TheContainer', {
image: ecs.ContainerImage.fromRegistry('henk'),
});

const rule = new events.Rule(stack, 'Rule', {
schedule: events.Schedule.expression('rate(1 min)'),
});

// WHEN
rule.addTarget(new targets.EcsTask({
cluster,
taskDefinition,
taskCount: 1,
containerOverrides: [{
containerName: 'TheContainer',
command: ['echo', events.EventField.fromPath('$.detail.event')],
}],
propagateTags: true,
}));

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', {
Targets: [
Match.objectLike({
EcsParameters: Match.objectLike({
PropagateTags: ecs.PropagatedTagSource.TASK_DEFINITION,
}),
}),
],
});
});

test('sets tag lists', () => {
// GIVEN
const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef');
taskDefinition.addContainer('TheContainer', {
image: ecs.ContainerImage.fromRegistry('henk'),
});

const rule = new events.Rule(stack, 'Rule', {
schedule: events.Schedule.expression('rate(1 min)'),
});

// WHEN
rule.addTarget(new targets.EcsTask({
cluster,
taskDefinition,
taskCount: 1,
containerOverrides: [{
containerName: 'TheContainer',
command: ['echo', events.EventField.fromPath('$.detail.event')],
}],
tagList: [
{
key: 'my-tag',
value: 'my-tag-value',
},
],
}));

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', {
Targets: [
Match.objectLike({
EcsParameters: Match.objectLike({
TagList: [
{
Key: 'my-tag',
Value: 'my-tag-value',
},
],
}),
}),
],
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "21.0.0",
"version": "29.0.0",
"files": {
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
"source": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"version": "21.0.0",
"version": "29.0.0",
"files": {
"0872557613b8f4b6c7ef173ce71b7a0f06895bacc709f34d5a62ffabcc0f5700": {
"07b921a20dfe7af7de139cb4595c2f46d90f9625f61d13cff19dffca84918cd7": {
"source": {
"path": "aws-ecs-integ-ecs.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "0872557613b8f4b6c7ef173ce71b7a0f06895bacc709f34d5a62ffabcc0f5700.json",
"objectKey": "07b921a20dfe7af7de139cb4595c2f46d90f9625f61d13cff19dffca84918cd7.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,13 @@
}
},
"EcsParameters": {
"PropagateTags": "TASK_DEFINITION",
"TagList": [
{
"Key": "my-tag",
"Value": "my-tag-value"
}
],
"TaskCount": 1,
"TaskDefinitionArn": {
"Ref": "TaskDef54694570"
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"20.0.0"}
{"version":"29.0.0"}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "21.0.0",
"version": "29.0.0",
"testCases": {
"EcsTest/DefaultTest": {
"stacks": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
{
"version": "21.0.0",
"version": "29.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
"properties": {
"file": "tree.json"
}
},
"aws-ecs-integ-ecs.assets": {
"type": "cdk:asset-manifest",
"properties": {
Expand All @@ -23,7 +17,7 @@
"validateOnSynth": false,
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/0872557613b8f4b6c7ef173ce71b7a0f06895bacc709f34d5a62ffabcc0f5700.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/07b921a20dfe7af7de139cb4595c2f46d90f9625f61d13cff19dffca84918cd7.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
Expand Down Expand Up @@ -340,6 +334,12 @@
]
},
"displayName": "EcsTest/DefaultTest/DeployAssert"
},
"Tree": {
"type": "cdk:tree",
"properties": {
"file": "tree.json"
}
}
}
}
Loading