diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 239c943674c2e..d5142e2159d2b 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) @@ -305,31 +307,105 @@ 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. +#### 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'; -// See examples in ECS library for initialization of 'cluster' and 'taskDefinition' +const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { + isDefault: true, +}); -new ecs.RunEcsFargateTask({ - cluster, - taskDefinition, - containerOverrides: [ - { - containerName: 'TheContainer', - environment: [ - { - name: 'CONTAINER_INPUT', - value: JsonPath.stringAt('$.valueFromStateData'), - } - ] - } - ] +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, +}); + +taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, +}); + +const runTask = new tasks.EcsRunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + launchTarget: new tasks.EcsEc2LaunchTarget({ + 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 +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, }); -fargateTask.connections.allowToDefaultPort(rdsCluster, 'Read the database'); +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); -new sfn.Task(this, 'CallFargate', { - task: fargateTask +const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.FARGATE, +}); + +const containerDefinition = taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, +}); + +const runTask = new tasks.EcsRunTask(stack, 'RunFargate', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [{ + containerDefinition, + environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + }], + launchTarget: new tasks.EcsFargateLaunchTarget(), }); ``` 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) { 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 fc4d5f6efc448..12d1c71665577 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.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts new file mode 100644 index 0000000000000..d06d204641653 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -0,0 +1,383 @@ +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'; + +/** + * 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 + * + * Specify the container to use and the overrides to apply. + * + * @default - No overrides + */ + readonly containerOverrides?: ContainerOverride[]; + + /** + * Subnets to place the task's ENIs + * + * @default - Public subnets if assignPublicIp is set. Private subnets otherwise. + */ + readonly subnets?: ec2.SubnetSelection; + + /** + * Existing security groups to use for the tasks + * + * @default - A new security group is created + */ + readonly securityGroups?: 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, 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; +} + +/** + * 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 { + /** + * 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; +} + +/** + * 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, launchTargetOptions: LaunchTargetBindOptions): EcsLaunchTargetConfig { + if (!launchTargetOptions.taskDefinition.isFargateCompatible) { + throw new Error('Supplied TaskDefinition is not compatible with Fargate'); + } + + 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, launchTargetOptions: LaunchTargetBindOptions): EcsLaunchTargetConfig { + if (!launchTargetOptions.taskDefinition.isEc2Compatible) { + throw new Error('Supplied TaskDefinition is not compatible with EC2'); + } + + if (!launchTargetOptions.cluster?.hasEc2Capacity) { + throw new Error('Cluster for this service needs Ec2 capacity. Call addCapacity() on the cluster.'); + } + + 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)))), + }, + }; + + 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 securityGroups: 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.'); + } + + 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; + 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(); + } + + /** + * @internal + */ + protected _renderTask(): any { + 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, {taskDefinition: this.props.taskDefinition, cluster: this.props.cluster}).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.securityGroups?.map(sg => sg.securityGroupId) }), + }, + }; + + // Make sure we have a security group if we're using AWSVPC networking + 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.securityGroups !== undefined) { + throw new Error( + `Supplied TaskDefinition must have 'networkMode' of 'AWS_VPC' to use 'vpcSubnets' and 'securityGroup'. Received: ${this.props.taskDefinition.networkMode}`, + ); + } + } + + 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: 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 || containerOverrides.length === 0) { + 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?.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 ec24d7f518d9c..68d8c30b93997 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -10,6 +10,7 @@ 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'; 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 a644363a9c077..e87d5a6a66163 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,7 +48,7 @@ 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, }); @@ -62,7 +60,7 @@ test('Running a Fargate Task', () => { taskDefinition, containerOverrides: [ { - containerName: 'TheContainer', + containerDefinition, environment: [ {name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey')}, ], @@ -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,7 +159,7 @@ 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, }); @@ -171,7 +171,7 @@ test('Running an EC2 Task with bridge network', () => { taskDefinition, containerOverrides: [ { - containerName: 'TheContainer', + containerDefinition, environment: [ {name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey')}, ], @@ -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.JsonPath.listAt('$.TheCommand'), cpu: 5, memoryLimit: sfn.JsonPath.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 new file mode 100644 index 0000000000000..634506837f8d5 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json @@ -0,0 +1,719 @@ +{ + "Resources": { + "Ec2ClusterEE43E89D": { + "Type": "AWS::ECS::Cluster" + }, + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/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/Ec2Cluster/DefaultAutoScalingGroup" + } + ], + "VpcId": "vpc-60900905" + } + }, + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898": { + "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/Ec2Cluster/DefaultAutoScalingGroup" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD": { + "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": "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", + "Roles": [ + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupInstanceProfileDB232471" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupInstanceSecurityGroup149B0A9E", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "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" + ] + ] + } + } + }, + "DependsOn": [ + "Ec2ClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy6D2DC2FD", + "Ec2ClusterDefaultAutoScalingGroupInstanceRole73D80898" + ] + }, + "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "LaunchConfigurationName": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLaunchConfig7B2FED3A" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + "subnet-e19455ca", + "subnet-e0c24797", + "subnet-ccd77395" + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3": { + "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/Ec2Cluster/DefaultAutoScalingGroup" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33": { + "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": "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "Ec2ClusterEE43E89D", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33", + "Roles": [ + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31": { + "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": [ + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3", + "Arn" + ] + }, + "Runtime": "python3.6", + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "Ec2ClusterEE43E89D" + } + } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicy638C9E33", + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3" + ] + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionAllowInvokeawssfntasksecsec2integEc2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicEE9E39A29ACCEEA3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" + } + } + }, + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionTopic4795E0F6": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" + }, + "Endpoint": { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionE0DEFB31", + "Arn" + ] + } + } + }, + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7": { + "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/Ec2Cluster/DefaultAutoScalingGroup" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B", + "Roles": [ + { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-sfn-tasks-ecs-ec2-integ/Ec2Cluster/DefaultAutoScalingGroup" + } + ] + } + }, + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHook5CB1467E": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupASGC5A6D4C0" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicF7263B30" + }, + "RoleARN": { + "Fn::GetAtt": [ + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7", + "Arn" + ] + } + }, + "DependsOn": [ + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicyE499974B", + "Ec2ClusterDefaultAutoScalingGroupLifecycleHookDrainHookRole71045ED7" + ] + }, + "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": [ + "Ec2ClusterEE43E89D", + "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" + } + }, + "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 new file mode 100644 index 0000000000000..7713e04363465 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.ts @@ -0,0 +1,75 @@ +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'; + +/* + * * 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: { + 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, 'Ec2Cluster', { 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'); +const containerDefinition = 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.EcsRunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [ + { + containerDefinition, + environment: [ + { + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }, + ], + }, + ], + launchTarget: new tasks.EcsEc2LaunchTarget(), + }), +); + +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.ec2-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts index dc0a42c1d7bd8..9da3a9e76b72f 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.JsonPath.stringAt('$.SomeKey'), + containerDefinition, + environment: [ + { + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }, + ], }, ], - }, - ], -})})); + }), + }), +); new sfn.StateMachine(stack, 'StateMachine', { definition, 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..3d131dd24f6cf --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json @@ -0,0 +1,305 @@ +{ + "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\",\"PlatformVersion\":\"1.4.0\"}}}}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "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 new file mode 100644 index 0000000000000..c7cdab910cdf5 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.ts @@ -0,0 +1,77 @@ +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'; + +/* + * 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: { + 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, +}); +const containerDefinition = 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.EcsRunTask(stack, 'FargateTask', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + assignPublicIp: true, + containerOverrides: [ + { + containerDefinition, + environment: [ + { + name: 'SOME_KEY', + value: sfn.JsonPath.stringAt('$.SomeKey'), + }, + ], + }, + ], + launchTarget: new tasks.EcsFargateLaunchTarget({ + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + }), + }), +); + +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-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts index 4f981dfaa45b8..dbc470d8d283e 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.JsonPath.stringAt('$.SomeKey'), + containerDefinition, + environment: [ + { + name: 'SOME_KEY', + value: sfn.JsonPath.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 new file mode 100644 index 0000000000000..d124b1ac9c6ee --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -0,0 +1,377 @@ +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.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', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.FARGATE, + }); + 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', () => { + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + memoryMiB: '512', + cpu: '256', + compatibility: ecs.Compatibility.FARGATE, + }); + const containerDefinition = taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.EcsRunTask(stack, 'RunFargate', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [ + { + containerDefinition, + environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + }, + ], + launchTarget: new tasks.EcsFargateLaunchTarget({ + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + }), + }); + + 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' }], + }, + }, + PlatformVersion: '1.4.0', + 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, + }); + const containerDefinition = taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.EcsRunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [ + { + containerDefinition, + environment: [{ name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey') }], + }, + ], + launchTarget: new tasks.EcsEc2LaunchTarget(), + }); + + 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.EcsRunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + launchTarget: new tasks.EcsEc2LaunchTarget({ + 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, + }); + const containerDefinition = taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('foo/bar'), + memoryLimitMiB: 256, + }); + + // WHEN + const runTask = new tasks.EcsRunTask(stack, 'Run', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + cluster, + taskDefinition, + containerOverrides: [ + { + containerDefinition, + command: sfn.JsonPath.listAt('$.TheCommand'), + cpu: 5, + memoryLimit: sfn.JsonPath.numberAt('$.MemoryLimit'), + }, + ], + launchTarget: new tasks.EcsEc2LaunchTarget(), + }); + + // 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', + }); +});