From bd88a217efbdbc868e5ac59905d8700fc9dbe34f Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 8 Jun 2020 22:34:31 -0700 Subject: [PATCH 01/27] set up ecs base class --- .../lib/ecs/ec2-run-task.ts | 102 +++++++++ .../lib/ecs/run-task-base.ts | 200 ++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts new file mode 100644 index 0000000000000..3dc64662affdb --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts @@ -0,0 +1,102 @@ +// import * as ec2 from '@aws-cdk/aws-ec2'; +// import * as ecs from '@aws-cdk/aws-ecs'; +// import { EcsRunTaskCommonProps, EcsRunTaskStateBase } from './run-task-base'; + +// /** +// * Properties to run an ECS task on EC2 in Step Functions on ECS +// */ +// export interface EcsEc2RunTaskProps extends EcsRunTaskCommonProps { +// /** +// * In what subnets to place the task's ENIs +// * +// * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) +// * +// * @default - Private Subnets +// */ +// readonly subnets?: 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; + +// /** +// * Placement constraints +// * +// * @default - No constraints +// */ +// readonly placementConstraints?: ecs.PlacementConstraint[]; + +// /** +// * Placement strategies +// * +// * @default - No strategies +// */ +// readonly placementStrategies?: ecs.PlacementStrategy[]; +// } + +// /** +// * Run an ECS/EC2 Task in a StepFunctions workflow +// */ +// export class EcsEc2RunTask extends EcsRunTaskStateBase { +// constructor(props: EcsEc2RunTaskProps) { +// if (!props.taskDefinition.isEc2Compatible) { +// throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); +// } + +// if (!props.cluster.hasEc2Capacity) { +// throw new Error('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); +// } + +// if (!props.taskDefinition.defaultContainer) { +// throw new Error('A TaskDefinition must have at least one essential container'); +// } + +// // super({ +// // ...props, +// // parameters: { +// // LaunchType: 'EC2', +// // PlacementConstraints: noEmpty(flatten((props.placementConstraints || []).map(c => c.toJson().map(uppercaseKeys)))), +// // PlacementStrategy: noEmpty(flatten((props.placementStrategies || []).map(c => c.toJson().map(uppercaseKeys)))), +// // }, +// // }); + +// if (props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { +// this.configureAwsVpcNetworking(props.cluster.vpc, undefined, props.subnets, props.securityGroup); +// } else { +// // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. +// validateNoNetworkingProps(props); +// this.connections.addSecurityGroup(...props.cluster.connections.securityGroups); +// } +// } +// } + +// /** +// * Validate combinations of networking arguments +// */ +// function validateNoNetworkingProps(props: EcsEc2RunTaskProps) { +// if (props.subnets !== undefined || props.securityGroup !== undefined) { +// throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); +// } +// } + +// function uppercaseKeys(obj: {[key: string]: any}): {[key: string]: any} { +// const ret: {[key: string]: any} = {}; +// for (const key of Object.keys(obj)) { +// ret[key.slice(0, 1).toUpperCase() + key.slice(1)] = obj[key]; +// } +// return ret; +// } + +// function flatten(xs: A[][]): A[] { +// return Array.prototype.concat([], ...xs); +// } + +// function noEmpty(xs: A[]): A[] | undefined { +// if (xs.length === 0) { return undefined; } +// return xs; +// } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts new file mode 100644 index 0000000000000..14ae36dc0903c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts @@ -0,0 +1,200 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { ContainerOverride } from '..'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Basic properties for ECS Tasks + */ +export interface EcsRunTaskCommonProps { + /** + * The topic to run the task on + */ + readonly cluster: ecs.ICluster; + + /** + * Task Definition used for running tasks in the service. + * + * Note: this must be TaskDefinition, and not ITaskDefinition, + * as it requires properties that are not known for imported task definitions + */ + readonly taskDefinition: ecs.TaskDefinition; + + /** + * Container setting overrides + * + * Key is the name of the container to override, value is the + * values you want to override. + * + * @default - No overrides + */ + readonly containerOverrides?: ContainerOverride[]; +} + +/** + * Construction properties for the BaseRunTaskProps + */ +export interface EcsRunTaskStateBaseProps extends EcsRunTaskCommonProps, sfn.TaskStateBaseProps { + /** + * Additional parameters to pass to the base task + * + * @default - No additional parameters passed + */ + readonly parameters?: {[key: string]: any}; +} + +/** + * Run a Task on ECS or Fargate + */ +export class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConnectable { + + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + /** + * Manage allowed network traffic for this service + */ + public readonly connections: ec2.Connections = new ec2.Connections(); + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private securityGroup?: ec2.ISecurityGroup; + private networkConfiguration?: any; + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: cdk.Construct, id: string, private readonly props: EcsRunTaskStateBaseProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, EcsRunTaskStateBase.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN + && !sfn.FieldUtils.containsTaskToken(props.containerOverrides)) { + throw new Error('Task Token is required in `containerOverrides` for callback. Use Context.taskToken to set the token.'); + } + + for (const override of this.props.containerOverrides || []) { + const name = override.containerName; + if (!cdk.Token.isUnresolved(name)) { + const cont = this.props.taskDefinition.node.tryFindChild(name); + if (!cont) { + throw new Error(`Overrides mention container with name '${name}', but no such container in task definition`); + } + } + } + + this.taskPolicies = this.makePolicyStatements(); + } + + public renderTask(): any { + if (this.networkConfiguration !== undefined) { + // Make sure we have a security group if we're using AWSVPC networking + this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); + this.connections.addSecurityGroup(this.securityGroup); + } + + return { + Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + Cluster: this.props.cluster.clusterArn, + TaskDefinition: this.props.taskDefinition.taskDefinitionArn, + NetworkConfiguration: this.networkConfiguration, + Overrides: renderOverrides(this.props.containerOverrides), + ...this.props.parameters, + }), + }; + } + + protected configureAwsVpcNetworking( + vpc: ec2.IVpc, + assignPublicIp?: boolean, + subnetSelection?: ec2.SubnetSelection, + securityGroup?: ec2.ISecurityGroup) { + + if (subnetSelection === undefined) { + subnetSelection = { subnetType: assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE }; + } + + // If none is given here, one will be created later on during bind() + this.securityGroup = securityGroup; + + this.networkConfiguration = { + AwsvpcConfiguration: { + AssignPublicIp: assignPublicIp !== undefined ? (assignPublicIp ? 'ENABLED' : 'DISABLED') : undefined, + Subnets: vpc.selectSubnets(subnetSelection).subnetIds, + SecurityGroups: cdk.Lazy.listValue({ produce: () => [this.securityGroup!.securityGroupId] }), + }, + }; + } + + private makePolicyStatements(): iam.PolicyStatement[] { + const stack = cdk.Stack.of(this); + + // https://docs.aws.amazon.com/step-functions/latest/dg/ecs-iam.html + const policyStatements = [ + new iam.PolicyStatement({ + actions: ['ecs:RunTask'], + resources: [this.props.taskDefinition.taskDefinitionArn], + }), + new iam.PolicyStatement({ + actions: ['ecs:StopTask', 'ecs:DescribeTasks'], + resources: ['*'], + }), + new iam.PolicyStatement({ + actions: ['iam:PassRole'], + resources: cdk.Lazy.listValue({ produce: () => this.taskExecutionRoles().map(r => r.roleArn) }), + }), + ]; + + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push(new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForECSTaskRule', + })], + })); + } + + return policyStatements; + } + + private taskExecutionRoles(): iam.IRole[] { + // Need to be able to pass both Task and Execution role, apparently + const ret = new Array(); + ret.push(this.props.taskDefinition.taskRole); + if (this.props.taskDefinition.executionRole) { + ret.push(this.props.taskDefinition.executionRole); + } + return ret; + } +} + +function renderOverrides(containerOverrides?: ContainerOverride[]) { + if (!containerOverrides) { return undefined; } + + const ret = new Array(); + for (const override of containerOverrides) { + ret.push({ + Name: override.containerName, + Command: override.command, + Cpu: override.cpu, + Memory: override.memoryLimit, + MemoryReservation: override.memoryReservation, + Environment: override.environment && override.environment.map(e => ({ + Name: e.name, + Value: e.value, + })), + }); + } + + return { ContainerOverrides: ret }; +} From 277fc95ab03e22fe32529b5341a4ea738be0f74e Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Tue, 9 Jun 2020 01:01:03 -0700 Subject: [PATCH 02/27] wip --- .../lib/ecs/ec2-run-task.ts | 181 ++--- .../lib/ecs/fargate-run-task.ts | 64 ++ .../lib/ecs/run-task-base.ts | 56 +- .../aws-stepfunctions-tasks/lib/index.ts | 3 + .../aws-stepfunctions-tasks/package.json | 5 +- .../test/ecs/integ.ec2-run-task.expected.json | 712 ++++++++++++++++++ .../test/ecs/integ.ec2-run-task.ts | 60 ++ .../ecs/integ.fargate-run-task.expected.json | 298 ++++++++ .../test/ecs/integ.fargate-run-task.ts | 60 ++ .../test/ecs/run-tasks.test.ts | 365 +++++++++ 10 files changed, 1689 insertions(+), 115 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts index 3dc64662affdb..12a03529f344c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts @@ -1,102 +1,105 @@ -// import * as ec2 from '@aws-cdk/aws-ec2'; -// import * as ecs from '@aws-cdk/aws-ecs'; -// import { EcsRunTaskCommonProps, EcsRunTaskStateBase } from './run-task-base'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import { Construct } from '@aws-cdk/core'; +import { EcsRunTaskCommonProps, EcsRunTaskStateBase } from './run-task-base'; -// /** -// * Properties to run an ECS task on EC2 in Step Functions on ECS -// */ -// export interface EcsEc2RunTaskProps extends EcsRunTaskCommonProps { -// /** -// * In what subnets to place the task's ENIs -// * -// * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) -// * -// * @default - Private Subnets -// */ -// readonly subnets?: ec2.SubnetSelection; +/** + * Properties to run an ECS task on EC2 in StepFunctions and ECS + */ +export interface EcsEc2RunTaskProps extends EcsRunTaskCommonProps { + /** + * In what subnets to place the task's ENIs + * + * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) + * + * @default - Private subnets + */ + readonly subnets?: 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; + /** + * 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; -// /** -// * Placement constraints -// * -// * @default - No constraints -// */ -// readonly placementConstraints?: ecs.PlacementConstraint[]; + /** + * Placement constraints + * + * @default - None + */ + readonly placementConstraints?: ecs.PlacementConstraint[]; -// /** -// * Placement strategies -// * -// * @default - No strategies -// */ -// readonly placementStrategies?: ecs.PlacementStrategy[]; -// } + /** + * Placement strategies + * + * @default - None + */ + readonly placementStrategies?: ecs.PlacementStrategy[]; +} -// /** -// * Run an ECS/EC2 Task in a StepFunctions workflow -// */ -// export class EcsEc2RunTask extends EcsRunTaskStateBase { -// constructor(props: EcsEc2RunTaskProps) { -// if (!props.taskDefinition.isEc2Compatible) { -// throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); -// } +/** + * Run an ECS/EC2 Task in a Step Functions workflow + */ +export class EcsEc2RunTask extends EcsRunTaskStateBase { + constructor(scope: Construct, id: string, props: EcsEc2RunTaskProps) { + if (!props.taskDefinition.isEc2Compatible) { + throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); + } -// if (!props.cluster.hasEc2Capacity) { -// throw new Error('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); -// } + if (!props.cluster.hasEc2Capacity) { + throw new Error('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); + } -// if (!props.taskDefinition.defaultContainer) { -// throw new Error('A TaskDefinition must have at least one essential container'); -// } + if (!props.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } -// // super({ -// // ...props, -// // parameters: { -// // LaunchType: 'EC2', -// // PlacementConstraints: noEmpty(flatten((props.placementConstraints || []).map(c => c.toJson().map(uppercaseKeys)))), -// // PlacementStrategy: noEmpty(flatten((props.placementStrategies || []).map(c => c.toJson().map(uppercaseKeys)))), -// // }, -// // }); + super(scope, id, { + ...props, + parameters: { + LaunchType: 'EC2', + PlacementConstraints: noEmpty(flatten((props.placementConstraints || []).map((c) => c.toJson().map(uppercaseKeys)))), + PlacementStrategy: noEmpty(flatten((props.placementStrategies || []).map((c) => c.toJson().map(uppercaseKeys)))), + }, + }); -// if (props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { -// this.configureAwsVpcNetworking(props.cluster.vpc, undefined, props.subnets, props.securityGroup); -// } else { -// // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. -// validateNoNetworkingProps(props); -// this.connections.addSecurityGroup(...props.cluster.connections.securityGroups); -// } -// } -// } + if (props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { + this.configureAwsVpcNetworking(props.cluster.vpc, undefined, props.subnets, props.securityGroup); + } else { + // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. + validateNoNetworkingProps(props); + this.connections.addSecurityGroup(...props.cluster.connections.securityGroups); + } + } +} -// /** -// * Validate combinations of networking arguments -// */ -// function validateNoNetworkingProps(props: EcsEc2RunTaskProps) { -// if (props.subnets !== undefined || props.securityGroup !== undefined) { -// throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); -// } -// } +/** + * Validate combinations of networking arguments + */ +function validateNoNetworkingProps(props: EcsEc2RunTaskProps) { + if (props.subnets !== undefined || props.securityGroup !== undefined) { + throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); + } +} -// function uppercaseKeys(obj: {[key: string]: any}): {[key: string]: any} { -// const ret: {[key: string]: any} = {}; -// for (const key of Object.keys(obj)) { -// ret[key.slice(0, 1).toUpperCase() + key.slice(1)] = obj[key]; -// } -// return ret; -// } +function uppercaseKeys(obj: { [key: string]: any }): { [key: string]: any } { + const ret: { [key: string]: any } = {}; + for (const key of Object.keys(obj)) { + ret[key.slice(0, 1).toUpperCase() + key.slice(1)] = obj[key]; + } + return ret; +} -// function flatten(xs: A[][]): A[] { -// return Array.prototype.concat([], ...xs); -// } +function flatten(xs: A[][]): A[] { + return Array.prototype.concat([], ...xs); +} -// function noEmpty(xs: A[]): A[] | undefined { -// if (xs.length === 0) { return undefined; } -// return xs; -// } +function noEmpty(xs: A[]): A[] | undefined { + if (xs.length === 0) { + return undefined; + } + return xs; +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts new file mode 100644 index 0000000000000..4b6299eed3487 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts @@ -0,0 +1,64 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import { Construct } from '@aws-cdk/core'; +import { EcsRunTaskCommonProps, EcsRunTaskStateBase } from './run-task-base'; + +/** + * Properties to define an ECS service + */ +export interface EcsFargateRunTaskProps extends EcsRunTaskCommonProps { + /** + * Assign public IP addresses to each task + * + * @default false + */ + readonly assignPublicIp?: boolean; + + /** + * In what subnets to place the task's ENIs + * + * @default - Private subnet if assignPublicIp, public subnets otherwise + */ + readonly subnets?: ec2.SubnetSelection; + + /** + * Existing security group to use for the tasks + * + * @default - A new security group is created + */ + readonly securityGroup?: ec2.ISecurityGroup; + + /** + * Fargate platform version to run this service on + * + * Unless you have specific compatibility requirements, you don't need to + * specify this. + * + * @default FargatePlatformVersion.LATEST + */ + readonly platformVersion?: ecs.FargatePlatformVersion; +} + +/** + * Start a service on an ECS cluster + */ +export class EcsFargateRunTask extends EcsRunTaskStateBase { + constructor(scope: Construct, id: string, props: EcsFargateRunTaskProps) { + if (!props.taskDefinition.isFargateCompatible) { + throw new Error('Supplied TaskDefinition is not configured for compatibility with Fargate'); + } + + if (!props.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + + super(scope, id, { + ...props, + parameters: { + LaunchType: 'FARGATE', + }, + }); + + this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.subnets, props.securityGroup); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts index 14ae36dc0903c..d0ccc3f8adb20 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts @@ -9,7 +9,7 @@ import { integrationResourceArn, validatePatternSupported } from '../private/tas /** * Basic properties for ECS Tasks */ -export interface EcsRunTaskCommonProps { +export interface EcsRunTaskCommonProps extends sfn.TaskStateBaseProps { /** * The topic to run the task on */ @@ -37,21 +37,20 @@ export interface EcsRunTaskCommonProps { /** * Construction properties for the BaseRunTaskProps */ -export interface EcsRunTaskStateBaseProps extends EcsRunTaskCommonProps, sfn.TaskStateBaseProps { +export interface EcsRunTaskStateBaseProps extends EcsRunTaskCommonProps { /** * Additional parameters to pass to the base task * * @default - No additional parameters passed */ - readonly parameters?: {[key: string]: any}; + readonly parameters?: { [key: string]: any }; } /** * Run a Task on ECS or Fargate */ -export class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConnectable { - - private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ +export abstract class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConnectable { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ sfn.IntegrationPattern.REQUEST_RESPONSE, sfn.IntegrationPattern.RUN_JOB, sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, @@ -75,8 +74,7 @@ export class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConne validatePatternSupported(this.integrationPattern, EcsRunTaskStateBase.SUPPORTED_INTEGRATION_PATTERNS); - if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN - && !sfn.FieldUtils.containsTaskToken(props.containerOverrides)) { + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.containerOverrides)) { throw new Error('Task Token is required in `containerOverrides` for callback. Use Context.taskToken to set the token.'); } @@ -93,7 +91,7 @@ export class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConne this.taskPolicies = this.makePolicyStatements(); } - public renderTask(): any { + protected renderTask(): any { if (this.networkConfiguration !== undefined) { // Make sure we have a security group if we're using AWSVPC networking this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); @@ -116,8 +114,8 @@ export class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConne vpc: ec2.IVpc, assignPublicIp?: boolean, subnetSelection?: ec2.SubnetSelection, - securityGroup?: ec2.ISecurityGroup) { - + securityGroup?: ec2.ISecurityGroup, + ) { if (subnetSelection === undefined) { subnetSelection = { subnetType: assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE }; } @@ -149,19 +147,23 @@ export class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConne }), new iam.PolicyStatement({ actions: ['iam:PassRole'], - resources: cdk.Lazy.listValue({ produce: () => this.taskExecutionRoles().map(r => r.roleArn) }), + resources: cdk.Lazy.listValue({ produce: () => this.taskExecutionRoles().map((r) => r.roleArn) }), }), ]; if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { - policyStatements.push(new iam.PolicyStatement({ - actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], - resources: [stack.formatArn({ - service: 'events', - resource: 'rule', - resourceName: 'StepFunctionsGetEventsForECSTaskRule', - })], - })); + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForECSTaskRule', + }), + ], + }), + ); } return policyStatements; @@ -179,7 +181,9 @@ export class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConne } function renderOverrides(containerOverrides?: ContainerOverride[]) { - if (!containerOverrides) { return undefined; } + if (!containerOverrides) { + return undefined; + } const ret = new Array(); for (const override of containerOverrides) { @@ -189,10 +193,12 @@ function renderOverrides(containerOverrides?: ContainerOverride[]) { Cpu: override.cpu, Memory: override.memoryLimit, MemoryReservation: override.memoryReservation, - Environment: override.environment && override.environment.map(e => ({ - Name: e.name, - Value: e.value, - })), + Environment: + override.environment && + override.environment.map((e) => ({ + Name: e.name, + Value: e.value, + })), }); } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index b9a5cd0a9f062..8d5cd2b9cf199 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -10,6 +10,9 @@ export * from './sqs/send-to-queue'; export * from './sqs/send-message'; export * from './ecs/run-ecs-ec2-task'; export * from './ecs/run-ecs-fargate-task'; +export * from './ecs/ec2-run-task'; +export * from './ecs/fargate-run-task'; +export * from './ecs/run-task-base'; export * from './sagemaker/sagemaker-task-base-types'; export * from './sagemaker/sagemaker-train-task'; export * from './sagemaker/sagemaker-transform-task'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 8f62cc2143eb5..9f695a25393fb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -113,7 +113,10 @@ "stability": "experimental", "maturity": "experimental", "awslint": { - "exclude": [] + "exclude": [ + "ref-via-interface:@aws-cdk/aws-stepfunctions-tasks.EcsEc2RunTaskProps.taskDefinition", + "ref-via-interface:@aws-cdk/aws-stepfunctions-tasks.EcsFargateRunTaskProps.taskDefinition" + ] }, "awscdkio": { "announce": false diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json new file mode 100644 index 0000000000000..64d2c65ff920f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json @@ -0,0 +1,712 @@ +{ + "Resources": { + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "FargateClusterDefaultAutoScalingGroupInstanceSecurityGroup42AF8A40": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + } + ], + "VpcId": "vpc-60900905" + } + }, + "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E", + "Roles": [ + { + "Ref": "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupInstanceProfile2C0FEF3B": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupLaunchConfig57306899": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "FargateClusterDefaultAutoScalingGroupInstanceProfile2C0FEF3B" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupInstanceSecurityGroup42AF8A40", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "FargateCluster7CCD5F93" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E", + "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7" + ] + }, + "FargateClusterDefaultAutoScalingGroupASG36A4948F": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "LaunchConfigurationName": { + "Ref": "FargateClusterDefaultAutoScalingGroupLaunchConfig57306899" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + "subnet-e19455ca", + "subnet-e0c24797", + "subnet-ccd77395" + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "autoscaling:CompleteLifecycleAction", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":autoscaling:test-region:12345678:autoScalingGroup:*:autoScalingGroupName/", + { + "Ref": "FargateClusterDefaultAutoScalingGroupASG36A4948F" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343", + "Roles": [ + { + "Ref": "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" + }, + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32", + "Arn" + ] + }, + "Runtime": "python3.6", + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "FargateCluster7CCD5F93" + } + } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343", + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32" + ] + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionAllowInvokeawssfntasksecsec2integFargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic4F1711EC894BE644": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10" + } + } + }, + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionTopic37856E82": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10" + }, + "Endpoint": { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8", + "Arn" + ] + } + } + }, + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "autoscaling.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D", + "Roles": [ + { + "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + } + ] + } + }, + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHook2AE13680": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "FargateClusterDefaultAutoScalingGroupASG36A4948F" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10" + }, + "RoleARN": { + "Fn::GetAtt": [ + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D", + "Arn" + ] + } + }, + "DependsOn": [ + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D", + "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D" + ] + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + "12345678.dkr.ecr.test-region.", + { + "Ref": "AWS::URLSuffix" + }, + "/aws-cdk/assets:1f37178655533422f6654c973b99eadec99a723c7181c912e4fb0976187c687c" + ] + ] + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "TaskDefTheContainerLogGroupD94C8EF5" + }, + "awslogs-stream-prefix": "EventDemo", + "awslogs-region": "test-region" + } + }, + "Memory": 256, + "Name": "TheContainer" + } + ], + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + }, + "Family": "awssfntasksecsec2integTaskDefFAFE2BE7", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "TaskDefTheContainerLogGroupD94C8EF5": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TaskDefExecutionRoleB4775C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefExecutionRoleDefaultPolicy0DBB737A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:test-region:12345678:repository/aws-cdk/assets" + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TaskDefTheContainerLogGroupD94C8EF5", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefExecutionRoleDefaultPolicy0DBB737A", + "Roles": [ + { + "Ref": "TaskDefExecutionRoleB4775C97" + } + ] + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.test-region.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecs:RunTask", + "Effect": "Allow", + "Resource": { + "Ref": "TaskDef54694570" + } + }, + { + "Action": [ + "ecs:StopTask", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + } + ] + }, + { + "Action": [ + "events:PutTargets", + "events:PutRule", + "events:DescribeRule" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:test-region:12345678:rule/StepFunctionsGetEventsForECSTaskRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"SomeKey\":\"SomeValue\"},\"Next\":\"Run\"},\"Run\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::ecs:runTask.sync\",\"Parameters\":{\"Cluster\":\"", + { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + }, + "\",\"TaskDefinition\":\"", + { + "Ref": "TaskDef54694570" + }, + "\",\"Overrides\":{\"ContainerOverrides\":[{\"Name\":\"TheContainer\",\"Environment\":[{\"Name\":\"SOME_KEY\",\"Value.$\":\"$.SomeKey\"}]}]},\"LaunchType\":\"EC2\"}}}}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts new file mode 100644 index 0000000000000..54ba0a70d2a9c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts @@ -0,0 +1,60 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import * as tasks from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-sfn-tasks-ecs-ec2-integ', { + env: { + account: process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_INTEG_REGION || process.env.CDK_DEFAULT_REGION, + }, +}); + +const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { + isDefault: true, +}); + +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); +cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, +}); + +// Build task definition +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); +taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'eventhandler-image')), + memoryLimitMiB: 256, + logging: new ecs.AwsLogDriver({ streamPrefix: 'EventDemo' }), +}); + +// Build state machine +const definition = new sfn.Pass(stack, 'Start', { + result: sfn.Result.fromObject({ SomeKey: 'SomeValue' }), +}).next( + new tasks.EcsEc2RunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [ + { + containerName: 'TheContainer', + environment: [ + { + name: 'SOME_KEY', + value: sfn.Data.stringAt('$.SomeKey'), + }, + ], + }, + ], + }), +); + +new sfn.StateMachine(stack, 'StateMachine', { + definition, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json new file mode 100644 index 0000000000000..7ce6b93c4e3bc --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json @@ -0,0 +1,298 @@ +{ + "Resources": { + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + "12345678.dkr.ecr.test-region.", + { + "Ref": "AWS::URLSuffix" + }, + "/aws-cdk/assets:1f37178655533422f6654c973b99eadec99a723c7181c912e4fb0976187c687c" + ] + ] + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "TaskDefTheContainerLogGroupD94C8EF5" + }, + "awslogs-stream-prefix": "EventDemo", + "awslogs-region": "test-region" + } + }, + "Memory": 256, + "Name": "TheContainer" + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + }, + "Family": "awssfntasksecsfargateintegTaskDefD0F4AD10", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "TaskDefTheContainerLogGroupD94C8EF5": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TaskDefExecutionRoleB4775C97": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDefExecutionRoleDefaultPolicy0DBB737A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:test-region:12345678:repository/aws-cdk/assets" + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TaskDefTheContainerLogGroupD94C8EF5", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TaskDefExecutionRoleDefaultPolicy0DBB737A", + "Roles": [ + { + "Ref": "TaskDefExecutionRoleB4775C97" + } + ] + } + }, + "FargateTaskSecurityGroup0BBB27CB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-sfn-tasks-ecs-fargate-integ/FargateTask/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": "vpc-60900905" + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "states.test-region.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ecs:RunTask", + "Effect": "Allow", + "Resource": { + "Ref": "TaskDef54694570" + } + }, + { + "Action": [ + "ecs:StopTask", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "TaskDefExecutionRoleB4775C97", + "Arn" + ] + } + ] + }, + { + "Action": [ + "events:PutTargets", + "events:PutRule", + "events:DescribeRule" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:test-region:12345678:rule/StepFunctionsGetEventsForECSTaskRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"SomeKey\":\"SomeValue\"},\"Next\":\"FargateTask\"},\"FargateTask\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::ecs:runTask.sync\",\"Parameters\":{\"Cluster\":\"", + { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + }, + "\",\"TaskDefinition\":\"", + { + "Ref": "TaskDef54694570" + }, + "\",\"NetworkConfiguration\":{\"AwsvpcConfiguration\":{\"AssignPublicIp\":\"ENABLED\",\"Subnets\":[\"subnet-e19455ca\",\"subnet-e0c24797\",\"subnet-ccd77395\"],\"SecurityGroups\":[\"", + { + "Fn::GetAtt": [ + "FargateTaskSecurityGroup0BBB27CB", + "GroupId" + ] + }, + "\"]}},\"Overrides\":{\"ContainerOverrides\":[{\"Name\":\"TheContainer\",\"Environment\":[{\"Name\":\"SOME_KEY\",\"Value.$\":\"$.SomeKey\"}]}]},\"LaunchType\":\"FARGATE\"}}}}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts new file mode 100644 index 0000000000000..3977bd25c382f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts @@ -0,0 +1,60 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import * as tasks from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-sfn-tasks-ecs-fargate-integ', { + env: { + account: process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_INTEG_REGION || process.env.CDK_DEFAULT_REGION, + }, +}); + +const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { + isDefault: true, +}); + +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); + +// Build task definition +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { + memoryLimitMiB: 512, + cpu: 256, +}); +taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'eventhandler-image')), + memoryLimitMiB: 256, + logging: new ecs.AwsLogDriver({ streamPrefix: 'EventDemo' }), +}); + +// Build state machine +const definition = new sfn.Pass(stack, 'Start', { + result: sfn.Result.fromObject({ SomeKey: 'SomeValue' }), +}).next( + new tasks.EcsFargateRunTask(stack, 'FargateTask', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + assignPublicIp: true, + containerOverrides: [ + { + containerName: 'TheContainer', + environment: [ + { + name: 'SOME_KEY', + value: sfn.Data.stringAt('$.SomeKey'), + }, + ], + }, + ], + }), +); + +new sfn.StateMachine(stack, 'StateMachine', { + definition, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts new file mode 100644 index 0000000000000..84f18af57adfd --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -0,0 +1,365 @@ +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Stack } from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: Stack; +let vpc: ec2.Vpc; +let cluster: ecs.Cluster; + +// tslint:disable: object-literal-key-quotes + +beforeEach(() => { + // GIVEN + stack = new Stack(); + vpc = new ec2.Vpc(stack, 'Vpc'); + cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('Capacity', { + instanceType: new ec2.InstanceType('t3.medium'), + }); +}); + +test('Cannot create a Fargate task with a fargate-incompatible task definition', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.EC2, + }); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + expect(() => new tasks.EcsFargateRunTask(stack, 'task', { cluster, taskDefinition })).toThrowError(/not configured for compatibility with Fargate/); +}); + +test('Cannot create a Fargate task without a default container', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.FARGATE, + }); + expect(() => new tasks.EcsFargateRunTask(stack, 'task', { cluster, taskDefinition })).toThrowError(/must have at least one essential container/); +}); + +test('Running a Fargate Task', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.FARGATE, + }); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.EcsFargateRunTask(stack, 'RunFargate', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [ + { + containerName: 'TheContainer', + environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + }, + ], + }); + + new sfn.StateMachine(stack, 'SM', { + definition: runTask, + }); + + // THEN + expect(stack.resolve(runTask.toStateJson())).toEqual({ + End: true, + Parameters: { + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, + LaunchType: 'FARGATE', + NetworkConfiguration: { + AwsvpcConfiguration: { + SecurityGroups: [{ 'Fn::GetAtt': ['RunFargateSecurityGroup709740F2', 'GroupId'] }], + Subnets: [{ Ref: 'VpcPrivateSubnet1Subnet536B997A' }, { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }], + }, + }, + TaskDefinition: { Ref: 'TD49C78F36' }, + Overrides: { + ContainerOverrides: [ + { + Environment: [ + { + Name: 'SOME_KEY', + 'Value.$': '$.SomeKey', + }, + ], + Name: 'TheContainer', + }, + ], + }, + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::ecs:runTask.sync', + ], + ], + }, + Type: 'Task', + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'ecs:RunTask', + Effect: 'Allow', + Resource: { Ref: 'TD49C78F36' }, + }, + { + Action: ['ecs:StopTask', 'ecs:DescribeTasks'], + Effect: 'Allow', + Resource: '*', + }, + { + Action: 'iam:PassRole', + Effect: 'Allow', + Resource: [{ 'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn'] }], + }, + { + Action: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':events:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':rule/StepFunctionsGetEventsForECSTaskRule', + ], + ], + }, + }, + ], + }, + }); +}); + +test('Running an EC2 Task with bridge network', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + compatibility: ecs.Compatibility.EC2, + }); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [ + { + containerName: 'TheContainer', + environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + }, + ], + }); + + new sfn.StateMachine(stack, 'SM', { + definition: runTask, + }); + + // THEN + expect(stack.resolve(runTask.toStateJson())).toEqual({ + End: true, + Parameters: { + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, + LaunchType: 'EC2', + TaskDefinition: { Ref: 'TD49C78F36' }, + Overrides: { + ContainerOverrides: [ + { + Environment: [ + { + Name: 'SOME_KEY', + 'Value.$': '$.SomeKey', + }, + ], + Name: 'TheContainer', + }, + ], + }, + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::ecs:runTask.sync', + ], + ], + }, + Type: 'Task', + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'ecs:RunTask', + Effect: 'Allow', + Resource: { Ref: 'TD49C78F36' }, + }, + { + Action: ['ecs:StopTask', 'ecs:DescribeTasks'], + Effect: 'Allow', + Resource: '*', + }, + { + Action: 'iam:PassRole', + Effect: 'Allow', + Resource: [{ 'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn'] }], + }, + { + Action: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':events:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':rule/StepFunctionsGetEventsForECSTaskRule', + ], + ], + }, + }, + ], + }, + }); +}); + +test('Running an EC2 Task with placement strategies', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + compatibility: ecs.Compatibility.EC2, + }); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + placementStrategies: [ecs.PlacementStrategy.spreadAcrossInstances(), ecs.PlacementStrategy.packedByCpu(), ecs.PlacementStrategy.randomly()], + placementConstraints: [ecs.PlacementConstraint.memberOf('blieptuut')], + }); + + new sfn.StateMachine(stack, 'SM', { + definition: runTask, + }); + + // THEN + expect(stack.resolve(runTask.toStateJson())).toEqual({ + End: true, + Parameters: { + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, + LaunchType: 'EC2', + TaskDefinition: { Ref: 'TD49C78F36' }, + PlacementConstraints: [{ Type: 'memberOf', Expression: 'blieptuut' }], + PlacementStrategy: [{ Field: 'instanceId', Type: 'spread' }, { Field: 'cpu', Type: 'binpack' }, { Type: 'random' }], + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::ecs:runTask.sync', + ], + ], + }, + Type: 'Task', + }); +}); + +test('Running an EC2 Task with overridden number values', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + compatibility: ecs.Compatibility.EC2, + }); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [ + { + containerName: 'TheContainer', + command: sfn.Data.listAt('$.TheCommand'), + cpu: 5, + memoryLimit: sfn.Data.numberAt('$.MemoryLimit'), + }, + ], + }); + + // THEN + expect(stack.resolve(runTask.toStateJson())).toEqual({ + End: true, + Parameters: { + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, + LaunchType: 'EC2', + TaskDefinition: { Ref: 'TD49C78F36' }, + Overrides: { + ContainerOverrides: [ + { + 'Command.$': '$.TheCommand', + Cpu: 5, + 'Memory.$': '$.MemoryLimit', + Name: 'TheContainer', + }, + ], + }, + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::ecs:runTask.sync', + ], + ], + }, + Type: 'Task', + }); +}); From c0ba4c77b0cfc71acf43ab8b363e702cb98809d9 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Tue, 9 Jun 2020 09:23:00 -0700 Subject: [PATCH 03/27] add integ tests, update README --- .../aws-stepfunctions-tasks/README.md | 61 ++++++++++++++----- .../lib/ecs/run-task-base.ts | 1 + .../aws-stepfunctions-tasks/package.json | 5 +- .../test/ecs/integ.ec2-run-task.expected.json | 7 +++ .../test/ecs/integ.ec2-run-task.ts | 16 ++++- .../ecs/integ.fargate-run-task.expected.json | 7 +++ .../test/ecs/integ.fargate-run-task.ts | 16 ++++- 7 files changed, 91 insertions(+), 22 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index c8482f9e57f09..7c92780b325fd 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -327,31 +327,60 @@ Step Functions supports [ECS/Fargate](https://docs.aws.amazon.com/step-functions [RunTask](https://docs.aws.amazon.com/step-functions/latest/dg/connect-ecs.html) starts a new task using the specified task definition. +#### Fargate + +The following example runs a job from a task definition on Fargate + ```ts import * as ecs from '@aws-cdk/aws-ecs'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; + -// See examples in ECS library for initialization of 'cluster' and 'taskDefinition' +const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.FARGATE, +}); + +taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, +}); -new ecs.RunEcsFargateTask({ +const runTask = new tasks.EcsFargateRunTask(stack, 'RunFargate', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, - containerOverrides: [ - { - containerName: 'TheContainer', - environment: [ - { - name: 'CONTAINER_INPUT', - value: Data.stringAt('$.valueFromStateData'), - } - ] - } - ] + containerOverrides: [{ + containerName: 'TheContainer', + environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + }], }); +``` + +#### EC2 + +The following example runs a job from a task definition on EC2 -fargateTask.connections.allowToDefaultPort(rdsCluster, 'Read the database'); +```ts +const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + compatibility: ecs.Compatibility.EC2, +}); -new sfn.Task(this, 'CallFargate', { - task: fargateTask +taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, +}); + +const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [{ + containerName: 'TheContainer', + environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + },], }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts index d0ccc3f8adb20..06c387f682b2d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts @@ -16,6 +16,7 @@ export interface EcsRunTaskCommonProps extends sfn.TaskStateBaseProps { readonly cluster: ecs.ICluster; /** + * [disable-awslint:ref-via-interface] * Task Definition used for running tasks in the service. * * Note: this must be TaskDefinition, and not ITaskDefinition, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 9f695a25393fb..8f62cc2143eb5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -113,10 +113,7 @@ "stability": "experimental", "maturity": "experimental", "awslint": { - "exclude": [ - "ref-via-interface:@aws-cdk/aws-stepfunctions-tasks.EcsEc2RunTaskProps.taskDefinition", - "ref-via-interface:@aws-cdk/aws-stepfunctions-tasks.EcsFargateRunTaskProps.taskDefinition" - ] + "exclude": [] }, "awscdkio": { "announce": false diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json index 64d2c65ff920f..8b6a5b79e1945 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json @@ -708,5 +708,12 @@ "Type": "AWS::SSM::Parameter::Value", "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts index 54ba0a70d2a9c..50b5d3085f145 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts @@ -5,6 +5,16 @@ import * as cdk from '@aws-cdk/core'; import * as path from 'path'; import * as tasks from '../../lib'; +/* + * * Creates a state machine with a task state to run a job with ECS on EC2 + * + * Stack verification steps: + * The generated State Machine can be executed from the CLI (or Step Functions console) + * and runs with an execution status of `Succeeded`. + * + * -- aws stepfunctions start-execution --state-machine-arn provides execution arn + * -- aws stepfunctions describe-execution --execution-arn returns a status of `Succeeded` + */ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-sfn-tasks-ecs-ec2-integ', { env: { @@ -53,8 +63,12 @@ const definition = new sfn.Pass(stack, 'Start', { }), ); -new sfn.StateMachine(stack, 'StateMachine', { +const sm = new sfn.StateMachine(stack, 'StateMachine', { definition, }); +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); + app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json index 7ce6b93c4e3bc..e9c5fc9a07617 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json @@ -294,5 +294,12 @@ "StateMachineRoleB840431D" ] } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts index 3977bd25c382f..6f22a1d810ed7 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts @@ -5,6 +5,16 @@ import * as cdk from '@aws-cdk/core'; import * as path from 'path'; import * as tasks from '../../lib'; +/* + * Creates a state machine with a task state to run a job with ECS on Fargate + * + * Stack verification steps: + * The generated State Machine can be executed from the CLI (or Step Functions console) + * and runs with an execution status of `Succeeded`. + * + * -- aws stepfunctions start-execution --state-machine-arn provides execution arn + * -- aws stepfunctions describe-execution --execution-arn returns a status of `Succeeded` + */ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-sfn-tasks-ecs-fargate-integ', { env: { @@ -53,8 +63,12 @@ const definition = new sfn.Pass(stack, 'Start', { }), ); -new sfn.StateMachine(stack, 'StateMachine', { +const sm = new sfn.StateMachine(stack, 'StateMachine', { definition, }); +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); + app.synth(); From 40fb7bf68a2be52e2b30c217a4afd15770ba28c9 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Tue, 9 Jun 2020 09:24:34 -0700 Subject: [PATCH 04/27] add anchor links in README --- .../aws-stepfunctions-tasks/README.md | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 7c92780b325fd..f74c143771558 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -37,6 +37,8 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [UpdateItem](#updateitem) - [ECS](#ecs) - [RunTask](#runtask) + - [EC2](#ec2) + - [Fargate](#fargate) - [EMR](#emr) - [Create Cluster](#create-cluster) - [Termination Protection](#termination-protection) @@ -327,20 +329,13 @@ Step Functions supports [ECS/Fargate](https://docs.aws.amazon.com/step-functions [RunTask](https://docs.aws.amazon.com/step-functions/latest/dg/connect-ecs.html) starts a new task using the specified task definition. -#### Fargate +#### EC2 -The following example runs a job from a task definition on Fargate +The following example runs a job from a task definition on EC2 ```ts -import * as ecs from '@aws-cdk/aws-ecs'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; - - const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { - memoryMiB: '512', - cpu: '256', - compatibility: ecs.Compatibility.FARGATE, + compatibility: ecs.Compatibility.EC2, }); taskDefinition.addContainer('TheContainer', { @@ -348,24 +343,31 @@ taskDefinition.addContainer('TheContainer', { memoryLimitMiB: 256, }); -const runTask = new tasks.EcsFargateRunTask(stack, 'RunFargate', { +const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, containerOverrides: [{ containerName: 'TheContainer', environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], - }], + },], }); ``` -#### EC2 +#### Fargate -The following example runs a job from a task definition on EC2 +The following example runs a job from a task definition on Fargate ```ts +import * as ecs from '@aws-cdk/aws-ecs'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; + + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { - compatibility: ecs.Compatibility.EC2, + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.FARGATE, }); taskDefinition.addContainer('TheContainer', { @@ -373,14 +375,14 @@ taskDefinition.addContainer('TheContainer', { memoryLimitMiB: 256, }); -const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { +const runTask = new tasks.EcsFargateRunTask(stack, 'RunFargate', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, containerOverrides: [{ containerName: 'TheContainer', environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], - },], + }], }); ``` From d2511e2e18f869d9a550d75a126380e6aaa2b299 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Tue, 9 Jun 2020 09:30:15 -0700 Subject: [PATCH 05/27] mark old implementations as deprecated --- .../aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts | 2 ++ .../aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts index 8fc7105c9ae0b..a14778d306e62 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts @@ -41,6 +41,8 @@ export interface RunEcsEc2TaskProps extends CommonEcsRunTaskProps { /** * Run an ECS/EC2 Task in a StepFunctions workflow + * + * @deprecated - replaced by `EcsEc2RunTask` */ export class RunEcsEc2Task extends EcsRunTaskBase { constructor(props: RunEcsEc2TaskProps) { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts index 63526106757bf..2910613c4d2c5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts @@ -40,6 +40,8 @@ export interface RunEcsFargateTaskProps extends CommonEcsRunTaskProps { /** * Start a service on an ECS cluster + * + * @deprecated - replaced by `EcsFargateRunTask` */ export class RunEcsFargateTask extends EcsRunTaskBase { constructor(props: RunEcsFargateTaskProps) { From f6e3f0d89aeec7a586b549e56e5159345a7017d7 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 18 Jun 2020 00:15:50 -0700 Subject: [PATCH 06/27] PR feedback --- .../aws-stepfunctions-tasks/README.md | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 56fd2abb0726b..58d1bd8db05d1 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -331,9 +331,22 @@ Step Functions supports [ECS/Fargate](https://docs.aws.amazon.com/step-functions #### EC2 +The EC2 launch type allows you to run your containerized applications on a cluster +of Amazon EC2 instances that you manage. + +When a task that uses the EC2 launch type is launched, Amazon ECS must determine where +to place the task based on the requirements specified in the task definition, such as +CPU and memory. Similarly, when you scale down the task count, Amazon ECS must determine +which tasks to terminate. You can apply task placement strategies and constraints to +customize how Amazon ECS places and terminates tasks. Learn more about [task placement](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement.html) + The following example runs a job from a task definition on EC2 ```ts +import * as ecs from '@aws-cdk/aws-ecs'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { compatibility: ecs.Compatibility.EC2, }); @@ -344,18 +357,32 @@ taskDefinition.addContainer('TheContainer', { }); const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { - integrationPattern: sfn.IntegrationPattern.RUN_JOB, - cluster, - taskDefinition, - containerOverrides: [{ - containerName: 'TheContainer', - environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], - },], -}); + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + placementStrategies: [ + ecs.PlacementStrategy.spreadAcrossInstances(), + ecs.PlacementStrategy.packedByCpu(), + ecs.PlacementStrategy.randomly(), + ], + placementConstraints: [ + ecs.PlacementConstraint.memberOf('blieptuut') + ], + }); ``` #### Fargate +AWS Fargate is a serverless compute engine for containers that works with Amazon +Elastic Container Service (ECS). Fargate makes it easy for you to focus on building +your applications. Fargate removes the need to provision and manage servers, lets you +specify and pay for resources per application, and improves security through application +isolation by design. Learn more about [Fargate](https://aws.amazon.com/fargate/) + +The Fargate launch type allows you to run your containerized applications without the need +to provision and manage the backend infrastructure. Just register your task definition and +Fargate launches the container for you. + The following example runs a job from a task definition on Fargate ```ts From c63756f9cb739bacca0fa02ceedec7eb0cd0e2ba Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 22 Jun 2020 22:48:35 -0700 Subject: [PATCH 07/27] add support for specifying fargate platform version --- .../lib/ecs/fargate-run-task.ts | 1 + .../lib/ecs/run-task.ts | 226 ++++++++++++++++++ .../test/ecs/run-tasks.test.ts | 2 + 3 files changed, 229 insertions(+) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts index 4b6299eed3487..fec94202fb2db 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts @@ -56,6 +56,7 @@ export class EcsFargateRunTask extends EcsRunTaskStateBase { ...props, parameters: { LaunchType: 'FARGATE', + PlatformVersion: props.platformVersion, }, }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts new file mode 100644 index 0000000000000..7405337de0e00 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -0,0 +1,226 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { ContainerOverride } from '..'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Basic properties for ECS Tasks + */ +export interface EcsRunTaskCommonProps extends sfn.TaskStateBaseProps { + /** + * The topic to run the task on + */ + readonly cluster: ecs.ICluster; + + /** + * [disable-awslint:ref-via-interface] + * Task Definition used for running tasks in the service. + * + * Note: this must be TaskDefinition, and not ITaskDefinition, + * as it requires properties that are not known for imported task definitions + */ + readonly taskDefinition: ecs.TaskDefinition; + + /** + * Container setting overrides + * + * Key is the name of the container to override, value is the + * values you want to override. + * + * @default - No overrides + */ + readonly containerOverrides?: ContainerOverride[]; + + /** + * ECS or Fargate Launch type + */ + readonly launchTarget: IEcsLaunchType; +} + +export interface IEcsLaunchType { + bind(): EcsLaunchConfig; +} + +export interface EcsLaunchConfig { + +} + +/** + * Construction properties for the BaseRunTaskProps + */ +export interface EcsRunTaskProps extends EcsRunTaskCommonProps { + /** + * Additional parameters to pass to the base task + * + * @default - No additional parameters passed + */ + readonly parameters?: { [key: string]: any }; +} + +/** + * Run a Task on ECS or Fargate + */ +export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + /** + * Manage allowed network traffic for this service + */ + public readonly connections: ec2.Connections = new ec2.Connections(); + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private securityGroup?: ec2.ISecurityGroup; + private networkConfiguration?: any; + private platformVersion?: ecs.FargatePlatformVersion; + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: cdk.Construct, id: string, private readonly props: EcsRunTaskProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, EcsRunTask.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.containerOverrides)) { + throw new Error('Task Token is required in `containerOverrides` for callback. Use Context.taskToken to set the token.'); + } + + for (const override of this.props.containerOverrides || []) { + const name = override.containerName; + if (!cdk.Token.isUnresolved(name)) { + const cont = this.props.taskDefinition.node.tryFindChild(name); + if (!cont) { + throw new Error(`Overrides mention container with name '${name}', but no such container in task definition`); + } + } + } + + this.taskPolicies = this.makePolicyStatements(); + } + + protected renderTask(): any { + if (this.networkConfiguration !== undefined) { + // Make sure we have a security group if we're using AWSVPC networking + this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); + this.connections.addSecurityGroup(this.securityGroup); + } + + return { + Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + Cluster: this.props.cluster.clusterArn, + TaskDefinition: this.props.taskDefinition.taskDefinitionArn, + NetworkConfiguration: this.networkConfiguration, + Overrides: renderOverrides(this.props.containerOverrides), + PlatformVersion: this.platformVersion, + ...this.props.parameters, + }), + }; + } + + protected configureAwsVpcNetworking( + vpc: ec2.IVpc, + assignPublicIp?: boolean, + subnetSelection?: ec2.SubnetSelection, + securityGroup?: ec2.ISecurityGroup, + ) { + if (subnetSelection === undefined) { + subnetSelection = { subnetType: assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE }; + } + + // If none is given here, one will be created later on during bind() + this.securityGroup = securityGroup; + + this.networkConfiguration = { + AwsvpcConfiguration: { + AssignPublicIp: assignPublicIp !== undefined ? (assignPublicIp ? 'ENABLED' : 'DISABLED') : undefined, + Subnets: vpc.selectSubnets(subnetSelection).subnetIds, + SecurityGroups: cdk.Lazy.listValue({ produce: () => [this.securityGroup!.securityGroupId] }), + }, + }; + } + + protected setupPlatformVersion(platformVersion?: ecs.FargatePlatformVersion) { + this.platformVersion = platformVersion; + } + + private makePolicyStatements(): iam.PolicyStatement[] { + const stack = cdk.Stack.of(this); + + // https://docs.aws.amazon.com/step-functions/latest/dg/ecs-iam.html + const policyStatements = [ + new iam.PolicyStatement({ + actions: ['ecs:RunTask'], + resources: [this.props.taskDefinition.taskDefinitionArn], + }), + new iam.PolicyStatement({ + actions: ['ecs:StopTask', 'ecs:DescribeTasks'], + resources: ['*'], + }), + new iam.PolicyStatement({ + actions: ['iam:PassRole'], + resources: cdk.Lazy.listValue({ produce: () => this.taskExecutionRoles().map((r) => r.roleArn) }), + }), + ]; + + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForECSTaskRule', + }), + ], + }), + ); + } + + return policyStatements; + } + + private taskExecutionRoles(): iam.IRole[] { + // Need to be able to pass both Task and Execution role, apparently + const ret = new Array(); + ret.push(this.props.taskDefinition.taskRole); + if (this.props.taskDefinition.executionRole) { + ret.push(this.props.taskDefinition.executionRole); + } + return ret; + } +} + +function renderOverrides(containerOverrides?: ContainerOverride[]) { + if (!containerOverrides) { + return undefined; + } + + const ret = new Array(); + for (const override of containerOverrides) { + ret.push({ + Name: override.containerName, + Command: override.command, + Cpu: override.cpu, + Memory: override.memoryLimit, + MemoryReservation: override.memoryReservation, + Environment: + override.environment && + override.environment.map((e) => ({ + Name: e.name, + Value: e.value, + })), + }); + } + + return { ContainerOverrides: ret }; +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts index 84f18af57adfd..2820ddc007218 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -66,6 +66,7 @@ test('Running a Fargate Task', () => { environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], }, ], + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, }); new sfn.StateMachine(stack, 'SM', { @@ -84,6 +85,7 @@ test('Running a Fargate Task', () => { Subnets: [{ Ref: 'VpcPrivateSubnet1Subnet536B997A' }, { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }], }, }, + 'PlatformVersion': '1.4.0', TaskDefinition: { Ref: 'TD49C78F36' }, Overrides: { ContainerOverrides: [ From 0c7b263777c4a40a58cb5dd962520b761b1cf762 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 22 Jun 2020 22:53:04 -0700 Subject: [PATCH 08/27] drop run task as the current organization is favorable to implementing the bind pattern --- .../lib/ecs/run-task.ts | 226 ------------------ 1 file changed, 226 deletions(-) delete mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts deleted file mode 100644 index 7405337de0e00..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ /dev/null @@ -1,226 +0,0 @@ -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecs from '@aws-cdk/aws-ecs'; -import * as iam from '@aws-cdk/aws-iam'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; -import { ContainerOverride } from '..'; -import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; - -/** - * Basic properties for ECS Tasks - */ -export interface EcsRunTaskCommonProps extends sfn.TaskStateBaseProps { - /** - * The topic to run the task on - */ - readonly cluster: ecs.ICluster; - - /** - * [disable-awslint:ref-via-interface] - * Task Definition used for running tasks in the service. - * - * Note: this must be TaskDefinition, and not ITaskDefinition, - * as it requires properties that are not known for imported task definitions - */ - readonly taskDefinition: ecs.TaskDefinition; - - /** - * Container setting overrides - * - * Key is the name of the container to override, value is the - * values you want to override. - * - * @default - No overrides - */ - readonly containerOverrides?: ContainerOverride[]; - - /** - * ECS or Fargate Launch type - */ - readonly launchTarget: IEcsLaunchType; -} - -export interface IEcsLaunchType { - bind(): EcsLaunchConfig; -} - -export interface EcsLaunchConfig { - -} - -/** - * Construction properties for the BaseRunTaskProps - */ -export interface EcsRunTaskProps extends EcsRunTaskCommonProps { - /** - * Additional parameters to pass to the base task - * - * @default - No additional parameters passed - */ - readonly parameters?: { [key: string]: any }; -} - -/** - * Run a Task on ECS or Fargate - */ -export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { - private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ - sfn.IntegrationPattern.REQUEST_RESPONSE, - sfn.IntegrationPattern.RUN_JOB, - sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, - ]; - - /** - * Manage allowed network traffic for this service - */ - public readonly connections: ec2.Connections = new ec2.Connections(); - - protected readonly taskMetrics?: sfn.TaskMetricsConfig; - protected readonly taskPolicies?: iam.PolicyStatement[]; - - private securityGroup?: ec2.ISecurityGroup; - private networkConfiguration?: any; - private platformVersion?: ecs.FargatePlatformVersion; - private readonly integrationPattern: sfn.IntegrationPattern; - - constructor(scope: cdk.Construct, id: string, private readonly props: EcsRunTaskProps) { - super(scope, id, props); - this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; - - validatePatternSupported(this.integrationPattern, EcsRunTask.SUPPORTED_INTEGRATION_PATTERNS); - - if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.containerOverrides)) { - throw new Error('Task Token is required in `containerOverrides` for callback. Use Context.taskToken to set the token.'); - } - - for (const override of this.props.containerOverrides || []) { - const name = override.containerName; - if (!cdk.Token.isUnresolved(name)) { - const cont = this.props.taskDefinition.node.tryFindChild(name); - if (!cont) { - throw new Error(`Overrides mention container with name '${name}', but no such container in task definition`); - } - } - } - - this.taskPolicies = this.makePolicyStatements(); - } - - protected renderTask(): any { - if (this.networkConfiguration !== undefined) { - // Make sure we have a security group if we're using AWSVPC networking - this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); - this.connections.addSecurityGroup(this.securityGroup); - } - - return { - Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - Cluster: this.props.cluster.clusterArn, - TaskDefinition: this.props.taskDefinition.taskDefinitionArn, - NetworkConfiguration: this.networkConfiguration, - Overrides: renderOverrides(this.props.containerOverrides), - PlatformVersion: this.platformVersion, - ...this.props.parameters, - }), - }; - } - - protected configureAwsVpcNetworking( - vpc: ec2.IVpc, - assignPublicIp?: boolean, - subnetSelection?: ec2.SubnetSelection, - securityGroup?: ec2.ISecurityGroup, - ) { - if (subnetSelection === undefined) { - subnetSelection = { subnetType: assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE }; - } - - // If none is given here, one will be created later on during bind() - this.securityGroup = securityGroup; - - this.networkConfiguration = { - AwsvpcConfiguration: { - AssignPublicIp: assignPublicIp !== undefined ? (assignPublicIp ? 'ENABLED' : 'DISABLED') : undefined, - Subnets: vpc.selectSubnets(subnetSelection).subnetIds, - SecurityGroups: cdk.Lazy.listValue({ produce: () => [this.securityGroup!.securityGroupId] }), - }, - }; - } - - protected setupPlatformVersion(platformVersion?: ecs.FargatePlatformVersion) { - this.platformVersion = platformVersion; - } - - private makePolicyStatements(): iam.PolicyStatement[] { - const stack = cdk.Stack.of(this); - - // https://docs.aws.amazon.com/step-functions/latest/dg/ecs-iam.html - const policyStatements = [ - new iam.PolicyStatement({ - actions: ['ecs:RunTask'], - resources: [this.props.taskDefinition.taskDefinitionArn], - }), - new iam.PolicyStatement({ - actions: ['ecs:StopTask', 'ecs:DescribeTasks'], - resources: ['*'], - }), - new iam.PolicyStatement({ - actions: ['iam:PassRole'], - resources: cdk.Lazy.listValue({ produce: () => this.taskExecutionRoles().map((r) => r.roleArn) }), - }), - ]; - - if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { - policyStatements.push( - new iam.PolicyStatement({ - actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], - resources: [ - stack.formatArn({ - service: 'events', - resource: 'rule', - resourceName: 'StepFunctionsGetEventsForECSTaskRule', - }), - ], - }), - ); - } - - return policyStatements; - } - - private taskExecutionRoles(): iam.IRole[] { - // Need to be able to pass both Task and Execution role, apparently - const ret = new Array(); - ret.push(this.props.taskDefinition.taskRole); - if (this.props.taskDefinition.executionRole) { - ret.push(this.props.taskDefinition.executionRole); - } - return ret; - } -} - -function renderOverrides(containerOverrides?: ContainerOverride[]) { - if (!containerOverrides) { - return undefined; - } - - const ret = new Array(); - for (const override of containerOverrides) { - ret.push({ - Name: override.containerName, - Command: override.command, - Cpu: override.cpu, - Memory: override.memoryLimit, - MemoryReservation: override.memoryReservation, - Environment: - override.environment && - override.environment.map((e) => ({ - Name: e.name, - Value: e.value, - })), - }); - } - - return { ContainerOverrides: ret }; -} From da6ee702a9965fc77d715594ff9bd016d607ab22 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 24 Jun 2020 03:53:17 -0700 Subject: [PATCH 09/27] PR feedback - single RunTask class with bind pattern to add fargate/ecs props. folded all the logic in and removed abstract class --- .../aws-stepfunctions-tasks/README.md | 42 +- .../lib/ecs/ec2-run-task.ts | 105 ----- .../lib/ecs/fargate-run-task.ts | 65 --- .../lib/ecs/run-ecs-task-base-types.ts | 4 +- .../lib/ecs/run-ecs-task-base.ts | 4 +- .../lib/ecs/run-task-base.ts | 4 +- .../lib/ecs/run-task.ts | 376 ++++++++++++++++++ .../aws-stepfunctions-tasks/lib/index.ts | 3 +- .../test/ecs/ecs-tasks.test.ts | 159 ++++---- .../test/ecs/integ.ec2-run-task.expected.json | 110 ++--- .../test/ecs/integ.ec2-run-task.ts | 9 +- .../test/ecs/integ.ec2-task.ts | 32 +- .../test/ecs/integ.fargate-run-task.ts | 7 +- .../test/ecs/integ.fargate-task.ts | 33 +- .../test/ecs/run-tasks.test.ts | 42 +- 15 files changed, 618 insertions(+), 377 deletions(-) delete mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 0fd6524f277e0..2e914881fcf62 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -346,6 +346,16 @@ import * as ecs from '@aws-cdk/aws-ecs'; import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; import * as sfn from '@aws-cdk/aws-stepfunctions'; +const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { + isDefault: true, +}); + +const cluster = new ecs.Cluster(stack, 'Ec2Cluster', { vpc }); +cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, +}); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { compatibility: ecs.Compatibility.EC2, }); @@ -355,18 +365,20 @@ taskDefinition.addContainer('TheContainer', { memoryLimitMiB: 256, }); -const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { +const runTask = new tasks.EcsRunTask(stack, 'Run', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, - placementStrategies: [ - ecs.PlacementStrategy.spreadAcrossInstances(), - ecs.PlacementStrategy.packedByCpu(), - ecs.PlacementStrategy.randomly(), - ], - placementConstraints: [ - ecs.PlacementConstraint.memberOf('blieptuut') - ], + launchTarget: new tasks.EcsEc2LaunchTarget({ + placementStrategies: [ + ecs.PlacementStrategy.spreadAcrossInstances(), + ecs.PlacementStrategy.packedByCpu(), + ecs.PlacementStrategy.randomly(), + ], + placementConstraints: [ + ecs.PlacementConstraint.memberOf('blieptuut') + ], + }), }); ``` @@ -389,6 +401,11 @@ import * as ecs from '@aws-cdk/aws-ecs'; import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; import * as sfn from '@aws-cdk/aws-stepfunctions'; +const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { + isDefault: true, +}); + +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { memoryMiB: '512', @@ -396,19 +413,20 @@ const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { compatibility: ecs.Compatibility.FARGATE, }); -taskDefinition.addContainer('TheContainer', { +const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromRegistry('foo/bar'), memoryLimitMiB: 256, }); -const runTask = new tasks.EcsFargateRunTask(stack, 'RunFargate', { +const runTask = new tasks.EcsRunTask(stack, 'RunFargate', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, containerOverrides: [{ - containerName: 'TheContainer', + containerDefinition, environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], }], + launchTarget: new tasks.EcsFargateLaunchTarget(), }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts deleted file mode 100644 index 12a03529f344c..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/ec2-run-task.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecs from '@aws-cdk/aws-ecs'; -import { Construct } from '@aws-cdk/core'; -import { EcsRunTaskCommonProps, EcsRunTaskStateBase } from './run-task-base'; - -/** - * Properties to run an ECS task on EC2 in StepFunctions and ECS - */ -export interface EcsEc2RunTaskProps extends EcsRunTaskCommonProps { - /** - * In what subnets to place the task's ENIs - * - * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) - * - * @default - Private subnets - */ - readonly subnets?: 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; - - /** - * Placement constraints - * - * @default - None - */ - readonly placementConstraints?: ecs.PlacementConstraint[]; - - /** - * Placement strategies - * - * @default - None - */ - readonly placementStrategies?: ecs.PlacementStrategy[]; -} - -/** - * Run an ECS/EC2 Task in a Step Functions workflow - */ -export class EcsEc2RunTask extends EcsRunTaskStateBase { - constructor(scope: Construct, id: string, props: EcsEc2RunTaskProps) { - if (!props.taskDefinition.isEc2Compatible) { - throw new Error('Supplied TaskDefinition is not configured for compatibility with EC2'); - } - - if (!props.cluster.hasEc2Capacity) { - throw new Error('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); - } - - if (!props.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } - - super(scope, id, { - ...props, - parameters: { - LaunchType: 'EC2', - PlacementConstraints: noEmpty(flatten((props.placementConstraints || []).map((c) => c.toJson().map(uppercaseKeys)))), - PlacementStrategy: noEmpty(flatten((props.placementStrategies || []).map((c) => c.toJson().map(uppercaseKeys)))), - }, - }); - - if (props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { - this.configureAwsVpcNetworking(props.cluster.vpc, undefined, props.subnets, props.securityGroup); - } else { - // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. - validateNoNetworkingProps(props); - this.connections.addSecurityGroup(...props.cluster.connections.securityGroups); - } - } -} - -/** - * Validate combinations of networking arguments - */ -function validateNoNetworkingProps(props: EcsEc2RunTaskProps) { - if (props.subnets !== undefined || props.securityGroup !== undefined) { - throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); - } -} - -function uppercaseKeys(obj: { [key: string]: any }): { [key: string]: any } { - const ret: { [key: string]: any } = {}; - for (const key of Object.keys(obj)) { - ret[key.slice(0, 1).toUpperCase() + key.slice(1)] = obj[key]; - } - return ret; -} - -function flatten(xs: A[][]): A[] { - return Array.prototype.concat([], ...xs); -} - -function noEmpty(xs: A[]): A[] | undefined { - if (xs.length === 0) { - return undefined; - } - return xs; -} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts deleted file mode 100644 index fec94202fb2db..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/fargate-run-task.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecs from '@aws-cdk/aws-ecs'; -import { Construct } from '@aws-cdk/core'; -import { EcsRunTaskCommonProps, EcsRunTaskStateBase } from './run-task-base'; - -/** - * Properties to define an ECS service - */ -export interface EcsFargateRunTaskProps extends EcsRunTaskCommonProps { - /** - * Assign public IP addresses to each task - * - * @default false - */ - readonly assignPublicIp?: boolean; - - /** - * In what subnets to place the task's ENIs - * - * @default - Private subnet if assignPublicIp, public subnets otherwise - */ - readonly subnets?: ec2.SubnetSelection; - - /** - * Existing security group to use for the tasks - * - * @default - A new security group is created - */ - readonly securityGroup?: ec2.ISecurityGroup; - - /** - * Fargate platform version to run this service on - * - * Unless you have specific compatibility requirements, you don't need to - * specify this. - * - * @default FargatePlatformVersion.LATEST - */ - readonly platformVersion?: ecs.FargatePlatformVersion; -} - -/** - * Start a service on an ECS cluster - */ -export class EcsFargateRunTask extends EcsRunTaskStateBase { - constructor(scope: Construct, id: string, props: EcsFargateRunTaskProps) { - if (!props.taskDefinition.isFargateCompatible) { - throw new Error('Supplied TaskDefinition is not configured for compatibility with Fargate'); - } - - if (!props.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } - - super(scope, id, { - ...props, - parameters: { - LaunchType: 'FARGATE', - PlatformVersion: props.platformVersion, - }, - }); - - this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.subnets, props.securityGroup); - } -} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base-types.ts index aee94c1af0af7..107f86362543b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base-types.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base-types.ts @@ -1,3 +1,5 @@ +import { ContainerDefinition } from '@aws-cdk/aws-ecs'; + /** * A list of container overrides that specify the name of a container * and the overrides it should receive. @@ -6,7 +8,7 @@ export interface ContainerOverride { /** * Name of the container inside the task definition */ - readonly containerName: string; + readonly containerDefinition: ContainerDefinition; /** * Command to run inside the container diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts index 1bc456d7c47c4..2874b9daad5cc 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts @@ -87,7 +87,7 @@ export class EcsRunTaskBase implements ec2.IConnectable, sfn.IStepFunctionsTask } for (const override of this.props.containerOverrides || []) { - const name = override.containerName; + const name = override.containerDefinition.containerName; if (!cdk.Token.isUnresolved(name)) { const cont = this.props.taskDefinition.node.tryFindChild(name); if (!cont) { @@ -191,7 +191,7 @@ function renderOverrides(containerOverrides?: ContainerOverride[]) { const ret = new Array(); for (const override of containerOverrides) { ret.push({ - Name: override.containerName, + Name: override.containerDefinition.containerName, Command: override.command, Cpu: override.cpu, Memory: override.memoryLimit, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts index 06c387f682b2d..2cdfadd494a0a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts @@ -80,7 +80,7 @@ export abstract class EcsRunTaskStateBase extends sfn.TaskStateBase implements e } for (const override of this.props.containerOverrides || []) { - const name = override.containerName; + const name = override.containerDefinition.containerName; if (!cdk.Token.isUnresolved(name)) { const cont = this.props.taskDefinition.node.tryFindChild(name); if (!cont) { @@ -189,7 +189,7 @@ function renderOverrides(containerOverrides?: ContainerOverride[]) { const ret = new Array(); for (const override of containerOverrides) { ret.push({ - Name: override.containerName, + Name: override.containerDefinition.containerName, Command: override.command, Cpu: override.cpu, Memory: override.memoryLimit, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts new file mode 100644 index 0000000000000..aa2e27b552db1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -0,0 +1,376 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { ContainerOverride } from '..'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Basic properties for ECS Tasks + */ +export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { + /** + * The ECS cluster to run the task on + */ + readonly cluster: ecs.ICluster; + + /** + * [disable-awslint:ref-via-interface] + * Task Definition used for running tasks in the service. + * + * Note: this must be TaskDefinition, and not ITaskDefinition, + * as it requires properties that are not known for imported task definitions + */ + readonly taskDefinition: ecs.TaskDefinition; + + /** + * Container setting overrides + * + * Key is the name of the container to override, value is the + * values you want to override. + * + * @default - No overrides + */ + readonly containerOverrides?: ContainerOverride[]; + + /** + * In what subnets to place the task's ENIs + * + * @default - Private subnet if assignPublicIp, public subnets otherwise + */ + readonly subnets?: ec2.SubnetSelection; + + /** + * Existing security group to use for the tasks + * + * @default - A new security group is created + */ + readonly securityGroup?: ec2.ISecurityGroup; + + /** + * Assign public IP addresses to each task + * + * @default false + */ + readonly assignPublicIp?: boolean; + + /** + * An Amazon ECS launch type determines the type of infrastructure on which your + * tasks and services are hosted. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html + */ + readonly launchTarget: IEcsLaunchTarget; +} + +/** + * An Amazon ECS launch type determines the type of infrastructure on which your tasks and services are hosted. + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html + */ +export interface IEcsLaunchTarget { + /** + * called when the ECS launch target is configured on RunTask + */ + bind(_task: EcsRunTask): EcsLaunchTargetConfig; +} + +/** + * Configuration options for the ECS launch type + */ +export interface EcsLaunchTargetConfig { + /** + * Additional parameters to pass to the base task + * + * @default - No additional parameters passed + */ + readonly parameters?: { [key: string]: any }; +} + +/** + * Properties to define an ECS service + */ +export interface EcsFargateLaunchTargetOptions { + /** + * Fargate platform version to run this service on + * + * Unless you have specific compatibility requirements, you don't need to + * specify this. + * + * @default FargatePlatformVersion.LATEST + */ + readonly platformVersion?: ecs.FargatePlatformVersion; +} + +/** + * Options to run an ECS task on EC2 in StepFunctions and ECS + */ +export interface EcsEc2LaunchTargetOptions { + /** + * Placement constraints + * + * @default - None + */ + readonly placementConstraints?: ecs.PlacementConstraint[]; + + /** + * Placement strategies + * + * @default - None + */ + readonly placementStrategies?: ecs.PlacementStrategy[]; +} + +/** + * Configuration for running an ECS task on Fargate + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/userguide/launch_types.html#launch-type-fargate + */ +export class EcsFargateLaunchTarget implements IEcsLaunchTarget { + constructor(private readonly options?: EcsFargateLaunchTargetOptions) {} + + /** + * Called when the Fargate launch type configured on RunTask + */ + public bind(_task: EcsRunTask): EcsLaunchTargetConfig { + return { + parameters: { + LaunchType: 'FARGATE', + PlatformVersion: this.options?.platformVersion, + }, + }; + } +} + +/** + * Configuration for running an ECS task on EC2 + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/userguide/launch_types.html#launch-type-ec2 + */ +export class EcsEc2LaunchTarget implements IEcsLaunchTarget { + constructor(private readonly options?: EcsEc2LaunchTargetOptions) {} + /** + * Called when the EC2 launch type is configured on RunTask + */ + public bind(_task: EcsRunTask): EcsLaunchTargetConfig { + return { + parameters: { + LaunchType: 'EC2', + PlacementConstraints: noEmpty(flatten((this.options?.placementConstraints || []).map((c) => c.toJson().map(uppercaseKeys)))), + PlacementStrategy: noEmpty(flatten((this.options?.placementStrategies || []).map((c) => c.toJson().map(uppercaseKeys)))), + }, + }; + + function uppercaseKeys(obj: { [key: string]: any }): { [key: string]: any } { + const ret: { [key: string]: any } = {}; + for (const key of Object.keys(obj)) { + ret[key.slice(0, 1).toUpperCase() + key.slice(1)] = obj[key]; + } + return ret; + } + + function flatten(xs: A[][]): A[] { + return Array.prototype.concat([], ...xs); + } + + function noEmpty(xs: A[]): A[] | undefined { + if (xs.length === 0) { + return undefined; + } + return xs; + } + } +} + +/** + * Run a Task on ECS or Fargate + */ +export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + /** + * Manage allowed network traffic for this service + */ + public readonly connections: ec2.Connections = new ec2.Connections(); + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private securityGroup?: ec2.ISecurityGroup; + private networkConfiguration?: any; + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: cdk.Construct, id: string, private readonly props: EcsRunTaskProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, EcsRunTask.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.containerOverrides)) { + throw new Error('Task Token is required in `containerOverrides` for callback. Use Context.taskToken to set the token.'); + } + + this.validateLaunchTarget(); + + for (const override of this.props.containerOverrides || []) { + const name = override.containerDefinition.containerName; + if (!cdk.Token.isUnresolved(name)) { + const cont = this.props.taskDefinition.node.tryFindChild(name); + if (!cont) { + throw new Error(`Overrides mention container with name '${name}', but no such container in task definition`); + } + } + } + + this.taskPolicies = this.makePolicyStatements(); + } + + protected renderTask(): any { + if (this.networkConfiguration !== undefined) { + // Make sure we have a security group if we're using AWSVPC networking + this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); + this.connections.addSecurityGroup(this.securityGroup); + } + + return { + Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + Cluster: this.props.cluster.clusterArn, + TaskDefinition: this.props.taskDefinition.taskDefinitionArn, + NetworkConfiguration: this.networkConfiguration, + Overrides: renderOverrides(this.props.containerOverrides), + ...this.props.launchTarget.bind(this).parameters, + }), + }; + } + + private configureAwsVpcNetworking() { + const subnetSelection = this.props.subnets ?? { subnetType: this.props.assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE }; + + this.networkConfiguration = { + AwsvpcConfiguration: { + AssignPublicIp: this.props.assignPublicIp ? (this.props.assignPublicIp ? 'ENABLED' : 'DISABLED') : undefined, + Subnets: this.props.cluster.vpc.selectSubnets(subnetSelection).subnetIds, + SecurityGroups: cdk.Lazy.listValue({ produce: () => [this.securityGroup!.securityGroupId] }), + }, + }; + } + + private validateLaunchTarget() { + if (this.props.launchTarget instanceof EcsFargateLaunchTarget) { + this.validateFargateLaunchType(); + } + + if (this.props.launchTarget instanceof EcsEc2LaunchTarget) { + this.validateEc2LaunchType(); + } + + if (!this.props.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + } + + private validateEc2LaunchType() { + if (!this.props.cluster.hasEc2Capacity) { + throw new Error('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); + } + + if (this.props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { + this.configureAwsVpcNetworking(); + } else { + // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. + this.validateNoNetworkingProps(); + this.connections.addSecurityGroup(...this.props.cluster.connections.securityGroups); + } + } + + private validateNoNetworkingProps() { + if (this.props.subnets !== undefined || this.props.securityGroup !== undefined) { + throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); + } + } + + private validateFargateLaunchType() { + if (!this.props.taskDefinition.isFargateCompatible) { + throw new Error('Supplied TaskDefinition is not configured for compatibility with Fargate'); + } + + this.configureAwsVpcNetworking(); + } + + private makePolicyStatements(): iam.PolicyStatement[] { + const stack = cdk.Stack.of(this); + + // https://docs.aws.amazon.com/step-functions/latest/dg/ecs-iam.html + const policyStatements = [ + new iam.PolicyStatement({ + actions: ['ecs:RunTask'], + resources: [this.props.taskDefinition.taskDefinitionArn], + }), + new iam.PolicyStatement({ + actions: ['ecs:StopTask', 'ecs:DescribeTasks'], + resources: ['*'], + }), + new iam.PolicyStatement({ + actions: ['iam:PassRole'], + resources: cdk.Lazy.listValue({ produce: () => this.taskExecutionRoles().map((r) => r.roleArn) }), + }), + ]; + + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForECSTaskRule', + }), + ], + }), + ); + } + + return policyStatements; + } + + private taskExecutionRoles(): iam.IRole[] { + // Need to be able to pass both Task and Execution role, apparently + const ret = new Array(); + ret.push(this.props.taskDefinition.taskRole); + if (this.props.taskDefinition.executionRole) { + ret.push(this.props.taskDefinition.executionRole); + } + return ret; + } +} + +function renderOverrides(containerOverrides?: ContainerOverride[]) { + if (!containerOverrides) { + return undefined; + } + + const ret = new Array(); + for (const override of containerOverrides) { + ret.push({ + Name: override.containerDefinition.containerName, + Command: override.command, + Cpu: override.cpu, + Memory: override.memoryLimit, + MemoryReservation: override.memoryReservation, + Environment: + override.environment && + override.environment.map((e) => ({ + Name: e.name, + Value: e.value, + })), + }); + } + + return { ContainerOverrides: ret }; +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 61dd965141dcb..672defd4d4c4a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -10,9 +10,8 @@ export * from './sqs/send-to-queue'; export * from './sqs/send-message'; export * from './ecs/run-ecs-ec2-task'; export * from './ecs/run-ecs-fargate-task'; -export * from './ecs/ec2-run-task'; -export * from './ecs/fargate-run-task'; export * from './ecs/run-task-base'; +export * from './ecs/run-task'; export * from './sagemaker/base-types'; export * from './sagemaker/create-training-job'; export * from './sagemaker/create-transform-job'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/ecs-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/ecs-tasks.test.ts index 611339b3fe08a..dd8a0498763c9 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/ecs-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/ecs-tasks.test.ts @@ -30,8 +30,7 @@ test('Cannot create a Fargate task with a fargate-incompatible task definition', memoryLimitMiB: 256, }); - expect(() => new tasks.RunEcsFargateTask({ cluster, taskDefinition })) - .toThrowError(/not configured for compatibility with Fargate/); + expect(() => new tasks.RunEcsFargateTask({ cluster, taskDefinition })).toThrowError(/not configured for compatibility with Fargate/); }); test('Cannot create a Fargate task without a default container', () => { @@ -40,8 +39,7 @@ test('Cannot create a Fargate task without a default container', () => { cpu: '256', compatibility: ecs.Compatibility.FARGATE, }); - expect(() => new tasks.RunEcsFargateTask({ cluster, taskDefinition })) - .toThrowError(/must have at least one essential container/); + expect(() => new tasks.RunEcsFargateTask({ cluster, taskDefinition })).toThrowError(/must have at least one essential container/); }); test('Running a Fargate Task', () => { @@ -50,25 +48,25 @@ test('Running a Fargate Task', () => { cpu: '256', compatibility: ecs.Compatibility.FARGATE, }); - taskDefinition.addContainer('TheContainer', { + const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromRegistry('foo/bar'), memoryLimitMiB: 256, }); // WHEN - const runTask = new sfn.Task(stack, 'RunFargate', { task: new tasks.RunEcsFargateTask({ - integrationPattern: sfn.ServiceIntegrationPattern.SYNC, - cluster, - taskDefinition, - containerOverrides: [ - { - containerName: 'TheContainer', - environment: [ - {name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey')}, - ], - }, - ], - }) }); + const runTask = new sfn.Task(stack, 'RunFargate', { + task: new tasks.RunEcsFargateTask({ + integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + cluster, + taskDefinition, + containerOverrides: [ + { + containerDefinition, + environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + }, + ], + }), + }); new sfn.StateMachine(stack, 'SM', { definition: runTask, @@ -78,18 +76,15 @@ test('Running a Fargate Task', () => { expect(stack.resolve(runTask.toStateJson())).toEqual({ End: true, Parameters: { - Cluster: {'Fn::GetAtt': ['ClusterEB0386A7', 'Arn']}, + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, LaunchType: 'FARGATE', NetworkConfiguration: { AwsvpcConfiguration: { - SecurityGroups: [{'Fn::GetAtt': ['RunFargateSecurityGroup709740F2', 'GroupId']}], - Subnets: [ - {Ref: 'VpcPrivateSubnet1Subnet536B997A'}, - {Ref: 'VpcPrivateSubnet2Subnet3788AAA1'}, - ], + SecurityGroups: [{ 'Fn::GetAtt': ['RunFargateSecurityGroup709740F2', 'GroupId'] }], + Subnets: [{ Ref: 'VpcPrivateSubnet1Subnet536B997A' }, { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }], }, }, - TaskDefinition: {Ref: 'TD49C78F36'}, + TaskDefinition: { Ref: 'TD49C78F36' }, Overrides: { ContainerOverrides: [ { @@ -125,7 +120,7 @@ test('Running a Fargate Task', () => { { Action: 'ecs:RunTask', Effect: 'Allow', - Resource: {Ref: 'TD49C78F36'}, + Resource: { Ref: 'TD49C78F36' }, }, { Action: ['ecs:StopTask', 'ecs:DescribeTasks'], @@ -135,20 +130,25 @@ test('Running a Fargate Task', () => { { Action: 'iam:PassRole', Effect: 'Allow', - Resource: [{'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn']}], + Resource: [{ 'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn'] }], }, { Action: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], Effect: 'Allow', - Resource: {'Fn::Join': ['', [ - 'arn:', - {Ref: 'AWS::Partition'}, - ':events:', - {Ref: 'AWS::Region'}, - ':', - {Ref: 'AWS::AccountId'}, - ':rule/StepFunctionsGetEventsForECSTaskRule', - ]]}, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':events:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':rule/StepFunctionsGetEventsForECSTaskRule', + ], + ], + }, }, ], }, @@ -159,25 +159,25 @@ test('Running an EC2 Task with bridge network', () => { const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { compatibility: ecs.Compatibility.EC2, }); - taskDefinition.addContainer('TheContainer', { + const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromRegistry('foo/bar'), memoryLimitMiB: 256, }); // WHEN - const runTask = new sfn.Task(stack, 'Run', { task: new tasks.RunEcsEc2Task({ - integrationPattern: sfn.ServiceIntegrationPattern.SYNC, - cluster, - taskDefinition, - containerOverrides: [ - { - containerName: 'TheContainer', - environment: [ - {name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey')}, - ], - }, - ], - }) }); + const runTask = new sfn.Task(stack, 'Run', { + task: new tasks.RunEcsEc2Task({ + integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + cluster, + taskDefinition, + containerOverrides: [ + { + containerDefinition, + environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + }, + ], + }), + }); new sfn.StateMachine(stack, 'SM', { definition: runTask, @@ -187,9 +187,9 @@ test('Running an EC2 Task with bridge network', () => { expect(stack.resolve(runTask.toStateJson())).toEqual({ End: true, Parameters: { - Cluster: {'Fn::GetAtt': ['ClusterEB0386A7', 'Arn']}, + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, LaunchType: 'EC2', - TaskDefinition: {Ref: 'TD49C78F36'}, + TaskDefinition: { Ref: 'TD49C78F36' }, Overrides: { ContainerOverrides: [ { @@ -225,7 +225,7 @@ test('Running an EC2 Task with bridge network', () => { { Action: 'ecs:RunTask', Effect: 'Allow', - Resource: {Ref: 'TD49C78F36'}, + Resource: { Ref: 'TD49C78F36' }, }, { Action: ['ecs:StopTask', 'ecs:DescribeTasks'], @@ -235,20 +235,25 @@ test('Running an EC2 Task with bridge network', () => { { Action: 'iam:PassRole', Effect: 'Allow', - Resource: [{'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn']}], + Resource: [{ 'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn'] }], }, { Action: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], Effect: 'Allow', - Resource: {'Fn::Join': ['', [ - 'arn:', - {Ref: 'AWS::Partition'}, - ':events:', - {Ref: 'AWS::Region'}, - ':', - {Ref: 'AWS::AccountId'}, - ':rule/StepFunctionsGetEventsForECSTaskRule', - ]]}, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':events:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':rule/StepFunctionsGetEventsForECSTaskRule', + ], + ], + }, }, ], }, @@ -268,11 +273,7 @@ test('Running an EC2 Task with placement strategies', () => { integrationPattern: sfn.ServiceIntegrationPattern.SYNC, cluster, taskDefinition, - placementStrategies: [ - ecs.PlacementStrategy.spreadAcrossInstances(), - ecs.PlacementStrategy.packedByCpu(), - ecs.PlacementStrategy.randomly(), - ], + placementStrategies: [ecs.PlacementStrategy.spreadAcrossInstances(), ecs.PlacementStrategy.packedByCpu(), ecs.PlacementStrategy.randomly()], placementConstraints: [ecs.PlacementConstraint.memberOf('blieptuut')], }); @@ -287,17 +288,11 @@ test('Running an EC2 Task with placement strategies', () => { expect(stack.resolve(runTask.toStateJson())).toEqual({ End: true, Parameters: { - Cluster: {'Fn::GetAtt': ['ClusterEB0386A7', 'Arn']}, + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, LaunchType: 'EC2', - TaskDefinition: {Ref: 'TD49C78F36'}, - PlacementConstraints: [ - { Type: 'memberOf', Expression: 'blieptuut' }, - ], - PlacementStrategy: [ - { Field: 'instanceId', Type: 'spread' }, - { Field: 'cpu', Type: 'binpack' }, - { Type: 'random' }, - ], + TaskDefinition: { Ref: 'TD49C78F36' }, + PlacementConstraints: [{ Type: 'memberOf', Expression: 'blieptuut' }], + PlacementStrategy: [{ Field: 'instanceId', Type: 'spread' }, { Field: 'cpu', Type: 'binpack' }, { Type: 'random' }], }, Resource: { 'Fn::Join': [ @@ -319,7 +314,7 @@ test('Running an EC2 Task with overridden number values', () => { const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { compatibility: ecs.Compatibility.EC2, }); - taskDefinition.addContainer('TheContainer', { + const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromRegistry('foo/bar'), memoryLimitMiB: 256, }); @@ -330,7 +325,7 @@ test('Running an EC2 Task with overridden number values', () => { taskDefinition, containerOverrides: [ { - containerName: 'TheContainer', + containerDefinition, command: sfn.Data.listAt('$.TheCommand'), cpu: 5, memoryLimit: sfn.Data.numberAt('$.MemoryLimit'), @@ -345,9 +340,9 @@ test('Running an EC2 Task with overridden number values', () => { expect(stack.resolve(runTask.toStateJson())).toEqual({ End: true, Parameters: { - Cluster: {'Fn::GetAtt': ['ClusterEB0386A7', 'Arn']}, + Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, LaunchType: 'EC2', - TaskDefinition: {Ref: 'TD49C78F36'}, + TaskDefinition: { Ref: 'TD49C78F36' }, Overrides: { ContainerOverrides: [ { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json index 8b6a5b79e1945..634506837f8d5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json @@ -1,12 +1,12 @@ { "Resources": { - "FargateCluster7CCD5F93": { + "Ec2ClusterEE43E89D": { "Type": "AWS::ECS::Cluster" }, - "FargateClusterDefaultAutoScalingGroupInstanceSecurityGroup42AF8A40": { + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E": { "Type": "AWS::EC2::SecurityGroup", "Properties": { - "GroupDescription": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "GroupDescription": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", @@ -17,13 +17,13 @@ "Tags": [ { "Key": "Name", - "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" } ], "VpcId": "vpc-60900905" } }, - "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7": { + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -51,12 +51,12 @@ "Tags": [ { "Key": "Name", - "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" } ] } }, - "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E": { + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -80,25 +80,25 @@ ], "Version": "2012-10-17" }, - "PolicyName": "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E", + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", "Roles": [ { - "Ref": "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" } ] } }, - "FargateClusterDefaultAutoScalingGroupInstanceProfile2C0FEF3B": { + "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Roles": [ { - "Ref": "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" } ] } }, - "FargateClusterDefaultAutoScalingGroupLaunchConfig57306899": { + "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": { @@ -106,12 +106,12 @@ }, "InstanceType": "t2.micro", "IamInstanceProfile": { - "Ref": "FargateClusterDefaultAutoScalingGroupInstanceProfile2C0FEF3B" + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" }, "SecurityGroups": [ { "Fn::GetAtt": [ - "FargateClusterDefaultAutoScalingGroupInstanceSecurityGroup42AF8A40", + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", "GroupId" ] } @@ -123,7 +123,7 @@ [ "#!/bin/bash\necho ECS_CLUSTER=", { - "Ref": "FargateCluster7CCD5F93" + "Ref": "Ec2ClusterEE43E89D" }, " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" ] @@ -132,23 +132,23 @@ } }, "DependsOn": [ - "FargateClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy3BD78F3E", - "FargateClusterDefaultAutoScalingGroupInstanceRole0C1F7FF7" + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" ] }, - "FargateClusterDefaultAutoScalingGroupASG36A4948F": { + "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "MaxSize": "1", "MinSize": "1", "LaunchConfigurationName": { - "Ref": "FargateClusterDefaultAutoScalingGroupLaunchConfig57306899" + "Ref": "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" }, "Tags": [ { "Key": "Name", "PropagateAtLaunch": true, - "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" } ], "VPCZoneIdentifier": [ @@ -166,7 +166,7 @@ } } }, - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32": { + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -198,12 +198,12 @@ "Tags": [ { "Key": "Name", - "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" } ] } }, - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343": { + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -231,7 +231,7 @@ }, ":autoscaling:test-region:12345678:autoScalingGroup:*:autoScalingGroupName/", { - "Ref": "FargateClusterDefaultAutoScalingGroupASG36A4948F" + "Ref": "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0" } ] ] @@ -254,7 +254,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "FargateCluster7CCD5F93", + "Ec2ClusterEE43E89D", "Arn" ] } @@ -268,7 +268,7 @@ "ArnEquals": { "ecs:cluster": { "Fn::GetAtt": [ - "FargateCluster7CCD5F93", + "Ec2ClusterEE43E89D", "Arn" ] } @@ -280,15 +280,15 @@ ], "Version": "2012-10-17" }, - "PolicyName": "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343", + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33", "Roles": [ { - "Ref": "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32" + "Ref": "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3" } ] } }, - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8": { + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { @@ -297,7 +297,7 @@ "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32", + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3", "Arn" ] }, @@ -305,55 +305,55 @@ "Environment": { "Variables": { "CLUSTER": { - "Ref": "FargateCluster7CCD5F93" + "Ref": "Ec2ClusterEE43E89D" } } }, "Tags": [ { "Key": "Name", - "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" } ], "Timeout": 310 }, "DependsOn": [ - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyB91C5343", - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32" + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33", + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3" ] }, - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionAllowInvokeawssfntasksecsec2integFargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic4F1711EC894BE644": { + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionAllowInvokeawssfntasksecsec2integEc2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicEE9E39A29ACCEEA3": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { "Fn::GetAtt": [ - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8", + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31", "Arn" ] }, "Principal": "sns.amazonaws.com", "SourceArn": { - "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10" + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" } } }, - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionTopic37856E82": { + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionTopic4795E0F6": { "Type": "AWS::SNS::Subscription", "Properties": { "Protocol": "lambda", "TopicArn": { - "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10" + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" }, "Endpoint": { "Fn::GetAtt": [ - "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionE3D5BEE8", + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31", "Arn" ] } } }, - "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D": { + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -371,12 +371,12 @@ "Tags": [ { "Key": "Name", - "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" } ] } }, - "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D": { + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -385,53 +385,53 @@ "Action": "sns:Publish", "Effect": "Allow", "Resource": { - "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10" + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" } } ], "Version": "2012-10-17" }, - "PolicyName": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D", + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B", "Roles": [ { - "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D" + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7" } ] } }, - "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10": { + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30": { "Type": "AWS::SNS::Topic", "Properties": { "Tags": [ { "Key": "Name", - "Value": "aws-sfn-tasks-ecs-ec2-integ/FargateCluster/DefaultAutoScalingGroup" + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" } ] } }, - "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHook2AE13680": { + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHook5CB1467E": { "Type": "AWS::AutoScaling::LifecycleHook", "Properties": { "AutoScalingGroupName": { - "Ref": "FargateClusterDefaultAutoScalingGroupASG36A4948F" + "Ref": "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0" }, "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", "DefaultResult": "CONTINUE", "HeartbeatTimeout": 300, "NotificationTargetARN": { - "Ref": "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic49146C10" + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" }, "RoleARN": { "Fn::GetAtt": [ - "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D", + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7", "Arn" ] } }, "DependsOn": [ - "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy4958D19D", - "FargateClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole410D556D" + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B", + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7" ] }, "TaskDefTaskRole1EDB4A67": { @@ -684,7 +684,7 @@ ":states:::ecs:runTask.sync\",\"Parameters\":{\"Cluster\":\"", { "Fn::GetAtt": [ - "FargateCluster7CCD5F93", + "Ec2ClusterEE43E89D", "Arn" ] }, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts index 50b5d3085f145..1a4a01e6c78d7 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts @@ -27,7 +27,7 @@ const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { isDefault: true, }); -const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); +const cluster = new ecs.Cluster(stack, 'Ec2Cluster', { vpc }); cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, @@ -35,7 +35,7 @@ cluster.addCapacity('DefaultAutoScalingGroup', { // Build task definition const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); -taskDefinition.addContainer('TheContainer', { +const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'eventhandler-image')), memoryLimitMiB: 256, logging: new ecs.AwsLogDriver({ streamPrefix: 'EventDemo' }), @@ -45,13 +45,13 @@ taskDefinition.addContainer('TheContainer', { const definition = new sfn.Pass(stack, 'Start', { result: sfn.Result.fromObject({ SomeKey: 'SomeValue' }), }).next( - new tasks.EcsEc2RunTask(stack, 'Run', { + new tasks.EcsRunTask(stack, 'Run', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, containerOverrides: [ { - containerName: 'TheContainer', + containerDefinition, environment: [ { name: 'SOME_KEY', @@ -60,6 +60,7 @@ const definition = new sfn.Pass(stack, 'Start', { ], }, ], + launchTarget: new tasks.EcsEc2LaunchTarget(), }), ); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts index aa4238f85a218..963d1256d79ae 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts @@ -25,7 +25,7 @@ cluster.addCapacity('DefaultAutoScalingGroup', { // Build task definition const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); -taskDefinition.addContainer('TheContainer', { +const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'eventhandler-image')), memoryLimitMiB: 256, logging: new ecs.AwsLogDriver({ streamPrefix: 'EventDemo' }), @@ -34,22 +34,26 @@ taskDefinition.addContainer('TheContainer', { // Build state machine const definition = new sfn.Pass(stack, 'Start', { result: sfn.Result.fromObject({ SomeKey: 'SomeValue' }), -}).next(new sfn.Task(stack, 'Run', { task: new tasks.RunEcsEc2Task({ - integrationPattern: sfn.ServiceIntegrationPattern.SYNC, - cluster, - taskDefinition, - containerOverrides: [ - { - containerName: 'TheContainer', - environment: [ +}).next( + new sfn.Task(stack, 'Run', { + task: new tasks.RunEcsEc2Task({ + integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + cluster, + taskDefinition, + containerOverrides: [ { - name: 'SOME_KEY', - value: sfn.Data.stringAt('$.SomeKey'), + containerDefinition, + environment: [ + { + name: 'SOME_KEY', + value: sfn.Data.stringAt('$.SomeKey'), + }, + ], }, ], - }, - ], -})})); + }), + }), +); new sfn.StateMachine(stack, 'StateMachine', { definition, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts index 6f22a1d810ed7..c159881d36b2a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts @@ -34,7 +34,7 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryLimitMiB: 512, cpu: 256, }); -taskDefinition.addContainer('TheContainer', { +const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'eventhandler-image')), memoryLimitMiB: 256, logging: new ecs.AwsLogDriver({ streamPrefix: 'EventDemo' }), @@ -44,14 +44,14 @@ taskDefinition.addContainer('TheContainer', { const definition = new sfn.Pass(stack, 'Start', { result: sfn.Result.fromObject({ SomeKey: 'SomeValue' }), }).next( - new tasks.EcsFargateRunTask(stack, 'FargateTask', { + new tasks.EcsRunTask(stack, 'FargateTask', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, assignPublicIp: true, containerOverrides: [ { - containerName: 'TheContainer', + containerDefinition, environment: [ { name: 'SOME_KEY', @@ -60,6 +60,7 @@ const definition = new sfn.Pass(stack, 'Start', { ], }, ], + launchTarget: new tasks.EcsFargateLaunchTarget(), }), ); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts index 8c05adb3dbcba..7eca53d3c7d1a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts @@ -24,7 +24,7 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryLimitMiB: 512, cpu: 256, }); -taskDefinition.addContainer('TheContainer', { +const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'eventhandler-image')), memoryLimitMiB: 256, logging: new ecs.AwsLogDriver({ streamPrefix: 'EventDemo' }), @@ -33,22 +33,27 @@ taskDefinition.addContainer('TheContainer', { // Build state machine const definition = new sfn.Pass(stack, 'Start', { result: sfn.Result.fromObject({ SomeKey: 'SomeValue' }), -}).next(new sfn.Task(stack, 'FargateTask', { task: new tasks.RunEcsFargateTask({ - integrationPattern: sfn.ServiceIntegrationPattern.SYNC, - cluster, taskDefinition, - assignPublicIp: true, - containerOverrides: [ - { - containerName: 'TheContainer', - environment: [ +}).next( + new sfn.Task(stack, 'FargateTask', { + task: new tasks.RunEcsFargateTask({ + integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + cluster, + taskDefinition, + assignPublicIp: true, + containerOverrides: [ { - name: 'SOME_KEY', - value: sfn.Data.stringAt('$.SomeKey'), + containerDefinition, + environment: [ + { + name: 'SOME_KEY', + value: sfn.Data.stringAt('$.SomeKey'), + }, + ], }, ], - }, - ], -})})); + }), + }), +); new sfn.StateMachine(stack, 'StateMachine', { definition, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts index 2820ddc007218..31ec5f1f96306 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -32,7 +32,9 @@ test('Cannot create a Fargate task with a fargate-incompatible task definition', memoryLimitMiB: 256, }); - expect(() => new tasks.EcsFargateRunTask(stack, 'task', { cluster, taskDefinition })).toThrowError(/not configured for compatibility with Fargate/); + expect(() => new tasks.EcsRunTask(stack, 'task', { cluster, taskDefinition, launchTarget: new tasks.EcsFargateLaunchTarget() })).toThrowError( + /not configured for compatibility with Fargate/, + ); }); test('Cannot create a Fargate task without a default container', () => { @@ -41,7 +43,9 @@ test('Cannot create a Fargate task without a default container', () => { cpu: '256', compatibility: ecs.Compatibility.FARGATE, }); - expect(() => new tasks.EcsFargateRunTask(stack, 'task', { cluster, taskDefinition })).toThrowError(/must have at least one essential container/); + expect(() => new tasks.EcsRunTask(stack, 'task', { cluster, taskDefinition, launchTarget: new tasks.EcsFargateLaunchTarget() })).toThrowError( + /must have at least one essential container/, + ); }); test('Running a Fargate Task', () => { @@ -50,23 +54,25 @@ test('Running a Fargate Task', () => { cpu: '256', compatibility: ecs.Compatibility.FARGATE, }); - taskDefinition.addContainer('TheContainer', { + const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromRegistry('foo/bar'), memoryLimitMiB: 256, }); // WHEN - const runTask = new tasks.EcsFargateRunTask(stack, 'RunFargate', { + const runTask = new tasks.EcsRunTask(stack, 'RunFargate', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, containerOverrides: [ { - containerName: 'TheContainer', + containerDefinition, environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], }, ], - platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + launchTarget: new tasks.EcsFargateLaunchTarget({ + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + }), }); new sfn.StateMachine(stack, 'SM', { @@ -85,7 +91,7 @@ test('Running a Fargate Task', () => { Subnets: [{ Ref: 'VpcPrivateSubnet1Subnet536B997A' }, { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }], }, }, - 'PlatformVersion': '1.4.0', + PlatformVersion: '1.4.0', TaskDefinition: { Ref: 'TD49C78F36' }, Overrides: { ContainerOverrides: [ @@ -161,22 +167,23 @@ test('Running an EC2 Task with bridge network', () => { const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { compatibility: ecs.Compatibility.EC2, }); - taskDefinition.addContainer('TheContainer', { + const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromRegistry('foo/bar'), memoryLimitMiB: 256, }); // WHEN - const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { + const runTask = new tasks.EcsRunTask(stack, 'Run', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, containerOverrides: [ { - containerName: 'TheContainer', + containerDefinition, environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], }, ], + launchTarget: new tasks.EcsEc2LaunchTarget(), }); new sfn.StateMachine(stack, 'SM', { @@ -270,12 +277,14 @@ test('Running an EC2 Task with placement strategies', () => { }); // WHEN - const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { + const runTask = new tasks.EcsRunTask(stack, 'Run', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, - placementStrategies: [ecs.PlacementStrategy.spreadAcrossInstances(), ecs.PlacementStrategy.packedByCpu(), ecs.PlacementStrategy.randomly()], - placementConstraints: [ecs.PlacementConstraint.memberOf('blieptuut')], + launchTarget: new tasks.EcsEc2LaunchTarget({ + placementStrategies: [ecs.PlacementStrategy.spreadAcrossInstances(), ecs.PlacementStrategy.packedByCpu(), ecs.PlacementStrategy.randomly()], + placementConstraints: [ecs.PlacementConstraint.memberOf('blieptuut')], + }), }); new sfn.StateMachine(stack, 'SM', { @@ -312,24 +321,25 @@ test('Running an EC2 Task with overridden number values', () => { const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { compatibility: ecs.Compatibility.EC2, }); - taskDefinition.addContainer('TheContainer', { + const containerDefinition = taskDefinition.addContainer('TheContainer', { image: ecs.ContainerImage.fromRegistry('foo/bar'), memoryLimitMiB: 256, }); // WHEN - const runTask = new tasks.EcsEc2RunTask(stack, 'Run', { + const runTask = new tasks.EcsRunTask(stack, 'Run', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, containerOverrides: [ { - containerName: 'TheContainer', + containerDefinition, command: sfn.Data.listAt('$.TheCommand'), cpu: 5, memoryLimit: sfn.Data.numberAt('$.MemoryLimit'), }, ], + launchTarget: new tasks.EcsEc2LaunchTarget(), }); // THEN From 49dbaaa487509bb3ddd7ef95d0ccd8f16a917490 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 24 Jun 2020 03:55:46 -0700 Subject: [PATCH 10/27] remove unused task base --- .../lib/ecs/run-task-base.ts | 207 ------------------ .../aws-stepfunctions-tasks/lib/index.ts | 1 - 2 files changed, 208 deletions(-) delete mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts deleted file mode 100644 index 2cdfadd494a0a..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task-base.ts +++ /dev/null @@ -1,207 +0,0 @@ -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as ecs from '@aws-cdk/aws-ecs'; -import * as iam from '@aws-cdk/aws-iam'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; -import { ContainerOverride } from '..'; -import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; - -/** - * Basic properties for ECS Tasks - */ -export interface EcsRunTaskCommonProps extends sfn.TaskStateBaseProps { - /** - * The topic to run the task on - */ - readonly cluster: ecs.ICluster; - - /** - * [disable-awslint:ref-via-interface] - * Task Definition used for running tasks in the service. - * - * Note: this must be TaskDefinition, and not ITaskDefinition, - * as it requires properties that are not known for imported task definitions - */ - readonly taskDefinition: ecs.TaskDefinition; - - /** - * Container setting overrides - * - * Key is the name of the container to override, value is the - * values you want to override. - * - * @default - No overrides - */ - readonly containerOverrides?: ContainerOverride[]; -} - -/** - * Construction properties for the BaseRunTaskProps - */ -export interface EcsRunTaskStateBaseProps extends EcsRunTaskCommonProps { - /** - * Additional parameters to pass to the base task - * - * @default - No additional parameters passed - */ - readonly parameters?: { [key: string]: any }; -} - -/** - * Run a Task on ECS or Fargate - */ -export abstract class EcsRunTaskStateBase extends sfn.TaskStateBase implements ec2.IConnectable { - private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ - sfn.IntegrationPattern.REQUEST_RESPONSE, - sfn.IntegrationPattern.RUN_JOB, - sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, - ]; - - /** - * Manage allowed network traffic for this service - */ - public readonly connections: ec2.Connections = new ec2.Connections(); - - protected readonly taskMetrics?: sfn.TaskMetricsConfig; - protected readonly taskPolicies?: iam.PolicyStatement[]; - - private securityGroup?: ec2.ISecurityGroup; - private networkConfiguration?: any; - private readonly integrationPattern: sfn.IntegrationPattern; - - constructor(scope: cdk.Construct, id: string, private readonly props: EcsRunTaskStateBaseProps) { - super(scope, id, props); - this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; - - validatePatternSupported(this.integrationPattern, EcsRunTaskStateBase.SUPPORTED_INTEGRATION_PATTERNS); - - if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.containerOverrides)) { - throw new Error('Task Token is required in `containerOverrides` for callback. Use Context.taskToken to set the token.'); - } - - for (const override of this.props.containerOverrides || []) { - const name = override.containerDefinition.containerName; - if (!cdk.Token.isUnresolved(name)) { - const cont = this.props.taskDefinition.node.tryFindChild(name); - if (!cont) { - throw new Error(`Overrides mention container with name '${name}', but no such container in task definition`); - } - } - } - - this.taskPolicies = this.makePolicyStatements(); - } - - protected renderTask(): any { - if (this.networkConfiguration !== undefined) { - // Make sure we have a security group if we're using AWSVPC networking - this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); - this.connections.addSecurityGroup(this.securityGroup); - } - - return { - Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - Cluster: this.props.cluster.clusterArn, - TaskDefinition: this.props.taskDefinition.taskDefinitionArn, - NetworkConfiguration: this.networkConfiguration, - Overrides: renderOverrides(this.props.containerOverrides), - ...this.props.parameters, - }), - }; - } - - protected configureAwsVpcNetworking( - vpc: ec2.IVpc, - assignPublicIp?: boolean, - subnetSelection?: ec2.SubnetSelection, - securityGroup?: ec2.ISecurityGroup, - ) { - if (subnetSelection === undefined) { - subnetSelection = { subnetType: assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE }; - } - - // If none is given here, one will be created later on during bind() - this.securityGroup = securityGroup; - - this.networkConfiguration = { - AwsvpcConfiguration: { - AssignPublicIp: assignPublicIp !== undefined ? (assignPublicIp ? 'ENABLED' : 'DISABLED') : undefined, - Subnets: vpc.selectSubnets(subnetSelection).subnetIds, - SecurityGroups: cdk.Lazy.listValue({ produce: () => [this.securityGroup!.securityGroupId] }), - }, - }; - } - - private makePolicyStatements(): iam.PolicyStatement[] { - const stack = cdk.Stack.of(this); - - // https://docs.aws.amazon.com/step-functions/latest/dg/ecs-iam.html - const policyStatements = [ - new iam.PolicyStatement({ - actions: ['ecs:RunTask'], - resources: [this.props.taskDefinition.taskDefinitionArn], - }), - new iam.PolicyStatement({ - actions: ['ecs:StopTask', 'ecs:DescribeTasks'], - resources: ['*'], - }), - new iam.PolicyStatement({ - actions: ['iam:PassRole'], - resources: cdk.Lazy.listValue({ produce: () => this.taskExecutionRoles().map((r) => r.roleArn) }), - }), - ]; - - if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { - policyStatements.push( - new iam.PolicyStatement({ - actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], - resources: [ - stack.formatArn({ - service: 'events', - resource: 'rule', - resourceName: 'StepFunctionsGetEventsForECSTaskRule', - }), - ], - }), - ); - } - - return policyStatements; - } - - private taskExecutionRoles(): iam.IRole[] { - // Need to be able to pass both Task and Execution role, apparently - const ret = new Array(); - ret.push(this.props.taskDefinition.taskRole); - if (this.props.taskDefinition.executionRole) { - ret.push(this.props.taskDefinition.executionRole); - } - return ret; - } -} - -function renderOverrides(containerOverrides?: ContainerOverride[]) { - if (!containerOverrides) { - return undefined; - } - - const ret = new Array(); - for (const override of containerOverrides) { - ret.push({ - Name: override.containerDefinition.containerName, - Command: override.command, - Cpu: override.cpu, - Memory: override.memoryLimit, - MemoryReservation: override.memoryReservation, - Environment: - override.environment && - override.environment.map((e) => ({ - Name: e.name, - Value: e.value, - })), - }); - } - - return { ContainerOverrides: ret }; -} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 672defd4d4c4a..e46bc00c623e5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -10,7 +10,6 @@ export * from './sqs/send-to-queue'; export * from './sqs/send-message'; export * from './ecs/run-ecs-ec2-task'; export * from './ecs/run-ecs-fargate-task'; -export * from './ecs/run-task-base'; export * from './ecs/run-task'; export * from './sagemaker/base-types'; export * from './sagemaker/create-training-job'; From 15693b357062077436def48559310722cda5e47f Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 24 Jun 2020 22:19:44 -0700 Subject: [PATCH 11/27] update sfn.Data -> sfn.JsonPath --- packages/@aws-cdk/aws-stepfunctions-tasks/README.md | 2 +- .../test/ecs/integ.ec2-run-task.ts | 2 +- .../test/ecs/integ.fargate-run-task.ts | 2 +- .../aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 98c71f8f580a8..5df0f9ef2bea5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -406,7 +406,7 @@ const runTask = new tasks.EcsRunTask(stack, 'RunFargate', { taskDefinition, containerOverrides: [{ containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], }], launchTarget: new tasks.EcsFargateLaunchTarget(), }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts index 1a4a01e6c78d7..7713e04363465 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts @@ -55,7 +55,7 @@ const definition = new sfn.Pass(stack, 'Start', { environment: [ { name: 'SOME_KEY', - value: sfn.Data.stringAt('$.SomeKey'), + value: sfn.JsonPath.stringAt('$.SomeKey'), }, ], }, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts index c159881d36b2a..006ed077baf73 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts @@ -55,7 +55,7 @@ const definition = new sfn.Pass(stack, 'Start', { environment: [ { name: 'SOME_KEY', - value: sfn.Data.stringAt('$.SomeKey'), + value: sfn.JsonPath.stringAt('$.SomeKey'), }, ], }, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts index 31ec5f1f96306..75ab7a24eaa6f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -67,7 +67,7 @@ test('Running a Fargate Task', () => { containerOverrides: [ { containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], }, ], launchTarget: new tasks.EcsFargateLaunchTarget({ @@ -180,7 +180,7 @@ test('Running an EC2 Task with bridge network', () => { containerOverrides: [ { containerDefinition, - environment: [{ name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey') }], + environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], }, ], launchTarget: new tasks.EcsEc2LaunchTarget(), @@ -334,9 +334,9 @@ test('Running an EC2 Task with overridden number values', () => { containerOverrides: [ { containerDefinition, - command: sfn.Data.listAt('$.TheCommand'), + command: sfn.JsonPath.listAt('$.TheCommand'), cpu: 5, - memoryLimit: sfn.Data.numberAt('$.MemoryLimit'), + memoryLimit: sfn.JsonPath.numberAt('$.MemoryLimit'), }, ], launchTarget: new tasks.EcsEc2LaunchTarget(), From 9078d9764b1418a0cd7f9967f917102d9271e94f Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 25 Jun 2020 11:41:38 -0700 Subject: [PATCH 12/27] Apply suggestions from code review Co-authored-by: Niranjan Jayakar --- .../@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index aa2e27b552db1..90c7720268eb8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -35,7 +35,7 @@ export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { readonly containerOverrides?: ContainerOverride[]; /** - * In what subnets to place the task's ENIs + * Subnets to place the task's ENIs * * @default - Private subnet if assignPublicIp, public subnets otherwise */ @@ -156,8 +156,8 @@ export class EcsEc2LaunchTarget implements IEcsLaunchTarget { return { parameters: { LaunchType: 'EC2', - PlacementConstraints: noEmpty(flatten((this.options?.placementConstraints || []).map((c) => c.toJson().map(uppercaseKeys)))), - PlacementStrategy: noEmpty(flatten((this.options?.placementStrategies || []).map((c) => c.toJson().map(uppercaseKeys)))), + PlacementConstraints: noEmpty(flatten((this.options?.placementConstraints ?? []).map((c) => c.toJson().map(uppercaseKeys)))), + PlacementStrategy: noEmpty(flatten((this.options?.placementStrategies ?? []).map((c) => c.toJson().map(uppercaseKeys)))), }, }; @@ -296,7 +296,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { private validateFargateLaunchType() { if (!this.props.taskDefinition.isFargateCompatible) { - throw new Error('Supplied TaskDefinition is not configured for compatibility with Fargate'); + throw new Error('Supplied TaskDefinition is not compatible with Fargate'); } this.configureAwsVpcNetworking(); From ad4da0df34f5b29dabfab4fa4c1862ee3ec9af34 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 25 Jun 2020 17:02:30 -0700 Subject: [PATCH 13/27] move validation into bind method --- .../lib/ecs/run-task.ts | 57 ++++++++----------- .../test/ecs/run-tasks.test.ts | 12 ++-- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 90c7720268eb8..d5a981a3792d0 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -72,7 +72,7 @@ export interface IEcsLaunchTarget { /** * called when the ECS launch target is configured on RunTask */ - bind(_task: EcsRunTask): EcsLaunchTargetConfig; + bind(task: EcsRunTask, taskDefinition: ecs.ITaskDefinition, cluster?: ecs.ICluster): EcsLaunchTargetConfig; } /** @@ -132,7 +132,11 @@ export class EcsFargateLaunchTarget implements IEcsLaunchTarget { /** * Called when the Fargate launch type configured on RunTask */ - public bind(_task: EcsRunTask): EcsLaunchTargetConfig { + public bind(_task: EcsRunTask, taskDefinition: ecs.ITaskDefinition, _cluster?: ecs.ICluster): EcsLaunchTargetConfig { + if (!taskDefinition.isFargateCompatible) { + throw new Error('Supplied TaskDefinition is not compatible with Fargate'); + } + return { parameters: { LaunchType: 'FARGATE', @@ -152,7 +156,15 @@ export class EcsEc2LaunchTarget implements IEcsLaunchTarget { /** * Called when the EC2 launch type is configured on RunTask */ - public bind(_task: EcsRunTask): EcsLaunchTargetConfig { + public bind(_task: EcsRunTask, taskDefinition: ecs.ITaskDefinition, cluster?: ecs.ICluster): EcsLaunchTargetConfig { + if (!taskDefinition.isEc2Compatible) { + throw new Error('Supplied TaskDefinition is not compatible with Fargate'); + } + + if (!cluster?.hasEc2Capacity) { + throw new Error('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); + } + return { parameters: { LaunchType: 'EC2', @@ -206,7 +218,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { constructor(scope: cdk.Construct, id: string, private readonly props: EcsRunTaskProps) { super(scope, id, props); - this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; validatePatternSupported(this.integrationPattern, EcsRunTask.SUPPORTED_INTEGRATION_PATTERNS); @@ -216,7 +228,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { this.validateLaunchTarget(); - for (const override of this.props.containerOverrides || []) { + for (const override of this.props.containerOverrides ?? []) { const name = override.containerDefinition.containerName; if (!cdk.Token.isUnresolved(name)) { const cont = this.props.taskDefinition.node.tryFindChild(name); @@ -230,12 +242,6 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { } protected renderTask(): any { - if (this.networkConfiguration !== undefined) { - // Make sure we have a security group if we're using AWSVPC networking - this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); - this.connections.addSecurityGroup(this.securityGroup); - } - return { Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), Parameters: sfn.FieldUtils.renderObject({ @@ -243,7 +249,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { TaskDefinition: this.props.taskDefinition.taskDefinitionArn, NetworkConfiguration: this.networkConfiguration, Overrides: renderOverrides(this.props.containerOverrides), - ...this.props.launchTarget.bind(this).parameters, + ...this.props.launchTarget.bind(this, this.props.taskDefinition, this.props.cluster).parameters, }), }; } @@ -258,26 +264,17 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { SecurityGroups: cdk.Lazy.listValue({ produce: () => [this.securityGroup!.securityGroupId] }), }, }; + + // Make sure we have a security group if we're using AWSVPC networking + this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); + this.connections.addSecurityGroup(this.securityGroup); } private validateLaunchTarget() { - if (this.props.launchTarget instanceof EcsFargateLaunchTarget) { - this.validateFargateLaunchType(); - } - - if (this.props.launchTarget instanceof EcsEc2LaunchTarget) { - this.validateEc2LaunchType(); - } if (!this.props.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } - } - - private validateEc2LaunchType() { - if (!this.props.cluster.hasEc2Capacity) { - throw new Error('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); - } if (this.props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { this.configureAwsVpcNetworking(); @@ -290,16 +287,8 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { private validateNoNetworkingProps() { if (this.props.subnets !== undefined || this.props.securityGroup !== undefined) { - throw new Error('vpcPlacement and securityGroup can only be used in AwsVpc networking mode'); - } - } - - private validateFargateLaunchType() { - if (!this.props.taskDefinition.isFargateCompatible) { - throw new Error('Supplied TaskDefinition is not compatible with Fargate'); + throw new Error(`Supplied TaskDefinition must have 'networkMode' of 'AWS_VPC' to use 'vpcSubnets' and 'securityGroup'. Received: ${this.props.taskDefinition.networkMode}`); } - - this.configureAwsVpcNetworking(); } private makePolicyStatements(): iam.PolicyStatement[] { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts index 75ab7a24eaa6f..530a550d899f4 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -32,9 +32,9 @@ test('Cannot create a Fargate task with a fargate-incompatible task definition', memoryLimitMiB: 256, }); - expect(() => new tasks.EcsRunTask(stack, 'task', { cluster, taskDefinition, launchTarget: new tasks.EcsFargateLaunchTarget() })).toThrowError( - /not configured for compatibility with Fargate/, - ); + expect(() => + new tasks.EcsRunTask(stack, 'task', { cluster, taskDefinition, launchTarget: new tasks.EcsFargateLaunchTarget() }).toStateJson(), + ).toThrowError(/Supplied TaskDefinition is not compatible with Fargate/); }); test('Cannot create a Fargate task without a default container', () => { @@ -43,9 +43,9 @@ test('Cannot create a Fargate task without a default container', () => { cpu: '256', compatibility: ecs.Compatibility.FARGATE, }); - expect(() => new tasks.EcsRunTask(stack, 'task', { cluster, taskDefinition, launchTarget: new tasks.EcsFargateLaunchTarget() })).toThrowError( - /must have at least one essential container/, - ); + expect(() => + new tasks.EcsRunTask(stack, 'task', { cluster, taskDefinition, launchTarget: new tasks.EcsFargateLaunchTarget() }).toStateJson(), + ).toThrowError(/must have at least one essential container/); }); test('Running a Fargate Task', () => { From 1525f3e32503e52128c86ab2fb3487c59b287c4d Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 25 Jun 2020 22:04:24 -0700 Subject: [PATCH 14/27] modify incorrect default doc --- .../aws-stepfunctions-tasks/lib/ecs/run-task.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index d5a981a3792d0..823e64fedc939 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -37,7 +37,8 @@ export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { /** * Subnets to place the task's ENIs * - * @default - Private subnet if assignPublicIp, public subnets otherwise + * @default - Private subnets. + * Public subnets if assignPublicIp is set. */ readonly subnets?: ec2.SubnetSelection; @@ -249,7 +250,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { TaskDefinition: this.props.taskDefinition.taskDefinitionArn, NetworkConfiguration: this.networkConfiguration, Overrides: renderOverrides(this.props.containerOverrides), - ...this.props.launchTarget.bind(this, this.props.taskDefinition, this.props.cluster).parameters, + ...this.props.launchTarget.bind(this, this.props.taskDefinition, this.props.cluster), }), }; } @@ -271,7 +272,6 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { } private validateLaunchTarget() { - if (!this.props.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); } @@ -287,7 +287,9 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { private validateNoNetworkingProps() { if (this.props.subnets !== undefined || this.props.securityGroup !== undefined) { - throw new Error(`Supplied TaskDefinition must have 'networkMode' of 'AWS_VPC' to use 'vpcSubnets' and 'securityGroup'. Received: ${this.props.taskDefinition.networkMode}`); + throw new Error( + `Supplied TaskDefinition must have 'networkMode' of 'AWS_VPC' to use 'vpcSubnets' and 'securityGroup'. Received: ${this.props.taskDefinition.networkMode}`, + ); } } From 3c7e3bb99cdad53193e279d3b5639baa99ecea03 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 25 Jun 2020 22:11:39 -0700 Subject: [PATCH 15/27] reorder methods for easier readability --- .../lib/ecs/run-task.ts | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 823e64fedc939..dd26fdc21cf87 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -227,7 +227,17 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { throw new Error('Task Token is required in `containerOverrides` for callback. Use Context.taskToken to set the token.'); } - this.validateLaunchTarget(); + if (!this.props.taskDefinition.defaultContainer) { + throw new Error('A TaskDefinition must have at least one essential container'); + } + + if (this.props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { + this.configureAwsVpcNetworking(); + } else { + // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. + this.validateNoNetworkingProps(); + this.connections.addSecurityGroup(...this.props.cluster.connections.securityGroups); + } for (const override of this.props.containerOverrides ?? []) { const name = override.containerDefinition.containerName; @@ -271,20 +281,6 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { this.connections.addSecurityGroup(this.securityGroup); } - private validateLaunchTarget() { - if (!this.props.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } - - if (this.props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { - this.configureAwsVpcNetworking(); - } else { - // Either None, Bridge or Host networking. Copy SecurityGroup from ASG. - this.validateNoNetworkingProps(); - this.connections.addSecurityGroup(...this.props.cluster.connections.securityGroups); - } - } - private validateNoNetworkingProps() { if (this.props.subnets !== undefined || this.props.securityGroup !== undefined) { throw new Error( From 8bfe251092e09f6f302e3b502a777d93abd192a0 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 26 Jun 2020 01:22:08 -0700 Subject: [PATCH 16/27] grab parameters from the bind when rendering task --- packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index dd26fdc21cf87..ccd79d90653ae 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -260,7 +260,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { TaskDefinition: this.props.taskDefinition.taskDefinitionArn, NetworkConfiguration: this.networkConfiguration, Overrides: renderOverrides(this.props.containerOverrides), - ...this.props.launchTarget.bind(this, this.props.taskDefinition, this.props.cluster), + ...this.props.launchTarget.bind(this, this.props.taskDefinition, this.props.cluster).parameters, }), }; } From bedce79189aa8d933e9fd4425e58d2f61e25ccd9 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 26 Jun 2020 02:30:07 -0700 Subject: [PATCH 17/27] Apply suggestions from code review Co-authored-by: Niranjan Jayakar --- .../aws-stepfunctions-tasks/lib/ecs/run-task.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index ccd79d90653ae..116eb955ad0cd 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -7,7 +7,7 @@ import { ContainerOverride } from '..'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; /** - * Basic properties for ECS Tasks + * Properties for ECS Tasks */ export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { /** @@ -27,8 +27,7 @@ export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { /** * Container setting overrides * - * Key is the name of the container to override, value is the - * values you want to override. + * Specify the container to use and the overrides to apply. * * @default - No overrides */ @@ -37,8 +36,7 @@ export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { /** * Subnets to place the task's ENIs * - * @default - Private subnets. - * Public subnets if assignPublicIp is set. + * @default - Public subnets if assignPublicIp is set. Private subnets otherwise. */ readonly subnets?: ec2.SubnetSelection; @@ -159,11 +157,11 @@ export class EcsEc2LaunchTarget implements IEcsLaunchTarget { */ public bind(_task: EcsRunTask, taskDefinition: ecs.ITaskDefinition, cluster?: ecs.ICluster): EcsLaunchTargetConfig { if (!taskDefinition.isEc2Compatible) { - throw new Error('Supplied TaskDefinition is not compatible with Fargate'); + throw new Error('Supplied TaskDefinition is not compatible with EC2'); } if (!cluster?.hasEc2Capacity) { - throw new Error('Cluster for this service needs Ec2 capacity. Call addXxxCapacity() on the cluster.'); + throw new Error('Cluster for this service needs Ec2 capacity. Call addCapacity() on the cluster.'); } return { From 6323faed2c04fc60fe29343b730d5bb8c242a93e Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 26 Jun 2020 02:55:27 -0700 Subject: [PATCH 18/27] Apply suggestions from code review Co-authored-by: Niranjan Jayakar --- .../@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 116eb955ad0cd..5ea968ce88073 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -336,7 +336,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { } function renderOverrides(containerOverrides?: ContainerOverride[]) { - if (!containerOverrides) { + if (!containerOverrides || containerOverrides.length === 0) { return undefined; } @@ -349,8 +349,7 @@ function renderOverrides(containerOverrides?: ContainerOverride[]) { Memory: override.memoryLimit, MemoryReservation: override.memoryReservation, Environment: - override.environment && - override.environment.map((e) => ({ + override.environment?.map((e) => ({ Name: e.name, Value: e.value, })), From e9b10be655d5c7218e4761ca04b8f49289f46b7b Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 26 Jun 2020 08:55:37 -0700 Subject: [PATCH 19/27] use customer provided security group when supplied --- packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 116eb955ad0cd..ad1cafac9a847 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -275,7 +275,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { }; // Make sure we have a security group if we're using AWSVPC networking - this.securityGroup = this.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); + this.securityGroup = this.props.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); this.connections.addSecurityGroup(this.securityGroup); } From 062ec0b227384440ab566dcf5789cb6d87d68bac Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 26 Jun 2020 10:10:37 -0700 Subject: [PATCH 20/27] add LaunchTargetBindOptions --- .../lib/ecs/run-task.ts | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index fce56bcaad825..67862a6808b2c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -71,7 +71,25 @@ export interface IEcsLaunchTarget { /** * called when the ECS launch target is configured on RunTask */ - bind(task: EcsRunTask, taskDefinition: ecs.ITaskDefinition, cluster?: ecs.ICluster): EcsLaunchTargetConfig; + bind(task: EcsRunTask, launchTargetOptions: LaunchTargetBindOptions): EcsLaunchTargetConfig; +} + +/** + * Options for binding a launch target to an ECS run job task + */ +export interface LaunchTargetBindOptions { + /** + * Task definition to run Docker containers in Amazon ECS + */ + readonly taskDefinition: ecs.ITaskDefinition; + + /** + * A regional grouping of one or more container instances on which you can run + * tasks and services. + * + * @default - No cluster + */ + readonly cluster?: ecs.ICluster; } /** @@ -131,8 +149,8 @@ export class EcsFargateLaunchTarget implements IEcsLaunchTarget { /** * Called when the Fargate launch type configured on RunTask */ - public bind(_task: EcsRunTask, taskDefinition: ecs.ITaskDefinition, _cluster?: ecs.ICluster): EcsLaunchTargetConfig { - if (!taskDefinition.isFargateCompatible) { + public bind(_task: EcsRunTask, launchTargetOptions: LaunchTargetBindOptions): EcsLaunchTargetConfig { + if (!launchTargetOptions.taskDefinition.isFargateCompatible) { throw new Error('Supplied TaskDefinition is not compatible with Fargate'); } @@ -155,12 +173,12 @@ export class EcsEc2LaunchTarget implements IEcsLaunchTarget { /** * Called when the EC2 launch type is configured on RunTask */ - public bind(_task: EcsRunTask, taskDefinition: ecs.ITaskDefinition, cluster?: ecs.ICluster): EcsLaunchTargetConfig { - if (!taskDefinition.isEc2Compatible) { + public bind(_task: EcsRunTask, launchTargetOptions: LaunchTargetBindOptions): EcsLaunchTargetConfig { + if (!launchTargetOptions.taskDefinition.isEc2Compatible) { throw new Error('Supplied TaskDefinition is not compatible with EC2'); } - if (!cluster?.hasEc2Capacity) { + if (!launchTargetOptions.cluster?.hasEc2Capacity) { throw new Error('Cluster for this service needs Ec2 capacity. Call addCapacity() on the cluster.'); } @@ -258,7 +276,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { TaskDefinition: this.props.taskDefinition.taskDefinitionArn, NetworkConfiguration: this.networkConfiguration, Overrides: renderOverrides(this.props.containerOverrides), - ...this.props.launchTarget.bind(this, this.props.taskDefinition, this.props.cluster).parameters, + ...this.props.launchTarget.bind(this, {taskDefinition: this.props.taskDefinition, cluster: this.props.cluster}).parameters, }), }; } From bae805177324a2b8aacec7f4ba637654be8b0148 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 26 Jun 2020 10:41:27 -0700 Subject: [PATCH 21/27] modify securityGroup to be securityGroups of type ec2.ISecurityGroup[] --- .../aws-stepfunctions-tasks/lib/ecs/run-task.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 67862a6808b2c..dae1d4314894a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -41,11 +41,11 @@ export interface EcsRunTaskProps extends sfn.TaskStateBaseProps { readonly subnets?: ec2.SubnetSelection; /** - * Existing security group to use for the tasks + * Existing security groups to use for the tasks * * @default - A new security group is created */ - readonly securityGroup?: ec2.ISecurityGroup; + readonly securityGroups?: ec2.ISecurityGroup[]; /** * Assign public IP addresses to each task @@ -229,7 +229,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { protected readonly taskMetrics?: sfn.TaskMetricsConfig; protected readonly taskPolicies?: iam.PolicyStatement[]; - private securityGroup?: ec2.ISecurityGroup; + private securityGroups: ec2.ISecurityGroup[] = []; private networkConfiguration?: any; private readonly integrationPattern: sfn.IntegrationPattern; @@ -288,17 +288,17 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { AwsvpcConfiguration: { AssignPublicIp: this.props.assignPublicIp ? (this.props.assignPublicIp ? 'ENABLED' : 'DISABLED') : undefined, Subnets: this.props.cluster.vpc.selectSubnets(subnetSelection).subnetIds, - SecurityGroups: cdk.Lazy.listValue({ produce: () => [this.securityGroup!.securityGroupId] }), + SecurityGroups: cdk.Lazy.listValue({ produce: () => this.securityGroups?.map(sg => sg.securityGroupId) }), }, }; // Make sure we have a security group if we're using AWSVPC networking - this.securityGroup = this.props.securityGroup ?? new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc }); - this.connections.addSecurityGroup(this.securityGroup); + this.securityGroups = this.props.securityGroups ?? [new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: this.props.cluster.vpc })]; + this.connections.addSecurityGroup(...this.securityGroups); } private validateNoNetworkingProps() { - if (this.props.subnets !== undefined || this.props.securityGroup !== undefined) { + if (this.props.subnets !== undefined || this.props.securityGroups !== undefined) { throw new Error( `Supplied TaskDefinition must have 'networkMode' of 'AWS_VPC' to use 'vpcSubnets' and 'securityGroup'. Received: ${this.props.taskDefinition.networkMode}`, ); From 54baf07eecfeebffd9b15f72475f632cf3abc9e4 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 26 Jun 2020 11:22:18 -0700 Subject: [PATCH 22/27] drop unnecessary Lazy call for task/execution role --- packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 2 +- .../aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index dae1d4314894a..9f5e799d1a1b9 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -320,7 +320,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { }), new iam.PolicyStatement({ actions: ['iam:PassRole'], - resources: cdk.Lazy.listValue({ produce: () => this.taskExecutionRoles().map((r) => r.roleArn) }), + resources: this.taskExecutionRoles().map((r) => r.roleArn), }), ]; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts index 530a550d899f4..d124b1ac9c6ee 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -138,7 +138,7 @@ test('Running a Fargate Task', () => { { Action: 'iam:PassRole', Effect: 'Allow', - Resource: [{ 'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn'] }], + Resource: { 'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn'] }, }, { Action: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], @@ -242,7 +242,7 @@ test('Running an EC2 Task with bridge network', () => { { Action: 'iam:PassRole', Effect: 'Allow', - Resource: [{ 'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn'] }], + Resource: { 'Fn::GetAtt': ['TDTaskRoleC497AFFC', 'Arn'] }, }, { Action: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], From d660d0c27d7efd9218ee26b63af9a3695d8e0587 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Sat, 27 Jun 2020 22:20:28 -0700 Subject: [PATCH 23/27] make platformVersion required as we don't want to box ourselves in by defaulting to latest --- packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 2 +- .../test/ecs/integ.fargate-run-task.expected.json | 2 +- .../test/ecs/integ.fargate-run-task.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 9f5e799d1a1b9..6da19abcf2b03 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -116,7 +116,7 @@ export interface EcsFargateLaunchTargetOptions { * * @default FargatePlatformVersion.LATEST */ - readonly platformVersion?: ecs.FargatePlatformVersion; + readonly platformVersion: ecs.FargatePlatformVersion; } /** diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json index e9c5fc9a07617..3d131dd24f6cf 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json @@ -284,7 +284,7 @@ "GroupId" ] }, - "\"]}},\"Overrides\":{\"ContainerOverrides\":[{\"Name\":\"TheContainer\",\"Environment\":[{\"Name\":\"SOME_KEY\",\"Value.$\":\"$.SomeKey\"}]}]},\"LaunchType\":\"FARGATE\"}}}}" + "\"]}},\"Overrides\":{\"ContainerOverrides\":[{\"Name\":\"TheContainer\",\"Environment\":[{\"Name\":\"SOME_KEY\",\"Value.$\":\"$.SomeKey\"}]}]},\"LaunchType\":\"FARGATE\",\"PlatformVersion\":\"1.4.0\"}}}}" ] ] } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts index 006ed077baf73..c7cdab910cdf5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts @@ -60,7 +60,9 @@ const definition = new sfn.Pass(stack, 'Start', { ], }, ], - launchTarget: new tasks.EcsFargateLaunchTarget(), + launchTarget: new tasks.EcsFargateLaunchTarget({ + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + }), }), ); From b123be8d77f828556186d25018cdb78c8089fb3a Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Sat, 27 Jun 2020 22:22:08 -0700 Subject: [PATCH 24/27] update docstring --- .../aws-stepfunctions-tasks/lib/ecs/run-task.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 6da19abcf2b03..3e1e9a4927f4e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -109,12 +109,10 @@ export interface EcsLaunchTargetConfig { */ export interface EcsFargateLaunchTargetOptions { /** - * Fargate platform version to run this service on - * - * Unless you have specific compatibility requirements, you don't need to - * specify this. - * - * @default FargatePlatformVersion.LATEST + * Refers to a specific runtime environment for Fargate task infrastructure. + * Fargate platform version is a combination of the kernel and container runtime versions. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html */ readonly platformVersion: ecs.FargatePlatformVersion; } From f35ec4bc4f90527a7c966f66d531c4bec29bede2 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Sun, 28 Jun 2020 23:45:19 -0700 Subject: [PATCH 25/27] remove errant trailing whitespace --- packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 3e1e9a4927f4e..4d5cac5306082 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -111,7 +111,7 @@ export interface EcsFargateLaunchTargetOptions { /** * Refers to a specific runtime environment for Fargate task infrastructure. * Fargate platform version is a combination of the kernel and container runtime versions. - * + * * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html */ readonly platformVersion: ecs.FargatePlatformVersion; From c167ad900fdd006d078a48da2d9480e86113f382 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 29 Jun 2020 07:24:46 -0700 Subject: [PATCH 26/27] mark renderTask as internal --- .../@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index 4d5cac5306082..d41ef094e1ae9 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -266,7 +266,10 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { this.taskPolicies = this.makePolicyStatements(); } - protected renderTask(): any { + /** + * @internal + */ + protected _renderTask(): any { return { Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), Parameters: sfn.FieldUtils.renderObject({ From 98af0200153452b8520689cc229c6ed216d2306f Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Mon, 29 Jun 2020 23:44:24 -0700 Subject: [PATCH 27/27] add comment to describe behaviour of placement constraints --- packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index d41ef094e1ae9..d06d204641653 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -183,6 +183,10 @@ export class EcsEc2LaunchTarget implements IEcsLaunchTarget { return { parameters: { LaunchType: 'EC2', + // takes an array of placement constraints each of which contain a single item array of constraints, flattens it + // and renders the Json to be passed as a parameter in the state machine. + // input: [ecs.PlacementConstraint.distinctInstances()] - distinctInstances() returns [{ type: 'distinctInstance' }] + // output: {Type: 'distinctInstance'} PlacementConstraints: noEmpty(flatten((this.options?.placementConstraints ?? []).map((c) => c.toJson().map(uppercaseKeys)))), PlacementStrategy: noEmpty(flatten((this.options?.placementStrategies ?? []).map((c) => c.toJson().map(uppercaseKeys)))), },