From 68e1e85429a63a1ef9b589773b8b03b0676f53ad Mon Sep 17 00:00:00 2001 From: Penghao He Date: Wed, 2 Oct 2019 13:32:28 -0700 Subject: [PATCH] feat(ecs): add support for ProxyConfiguration in ECS TaskDefinition (#4007) * Add proxyConfiguration support * Fix test case * Improve the user experience when setting proxy configuration * Add base proxy configuration class and an app mesh proxy configuration class * Add generic proxy configuration class * Remove generic proxy configuration and minor changes * Add integ test, reword error message * Fix integ test and unit test * Update with support for empty string for egressIgnoredIPs/Ports * Refactoring bind method * Minor refactoring to renderProperties method --- .../aws-ecs/lib/base/task-definition.ts | 9 + packages/@aws-cdk/aws-ecs/lib/index.ts | 4 + .../app-mesh-proxy-configuration.ts | 112 +++ .../proxy-configuration.ts | 13 + .../proxy-configurations.ts | 14 + .../integ.app-mesh-proxy-config.expected.json | 922 ++++++++++++++++++ .../test/ec2/integ.app-mesh-proxy-config.ts | 43 + .../test/ec2/test.ec2-task-definition.ts | 3 +- .../test/test.app-mesh-proxy-configuration.ts | 195 ++++ 9 files changed, 1313 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/lib/proxy-configuration/app-mesh-proxy-configuration.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/proxy-configuration/proxy-configuration.ts create mode 100644 packages/@aws-cdk/aws-ecs/lib/proxy-configuration/proxy-configurations.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json create mode 100644 packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.ts create mode 100644 packages/@aws-cdk/aws-ecs/test/test.app-mesh-proxy-configuration.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 8c536b6a7c80d..9a1ed5b01bbea 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -4,6 +4,7 @@ import { Construct, IResource, Lazy, Resource } from '@aws-cdk/core'; import { ContainerDefinition, ContainerDefinitionOptions, PortMapping, Protocol } from '../container-definition'; import { CfnTaskDefinition } from '../ecs.generated'; import { PlacementConstraint } from '../placement'; +import { ProxyConfiguration } from '../proxy-configuration/proxy-configuration'; /** * The interface for all task definitions. @@ -64,6 +65,13 @@ export interface CommonTaskDefinitionProps { */ readonly taskRole?: iam.IRole; + /** + * The configuration details for the App Mesh proxy. + * + * @default - No proxy configuration. + */ + readonly proxyConfiguration?: ProxyConfiguration; + /** * The list of volume definitions for the task. For more information, see * [Task Definition Parameter Volumes](https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes). @@ -279,6 +287,7 @@ export class TaskDefinition extends TaskDefinitionBase { placementConstraints: Lazy.anyValue({ produce: () => !isFargateCompatible(this.compatibility) ? this.placementConstraints : undefined }, { omitEmptyArray: true }), + proxyConfiguration: props.proxyConfiguration ? props.proxyConfiguration.bind(this.stack, this) : undefined, cpu: props.cpu, memory: props.memoryMiB, }); diff --git a/packages/@aws-cdk/aws-ecs/lib/index.ts b/packages/@aws-cdk/aws-ecs/lib/index.ts index 5acd047c326fd..9164f0278603e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/index.ts +++ b/packages/@aws-cdk/aws-ecs/lib/index.ts @@ -30,6 +30,10 @@ export * from './log-drivers/syslog-log-driver'; export * from './log-drivers/log-driver'; export * from './log-drivers/log-drivers'; +export * from './proxy-configuration/app-mesh-proxy-configuration'; +export * from './proxy-configuration/proxy-configuration'; +export * from './proxy-configuration/proxy-configurations'; + // AWS::ECS CloudFormation Resources: // export * from './ecs.generated'; diff --git a/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/app-mesh-proxy-configuration.ts b/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/app-mesh-proxy-configuration.ts new file mode 100644 index 0000000000000..b03ac12e3e335 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/app-mesh-proxy-configuration.ts @@ -0,0 +1,112 @@ +import { Construct } from '@aws-cdk/core'; +import { TaskDefinition } from '../base/task-definition'; +import { CfnTaskDefinition } from '../ecs.generated'; +import { ProxyConfiguration } from './proxy-configuration'; + +/** + * Interface for setting the properties of proxy configuration. + */ +export interface AppMeshProxyConfigurationProps { + /** + * The user ID (UID) of the proxy container as defined by the user parameter in a container definition. + * This is used to ensure the proxy ignores its own traffic. If IgnoredGID is specified, this field can be empty. + */ + readonly ignoredUID?: number; + + /** + * The group ID (GID) of the proxy container as defined by the user parameter in a container definition. + * This is used to ensure the proxy ignores its own traffic. If IgnoredUID is specified, this field can be empty. + */ + readonly ignoredGID?: number; + + /** + * The list of ports that the application uses. + * Network traffic to these ports is forwarded to the ProxyIngressPort and ProxyEgressPort. + */ + readonly appPorts: number[]; + + /** + * Specifies the port that incoming traffic to the AppPorts is directed to. + */ + readonly proxyIngressPort: number; + + /** + * Specifies the port that outgoing traffic from the AppPorts is directed to. + */ + readonly proxyEgressPort: number; + + /** + * The egress traffic going to these specified ports is ignored and not redirected to the ProxyEgressPort. It can be an empty list. + */ + readonly egressIgnoredPorts?: number[]; + + /** + * The egress traffic going to these specified IP addresses is ignored and not redirected to the ProxyEgressPort. It can be an empty list. + */ + readonly egressIgnoredIPs?: string[]; +} + +/** + * The configuration to use when setting an App Mesh proxy configuration. + */ +export interface AppMeshProxyConfigurationConfigProps { + /** + * The name of the container that will serve as the App Mesh proxy. + */ + readonly containerName: string; + + /** + * The set of network configuration parameters to provide the Container Network Interface (CNI) plugin. + */ + readonly properties: AppMeshProxyConfigurationProps; +} + +/** + * The class for App Mesh proxy configurations. + * + * For tasks using the EC2 launch type, the container instances require at least version 1.26.0 of the container agent and at least version + * 1.26.0-1 of the ecs-init package to enable a proxy configuration. If your container instances are launched from the Amazon ECS-optimized + * AMI version 20190301 or later, then they contain the required versions of the container agent and ecs-init. + * For more information, see [Amazon ECS-optimized AMIs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html). + * + * For tasks using the Fargate launch type, the task or service requires platform version 1.3.0 or later. + */ +export class AppMeshProxyConfiguration extends ProxyConfiguration { + /** + * Constructs a new instance of the AppMeshProxyConfiguration class. + */ + constructor(private readonly props: AppMeshProxyConfigurationConfigProps) { + super(); + if (props.properties) { + if (!props.properties.ignoredUID && !props.properties.ignoredGID) { + throw new Error("At least one of ignoredUID or ignoredGID should be specified."); + } + } + } + + /** + * Called when the proxy configuration is configured on a task definition. + */ + public bind(_scope: Construct, _taskDefinition: TaskDefinition): CfnTaskDefinition.ProxyConfigurationProperty { + const configProps = this.props.properties; + const configType = "APPMESH"; + return { + containerName: this.props.containerName, + proxyConfigurationProperties: renderProperties(configProps), + type: configType + }; + } +} + +function renderProperties(props: AppMeshProxyConfigurationProps): CfnTaskDefinition.KeyValuePairProperty[] { + const ret = []; + for (const [k, v] of Object.entries(props)) { + const key = String(k); + const value = String(v); + if (value !== "undefined" && value !== "") { + const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1); + ret.push({ ["name"]: capitalizedKey, ["value"]: value }); + } + } + return ret; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/proxy-configuration.ts b/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/proxy-configuration.ts new file mode 100644 index 0000000000000..dfd8375d8d382 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/proxy-configuration.ts @@ -0,0 +1,13 @@ +import { Construct } from '@aws-cdk/core'; +import { TaskDefinition } from '../base/task-definition'; +import { CfnTaskDefinition } from '../ecs.generated'; + +/** + * The base class for proxy configurations. + */ +export abstract class ProxyConfiguration { + /** + * Called when the proxy configuration is configured on a task definition. + */ + public abstract bind(_scope: Construct, _taskDefinition: TaskDefinition): CfnTaskDefinition.ProxyConfigurationProperty; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/proxy-configurations.ts b/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/proxy-configurations.ts new file mode 100644 index 0000000000000..91a5a479f2b9a --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/proxy-configuration/proxy-configurations.ts @@ -0,0 +1,14 @@ +import { AppMeshProxyConfiguration, AppMeshProxyConfigurationConfigProps } from './app-mesh-proxy-configuration'; +import { ProxyConfiguration } from './proxy-configuration'; + +/** + * The base class for proxy configurations. + */ +export class ProxyConfigurations { + /** + * Constructs a new instance of the ProxyConfiguration class. + */ + public static appMeshProxyConfiguration(props: AppMeshProxyConfigurationConfigProps): ProxyConfiguration { + return new AppMeshProxyConfiguration(props); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json new file mode 100644 index 0000000000000..3543b8e7b1f84 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json @@ -0,0 +1,922 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/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": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "EcsCluster97242B84": { + "Type": "AWS::ECS::Cluster" + }, + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80": { + "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": "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t2.micro", + "IamInstanceProfile": { + "Ref": "EcsClusterDefaultAutoScalingGroupInstanceProfile2CE606B3" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupInstanceSecurityGroup912E1231", + "GroupId" + ] + } + ], + "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" + ] + ] + } + } + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupInstanceRoleDefaultPolicy04DC6C80", + "EcsClusterDefaultAutoScalingGroupInstanceRole3C026863" + ] + }, + "EcsClusterDefaultAutoScalingGroupASGC1A785DB": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "EcsClusterDefaultAutoScalingGroupLaunchConfigB7E376C1" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396": { + "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": "EcsClusterDefaultAutoScalingGroupASGC1A785DB" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EcsCluster97242B84", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E": { + "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": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA", + "Arn" + ] + }, + "Runtime": "python3.6", + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "EcsCluster97242B84" + } + } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRoleDefaultPolicyA45BF396", + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA" + ] + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionAllowInvokeawsecsintegEcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopic7A89925AFDCBEE50": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4" + } + } + }, + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionTopic8F34E394": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4" + }, + "Endpoint": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionE17A5F5E", + "Arn" + ] + } + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "autoscaling.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsCluster/DefaultAutoScalingGroup" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88", + "Roles": [ + { + "Ref": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B" + } + ] + } + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4": { + "Type": "AWS::SNS::Topic" + }, + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookFFA63029": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "EcsClusterDefaultAutoScalingGroupASGC1A785DB" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4" + }, + "RoleARN": { + "Fn::GetAtt": [ + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B", + "Arn" + ] + } + }, + "DependsOn": [ + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleDefaultPolicy75002F88", + "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B" + ] + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "Memory": 256, + "Name": "web" + }, + { + "Essential": true, + "Image": "envoyproxy/envoy", + "Memory": 256, + "Name": "envoy" + } + ], + "Family": "awsecsintegTaskDef6FDFB69A", + "NetworkMode": "awsvpc", + "ProxyConfiguration": { + "ContainerName": "envoy", + "ProxyConfigurationProperties": [ + { + "Name": "IgnoredUID", + "Value": "1337" + }, + { + "Name": "ProxyIngressPort", + "Value": "15000" + }, + { + "Name": "ProxyEgressPort", + "Value": "15001" + }, + { + "Name": "AppPorts", + "Value": "9080,9081" + }, + { + "Name": "EgressIgnoredIPs", + "Value": "169.254.170.2,169.254.169.254" + } + ], + "Type": "APPMESH" + }, + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "ServiceD69D759B": { + "Type": "AWS::ECS::Service", + "Properties": { + "TaskDefinition": { + "Ref": "TaskDef54694570" + }, + "Cluster": { + "Ref": "EcsCluster97242B84" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "LaunchType": "EC2", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ServiceSecurityGroupC96ED6A7", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + }, + "SchedulingStrategy": "REPLICA" + } + }, + "ServiceSecurityGroupC96ED6A7": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/Service/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + } + }, + "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.app-mesh-proxy-config.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.ts new file mode 100644 index 0000000000000..0477622a74503 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.ts @@ -0,0 +1,43 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/core'); +import ecs = require('../../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); + +// Create a cluster +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); + +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); +cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), +}); + +const prox = ecs.ProxyConfigurations.appMeshProxyConfiguration({ + containerName: 'envoy', + properties: { + ignoredUID: 1337, + proxyIngressPort: 15000, + proxyEgressPort: 15001, + appPorts: [9080, 9081], + egressIgnoredIPs: ["169.254.170.2", "169.254.169.254"] + } +}); +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.AWS_VPC, proxyConfiguration: prox }); + +taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 256, +}); + +taskDefinition.addContainer('envoy', { + image: ecs.ContainerImage.fromRegistry("envoyproxy/envoy"), + memoryLimitMiB: 256, +}); + +new ecs.Ec2Service(stack, "Service", { + cluster, + taskDefinition +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index 0b75d1e8dd6e5..4743c10ed2ad6 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -1030,7 +1030,6 @@ export = { })); test.done(); - }, - + } } }; diff --git a/packages/@aws-cdk/aws-ecs/test/test.app-mesh-proxy-configuration.ts b/packages/@aws-cdk/aws-ecs/test/test.app-mesh-proxy-configuration.ts new file mode 100644 index 0000000000000..a7f405be63a12 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/test.app-mesh-proxy-configuration.ts @@ -0,0 +1,195 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/core'); +import { Test } from 'nodeunit'; +import ecs = require('../lib'); + +export = { + "correctly sets all appMeshProxyConfiguration"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { proxyConfiguration: ecs.ProxyConfigurations.appMeshProxyConfiguration({ + containerName: "envoy", + properties: { + ignoredUID: 1337, + ignoredGID: 1338, + appPorts: [80, 81], + proxyIngressPort: 80, + proxyEgressPort: 81, + egressIgnoredPorts: [8081], + egressIgnoredIPs: ["169.254.170.2", "169.254.169.254"], + } + })}); + taskDefinition.addContainer("web", { + memoryLimitMiB: 1024, + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample") + }); + taskDefinition.addContainer("envoy", { + memoryLimitMiB: 1024, + image: ecs.ContainerImage.fromRegistry("envoyproxy/envoy") + }); + + // THEN + expect(stack).to(haveResourceLike("AWS::ECS::TaskDefinition", { + ProxyConfiguration: { + ContainerName: "envoy", + ProxyConfigurationProperties: [ + { + Name: "IgnoredUID", + Value: "1337" + }, + { + Name: "IgnoredGID", + Value: "1338" + }, + { + Name: "AppPorts", + Value: "80,81" + }, + { + Name: "ProxyIngressPort", + Value: "80" + }, + { + Name: "ProxyEgressPort", + Value: "81" + }, + { + Name: "EgressIgnoredPorts", + Value: "8081" + }, + { + Name: "EgressIgnoredIPs", + Value: "169.254.170.2,169.254.169.254" + } + ], + Type: "APPMESH" + } + })); + test.done(); + }, + + "correctly sets appMeshProxyConfiguration with default properties set"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { proxyConfiguration: ecs.ProxyConfigurations.appMeshProxyConfiguration({ + containerName: "envoy", + properties: { + ignoredUID: 1337, + appPorts: [80, 81], + proxyIngressPort: 80, + proxyEgressPort: 81 + } + })}); + taskDefinition.addContainer("web", { + memoryLimitMiB: 1024, + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample") + }); + taskDefinition.addContainer("envoy", { + memoryLimitMiB: 1024, + image: ecs.ContainerImage.fromRegistry("envoyproxy/envoy") + }); + + // THEN + expect(stack).to(haveResourceLike("AWS::ECS::TaskDefinition", { + ProxyConfiguration: { + ContainerName: "envoy", + ProxyConfigurationProperties: [ + { + Name: "IgnoredUID", + Value: "1337" + }, + { + Name: "AppPorts", + Value: "80,81" + }, + { + Name: "ProxyIngressPort", + Value: "80" + }, + { + Name: "ProxyEgressPort", + Value: "81" + } + ], + Type: "APPMESH" + } + })); + test.done(); + }, + + "correctly sets appMeshProxyConfiguration with empty egressIgnoredPorts and egressIgnoredIPs"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { proxyConfiguration: ecs.ProxyConfigurations.appMeshProxyConfiguration({ + containerName: "envoy", + properties: { + ignoredUID: 1337, + appPorts: [80, 81], + proxyIngressPort: 80, + proxyEgressPort: 81, + egressIgnoredIPs: [], + egressIgnoredPorts: [] + } + })}); + taskDefinition.addContainer("web", { + memoryLimitMiB: 1024, + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample") + }); + taskDefinition.addContainer("envoy", { + memoryLimitMiB: 1024, + image: ecs.ContainerImage.fromRegistry("envoyproxy/envoy") + }); + + // THEN + expect(stack).to(haveResourceLike("AWS::ECS::TaskDefinition", { + ProxyConfiguration: { + ContainerName: "envoy", + ProxyConfigurationProperties: [ + { + Name: "IgnoredUID", + Value: "1337" + }, + { + Name: "AppPorts", + Value: "80,81" + }, + { + Name: "ProxyIngressPort", + Value: "80" + }, + { + Name: "ProxyEgressPort", + Value: "81" + } + ], + Type: "APPMESH" + } + })); + test.done(); + }, + + "throws when neither of IgnoredUID and IgnoredGID is set"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + test.throws(() => { + new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { proxyConfiguration: ecs.ProxyConfigurations.appMeshProxyConfiguration({ + containerName: "envoy", + properties: { + appPorts: [80, 81], + proxyIngressPort: 80, + proxyEgressPort: 81 + } + })}); + }, /At least one of ignoredUID or ignoredGID should be specified./); + + test.done(); + } +}; \ No newline at end of file