Skip to content

Commit 3fe58b5

Browse files
authored
feat(scheduler-targets): EcsRunTask scheduler target (#33697)
### Issue # (if applicable) Closes #27456 ### Reason for this change Currently the module supports all templated targets for EventBridge scheduler except for `EcsRunTask`. ### Description of changes - Added new base class `EcsRunTask` with subclasses `EcsRunFargateTask` and `EcsRunEc2Task` depending on where user wants to schedule their ECS task. Decided on this design since some of the parameters in `EcsParameters` only apply one of Fargate or EC2. ### Describe any new or updated permissions being added - Grant `ecs:RunTask` to the schedule execution role for the task definition and `iam:passRole` using existing `grantRun()` method ([docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/CWE_IAM_role.html)) ```ts this.props.taskDefinition.grantRun(role); // TaskDefinition grant method public grantRun(grantee: iam.IGrantable) { grantee.grantPrincipal.addToPrincipalPolicy(this.passRoleStatement); return iam.Grant.addToPrincipal({ grantee, actions: ['ecs:RunTask'], resourceArns: [this.taskDefinitionArn], }); } // passRoleStatement private get passRoleStatement() { if (!this._passRoleStatement) { this._passRoleStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['iam:PassRole'], resources: this.executionRole ? [this.taskRole.roleArn, this.executionRole.roleArn] : [this.taskRole.roleArn], conditions: { StringLike: { 'iam:PassedToService': 'ecs-tasks.amazonaws.com' }, }, }); } return this._passRoleStatement; } ``` - If tags are defined, grant `ecs:TagResource`to the schedule execution tole for tasks in the cluster ```ts if (this.props.propagateTags === true || this.props.tags) { role.addToPrincipalPolicy(new PolicyStatement({ actions: ['ecs:TagResource'], resources: [`arn:${this.cluster.stack.partition}:ecs:${this.cluster.env.region}:${this.props.taskDefinition.env.account}:task/${this.cluster.clusterName}/*`], })); } ``` ### Description of how you validated changes Added unit tests and integration tests with assertions. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent ed5df9c commit 3fe58b5

24 files changed

+77237
-1
lines changed

packages/@aws-cdk/aws-scheduler-targets-alpha/README.md

+38-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ The following targets are supported:
3434
9. `targets.FirehosePutRecord`: [Put a record to an Amazon Data Firehose](#put-a-record-to-an-amazon-data-firehose)
3535
10. `targets.CodePipelineStartPipelineExecution`: [Start a CodePipeline execution](#start-a-codepipeline-execution)
3636
11. `targets.SageMakerStartPipelineExecution`: [Start a SageMaker pipeline execution](#start-a-sagemaker-pipeline-execution)
37-
12. `targets.Universal`: [Invoke a wider set of AWS API](#invoke-a-wider-set-of-aws-api)
37+
12. `targets.EcsRunTask`: [Start a new ECS task](#schedule-an-ecs-task-run)
38+
13. `targets.Universal`: [Invoke a wider set of AWS API](#invoke-a-wider-set-of-aws-api)
3839

3940
## Invoke a Lambda function
4041

@@ -316,6 +317,42 @@ new Schedule(this, 'Schedule', {
316317
});
317318
```
318319

320+
## Schedule an ECS task run
321+
322+
Use the `EcsRunTask` target to schedule an ECS task run for a cluster.
323+
324+
The code snippet below creates an event rule with a Fargate task definition and cluster as the target which is called every hour by EventBridge Scheduler.
325+
326+
```ts
327+
import * as ecs from 'aws-cdk-lib/aws-ecs';
328+
329+
declare const cluster: ecs.ICluster;
330+
declare const taskDefinition: ecs.FargateTaskDefinition;
331+
332+
new Schedule(this, 'Schedule', {
333+
schedule: ScheduleExpression.rate(cdk.Duration.minutes(60)),
334+
target: new targets.EcsRunFargateTask(cluster, {
335+
taskDefinition,
336+
}),
337+
});
338+
```
339+
340+
The code snippet below creates an event rule with a EC2 task definition and cluster as the target which is called every hour by EventBridge Scheduler.
341+
342+
```ts
343+
import * as ecs from 'aws-cdk-lib/aws-ecs';
344+
345+
declare const cluster: ecs.ICluster;
346+
declare const taskDefinition: ecs.Ec2TaskDefinition;
347+
348+
new Schedule(this, 'Schedule', {
349+
schedule: ScheduleExpression.rate(cdk.Duration.minutes(60)),
350+
target: new targets.EcsRunEc2Task(cluster, {
351+
taskDefinition,
352+
}),
353+
});
354+
```
355+
319356
## Invoke a wider set of AWS API
320357

321358
Use the `Universal` target to invoke AWS API. See <https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-targets-universal.html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import { ISchedule, IScheduleTarget, ScheduleTargetConfig } from '@aws-cdk/aws-scheduler-alpha';
2+
import { Lazy, ValidationError } from 'aws-cdk-lib';
3+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
4+
import * as ecs from 'aws-cdk-lib/aws-ecs';
5+
import { IRole, PolicyStatement } from 'aws-cdk-lib/aws-iam';
6+
import { ScheduleTargetBase, ScheduleTargetBaseProps } from './target';
7+
8+
/**
9+
* Metadata that you apply to a resource to help categorize and organize the resource. Each tag consists of a key and an optional value, both of which you define.
10+
*/
11+
export interface Tag {
12+
/**
13+
* Key is the name of the tag
14+
*/
15+
readonly key: string;
16+
/**
17+
* Value is the metadata contents of the tag
18+
*/
19+
readonly value: string;
20+
}
21+
22+
/**
23+
* Parameters for scheduling ECS Run Task (common to EC2 and Fargate launch types).
24+
*/
25+
export interface EcsRunTaskBaseProps extends ScheduleTargetBaseProps {
26+
/**
27+
* The task definition to use for scheduled tasks.
28+
*
29+
* Note: this must be TaskDefinition, and not ITaskDefinition,
30+
* as it requires properties that are not known for imported task definitions
31+
* If you want to run a RunTask with an imported task definition,
32+
* consider using a Universal target.
33+
*/
34+
readonly taskDefinition: ecs.TaskDefinition;
35+
36+
/**
37+
* The capacity provider strategy to use for the task.
38+
*
39+
* @default - No capacity provider strategy
40+
*/
41+
readonly capacityProviderStrategies?: ecs.CapacityProviderStrategy[];
42+
43+
/**
44+
* The subnets associated with the task. These subnets must all be in the same VPC.
45+
* The task will be launched in these subnets.
46+
*
47+
* @default - all private subnets of the VPC are selected.
48+
*/
49+
readonly vpcSubnets?: ec2.SubnetSelection;
50+
51+
/**
52+
* The security groups associated with the task. These security groups must all be in the same VPC.
53+
* Controls inbound and outbound network access for the task.
54+
*
55+
* @default - The security group for the VPC is used.
56+
*/
57+
readonly securityGroups?: ec2.ISecurityGroup[];
58+
59+
/**
60+
* Specifies whether to enable Amazon ECS managed tags for the task.
61+
* @default - false
62+
*/
63+
readonly enableEcsManagedTags?: boolean;
64+
65+
/**
66+
* Whether to enable execute command functionality for the containers in this task.
67+
* If true, this enables execute command functionality on all containers in the task.
68+
*
69+
* @default - false
70+
*/
71+
readonly enableExecuteCommand?: boolean;
72+
73+
/**
74+
* Specifies an ECS task group for the task.
75+
*
76+
* @default - No group
77+
*/
78+
readonly group?: string;
79+
80+
/**
81+
* Specifies whether to propagate the tags from the task definition to the task.
82+
* If no value is specified, the tags are not propagated.
83+
*
84+
* @default - No tag propagation
85+
*/
86+
readonly propagateTags?: boolean;
87+
88+
/**
89+
* The reference ID to use for the task.
90+
*
91+
* @default - No reference ID.
92+
*/
93+
readonly referenceId?: string;
94+
95+
/**
96+
* The metadata that you apply to the task to help you categorize and organize them.
97+
* Each tag consists of a key and an optional value, both of which you define.
98+
*
99+
* @default - No tags
100+
*/
101+
readonly tags?: Tag[];
102+
103+
/**
104+
* The number of tasks to create based on TaskDefinition.
105+
*
106+
* @default 1
107+
*/
108+
readonly taskCount?: number;
109+
110+
}
111+
112+
/**
113+
* Properties for scheduling an ECS Fargate Task.
114+
*/
115+
export interface FargateTaskProps extends EcsRunTaskBaseProps {
116+
/**
117+
* Specifies whether the task's elastic network interface receives a public IP address.
118+
* If true, the task will receive a public IP address and be accessible from the internet.
119+
* Should only be set to true when using public subnets.
120+
*
121+
* @default - true if the subnet type is PUBLIC, otherwise false
122+
*/
123+
readonly assignPublicIp?: boolean;
124+
125+
/**
126+
* Specifies the platform version for the task.
127+
* Specify only the numeric portion of the platform version, such as 1.1.0.
128+
* Platform versions determine the underlying runtime environment for the task.
129+
*
130+
* @default - LATEST
131+
*/
132+
readonly platformVersion?: ecs.FargatePlatformVersion;
133+
}
134+
135+
/**
136+
* Properties for scheduling an ECS Task on EC2.
137+
*/
138+
export interface Ec2TaskProps extends EcsRunTaskBaseProps {
139+
/**
140+
* The rules that must be met in order to place a task on a container instance.
141+
*
142+
* @default - No placement constraints.
143+
*/
144+
readonly placementConstraints?: ecs.PlacementConstraint[];
145+
146+
/**
147+
* The algorithm for selecting container instances for task placement.
148+
*
149+
* @default - No placement strategies.
150+
*/
151+
readonly placementStrategies?: ecs.PlacementStrategy[];
152+
}
153+
154+
/**
155+
* Schedule an ECS Task using AWS EventBridge Scheduler.
156+
*/
157+
export abstract class EcsRunTask extends ScheduleTargetBase implements IScheduleTarget {
158+
constructor(
159+
protected readonly cluster: ecs.ICluster,
160+
protected readonly props: EcsRunTaskBaseProps,
161+
) {
162+
super(props, cluster.clusterArn);
163+
}
164+
165+
protected addTargetActionToRole(role: IRole): void {
166+
// grantRun already adds the necessary PassRole permissions for both task role and execution role
167+
this.props.taskDefinition.grantRun(role);
168+
169+
// Add permissions for tagging if needed
170+
if (this.props.propagateTags === true || this.props.tags) {
171+
role.addToPrincipalPolicy(new PolicyStatement({
172+
actions: ['ecs:TagResource'],
173+
resources: [`arn:${this.cluster.stack.partition}:ecs:${this.cluster.env.region}:${this.props.taskDefinition.env.account}:task/${this.cluster.clusterName}/*`],
174+
}));
175+
}
176+
}
177+
178+
protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig {
179+
return {
180+
...super.bindBaseTargetConfig(_schedule),
181+
ecsParameters: {
182+
taskDefinitionArn: this.props.taskDefinition.taskDefinitionArn,
183+
capacityProviderStrategy: this.props.capacityProviderStrategies,
184+
taskCount: this.props.taskCount,
185+
tags: this.props.tags,
186+
propagateTags: this.props.propagateTags ? ecs.PropagatedTagSource.TASK_DEFINITION : undefined,
187+
enableEcsManagedTags: this.props.enableEcsManagedTags,
188+
enableExecuteCommand: this.props.enableExecuteCommand,
189+
group: this.props.group,
190+
referenceId: this.props.referenceId,
191+
},
192+
};
193+
}
194+
}
195+
196+
/**
197+
* Schedule an ECS Task on Fargate using AWS EventBridge Scheduler.
198+
*/
199+
export class EcsRunFargateTask extends EcsRunTask {
200+
private readonly subnetSelection?: ec2.SubnetSelection;
201+
private readonly assignPublicIp?: boolean;
202+
private readonly platformVersion?: string;
203+
private readonly capacityProviderStrategies?: ecs.CapacityProviderStrategy[];
204+
205+
constructor(
206+
cluster: ecs.ICluster,
207+
props: FargateTaskProps,
208+
) {
209+
super(cluster, props);
210+
this.subnetSelection = props.vpcSubnets;
211+
this.assignPublicIp = props.assignPublicIp;
212+
this.platformVersion = props.platformVersion;
213+
this.capacityProviderStrategies = props.capacityProviderStrategies;
214+
}
215+
216+
protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig {
217+
if (!this.props.taskDefinition.isFargateCompatible) {
218+
throw new ValidationError('TaskDefinition is not compatible with Fargate launch type.', _schedule);
219+
}
220+
221+
const subnetSelection = this.subnetSelection || { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS };
222+
223+
// Throw an error if assignPublicIp is true and the subnet type is not public
224+
if (this.assignPublicIp && subnetSelection.subnetType !== ec2.SubnetType.PUBLIC) {
225+
throw new ValidationError('assignPublicIp should be set to true only for public subnets', _schedule);
226+
}
227+
228+
const assignPublicIp = this.assignPublicIp !== undefined
229+
? (this.assignPublicIp ? 'ENABLED' : 'DISABLED')
230+
: (subnetSelection.subnetType === ec2.SubnetType.PUBLIC ? 'ENABLED' : 'DISABLED');
231+
232+
// Only one of capacityProviderStrategies or launchType can be set
233+
// See https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html#ECS-RunTask-request-launchType
234+
const launchType = this.capacityProviderStrategies ? undefined : ecs.LaunchType.FARGATE;
235+
236+
const bindBaseTargetConfigParameters = super.bindBaseTargetConfig(_schedule).ecsParameters!;
237+
238+
return {
239+
...super.bindBaseTargetConfig(_schedule),
240+
ecsParameters: {
241+
...bindBaseTargetConfigParameters,
242+
launchType,
243+
platformVersion: this.platformVersion,
244+
networkConfiguration: {
245+
awsvpcConfiguration: {
246+
assignPublicIp,
247+
subnets: this.cluster.vpc.selectSubnets(subnetSelection).subnetIds,
248+
securityGroups: (this.props.securityGroups && this.props.securityGroups.length > 0)
249+
?
250+
this.props.securityGroups?.map((sg) => sg.securityGroupId)
251+
: undefined,
252+
},
253+
},
254+
},
255+
};
256+
}
257+
}
258+
259+
/**
260+
* Schedule an ECS Task on EC2 using AWS EventBridge Scheduler.
261+
*/
262+
export class EcsRunEc2Task extends EcsRunTask {
263+
private readonly capacityProviderStrategies?: ecs.CapacityProviderStrategy[];
264+
private readonly placementConstraints?: ecs.PlacementConstraint[];
265+
private readonly placementStrategies?: ecs.PlacementStrategy[];
266+
267+
constructor(
268+
cluster: ecs.ICluster,
269+
props: Ec2TaskProps,
270+
) {
271+
super(cluster, props);
272+
this.placementConstraints = props.placementConstraints;
273+
this.placementStrategies = props.placementStrategies;
274+
this.capacityProviderStrategies = props.capacityProviderStrategies;
275+
}
276+
277+
protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig {
278+
if (this.props.taskDefinition.compatibility === ecs.Compatibility.FARGATE) {
279+
throw new ValidationError('TaskDefinition is not compatible with EC2 launch type', _schedule);
280+
}
281+
282+
// Only one of capacityProviderStrategy or launchType can be set
283+
// See https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html#ECS-RunTask-request-launchType
284+
const launchType = this.capacityProviderStrategies ? undefined : ecs.LaunchType.EC2;
285+
286+
const taskDefinitionUsesAwsVpc = this.props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC;
287+
288+
// Security groups are only configurable with the "awsvpc" network mode.
289+
// See https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html#ECS-RunTask-request-networkConfiguration
290+
if (!taskDefinitionUsesAwsVpc && (this.props.securityGroups || this.props.vpcSubnets)) {
291+
throw new ValidationError('Security groups and subnets can only be used with awsvpc network mode', _schedule);
292+
}
293+
294+
const subnetSelection =
295+
taskDefinitionUsesAwsVpc ? this.props.vpcSubnets || { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }
296+
: undefined;
297+
298+
const bindBaseTargetConfigParameters = super.bindBaseTargetConfig(_schedule).ecsParameters!;
299+
300+
return {
301+
...super.bindBaseTargetConfig(_schedule),
302+
ecsParameters: {
303+
...bindBaseTargetConfigParameters,
304+
launchType,
305+
placementConstraints: Lazy.any({
306+
produce: () => {
307+
// Only map if placementConstraints is defined and has items
308+
return this.placementConstraints?.length
309+
? this.placementConstraints?.map((constraint) => constraint.toJson()).flat()
310+
: undefined;
311+
},
312+
}),
313+
placementStrategy: Lazy.any({
314+
produce: () => {
315+
return this.placementStrategies?.length
316+
? this.placementStrategies?.map((strategy) => strategy.toJson()).flat()
317+
: undefined;
318+
},
319+
}, { omitEmptyArray: true }),
320+
... (taskDefinitionUsesAwsVpc && {
321+
networkConfiguration: {
322+
awsvpcConfiguration: {
323+
subnets: this.cluster.vpc.selectSubnets(subnetSelection).subnetIds,
324+
securityGroups: (this.props.securityGroups && this.props.securityGroups.length > 0)
325+
?
326+
this.props.securityGroups.map((sg) => sg.securityGroupId)
327+
: undefined,
328+
},
329+
},
330+
}),
331+
},
332+
};
333+
}
334+
}

packages/@aws-cdk/aws-scheduler-targets-alpha/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './codebuild-start-build';
22
export * from './codepipeline-start-pipeline-execution';
33
export * from './event-bridge-put-events';
4+
export * from './ecs-run-task';
45
export * from './inspector-start-assessment-run';
56
export * from './firehose-put-record';
67
export * from './kinesis-stream-put-record';

0 commit comments

Comments
 (0)