From 8bfc469affb2b104102fe1da21a913bb9195f444 Mon Sep 17 00:00:00 2001 From: Piradeep Kandasamy Date: Fri, 19 Apr 2019 10:32:04 -0700 Subject: [PATCH] Defined a new construct to enable creating a scheduled ec2 task using cloudwatch. --- .../aws-ecs-scheduled-ecs-task-construct.md | 133 ++++++ packages/@aws-cdk/aws-ecs/README.md | 9 + packages/@aws-cdk/aws-ecs/lib/index.ts | 1 + .../aws-ecs/lib/scheduled-ecs-task.ts | 112 +++++ .../ec2/integ.event-task.lit.expected.json | 430 +++++++++--------- .../aws-ecs/test/ec2/integ.event-task.lit.ts | 37 +- .../aws-ecs/test/test.scheduled-ecs-task.ts | 281 ++++++++++++ 7 files changed, 764 insertions(+), 239 deletions(-) create mode 100644 design/aws-ecs-scheduled-ecs-task-construct.md create mode 100644 packages/@aws-cdk/aws-ecs/lib/scheduled-ecs-task.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/test.scheduled-ecs-task.ts diff --git a/design/aws-ecs-scheduled-ecs-task-construct.md b/design/aws-ecs-scheduled-ecs-task-construct.md new file mode 100644 index 0000000000000..52073b792713b --- /dev/null +++ b/design/aws-ecs-scheduled-ecs-task-construct.md @@ -0,0 +1,133 @@ +# AWS ECS - L3 Construct for Scheduling EC2 Tasks + +To address issue [#2352](https://github.com/awslabs/aws-cdk/issues/2352), the ECS CDK construct library should provide a way for customers to create a standalone scheduled task, without creating a service, for their container. The task will be initiated by a cloudwatch event that is scheduled based on [CW Scheduled Events](http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html). + +This would mean adding a new ECS CDK construct `ScheduledEc2Task`, that would take in the necessary properties required to create a Task Definition, an EventRuleTarget as well as an AWS EventRule. + +Note: Currently, ScheduledTasks, via CloudFormation, are only supported for EC2 and not for Fargate. CloudFormation does not support every [EcsParameter](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-ecsparameters.html) that is supported by the [Amazon CloudWatch Events API](https://docs.aws.amazon.com/AmazonCloudWatchEvents/latest/APIReference/API_EcsParameters.html). It is currently missing the following custom parameters: + +* Group +* LaunchType +* NetworkConfiguration +* PlatformVersion + +TaskDefinitions contain a NetworkMode, and EC2 can support none, bridge, host or awsvpc (which requires a NetworkConfiguration attribute). However, TaskDefinitions running on Fargate can only support a NetworkMode of awsvpc. For this construct, we're creating a scheduled task (that is standalone task, not backed with a service). It works for ECS because we're using bridge as the NetworkMode, which does not require a NetworkConfiguration to be present. + +## General approach + +The new [`ecs.ScheduledEc2Task`] class will include an L3 construct for: + +* ScheduledEc2Task + +A `ScheduledEc2Task` will create a task definition with the specified container. An `Ec2EventRuleTarget` will be created and associated as the target to an `Amazon Cloudwatch Event Rule` (indicating how frequently the task should be run). Based on the `Amazon Cloudwatch Event Rule` schedule, a task will run on the EC2 instances specified in the cluster. + +## Code changes + +Given the above, we should make the following changes to support scheduled tasks on ECS: +1. Create `ScheduledEc2TaskProps` interface and `ScheduledEc2Task` construct + +# Part 1: Create `ScheduledEc2TaskProps` interface and `ScheduledEc2Task` construct + +The `ScheduledEc2TaskProps` interface will contain properties to construct the Ec2TaskDefinition, Ec2EventRuleTarget and EventRule: + +```ts +export interface ScheduledEc2TaskProps { + /** + * The cluster where your service will be deployed. + */ + readonly cluster: ICluster; + + /** + * The image to start. + */ + readonly image: ContainerImage; + + /** + * The schedule or rate (frequency) that determines when CloudWatch Events + * runs the rule. For more information, see Schedule Expression Syntax for + * Rules in the Amazon CloudWatch User Guide. + * + * @see http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html + * + * You must specify this property. + */ + readonly scheduleExpression: string; + + /** + * The CMD value to pass to the container. A string with commands delimited by commas. + * + * @default none + */ + readonly command?: string; + + /** + * The minimum number of CPU units to reserve for the container. + * + * @default none + */ + readonly cpu?: number; + + /** + * Number of desired copies of running tasks. + * + * @default 1 + */ + readonly desiredTaskCount?: number; + + /** + * The environment variables to pass to the container. + * + * @default none + */ + readonly environment?: { [key: string]: string }; + + /** + * The hard limit (in MiB) of memory to present to the container. + * + * If your container attempts to exceed the allocated memory, the container + * is terminated. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. + */ + readonly memoryLimitMiB?: number; + + /** + * The soft limit (in MiB) of memory to reserve for the container. + * + * When system memory is under contention, Docker attempts to keep the + * container memory within the limit. If the container requires more memory, + * it can consume up to the value specified by the Memory property or all of + * the available memory on the container instance—whichever comes first. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. + */ + readonly memoryReservationMiB?: number; +} +``` + +The `ScheduledEc2Task` construct will use the following existing constructs: + +* Ec2TaskDefinition - To create a Task Definition for the container to start +* Ec2EventRuleTarget - The target of the aws event +* EventRule - To describe the event trigger (in this case, a scheduled run) + +An example use case to create a task that is scheduled to run every minute: +```ts +// Create the vpc and cluster used by the scheduled task +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); +cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') +}); + +// Create the scheduled task +new ScheduledEc2Task(stack, 'ScheduledEc2Task', { + cluster, + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + desiredTaskCount: 2, + memoryLimitMiB: 512, + cpu: 1, + environment: { name: 'TRIGGER', value: 'CloudWatch Events' }, + scheduleExpression: 'rate(1 minute)' +}); +``` diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 6539c47812830..5712bd8e3fb4e 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -32,6 +32,15 @@ const ecsService = new ecs.LoadBalancedEc2Service(this, 'Service', { memoryLimitMiB: 512, image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), }); + +// Instantiate an Amazon EC2 Task to run at a scheduled interval +const ecsScheduledTask = new ScheduledEc2Task(this, 'ScheduledTask', { + cluster, + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + scheduleExpression: 'rate(1 minute)', + environment: [{ name: 'TRIGGER', value: 'CloudWatch Events' }], + memoryLimitMiB: 256 +}); ``` ## AWS Fargate vs Amazon ECS diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 2e575685d7bff..482c2faaeba47 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -26,6 +26,7 @@ export * from './images/ecr'; export * from './log-drivers/aws-log-driver'; export * from './log-drivers/log-driver'; +export * from './scheduled-ecs-task'; // AWS::ECS CloudFormation Resources: // diff --git a/packages/@aws-cdk/aws-ecs/lib/scheduled-ecs-task.ts b/packages/@aws-cdk/aws-ecs/lib/scheduled-ecs-task.ts new file mode 100644 index 0000000000000..9d5299874803e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/scheduled-ecs-task.ts @@ -0,0 +1,112 @@ +import events = require('@aws-cdk/aws-events'); +import cdk = require('@aws-cdk/cdk'); +import { ICluster } from './cluster'; +import { ContainerImage } from './container-image'; +import { Ec2EventRuleTarget } from './ec2/ec2-event-rule-target'; +import { Ec2TaskDefinition } from './ec2/ec2-task-definition'; +import { AwsLogDriver } from './log-drivers/aws-log-driver'; + +export interface ScheduledEc2TaskProps { + /** + * The cluster where your service will be deployed. + */ + readonly cluster: ICluster; + + /** + * The image to start. + */ + readonly image: ContainerImage; + + /** + * The schedule or rate (frequency) that determines when CloudWatch Events + * runs the rule. For more information, see Schedule Expression Syntax for + * Rules in the Amazon CloudWatch User Guide. + * + * @see http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html + */ + readonly scheduleExpression: string; + + /** + * The CMD value to pass to the container. A string with commands delimited by commas. + * + * @default none + */ + readonly command?: string; + + /** + * The minimum number of CPU units to reserve for the container. + * + * @default none + */ + readonly cpu?: number; + + /** + * Number of desired copies of running tasks. + * + * @default 1 + */ + readonly desiredTaskCount?: number; + + /** + * The environment variables to pass to the container. + * + * @default none + */ + readonly environment?: { [key: string]: string }; + + /** + * The hard limit (in MiB) of memory to present to the container. + * + * If your container attempts to exceed the allocated memory, the container + * is terminated. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. + */ + readonly memoryLimitMiB?: number; + + /** + * The soft limit (in MiB) of memory to reserve for the container. + * + * When system memory is under contention, Docker attempts to keep the + * container memory within the limit. If the container requires more memory, + * it can consume up to the value specified by the Memory property or all of + * the available memory on the container instance—whichever comes first. + * + * At least one of memoryLimitMiB and memoryReservationMiB is required for non-Fargate services. + */ + readonly memoryReservationMiB?: number; +} + +/** + * A scheduled Ec2 task that will be initiated off of cloudwatch events. + */ +export class ScheduledEc2Task extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props: ScheduledEc2TaskProps) { + super(scope, id); + + // Create a Task Definition for the container to start, also creates a log driver + const taskDefinition = new Ec2TaskDefinition(this, 'ScheduledTaskDef'); + taskDefinition.addContainer('ScheduledContainer', { + image: props.image, + memoryLimitMiB: props.memoryLimitMiB, + memoryReservationMiB: props.memoryReservationMiB, + cpu: props.cpu, + command: props.command !== undefined ? cdk.Fn.split(",", props.command) : undefined, + environment: props.environment, + logging: new AwsLogDriver(this, 'ScheduledTaskLogging', { streamPrefix: this.node.id }) + }); + + // Use Ec2TaskEventRuleTarget as the target of the EventRule + const eventRuleTarget = new Ec2EventRuleTarget(this, 'ScheduledEventRuleTarget', { + cluster: props.cluster, + taskDefinition, + taskCount: props.desiredTaskCount !== undefined ? props.desiredTaskCount : 1 + }); + + // An EventRule that describes the event trigger (in this case a scheduled run) + const eventRule = new events.EventRule(this, 'ScheduledEventRule', { + scheduleExpression: props.scheduleExpression, + }); + eventRule.addTarget(eventRuleTarget); + } +} diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json index cf64430816ede..579d7248a8cec 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.expected.json @@ -575,7 +575,187 @@ "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B" ] }, - "TaskDefTaskRole1EDB4A67": { + "EventImageAdoptRepositoryDFAAC242": { + "Type": "Custom::ECRAdoptedRepository", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", + "Arn" + ] + }, + "RepositoryName": { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "EventImageImageNameE972A8B1" + } + ] + } + ] + } + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:GetRepositoryPolicy", + "ecr:SetRepositoryPolicy", + "ecr:DeleteRepository", + "ecr:ListImages", + "ecr:BatchDeleteImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + ":", + { + "Ref": "EventImageImageNameE972A8B1" + } + ] + } + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "Roles": [ + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + } + ] + } + }, + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "handler.handler", + "Role": { + "Fn::GetAtt": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", + "Arn" + ] + }, + "Runtime": "nodejs8.10", + "Timeout": 300 + }, + "DependsOn": [ + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + ] + }, + "ScheduledTaskExampleScheduledTaskDefTaskRole81CE8D22": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -602,11 +782,22 @@ } } }, - "TaskDef54694570": { + "ScheduledTaskExampleScheduledTaskDef3D99A3DD": { "Type": "AWS::ECS::TaskDefinition", "Properties": { "ContainerDefinitions": [ { + "Cpu":1, + "Environment": [ + { + "Name": "name", + "Value": "TRIGGER" + }, + { + "Name": "value", + "Value": "CloudWatch Events" + } + ], "Essential": true, "Image": { "Fn::Join": [ @@ -722,17 +913,17 @@ "LogDriver": "awslogs", "Options": { "awslogs-group": { - "Ref": "TaskLoggingLogGroupC7E938D4" + "Ref": "ScheduledTaskExampleScheduledTaskLoggingLogGroupBB9B0B9F" }, - "awslogs-stream-prefix": "EventDemo", + "awslogs-stream-prefix": "ScheduledTaskExample", "awslogs-region": { "Ref": "AWS::Region" } } }, - "Memory": 256, + "Memory": 512, "MountPoints": [], - "Name": "TheContainer", + "Name": "ScheduledContainer", "PortMappings": [], "Ulimits": [], "VolumesFrom": [] @@ -740,26 +931,28 @@ ], "ExecutionRoleArn": { "Fn::GetAtt": [ - "TaskDefExecutionRoleB4775C97", + "ScheduledTaskExampleScheduledTaskDefExecutionRole9F56827C", "Arn" ] }, - "Family": "awsecsintegecsTaskDef8DD0C801", + "Family": "awsecsintegecsScheduledTaskExampleScheduledTaskDef33A3630E", "NetworkMode": "bridge", - "PlacementConstraints": [], + "PlacementConstraints": [ + + ], "RequiresCompatibilities": [ "EC2" ], "TaskRoleArn": { "Fn::GetAtt": [ - "TaskDefTaskRole1EDB4A67", + "ScheduledTaskExampleScheduledTaskDefTaskRole81CE8D22", "Arn" ] }, "Volumes": [] } }, - "TaskDefExecutionRoleB4775C97": { + "ScheduledTaskExampleScheduledTaskDefExecutionRole9F56827C": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -786,7 +979,7 @@ } } }, - "TaskDefExecutionRoleDefaultPolicy0DBB737A": { + "ScheduledTaskExampleScheduledTaskDefExecutionRoleDefaultPolicy2CF2ABB9": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -838,7 +1031,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "TaskLoggingLogGroupC7E938D4", + "ScheduledTaskExampleScheduledTaskLoggingLogGroupBB9B0B9F", "Arn" ] } @@ -846,15 +1039,15 @@ ], "Version": "2012-10-17" }, - "PolicyName": "TaskDefExecutionRoleDefaultPolicy0DBB737A", + "PolicyName": "ScheduledTaskExampleScheduledTaskDefExecutionRoleDefaultPolicy2CF2ABB9", "Roles": [ { - "Ref": "TaskDefExecutionRoleB4775C97" + "Ref": "ScheduledTaskExampleScheduledTaskDefExecutionRole9F56827C" } ] } }, - "TaskDefEventsRoleFB3B67B8": { + "ScheduledTaskExampleScheduledTaskDefEventsRoleE7141C0E": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -881,7 +1074,7 @@ } } }, - "TaskDefEventsRoleDefaultPolicyA124E85B": { + "ScheduledTaskExampleScheduledTaskDefEventsRoleDefaultPolicyCF412441": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -900,7 +1093,7 @@ }, "Effect": "Allow", "Resource": { - "Ref": "TaskDef54694570" + "Ref": "ScheduledTaskExampleScheduledTaskDef3D99A3DD" } }, { @@ -908,7 +1101,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "TaskDefExecutionRoleB4775C97", + "ScheduledTaskExampleScheduledTaskDefExecutionRole9F56827C", "Arn" ] } @@ -916,202 +1109,22 @@ ], "Version": "2012-10-17" }, - "PolicyName": "TaskDefEventsRoleDefaultPolicyA124E85B", - "Roles": [ - { - "Ref": "TaskDefEventsRoleFB3B67B8" - } - ] - } - }, - "EventImageAdoptRepositoryDFAAC242": { - "Type": "Custom::ECRAdoptedRepository", - "Properties": { - "ServiceToken": { - "Fn::GetAtt": [ - "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9", - "Arn" - ] - }, - "RepositoryName": { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "EventImageImageNameE972A8B1" - } - ] - } - ] - } - } - }, - "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "lambda.", - { - "Ref": "AWS::URLSuffix" - } - ] - ] - } - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] - ] - } - ] - } - }, - "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "ecr:GetRepositoryPolicy", - "ecr:SetRepositoryPolicy", - "ecr:DeleteRepository", - "ecr:ListImages", - "ecr:BatchDeleteImage" - ], - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":ecr:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":repository/", - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - ":", - { - "Ref": "EventImageImageNameE972A8B1" - } - ] - } - ] - } - ] - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", + "PolicyName": "ScheduledTaskExampleScheduledTaskDefEventsRoleDefaultPolicyCF412441", "Roles": [ { - "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" + "Ref": "ScheduledTaskExampleScheduledTaskDefEventsRoleE7141C0E" } ] } }, - "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62c52BE89E9": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "S3Bucket": { - "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3Bucket92AB06B6" - }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cCodeS3VersionKey393B7276" - } - ] - } - ] - } - ] - ] - } - }, - "Handler": "handler.handler", - "Role": { - "Fn::GetAtt": [ - "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17", - "Arn" - ] - }, - "Runtime": "nodejs8.10", - "Timeout": 300 - }, - "DependsOn": [ - "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleDefaultPolicy6BC8737C", - "AdoptEcrRepositorydbc60defc59544bcaa5c28c95d68f62cServiceRoleD788AA17" - ] - }, - "TaskLoggingLogGroupC7E938D4": { + "ScheduledTaskExampleScheduledTaskLoggingLogGroupBB9B0B9F": { "Type": "AWS::Logs::LogGroup", "Properties": { "RetentionInDays": 365 }, "DeletionPolicy": "Retain" }, - "Rule4C995B7F": { + "ScheduledTaskExampleScheduledEventRuleF44AC7A1": { "Type": "AWS::Events::Rule", "Properties": { "ScheduleExpression": "rate(1 minute)", @@ -1125,18 +1138,15 @@ ] }, "EcsParameters": { - "TaskCount": 1, + "TaskCount": 2, "TaskDefinitionArn": { - "Ref": "TaskDef54694570" + "Ref": "ScheduledTaskExampleScheduledTaskDef3D99A3DD" } }, - "Id": "EventTarget", - "InputTransformer": { - "InputTemplate": "{\"containerOverrides\":[{\"name\":\"TheContainer\",\"environment\":[{\"name\":\"I_WAS_TRIGGERED\",\"value\":\"From CloudWatch Events\"}]}]}" - }, + "Id": "ScheduledEventRuleTarget", "RoleArn": { "Fn::GetAtt": [ - "TaskDefEventsRoleFB3B67B8", + "ScheduledTaskExampleScheduledTaskDefEventsRoleE7141C0E", "Arn" ] } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts index 3b6844b81c1fb..ba348e9aeae1b 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts @@ -1,5 +1,4 @@ import ec2 = require('@aws-cdk/aws-ec2'); -import events = require('@aws-cdk/aws-events'); import cdk = require('@aws-cdk/cdk'); import ecs = require('../../lib'); @@ -19,36 +18,16 @@ class EventStack extends cdk.Stack { }); /// !show - // Create a Task Definition for the container to start - const taskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef'); - taskDefinition.addContainer('TheContainer', { + new ecs.ScheduledEc2Task(this, 'ScheduledTaskExample', { + cluster, image: ecs.ContainerImage.fromAsset(this, 'EventImage', { - directory: path.resolve(__dirname, '..', 'eventhandler-image') + directory: path.resolve(__dirname, "..", 'eventhandler-image') }), - memoryLimitMiB: 256, - logging: new ecs.AwsLogDriver(this, 'TaskLogging', { streamPrefix: 'EventDemo' }) - }); - - // An EventRule that describes the event trigger (in this case a scheduled run) - const rule = new events.EventRule(this, 'Rule', { - scheduleExpression: 'rate(1 minute)', - }); - - // Use Ec2TaskEventRuleTarget as the target of the EventRule - const target = new ecs.Ec2EventRuleTarget(this, 'EventTarget', { - cluster, - taskDefinition, - taskCount: 1 - }); - - // Pass an environment variable to the container 'TheContainer' in the task - rule.addTarget(target, { - jsonTemplate: JSON.stringify({ - containerOverrides: [{ - name: 'TheContainer', - environment: [{ name: 'I_WAS_TRIGGERED', value: 'From CloudWatch Events' }] - }] - }) + desiredTaskCount: 2, + memoryLimitMiB: 512, + cpu: 1, + environment: { name: 'TRIGGER', value: 'CloudWatch Events' }, + scheduleExpression: 'rate(1 minute)' }); /// !hide } diff --git a/packages/@aws-cdk/aws-ecs/test/test.scheduled-ecs-task.ts b/packages/@aws-cdk/aws-ecs/test/test.scheduled-ecs-task.ts new file mode 100644 index 0000000000000..3a1252895c6f1 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.scheduled-ecs-task.ts @@ -0,0 +1,281 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); +import { ScheduledEc2Task } from '../lib'; + +export = { + "Can create a scheduled Ec2 Task - with only required props"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') + }); + + new ScheduledEc2Task(stack, 'ScheduledEc2Task', { + cluster, + image: ecs.ContainerImage.fromRegistry('henk'), + memoryLimitMiB: 512, + scheduleExpression: 'rate(1 minute)' + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: { "Fn::GetAtt": ["EcsCluster97242B84", "Arn"] }, + EcsParameters: { + TaskCount: 1, + TaskDefinitionArn: { Ref: "ScheduledEc2TaskScheduledTaskDef56328BA4" } + }, + Id: "ScheduledEventRuleTarget", + RoleArn: { "Fn::GetAtt": ["ScheduledEc2TaskScheduledTaskDefEventsRole64113C5F", "Arn"] } + } + ] + })); + + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Essential: true, + Image: "henk", + Links: [], + LinuxParameters: { + Capabilities: { + Add: [], + Drop: [] + }, + Devices: [], + Tmpfs: [] + }, + LogConfiguration: { + LogDriver: "awslogs", + Options: { + "awslogs-group": { + Ref: "ScheduledEc2TaskScheduledTaskLoggingLogGroupDBEF2BF3" + }, + "awslogs-stream-prefix": "ScheduledEc2Task", + "awslogs-region": { + Ref: "AWS::Region" + } + } + }, + Memory: 512, + MountPoints: [], + Name: "ScheduledContainer", + PortMappings: [], + Ulimits: [], + VolumesFrom: [] + } + ] + })); + + test.done(); + }, + + "Can create a scheduled Ec2 Task - with optional props"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') + }); + + new ScheduledEc2Task(stack, 'ScheduledEc2Task', { + cluster, + image: ecs.ContainerImage.fromRegistry('henk'), + desiredTaskCount: 2, + memoryLimitMiB: 512, + cpu: 2, + environment: { name: 'TRIGGER', value: 'CloudWatch Events' }, + scheduleExpression: 'rate(1 minute)' + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: { "Fn::GetAtt": ["EcsCluster97242B84", "Arn"] }, + EcsParameters: { + TaskCount: 2, + TaskDefinitionArn: { Ref: "ScheduledEc2TaskScheduledTaskDef56328BA4" } + }, + Id: "ScheduledEventRuleTarget", + RoleArn: { "Fn::GetAtt": ["ScheduledEc2TaskScheduledTaskDefEventsRole64113C5F", "Arn"] } + } + ] + })); + + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Cpu: 2, + Environment: [ + { + Name: "name", + Value: "TRIGGER" + }, + { + Name: "value", + Value: "CloudWatch Events" + } + ], + Essential: true, + Image: "henk", + Links: [], + LinuxParameters: { + Capabilities: { + Add: [], + Drop: [] + }, + Devices: [], + Tmpfs: [] + }, + LogConfiguration: { + LogDriver: "awslogs", + Options: { + "awslogs-group": { + Ref: "ScheduledEc2TaskScheduledTaskLoggingLogGroupDBEF2BF3" + }, + "awslogs-stream-prefix": "ScheduledEc2Task", + "awslogs-region": { + Ref: "AWS::Region" + } + } + }, + Memory: 512, + MountPoints: [], + Name: "ScheduledContainer", + PortMappings: [], + Ulimits: [], + VolumesFrom: [] + } + ] + })); + + test.done(); + }, + + "Scheduled Ec2 Task - with MemoryReservation defined"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') + }); + + new ScheduledEc2Task(stack, 'ScheduledEc2Task', { + cluster, + image: ecs.ContainerImage.fromRegistry('henk'), + memoryReservationMiB: 512, + scheduleExpression: 'rate(1 minute)' + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Essential: true, + Image: "henk", + Links: [], + LinuxParameters: { + Capabilities: { + Add: [], + Drop: [] + }, + Devices: [], + Tmpfs: [] + }, + LogConfiguration: { + LogDriver: "awslogs", + Options: { + "awslogs-group": { + Ref: "ScheduledEc2TaskScheduledTaskLoggingLogGroupDBEF2BF3" + }, + "awslogs-stream-prefix": "ScheduledEc2Task", + "awslogs-region": { + Ref: "AWS::Region" + } + } + }, + MemoryReservation: 512, + MountPoints: [], + Name: "ScheduledContainer", + PortMappings: [], + Ulimits: [], + VolumesFrom: [] + } + ] + })); + + test.done(); + }, + + "Scheduled Ec2 Task - with Command defined"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro') + }); + + new ScheduledEc2Task(stack, 'ScheduledEc2Task', { + cluster, + image: ecs.ContainerImage.fromRegistry('henk'), + memoryReservationMiB: 512, + command: "-c, 4, amazon.com", + scheduleExpression: 'rate(1 minute)' + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Command: { + "Fn::Split": [ + ",", + "-c, 4, amazon.com" + ] + }, + Essential: true, + Image: "henk", + Links: [], + LinuxParameters: { + Capabilities: { + Add: [], + Drop: [] + }, + Devices: [], + Tmpfs: [] + }, + LogConfiguration: { + LogDriver: "awslogs", + Options: { + "awslogs-group": { + Ref: "ScheduledEc2TaskScheduledTaskLoggingLogGroupDBEF2BF3" + }, + "awslogs-stream-prefix": "ScheduledEc2Task", + "awslogs-region": { + Ref: "AWS::Region" + } + } + }, + MemoryReservation: 512, + MountPoints: [], + Name: "ScheduledContainer", + PortMappings: [], + Ulimits: [], + VolumesFrom: [] + } + ] + })); + + test.done(); + }, +};