From 06d9c330824a437fea5df979cf931a69661ddba3 Mon Sep 17 00:00:00 2001 From: Pahud Date: Thu, 3 Oct 2019 18:25:29 +0800 Subject: [PATCH 1/8] feat(aws-ecs): add automated spot instance draining support --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 55 +- .../test/ec2/integ.spot-drain.expected.json | 1298 +++++++++++++++++ .../aws-ecs/test/ec2/integ.spot-drain.ts | 52 + 3 files changed, 1385 insertions(+), 20 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json create mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 522881c43171a..1b48f1fbe8f83 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -1,12 +1,12 @@ import autoscaling = require('@aws-cdk/aws-autoscaling'); -import cloudwatch = require ('@aws-cdk/aws-cloudwatch'); +import cloudwatch = require('@aws-cdk/aws-cloudwatch'); import ec2 = require('@aws-cdk/aws-ec2'); import iam = require('@aws-cdk/aws-iam'); import cloudmap = require('@aws-cdk/aws-servicediscovery'); import ssm = require('@aws-cdk/aws-ssm'); -import {Construct, Duration, IResource, Resource, Stack} from '@aws-cdk/core'; -import {InstanceDrainHook} from './drain-hook/instance-drain-hook'; -import {CfnCluster} from './ecs.generated'; +import { Construct, Duration, IResource, Resource, Stack } from '@aws-cdk/core'; +import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; +import { CfnCluster } from './ecs.generated'; /** * The properties used to define an ECS cluster. @@ -195,6 +195,12 @@ export class Cluster extends Resource implements ICluster { autoScalingGroup.addUserData('echo ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config'); } + if (options.spotInstanceDraining) { + // Automated Spot Instance Draining + // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-spot.html + autoScalingGroup.addUserData('echo ECS_ENABLE_SPOT_INSTANCE_DRAINING=true >> /etc/ecs/ecs.config'); + } + // ECS instances must be able to do these things // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html autoScalingGroup.addToRolePolicy(new iam.PolicyStatement({ @@ -252,7 +258,7 @@ export class Cluster extends Resource implements ICluster { * @default average over 5 minutes */ public metricMemoryReservation(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('MemoryReservation', props ); + return this.metric('MemoryReservation', props); } /** @@ -350,12 +356,12 @@ export class EcsOptimizedAmi implements ec2.IMachineImage { // set the SSM parameter name this.amiParameterName = "/aws/service/ecs/optimized-ami/" - + ( this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX ? "amazon-linux/" : "" ) - + ( this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 ? "amazon-linux-2/" : "" ) - + ( this.windowsVersion ? `windows_server/${this.windowsVersion}/english/full/` : "" ) - + ( this.hwType === AmiHardwareType.GPU ? "gpu/" : "" ) - + ( this.hwType === AmiHardwareType.ARM ? "arm64/" : "" ) - + "recommended/image_id"; + + (this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX ? "amazon-linux/" : "") + + (this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 ? "amazon-linux-2/" : "") + + (this.windowsVersion ? `windows_server/${this.windowsVersion}/english/full/` : "") + + (this.hwType === AmiHardwareType.GPU ? "gpu/" : "") + + (this.hwType === AmiHardwareType.ARM ? "arm64/" : "") + + "recommended/image_id"; } /** @@ -380,14 +386,14 @@ export class EcsOptimizedImage implements ec2.IMachineImage { * @param hardwareType ECS-optimized AMI variant to use */ public static amazonLinux2(hardwareType = AmiHardwareType.STANDARD): EcsOptimizedImage { - return new EcsOptimizedImage({generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, hardwareType}); + return new EcsOptimizedImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, hardwareType }); } /** * Construct an Amazon Linux AMI image from the latest ECS Optimized AMI published in SSM */ public static amazonLinux(): EcsOptimizedImage { - return new EcsOptimizedImage({generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX}); + return new EcsOptimizedImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX }); } /** @@ -396,7 +402,7 @@ export class EcsOptimizedImage implements ec2.IMachineImage { * @param windowsVersion Windows Version to use */ public static windows(windowsVersion: WindowsOptimizedVersion): EcsOptimizedImage { - return new EcsOptimizedImage({windowsVersion}); + return new EcsOptimizedImage({ windowsVersion }); } private readonly generation?: ec2.AmazonLinuxGeneration; @@ -421,12 +427,12 @@ export class EcsOptimizedImage implements ec2.IMachineImage { // set the SSM parameter name this.amiParameterName = "/aws/service/ecs/optimized-ami/" - + ( this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX ? "amazon-linux/" : "" ) - + ( this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 ? "amazon-linux-2/" : "" ) - + ( this.windowsVersion ? `windows_server/${this.windowsVersion}/english/full/` : "" ) - + ( this.hwType === AmiHardwareType.GPU ? "gpu/" : "" ) - + ( this.hwType === AmiHardwareType.ARM ? "arm64/" : "" ) - + "recommended/image_id"; + + (this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX ? "amazon-linux/" : "") + + (this.generation === ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 ? "amazon-linux-2/" : "") + + (this.windowsVersion ? `windows_server/${this.windowsVersion}/english/full/` : "") + + (this.hwType === AmiHardwareType.GPU ? "gpu/" : "") + + (this.hwType === AmiHardwareType.ARM ? "arm64/" : "") + + "recommended/image_id"; } /** @@ -614,6 +620,15 @@ export interface AddAutoScalingGroupCapacityOptions { * @default Duration.minutes(5) */ readonly taskDrainTime?: Duration; + + /** + * Automated Draining for Spot Instances running Amazon ECS Services + * + * [Using Spot Instances](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-spot.html) + * + * @default false + */ + readonly spotInstanceDraining?: boolean } /** diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json new file mode 100644 index 0000000000000..5710d63600873 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json @@ -0,0 +1,1298 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "ClusterEB0386A7": { + "Type": "AWS::ECS::Cluster" + }, + "ClusterasgSpotInstanceSecurityGroupD059418A": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "ecs-spot-test2/Cluster/asgSpot/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Cluster/asgSpot" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ClusterasgSpotInstanceRoleEB937075": { + "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": "ecs-spot-test2/Cluster/asgSpot" + } + ] + } + }, + "ClusterasgSpotInstanceRoleDefaultPolicy503C9952": { + "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": "ClusterasgSpotInstanceRoleDefaultPolicy503C9952", + "Roles": [ + { + "Ref": "ClusterasgSpotInstanceRoleEB937075" + } + ] + } + }, + "ClusterasgSpotInstanceProfile364F414B": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "ClusterasgSpotInstanceRoleEB937075" + } + ] + } + }, + "ClusterasgSpotLaunchConfig90657960": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "c5.xlarge", + "IamInstanceProfile": { + "Ref": "ClusterasgSpotInstanceProfile364F414B" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ClusterasgSpotInstanceSecurityGroupD059418A", + "GroupId" + ] + } + ], + "SpotPrice": "0.0735", + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "ClusterEB0386A7" + }, + " >> /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\necho ECS_ENABLE_SPOT_INSTANCE_DRAINING=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "ClusterasgSpotInstanceRoleDefaultPolicy503C9952", + "ClusterasgSpotInstanceRoleEB937075" + ] + }, + "ClusterasgSpotASGED3B9B73": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "2", + "MinSize": "2", + "DesiredCapacity": "2", + "LaunchConfigurationName": { + "Ref": "ClusterasgSpotLaunchConfig90657960" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "ecs-spot-test2/Cluster/asgSpot" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "ClusterasgSpotDrainECSHookFunctionServiceRoleE2084EAD": { + "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": "ecs-spot-test2/Cluster/asgSpot" + } + ] + } + }, + "ClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicyF2B44652": { + "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:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":autoScalingGroup:*:autoScalingGroupName/", + { + "Ref": "ClusterasgSpotASGED3B9B73" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ClusterEB0386A7", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "ClusterEB0386A7", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicyF2B44652", + "Roles": [ + { + "Ref": "ClusterasgSpotDrainECSHookFunctionServiceRoleE2084EAD" + } + ] + } + }, + "ClusterasgSpotDrainECSHookFunction7AA02A6D": { + "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": [ + "ClusterasgSpotDrainECSHookFunctionServiceRoleE2084EAD", + "Arn" + ] + }, + "Runtime": "python3.6", + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "ClusterEB0386A7" + } + } + }, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Cluster/asgSpot" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "ClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicyF2B44652", + "ClusterasgSpotDrainECSHookFunctionServiceRoleE2084EAD" + ] + }, + "ClusterasgSpotDrainECSHookFunctionAllowInvokeecsspottest2ClusterasgSpotLifecycleHookDrainHookTopic7B11F09852773705": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ClusterasgSpotDrainECSHookFunction7AA02A6D", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB" + } + } + }, + "ClusterasgSpotDrainECSHookFunctionTopic306D3AA1": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB" + }, + "Endpoint": { + "Fn::GetAtt": [ + "ClusterasgSpotDrainECSHookFunction7AA02A6D", + "Arn" + ] + } + } + }, + "ClusterasgSpotLifecycleHookDrainHookRoleBB0F19EF": { + "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": "ecs-spot-test2/Cluster/asgSpot" + } + ] + } + }, + "ClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyB8BAFD48": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyB8BAFD48", + "Roles": [ + { + "Ref": "ClusterasgSpotLifecycleHookDrainHookRoleBB0F19EF" + } + ] + } + }, + "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB": { + "Type": "AWS::SNS::Topic" + }, + "ClusterasgSpotLifecycleHookDrainHook15DDDB71": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "ClusterasgSpotASGED3B9B73" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB" + }, + "RoleARN": { + "Fn::GetAtt": [ + "ClusterasgSpotLifecycleHookDrainHookRoleBB0F19EF", + "Arn" + ] + } + }, + "DependsOn": [ + "ClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyB8BAFD48", + "ClusterasgSpotLifecycleHookDrainHookRoleBB0F19EF" + ] + }, + "ClusterasgOdInstanceSecurityGroup14297A08": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "ecs-spot-test2/Cluster/asgOd/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Cluster/asgOd" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ClusterasgOdInstanceRole46B2D1F2": { + "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": "ecs-spot-test2/Cluster/asgOd" + } + ] + } + }, + "ClusterasgOdInstanceRoleDefaultPolicy15EC01BC": { + "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": "ClusterasgOdInstanceRoleDefaultPolicy15EC01BC", + "Roles": [ + { + "Ref": "ClusterasgOdInstanceRole46B2D1F2" + } + ] + } + }, + "ClusterasgOdInstanceProfile8CCBFD8A": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "ClusterasgOdInstanceRole46B2D1F2" + } + ] + } + }, + "ClusterasgOdLaunchConfig230C5EC6": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t3.large", + "IamInstanceProfile": { + "Ref": "ClusterasgOdInstanceProfile8CCBFD8A" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ClusterasgOdInstanceSecurityGroup14297A08", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "ClusterEB0386A7" + }, + " >> /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": [ + "ClusterasgOdInstanceRoleDefaultPolicy15EC01BC", + "ClusterasgOdInstanceRole46B2D1F2" + ] + }, + "ClusterasgOdASG83F38D7D": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "2", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "ClusterasgOdLaunchConfig230C5EC6" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "ecs-spot-test2/Cluster/asgOd" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "ClusterasgOdDrainECSHookFunctionServiceRole819F43CE": { + "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": "ecs-spot-test2/Cluster/asgOd" + } + ] + } + }, + "ClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicy8500B712": { + "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:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":autoScalingGroup:*:autoScalingGroupName/", + { + "Ref": "ClusterasgOdASG83F38D7D" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ClusterEB0386A7", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "ClusterEB0386A7", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicy8500B712", + "Roles": [ + { + "Ref": "ClusterasgOdDrainECSHookFunctionServiceRole819F43CE" + } + ] + } + }, + "ClusterasgOdDrainECSHookFunctionE33FC308": { + "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": [ + "ClusterasgOdDrainECSHookFunctionServiceRole819F43CE", + "Arn" + ] + }, + "Runtime": "python3.6", + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "ClusterEB0386A7" + } + } + }, + "Tags": [ + { + "Key": "Name", + "Value": "ecs-spot-test2/Cluster/asgOd" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "ClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicy8500B712", + "ClusterasgOdDrainECSHookFunctionServiceRole819F43CE" + ] + }, + "ClusterasgOdDrainECSHookFunctionAllowInvokeecsspottest2ClusterasgOdLifecycleHookDrainHookTopicA1B632A8E35EF0EC": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ClusterasgOdDrainECSHookFunctionE33FC308", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1" + } + } + }, + "ClusterasgOdDrainECSHookFunctionTopic603DF471": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1" + }, + "Endpoint": { + "Fn::GetAtt": [ + "ClusterasgOdDrainECSHookFunctionE33FC308", + "Arn" + ] + } + } + }, + "ClusterasgOdLifecycleHookDrainHookRole07F838A4": { + "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": "ecs-spot-test2/Cluster/asgOd" + } + ] + } + }, + "ClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy037C17EB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy037C17EB", + "Roles": [ + { + "Ref": "ClusterasgOdLifecycleHookDrainHookRole07F838A4" + } + ] + } + }, + "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1": { + "Type": "AWS::SNS::Topic" + }, + "ClusterasgOdLifecycleHookDrainHookA099BA56": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "ClusterasgOdASG83F38D7D" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1" + }, + "RoleARN": { + "Fn::GetAtt": [ + "ClusterasgOdLifecycleHookDrainHookRole07F838A4", + "Arn" + ] + } + }, + "DependsOn": [ + "ClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy037C17EB", + "ClusterasgOdLifecycleHookDrainHookRole07F838A4" + ] + }, + "TaskTaskRoleE98524A1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Task79114B6B": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Memory": 512, + "Name": "PHP", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 0, + "Protocol": "tcp" + } + ] + } + ], + "Family": "ecsspottest2Task5842FD21", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskTaskRoleE98524A1", + "Arn" + ] + } + } + }, + "ServiceD69D759B": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "Task79114B6B" + }, + "Cluster": { + "Ref": "ClusterEB0386A7" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "LaunchType": "EC2", + "SchedulingStrategy": "REPLICA" + } + } + }, + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.ts new file mode 100644 index 0000000000000..91a1af3c4e70b --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.ts @@ -0,0 +1,52 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import { App, Construct, Stack, StackProps } from '@aws-cdk/core'; +import ecs = require('../../lib'); + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2 }); + + const cluster = new ecs.Cluster(this, 'Cluster', { + vpc + }); + + cluster.addCapacity('asgSpot', { + maxCapacity: 2, + minCapacity: 2, + desiredCapacity: 2, + instanceType: new ec2.InstanceType('c5.xlarge'), + spotPrice: '0.0735', + spotInstanceDraining: true, + }); + + cluster.addCapacity('asgOd', { + maxCapacity: 2, + minCapacity: 1, + desiredCapacity: 1, + instanceType: new ec2.InstanceType('t3.large'), + }); + + const taskDefinition = new ecs.TaskDefinition(this, 'Task', { + compatibility: ecs.Compatibility.EC2 + }); + + taskDefinition.addContainer('PHP', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }).addPortMappings({ + containerPort: 80, + }); + + new ecs.Ec2Service(this, 'Service', { + cluster, + taskDefinition + }); + } + +} + +const app = new App(); +new TestStack(app, 'ecs-spot-test2'); +app.synth(); From 1c675376d414ddd6dffcc58a21371c5c54c9b02d Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 3 Oct 2019 21:16:41 +0300 Subject: [PATCH 2/8] use @see in comments --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 1b48f1fbe8f83..ce7371c787e81 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -624,7 +624,7 @@ export interface AddAutoScalingGroupCapacityOptions { /** * Automated Draining for Spot Instances running Amazon ECS Services * - * [Using Spot Instances](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-spot.html) + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-spot.html * * @default false */ From b7939cd749ba007c5127daedd9fd3f1e33000507 Mon Sep 17 00:00:00 2001 From: Pahud Date: Fri, 4 Oct 2019 10:50:53 +0800 Subject: [PATCH 3/8] 2 spaces indentation and simplify the integ content --- .../test/ec2/integ.spot-drain.expected.json | 238 +++++++++--------- .../aws-ecs/test/ec2/integ.spot-drain.ts | 88 +++---- 2 files changed, 159 insertions(+), 167 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json index 5710d63600873..e7c49b9ab1f4e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json @@ -10,7 +10,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc" + "Value": "aws-ecs-integ-spot/Vpc" } ] } @@ -27,7 +27,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PublicSubnet1" + "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet1" }, { "Key": "aws-cdk:subnet-name", @@ -49,7 +49,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PublicSubnet1" + "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet1" } ] } @@ -101,7 +101,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PublicSubnet1" + "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet1" } ] } @@ -118,7 +118,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PublicSubnet2" + "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet2" }, { "Key": "aws-cdk:subnet-name", @@ -140,7 +140,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PublicSubnet2" + "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet2" } ] } @@ -192,7 +192,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PublicSubnet2" + "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet2" } ] } @@ -209,7 +209,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PrivateSubnet1" + "Value": "aws-ecs-integ-spot/Vpc/PrivateSubnet1" }, { "Key": "aws-cdk:subnet-name", @@ -231,7 +231,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PrivateSubnet1" + "Value": "aws-ecs-integ-spot/Vpc/PrivateSubnet1" } ] } @@ -271,7 +271,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PrivateSubnet2" + "Value": "aws-ecs-integ-spot/Vpc/PrivateSubnet2" }, { "Key": "aws-cdk:subnet-name", @@ -293,7 +293,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc/PrivateSubnet2" + "Value": "aws-ecs-integ-spot/Vpc/PrivateSubnet2" } ] } @@ -327,7 +327,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Vpc" + "Value": "aws-ecs-integ-spot/Vpc" } ] } @@ -343,13 +343,13 @@ } } }, - "ClusterEB0386A7": { + "EcsCluster97242B84": { "Type": "AWS::ECS::Cluster" }, - "ClusterasgSpotInstanceSecurityGroupD059418A": { + "EcsClusterasgSpotInstanceSecurityGroupEA17787D": { "Type": "AWS::EC2::SecurityGroup", "Properties": { - "GroupDescription": "ecs-spot-test2/Cluster/asgSpot/InstanceSecurityGroup", + "GroupDescription": "aws-ecs-integ-spot/EcsCluster/asgSpot/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", @@ -360,7 +360,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgSpot" + "Value": "aws-ecs-integ-spot/EcsCluster/asgSpot" } ], "VpcId": { @@ -368,7 +368,7 @@ } } }, - "ClusterasgSpotInstanceRoleEB937075": { + "EcsClusterasgSpotInstanceRole84AB6F93": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -396,12 +396,12 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgSpot" + "Value": "aws-ecs-integ-spot/EcsCluster/asgSpot" } ] } }, - "ClusterasgSpotInstanceRoleDefaultPolicy503C9952": { + "EcsClusterasgSpotInstanceRoleDefaultPolicyB1E3ABFA": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -425,25 +425,25 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ClusterasgSpotInstanceRoleDefaultPolicy503C9952", + "PolicyName": "EcsClusterasgSpotInstanceRoleDefaultPolicyB1E3ABFA", "Roles": [ { - "Ref": "ClusterasgSpotInstanceRoleEB937075" + "Ref": "EcsClusterasgSpotInstanceRole84AB6F93" } ] } }, - "ClusterasgSpotInstanceProfile364F414B": { + "EcsClusterasgSpotInstanceProfile0D6DD08D": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Roles": [ { - "Ref": "ClusterasgSpotInstanceRoleEB937075" + "Ref": "EcsClusterasgSpotInstanceRole84AB6F93" } ] } }, - "ClusterasgSpotLaunchConfig90657960": { + "EcsClusterasgSpotLaunchConfig75BCA823": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": { @@ -451,12 +451,12 @@ }, "InstanceType": "c5.xlarge", "IamInstanceProfile": { - "Ref": "ClusterasgSpotInstanceProfile364F414B" + "Ref": "EcsClusterasgSpotInstanceProfile0D6DD08D" }, "SecurityGroups": [ { "Fn::GetAtt": [ - "ClusterasgSpotInstanceSecurityGroupD059418A", + "EcsClusterasgSpotInstanceSecurityGroupEA17787D", "GroupId" ] } @@ -469,7 +469,7 @@ [ "#!/bin/bash\necho ECS_CLUSTER=", { - "Ref": "ClusterEB0386A7" + "Ref": "EcsCluster97242B84" }, " >> /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\necho ECS_ENABLE_SPOT_INSTANCE_DRAINING=true >> /etc/ecs/ecs.config" ] @@ -478,24 +478,24 @@ } }, "DependsOn": [ - "ClusterasgSpotInstanceRoleDefaultPolicy503C9952", - "ClusterasgSpotInstanceRoleEB937075" + "EcsClusterasgSpotInstanceRoleDefaultPolicyB1E3ABFA", + "EcsClusterasgSpotInstanceRole84AB6F93" ] }, - "ClusterasgSpotASGED3B9B73": { + "EcsClusterasgSpotASG0D77F041": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "MaxSize": "2", "MinSize": "2", "DesiredCapacity": "2", "LaunchConfigurationName": { - "Ref": "ClusterasgSpotLaunchConfig90657960" + "Ref": "EcsClusterasgSpotLaunchConfig75BCA823" }, "Tags": [ { "Key": "Name", "PropagateAtLaunch": true, - "Value": "ecs-spot-test2/Cluster/asgSpot" + "Value": "aws-ecs-integ-spot/EcsCluster/asgSpot" } ], "VPCZoneIdentifier": [ @@ -516,7 +516,7 @@ } } }, - "ClusterasgSpotDrainECSHookFunctionServiceRoleE2084EAD": { + "EcsClusterasgSpotDrainECSHookFunctionServiceRole8EEDDFE0": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -548,12 +548,12 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgSpot" + "Value": "aws-ecs-integ-spot/EcsCluster/asgSpot" } ] } }, - "ClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicyF2B44652": { + "EcsClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicy96377D7C": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -589,7 +589,7 @@ }, ":autoScalingGroup:*:autoScalingGroupName/", { - "Ref": "ClusterasgSpotASGED3B9B73" + "Ref": "EcsClusterasgSpotASG0D77F041" } ] ] @@ -612,7 +612,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "ClusterEB0386A7", + "EcsCluster97242B84", "Arn" ] } @@ -626,7 +626,7 @@ "ArnEquals": { "ecs:cluster": { "Fn::GetAtt": [ - "ClusterEB0386A7", + "EcsCluster97242B84", "Arn" ] } @@ -638,15 +638,15 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicyF2B44652", + "PolicyName": "EcsClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicy96377D7C", "Roles": [ { - "Ref": "ClusterasgSpotDrainECSHookFunctionServiceRoleE2084EAD" + "Ref": "EcsClusterasgSpotDrainECSHookFunctionServiceRole8EEDDFE0" } ] } }, - "ClusterasgSpotDrainECSHookFunction7AA02A6D": { + "EcsClusterasgSpotDrainECSHookFunction969F1553": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { @@ -655,7 +655,7 @@ "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ - "ClusterasgSpotDrainECSHookFunctionServiceRoleE2084EAD", + "EcsClusterasgSpotDrainECSHookFunctionServiceRole8EEDDFE0", "Arn" ] }, @@ -663,55 +663,55 @@ "Environment": { "Variables": { "CLUSTER": { - "Ref": "ClusterEB0386A7" + "Ref": "EcsCluster97242B84" } } }, "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgSpot" + "Value": "aws-ecs-integ-spot/EcsCluster/asgSpot" } ], "Timeout": 310 }, "DependsOn": [ - "ClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicyF2B44652", - "ClusterasgSpotDrainECSHookFunctionServiceRoleE2084EAD" + "EcsClusterasgSpotDrainECSHookFunctionServiceRoleDefaultPolicy96377D7C", + "EcsClusterasgSpotDrainECSHookFunctionServiceRole8EEDDFE0" ] }, - "ClusterasgSpotDrainECSHookFunctionAllowInvokeecsspottest2ClusterasgSpotLifecycleHookDrainHookTopic7B11F09852773705": { + "EcsClusterasgSpotDrainECSHookFunctionAllowInvokeawsecsintegspotEcsClusterasgSpotLifecycleHookDrainHookTopic92E2845E8BD3FE4E": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { "Fn::GetAtt": [ - "ClusterasgSpotDrainECSHookFunction7AA02A6D", + "EcsClusterasgSpotDrainECSHookFunction969F1553", "Arn" ] }, "Principal": "sns.amazonaws.com", "SourceArn": { - "Ref": "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB" + "Ref": "EcsClusterasgSpotLifecycleHookDrainHookTopic6212EC83" } } }, - "ClusterasgSpotDrainECSHookFunctionTopic306D3AA1": { + "EcsClusterasgSpotDrainECSHookFunctionTopic9648CAD4": { "Type": "AWS::SNS::Subscription", "Properties": { "Protocol": "lambda", "TopicArn": { - "Ref": "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB" + "Ref": "EcsClusterasgSpotLifecycleHookDrainHookTopic6212EC83" }, "Endpoint": { "Fn::GetAtt": [ - "ClusterasgSpotDrainECSHookFunction7AA02A6D", + "EcsClusterasgSpotDrainECSHookFunction969F1553", "Arn" ] } } }, - "ClusterasgSpotLifecycleHookDrainHookRoleBB0F19EF": { + "EcsClusterasgSpotLifecycleHookDrainHookRole1B427C77": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -729,12 +729,12 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgSpot" + "Value": "aws-ecs-integ-spot/EcsCluster/asgSpot" } ] } }, - "ClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyB8BAFD48": { + "EcsClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyFC0E3482": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -743,51 +743,51 @@ "Action": "sns:Publish", "Effect": "Allow", "Resource": { - "Ref": "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB" + "Ref": "EcsClusterasgSpotLifecycleHookDrainHookTopic6212EC83" } } ], "Version": "2012-10-17" }, - "PolicyName": "ClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyB8BAFD48", + "PolicyName": "EcsClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyFC0E3482", "Roles": [ { - "Ref": "ClusterasgSpotLifecycleHookDrainHookRoleBB0F19EF" + "Ref": "EcsClusterasgSpotLifecycleHookDrainHookRole1B427C77" } ] } }, - "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB": { + "EcsClusterasgSpotLifecycleHookDrainHookTopic6212EC83": { "Type": "AWS::SNS::Topic" }, - "ClusterasgSpotLifecycleHookDrainHook15DDDB71": { + "EcsClusterasgSpotLifecycleHookDrainHook91178D34": { "Type": "AWS::AutoScaling::LifecycleHook", "Properties": { "AutoScalingGroupName": { - "Ref": "ClusterasgSpotASGED3B9B73" + "Ref": "EcsClusterasgSpotASG0D77F041" }, "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", "DefaultResult": "CONTINUE", "HeartbeatTimeout": 300, "NotificationTargetARN": { - "Ref": "ClusterasgSpotLifecycleHookDrainHookTopic2C7162DB" + "Ref": "EcsClusterasgSpotLifecycleHookDrainHookTopic6212EC83" }, "RoleARN": { "Fn::GetAtt": [ - "ClusterasgSpotLifecycleHookDrainHookRoleBB0F19EF", + "EcsClusterasgSpotLifecycleHookDrainHookRole1B427C77", "Arn" ] } }, "DependsOn": [ - "ClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyB8BAFD48", - "ClusterasgSpotLifecycleHookDrainHookRoleBB0F19EF" + "EcsClusterasgSpotLifecycleHookDrainHookRoleDefaultPolicyFC0E3482", + "EcsClusterasgSpotLifecycleHookDrainHookRole1B427C77" ] }, - "ClusterasgOdInstanceSecurityGroup14297A08": { + "EcsClusterasgOdInstanceSecurityGroup301DFBED": { "Type": "AWS::EC2::SecurityGroup", "Properties": { - "GroupDescription": "ecs-spot-test2/Cluster/asgOd/InstanceSecurityGroup", + "GroupDescription": "aws-ecs-integ-spot/EcsCluster/asgOd/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", @@ -798,7 +798,7 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgOd" + "Value": "aws-ecs-integ-spot/EcsCluster/asgOd" } ], "VpcId": { @@ -806,7 +806,7 @@ } } }, - "ClusterasgOdInstanceRole46B2D1F2": { + "EcsClusterasgOdInstanceRoleC8290533": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -834,12 +834,12 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgOd" + "Value": "aws-ecs-integ-spot/EcsCluster/asgOd" } ] } }, - "ClusterasgOdInstanceRoleDefaultPolicy15EC01BC": { + "EcsClusterasgOdInstanceRoleDefaultPolicy0AE7FAB2": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -863,25 +863,25 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ClusterasgOdInstanceRoleDefaultPolicy15EC01BC", + "PolicyName": "EcsClusterasgOdInstanceRoleDefaultPolicy0AE7FAB2", "Roles": [ { - "Ref": "ClusterasgOdInstanceRole46B2D1F2" + "Ref": "EcsClusterasgOdInstanceRoleC8290533" } ] } }, - "ClusterasgOdInstanceProfile8CCBFD8A": { + "EcsClusterasgOdInstanceProfileE5B88756": { "Type": "AWS::IAM::InstanceProfile", "Properties": { "Roles": [ { - "Ref": "ClusterasgOdInstanceRole46B2D1F2" + "Ref": "EcsClusterasgOdInstanceRoleC8290533" } ] } }, - "ClusterasgOdLaunchConfig230C5EC6": { + "EcsClusterasgOdLaunchConfigD3B9E271": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "ImageId": { @@ -889,12 +889,12 @@ }, "InstanceType": "t3.large", "IamInstanceProfile": { - "Ref": "ClusterasgOdInstanceProfile8CCBFD8A" + "Ref": "EcsClusterasgOdInstanceProfileE5B88756" }, "SecurityGroups": [ { "Fn::GetAtt": [ - "ClusterasgOdInstanceSecurityGroup14297A08", + "EcsClusterasgOdInstanceSecurityGroup301DFBED", "GroupId" ] } @@ -906,7 +906,7 @@ [ "#!/bin/bash\necho ECS_CLUSTER=", { - "Ref": "ClusterEB0386A7" + "Ref": "EcsCluster97242B84" }, " >> /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" ] @@ -915,24 +915,24 @@ } }, "DependsOn": [ - "ClusterasgOdInstanceRoleDefaultPolicy15EC01BC", - "ClusterasgOdInstanceRole46B2D1F2" + "EcsClusterasgOdInstanceRoleDefaultPolicy0AE7FAB2", + "EcsClusterasgOdInstanceRoleC8290533" ] }, - "ClusterasgOdASG83F38D7D": { + "EcsClusterasgOdASG0E5C30EC": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "MaxSize": "2", "MinSize": "1", "DesiredCapacity": "1", "LaunchConfigurationName": { - "Ref": "ClusterasgOdLaunchConfig230C5EC6" + "Ref": "EcsClusterasgOdLaunchConfigD3B9E271" }, "Tags": [ { "Key": "Name", "PropagateAtLaunch": true, - "Value": "ecs-spot-test2/Cluster/asgOd" + "Value": "aws-ecs-integ-spot/EcsCluster/asgOd" } ], "VPCZoneIdentifier": [ @@ -953,7 +953,7 @@ } } }, - "ClusterasgOdDrainECSHookFunctionServiceRole819F43CE": { + "EcsClusterasgOdDrainECSHookFunctionServiceRoleFC088D55": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -985,12 +985,12 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgOd" + "Value": "aws-ecs-integ-spot/EcsCluster/asgOd" } ] } }, - "ClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicy8500B712": { + "EcsClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicyE54F1794": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -1026,7 +1026,7 @@ }, ":autoScalingGroup:*:autoScalingGroupName/", { - "Ref": "ClusterasgOdASG83F38D7D" + "Ref": "EcsClusterasgOdASG0E5C30EC" } ] ] @@ -1049,7 +1049,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "ClusterEB0386A7", + "EcsCluster97242B84", "Arn" ] } @@ -1063,7 +1063,7 @@ "ArnEquals": { "ecs:cluster": { "Fn::GetAtt": [ - "ClusterEB0386A7", + "EcsCluster97242B84", "Arn" ] } @@ -1075,15 +1075,15 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicy8500B712", + "PolicyName": "EcsClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicyE54F1794", "Roles": [ { - "Ref": "ClusterasgOdDrainECSHookFunctionServiceRole819F43CE" + "Ref": "EcsClusterasgOdDrainECSHookFunctionServiceRoleFC088D55" } ] } }, - "ClusterasgOdDrainECSHookFunctionE33FC308": { + "EcsClusterasgOdDrainECSHookFunction962490E0": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { @@ -1092,7 +1092,7 @@ "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ - "ClusterasgOdDrainECSHookFunctionServiceRole819F43CE", + "EcsClusterasgOdDrainECSHookFunctionServiceRoleFC088D55", "Arn" ] }, @@ -1100,55 +1100,55 @@ "Environment": { "Variables": { "CLUSTER": { - "Ref": "ClusterEB0386A7" + "Ref": "EcsCluster97242B84" } } }, "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgOd" + "Value": "aws-ecs-integ-spot/EcsCluster/asgOd" } ], "Timeout": 310 }, "DependsOn": [ - "ClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicy8500B712", - "ClusterasgOdDrainECSHookFunctionServiceRole819F43CE" + "EcsClusterasgOdDrainECSHookFunctionServiceRoleDefaultPolicyE54F1794", + "EcsClusterasgOdDrainECSHookFunctionServiceRoleFC088D55" ] }, - "ClusterasgOdDrainECSHookFunctionAllowInvokeecsspottest2ClusterasgOdLifecycleHookDrainHookTopicA1B632A8E35EF0EC": { + "EcsClusterasgOdDrainECSHookFunctionAllowInvokeawsecsintegspotEcsClusterasgOdLifecycleHookDrainHookTopicB293D7D8B41B2D12": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { "Fn::GetAtt": [ - "ClusterasgOdDrainECSHookFunctionE33FC308", + "EcsClusterasgOdDrainECSHookFunction962490E0", "Arn" ] }, "Principal": "sns.amazonaws.com", "SourceArn": { - "Ref": "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1" + "Ref": "EcsClusterasgOdLifecycleHookDrainHookTopic673CE202" } } }, - "ClusterasgOdDrainECSHookFunctionTopic603DF471": { + "EcsClusterasgOdDrainECSHookFunctionTopicE6BE4000": { "Type": "AWS::SNS::Subscription", "Properties": { "Protocol": "lambda", "TopicArn": { - "Ref": "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1" + "Ref": "EcsClusterasgOdLifecycleHookDrainHookTopic673CE202" }, "Endpoint": { "Fn::GetAtt": [ - "ClusterasgOdDrainECSHookFunctionE33FC308", + "EcsClusterasgOdDrainECSHookFunction962490E0", "Arn" ] } } }, - "ClusterasgOdLifecycleHookDrainHookRole07F838A4": { + "EcsClusterasgOdLifecycleHookDrainHookRole695B2DF1": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -1166,12 +1166,12 @@ "Tags": [ { "Key": "Name", - "Value": "ecs-spot-test2/Cluster/asgOd" + "Value": "aws-ecs-integ-spot/EcsCluster/asgOd" } ] } }, - "ClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy037C17EB": { + "EcsClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy85FA949A": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -1180,45 +1180,45 @@ "Action": "sns:Publish", "Effect": "Allow", "Resource": { - "Ref": "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1" + "Ref": "EcsClusterasgOdLifecycleHookDrainHookTopic673CE202" } } ], "Version": "2012-10-17" }, - "PolicyName": "ClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy037C17EB", + "PolicyName": "EcsClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy85FA949A", "Roles": [ { - "Ref": "ClusterasgOdLifecycleHookDrainHookRole07F838A4" + "Ref": "EcsClusterasgOdLifecycleHookDrainHookRole695B2DF1" } ] } }, - "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1": { + "EcsClusterasgOdLifecycleHookDrainHookTopic673CE202": { "Type": "AWS::SNS::Topic" }, - "ClusterasgOdLifecycleHookDrainHookA099BA56": { + "EcsClusterasgOdLifecycleHookDrainHook03AC5A9E": { "Type": "AWS::AutoScaling::LifecycleHook", "Properties": { "AutoScalingGroupName": { - "Ref": "ClusterasgOdASG83F38D7D" + "Ref": "EcsClusterasgOdASG0E5C30EC" }, "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", "DefaultResult": "CONTINUE", "HeartbeatTimeout": 300, "NotificationTargetARN": { - "Ref": "ClusterasgOdLifecycleHookDrainHookTopicCDC252E1" + "Ref": "EcsClusterasgOdLifecycleHookDrainHookTopic673CE202" }, "RoleARN": { "Fn::GetAtt": [ - "ClusterasgOdLifecycleHookDrainHookRole07F838A4", + "EcsClusterasgOdLifecycleHookDrainHookRole695B2DF1", "Arn" ] } }, "DependsOn": [ - "ClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy037C17EB", - "ClusterasgOdLifecycleHookDrainHookRole07F838A4" + "EcsClusterasgOdLifecycleHookDrainHookRoleDefaultPolicy85FA949A", + "EcsClusterasgOdLifecycleHookDrainHookRole695B2DF1" ] }, "TaskTaskRoleE98524A1": { @@ -1256,7 +1256,7 @@ ] } ], - "Family": "ecsspottest2Task5842FD21", + "Family": "awsecsintegspotTask1789BE14", "NetworkMode": "bridge", "RequiresCompatibilities": [ "EC2" @@ -1276,7 +1276,7 @@ "Ref": "Task79114B6B" }, "Cluster": { - "Ref": "ClusterEB0386A7" + "Ref": "EcsCluster97242B84" }, "DeploymentConfiguration": { "MaximumPercent": 200, diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.ts index 91a1af3c4e70b..59acbb2bbd8c7 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.ts @@ -1,52 +1,44 @@ import ec2 = require('@aws-cdk/aws-ec2'); -import { App, Construct, Stack, StackProps } from '@aws-cdk/core'; +import cdk = require('@aws-cdk/core'); import ecs = require('../../lib'); -class TestStack extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); - - const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2 }); - - const cluster = new ecs.Cluster(this, 'Cluster', { - vpc - }); - - cluster.addCapacity('asgSpot', { - maxCapacity: 2, - minCapacity: 2, - desiredCapacity: 2, - instanceType: new ec2.InstanceType('c5.xlarge'), - spotPrice: '0.0735', - spotInstanceDraining: true, - }); - - cluster.addCapacity('asgOd', { - maxCapacity: 2, - minCapacity: 1, - desiredCapacity: 1, - instanceType: new ec2.InstanceType('t3.large'), - }); - - const taskDefinition = new ecs.TaskDefinition(this, 'Task', { - compatibility: ecs.Compatibility.EC2 - }); - - taskDefinition.addContainer('PHP', { - image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), - memoryLimitMiB: 512, - }).addPortMappings({ - containerPort: 80, - }); - - new ecs.Ec2Service(this, 'Service', { - cluster, - taskDefinition - }); - } - -} - -const app = new App(); -new TestStack(app, 'ecs-spot-test2'); +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ-spot'); + +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); + +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + +cluster.addCapacity('asgSpot', { + maxCapacity: 2, + minCapacity: 2, + desiredCapacity: 2, + instanceType: new ec2.InstanceType('c5.xlarge'), + spotPrice: '0.0735', + spotInstanceDraining: true, +}); + +cluster.addCapacity('asgOd', { + maxCapacity: 2, + minCapacity: 1, + desiredCapacity: 1, + instanceType: new ec2.InstanceType('t3.large'), +}); + +const taskDefinition = new ecs.TaskDefinition(stack, 'Task', { + compatibility: ecs.Compatibility.EC2 +}); + +taskDefinition.addContainer('PHP', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, +}).addPortMappings({ + containerPort: 80, +}); + +new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition +}); + app.synth(); From 0a9da210866090fbbbf0309f8c0c3d82b964f71c Mon Sep 17 00:00:00 2001 From: Pahud Date: Sat, 5 Oct 2019 11:03:37 +0800 Subject: [PATCH 4/8] update aws-ecs/README.md about Spot Instances. --- packages/@aws-cdk/aws-ecs/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 2f0dd17d7418a..a2ed2f2236a19 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -123,6 +123,31 @@ cluster.addAutoScalingGroup(autoScalingGroup); If you omit the property `vpc`, the construct will create a new VPC with two AZs. +## Spot Instances + +To add spot instances into the cluster, you must specify the `spotPrice` in the `ecs.AddCapacityOptions` and optionally enable the `spotInstanceDraining` property. + +```ts +// Add an AutoScalingGroup with spot instances to the existing cluster +cluster.addCapacity('AsgSpot', { + maxCapacity: 2, + minCapacity: 2, + desiredCapacity: 2, + instanceType: new ec2.InstanceType('c5.xlarge'), + spotPrice: '0.0735', + // Enable the Automated Spot Draining support for Amazon ECS + spotInstanceDraining: true, +}); + +// And optionally add another AutoScalingGroup with on-deamand instances to the same cluster +cluster.addCapacity('AsgOd', { + maxCapacity: 2, + minCapacity: 1, + desiredCapacity: 1, + instanceType: new ec2.InstanceType('t3.large'), +}); +``` + ## Task definitions A task Definition describes what a single copy of a **task** should look like. From b012003e09005d8ede2867e289145bd4ec6dbd61 Mon Sep 17 00:00:00 2001 From: Pahud Date: Sat, 5 Oct 2019 11:10:17 +0800 Subject: [PATCH 5/8] fix typo --- packages/@aws-cdk/aws-ecs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index a2ed2f2236a19..c4767fdcd2188 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -139,7 +139,7 @@ cluster.addCapacity('AsgSpot', { spotInstanceDraining: true, }); -// And optionally add another AutoScalingGroup with on-deamand instances to the same cluster +// And optionally add another AutoScalingGroup with on-demand instances to the same cluster cluster.addCapacity('AsgOd', { maxCapacity: 2, minCapacity: 1, From 45bef1da7735bc55ff50668e70c7c3c9349b6944 Mon Sep 17 00:00:00 2001 From: Pahud Date: Wed, 9 Oct 2019 22:03:57 +0800 Subject: [PATCH 6/8] * update README * add unit test * remove redundant descriptions --- packages/@aws-cdk/aws-ecs/README.md | 10 +-- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 7 +- .../@aws-cdk/aws-ecs/test/test.ecs-cluster.ts | 73 ++++++++++++++----- 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index b0b236d6e20e3..fbcd349cd36d4 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -123,7 +123,7 @@ cluster.addAutoScalingGroup(autoScalingGroup); If you omit the property `vpc`, the construct will create a new VPC with two AZs. -## Spot Instances +### Spot Instances To add spot instances into the cluster, you must specify the `spotPrice` in the `ecs.AddCapacityOptions` and optionally enable the `spotInstanceDraining` property. @@ -138,14 +138,6 @@ cluster.addCapacity('AsgSpot', { // Enable the Automated Spot Draining support for Amazon ECS spotInstanceDraining: true, }); - -// And optionally add another AutoScalingGroup with on-demand instances to the same cluster -cluster.addCapacity('AsgOd', { - maxCapacity: 2, - minCapacity: 1, - desiredCapacity: 1, - instanceType: new ec2.InstanceType('t3.large'), -}); ``` ## Task definitions diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index ce7371c787e81..a79aa87eb5b03 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -196,8 +196,6 @@ export class Cluster extends Resource implements ICluster { } if (options.spotInstanceDraining) { - // Automated Spot Instance Draining - // Source: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-spot.html autoScalingGroup.addUserData('echo ECS_ENABLE_SPOT_INSTANCE_DRAINING=true >> /etc/ecs/ecs.config'); } @@ -622,9 +620,8 @@ export interface AddAutoScalingGroupCapacityOptions { readonly taskDrainTime?: Duration; /** - * Automated Draining for Spot Instances running Amazon ECS Services - * - * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-spot.html + * Specify whether to enable Automated Draining for Spot Instances running Amazon ECS Services. + * For more information, see [Using Spot Instances](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-spot.html). * * @default false */ diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index 9ee77c1021011..8f604e84a60b7 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -10,7 +10,7 @@ export = { "When creating an ECS Cluster": { "with no properties set, it correctly sets default properties"(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); cluster.addCapacity('DefaultAutoScalingGroup', { @@ -110,7 +110,7 @@ export = { })); expect(stack).to(haveResource("AWS::IAM::Role", { - AssumeRolePolicyDocument: { + AssumeRolePolicyDocument: { Statement: [ { Action: "sts:AssumeRole", @@ -153,7 +153,7 @@ export = { "with only vpc set, it correctly sets default properties"(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc, @@ -256,7 +256,7 @@ export = { })); expect(stack).to(haveResource("AWS::IAM::Role", { - AssumeRolePolicyDocument: { + AssumeRolePolicyDocument: { Statement: [ { Action: "sts:AssumeRole", @@ -299,7 +299,7 @@ export = { "multiple clusters with default capacity"(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); // WHEN @@ -315,7 +315,7 @@ export = { 'lifecycle hook is automatically added'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc, @@ -333,7 +333,7 @@ export = { DefaultResult: "CONTINUE", HeartbeatTimeout: 300, NotificationTargetARN: { Ref: "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4" }, - RoleARN: { "Fn::GetAtt": [ "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B", "Arn" ] } + RoleARN: { "Fn::GetAtt": ["EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B", "Arn"] } })); expect(stack).to(haveResource('AWS::Lambda::Function', { @@ -444,7 +444,7 @@ export = { "with capacity and cloudmap namespace properties set"(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); new ecs.Cluster(stack, 'EcsCluster', { vpc, @@ -459,9 +459,9 @@ export = { // THEN expect(stack).to(haveResource("AWS::ServiceDiscovery::PrivateDnsNamespace", { Name: 'foo.com', - Vpc: { - Ref: 'MyVpcF9F0CA6F' - } + Vpc: { + Ref: 'MyVpcF9F0CA6F' + } })); expect(stack).to(haveResource("AWS::ECS::Cluster")); @@ -557,7 +557,7 @@ export = { })); expect(stack).to(haveResource("AWS::IAM::Role", { - AssumeRolePolicyDocument: { + AssumeRolePolicyDocument: { Statement: [ { Action: "sts:AssumeRole", @@ -803,7 +803,7 @@ export = { const stack = new cdk.Stack(); test.equal(ecs.EcsOptimizedImage.amazonLinux().getImage(stack).osType, - ec2.OperatingSystemType.LINUX); + ec2.OperatingSystemType.LINUX); test.done(); }, @@ -813,7 +813,7 @@ export = { const stack = new cdk.Stack(); test.equal(ecs.EcsOptimizedImage.amazonLinux2().getImage(stack).osType, - ec2.OperatingSystemType.LINUX); + ec2.OperatingSystemType.LINUX); test.done(); }, @@ -823,7 +823,7 @@ export = { const stack = new cdk.Stack(); test.equal(ecs.EcsOptimizedImage.windows(ecs.WindowsOptimizedVersion.SERVER_2019).getImage(stack).osType, - ec2.OperatingSystemType.WINDOWS); + ec2.OperatingSystemType.WINDOWS); test.done(); }, @@ -957,6 +957,39 @@ export = { test.done(); }, + "allows specifying automated spot draining"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('c5.xlarge'), + spotPrice: '0.0735', + spotInstanceDraining: true + }); + + // THEN + expect(stack).to(haveResource("AWS::AutoScaling::LaunchConfiguration", { + UserData: { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + Ref: "EcsCluster97242B84" + }, + " >> /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\necho ECS_ENABLE_SPOT_INSTANCE_DRAINING=true >> /etc/ecs/ecs.config" + ] + ] + } + } + })); + + test.done(); + }, + "allows containers access to instance metadata service"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -1006,10 +1039,10 @@ export = { // THEN expect(stack).to(haveResource("AWS::ServiceDiscovery::PrivateDnsNamespace", { - Name: 'foo.com', - Vpc: { - Ref: 'MyVpcF9F0CA6F' - } + Name: 'foo.com', + Vpc: { + Ref: 'MyVpcF9F0CA6F' + } })); test.done(); @@ -1033,7 +1066,7 @@ export = { // THEN expect(stack).to(haveResource("AWS::ServiceDiscovery::PublicDnsNamespace", { - Name: 'foo.com', + Name: 'foo.com', })); test.equal(cluster.defaultCloudMapNamespace!.type, cloudmap.NamespaceType.DNS_PUBLIC); From df5092de2d1fc37c8ea6b3d3d0daf6e57251094b Mon Sep 17 00:00:00 2001 From: Pahud Date: Wed, 9 Oct 2019 22:28:28 +0800 Subject: [PATCH 7/8] fix trailing whitespace --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index a79aa87eb5b03..3f25e6faabba5 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -620,7 +620,7 @@ export interface AddAutoScalingGroupCapacityOptions { readonly taskDrainTime?: Duration; /** - * Specify whether to enable Automated Draining for Spot Instances running Amazon ECS Services. + * Specify whether to enable Automated Draining for Spot Instances running Amazon ECS Services. * For more information, see [Using Spot Instances](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-spot.html). * * @default false From ae46f3c3f25c66258a1d92baab29c93044140e23 Mon Sep 17 00:00:00 2001 From: Pahud Date: Thu, 10 Oct 2019 01:08:57 +0800 Subject: [PATCH 8/8] - verify ASG has spot capacity only --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 3f25e6faabba5..ad59fa21c1ec3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -195,7 +195,7 @@ export class Cluster extends Resource implements ICluster { autoScalingGroup.addUserData('echo ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config'); } - if (options.spotInstanceDraining) { + if (autoScalingGroup.spotPrice && options.spotInstanceDraining) { autoScalingGroup.addUserData('echo ECS_ENABLE_SPOT_INSTANCE_DRAINING=true >> /etc/ecs/ecs.config'); }