diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 0db4487a8f436..85071e61976e9 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -179,7 +179,7 @@ export abstract class RepositoryBase extends Resource implements IRepository { grantee, actions, resourceArns: [this.repositoryArn], - resourceSelfArns: ['*'], + resourceSelfArns: [], resource: this, }); } diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index fa15f68818df8..d3fc49569fc07 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -377,7 +377,6 @@ export = { ], "Effect": "Allow", "Principal": "*", - "Resource": "*", } ], "Version": "2012-10-17" diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/load-balanced-service-base.ts index 557630debefc5..f02b6c9a6ae5d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/load-balanced-service-base.ts @@ -18,49 +18,56 @@ export enum LoadBalancerType { */ export interface LoadBalancedServiceBaseProps { /** - * The cluster where your service will be deployed - * You can only specify either vpc or cluster. Alternatively, you can leave both blank + * The name of the cluster that hosts the service. * - * @default - create a new cluster; if you do not specify a cluster nor a vpc, a new VPC will be created for you as well + * You can only specify either vpc or cluster. Alternatively, you can leave both blank. + * @default - create a new cluster; if you do not specify a cluster nor a vpc, a new VPC will be created for you as well. */ readonly cluster?: ICluster; /** - * VPC that the cluster instances or tasks are running in - * You can only specify either vpc or cluster. Alternatively, you can leave both blank + * The VPC where the ECS instances will be running or the ENIs will be deployed. * - * @default - use vpc of cluster or create a new one + * You can only specify either vpc or cluster. Alternatively, you can leave both blank. + * @default - uses the vpc defined in the cluster or creates a new one. */ readonly vpc?: IVpc; /** - * The image to start. + * The image used to start a container. */ readonly image: ContainerImage; /** - * The container port of the application load balancer attached to your Fargate service. Corresponds to container port mapping. + * The port number on the container that is bound to the user-specified or automatically assigned host port. + * + * If you are using containers in a task with the awsvpc or host network mode, exposed ports should be specified using containerPort. + * If you are using containers in a task with the bridge network mode and you specify a container port and not a host port, + * your container automatically receives a host port in the ephemeral port range. + * + * For more information, see hostPort. + * Port mappings that are automatically assigned in this way do not count toward the 100 reserved ports limit of a container instance. * * @default 80 */ readonly containerPort?: number; /** - * Determines whether the Application Load Balancer will be internet-facing + * Determines whether the Load Balancer will be internet-facing. * * @default true */ readonly publicLoadBalancer?: boolean; /** - * Number of desired copies of running tasks + * The desired number of instantiations of the task definition to keep running on the service. * * @default 1 */ readonly desiredCount?: number; /** - * Whether to create an application load balancer or a network load balancer + * The type of the load balancer to be used. * * @default application */ @@ -75,28 +82,28 @@ export interface LoadBalancedServiceBaseProps { readonly certificate?: ICertificate; /** - * Environment variables to pass to the container + * The environment variables to pass to the container. * * @default - No environment variables. */ readonly environment?: { [key: string]: string }; /** - * Secret environment variables to pass to the container + * The secret environment variables to pass to the container * * @default - No secret environment variables. */ readonly secrets?: { [key: string]: Secret }; /** - * Whether to create an AWS log driver + * Flag to indicate whether to enable logging. * * @default true */ readonly enableLogging?: boolean; /** - * Determines whether your Fargate Service will be assigned a public IP address. + * Determines whether the Service will be assigned a public IP address. * * @default false */ @@ -124,23 +131,23 @@ export interface LoadBalancedServiceBaseProps { readonly executionRole?: IRole; /** - * Override for the Fargate Task Definition task role + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. * - * @default - No value + * @default - A task role is automatically created for you. */ readonly taskRole?: IRole; /** - * Override value for the container name + * The container name value to be specified in the task definition. * - * @default - No value + * @default - none */ readonly containerName?: string; /** - * Override value for the service name + * The name of the service. * - * @default CloudFormation-generated name + * @default - CloudFormation-generated name. */ readonly serviceName?: string; @@ -166,7 +173,9 @@ export interface LoadBalancedServiceBaseProps { */ export abstract class LoadBalancedServiceBase extends cdk.Construct { public readonly assignPublicIp: boolean; - + /** + * The desired number of instantiations of the task definition to keep running on the service. + */ public readonly desiredCount: number; public readonly loadBalancerType: LoadBalancerType; @@ -176,7 +185,9 @@ export abstract class LoadBalancedServiceBase extends cdk.Construct { public readonly listener: ApplicationListener | NetworkListener; public readonly targetGroup: ApplicationTargetGroup | NetworkTargetGroup; - + /** + * The cluster that hosts the service. + */ public readonly cluster: ICluster; public readonly logDriver?: LogDriver; diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index efaeaf0373e85..2006243311d99 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -9,27 +9,23 @@ import { CfnOutput, Construct, Stack } from '@aws-cdk/core'; */ export interface QueueProcessingServiceBaseProps { /** - * The cluster where your service will be deployed - * You can only specify either vpc or cluster. Alternatively, you can leave both blank + * The name of the cluster that hosts the service. * - * @default - create a new cluster; if you do not specify a cluster nor a vpc, a new VPC will be created for you as well + * You can only specify either vpc or cluster. Alternatively, you can leave both blank. + * @default - create a new cluster; if you do not specify a cluster nor a vpc, a new VPC will be created for you as well. */ readonly cluster?: ICluster; /** - * VPC that the cluster instances or tasks are running in - * You can only specify either vpc or cluster. Alternatively, you can leave both blank + * The VPC where the ECS instances will be running or the ENIs will be deployed. * - * @default - use vpc of cluster or create a new one + * You can only specify either vpc or cluster. Alternatively, you can leave both blank. + * @default - uses the vpc defined in the cluster or creates a new one. */ readonly vpc?: IVpc; /** * The image used to start a container. - * - * This string is passed directly to the Docker daemon. - * Images in the Docker Hub registry are available by default. - * Other repositories are specified with either repository-url/image:tag or repository-url/image@digest. */ readonly image: ContainerImage; @@ -50,7 +46,7 @@ export interface QueueProcessingServiceBaseProps { readonly desiredTaskCount?: number; /** - * Flag to indicate whether to enable logging + * Flag to indicate whether to enable logging. * * @default true */ @@ -67,7 +63,7 @@ export interface QueueProcessingServiceBaseProps { readonly environment?: { [key: string]: string }; /** - * Secret environment variables to pass to the container + * The secret environment variables to pass to the container. * * @default - No secret environment variables. */ @@ -131,22 +127,22 @@ export abstract class QueueProcessingServiceBase extends Construct { public readonly environment: { [key: string]: string }; /** - * Secret environment variables + * The secret environment variables. */ public readonly secrets?: { [key: string]: Secret }; /** - * The minimum number of tasks to run + * The minimum number of tasks to run. */ public readonly desiredCount: number; /** - * The maximum number of instances for autoscaling to scale up to + * The maximum number of instances for autoscaling to scale up to. */ public readonly maxCapacity: number; /** - * The scaling interval for autoscaling based off an SQS Queue size + * The scaling interval for autoscaling based off an SQS Queue size. */ public readonly scalingSteps: ScalingInterval[]; /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts index f1ada22e17382..ea9f8632ad962 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts @@ -1,8 +1,9 @@ import { Schedule } from "@aws-cdk/aws-applicationautoscaling"; -import { AwsLogDriver, ContainerImage, ICluster, LogDriver, Secret, TaskDefinition } from "@aws-cdk/aws-ecs"; +import { IVpc } from '@aws-cdk/aws-ec2'; +import { AwsLogDriver, Cluster, ContainerImage, ICluster, LogDriver, Secret, TaskDefinition } from "@aws-cdk/aws-ecs"; import { Rule } from "@aws-cdk/aws-events"; import { EcsTask } from "@aws-cdk/aws-events-targets"; -import { Construct } from "@aws-cdk/core"; +import { Construct, Stack } from "@aws-cdk/core"; /** * The properties for the base ScheduledEc2Task or ScheduledFargateTask task. @@ -10,15 +11,22 @@ import { Construct } from "@aws-cdk/core"; export interface ScheduledTaskBaseProps { /** * The name of the cluster that hosts the service. + * + * You can only specify either vpc or cluster. Alternatively, you can leave both blank. + * @default - create a new cluster; if you do not specify a cluster nor a vpc, a new VPC will be created for you as well. */ - readonly cluster: ICluster; + readonly cluster?: ICluster; /** - * The image used to start a container. + * The VPC where the ECS instances will be running or the ENIs will be deployed. * - * This string is passed directly to the Docker daemon. - * Images in the Docker Hub registry are available by default. - * Other repositories are specified with either repository-url/image:tag or repository-url/image@digest. + * You can only specify either vpc or cluster. Alternatively, you can leave both blank. + * @default - uses the vpc defined in the cluster or creates a new one. + */ + readonly vpc?: IVpc; + + /** + * The image used to start a container. */ readonly image: ContainerImage; @@ -55,7 +63,7 @@ export interface ScheduledTaskBaseProps { readonly environment?: { [key: string]: string }; /** - * Secret environment variables to pass to the container + * The secret environment variables to pass to the container * * @default - No secret environment variables. */ @@ -93,7 +101,7 @@ export abstract class ScheduledTaskBase extends Construct { constructor(scope: Construct, id: string, props: ScheduledTaskBaseProps) { super(scope, id); - this.cluster = props.cluster; + this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); this.desiredTaskCount = props.desiredTaskCount || 1; // An EventRule that describes the event trigger (in this case a scheduled run) @@ -124,6 +132,13 @@ export abstract class ScheduledTaskBase extends Construct { return eventRuleTarget; } + protected getDefaultCluster(scope: Construct, vpc?: IVpc): Cluster { + // magic string to avoid collision with user-defined constructs + const DEFAULT_CLUSTER_ID = `EcsDefaultClusterMnL3mNNYN${vpc ? vpc.node.id : ''}`; + const stack = Stack.of(scope); + return stack.node.tryFindChild(DEFAULT_CLUSTER_ID) as Cluster || new Cluster(stack, DEFAULT_CLUSTER_ID, { vpc }); + } + /** * Create an AWS Log Driver with the provided streamPrefix * diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/load-balanced-ecs-service.ts index 93415db7ebe66..6647554504e95 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/load-balanced-ecs-service.ts @@ -10,10 +10,15 @@ export interface LoadBalancedEc2ServiceProps extends LoadBalancedServiceBaseProp /** * The number of cpu units used by the task. * Valid values, which determines your range of valid values for the memory parameter: + * * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments * * This default is set in the underlying FargateTaskDefinition construct. @@ -49,7 +54,7 @@ export interface LoadBalancedEc2ServiceProps extends LoadBalancedServiceBaseProp } /** - * A single task running on an ECS cluster fronted by a load balancer + * An EC2 service running on an ECS cluster fronted by a load balancer. */ export class LoadBalancedEc2Service extends LoadBalancedServiceBase { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index a455abe0f855d..2002b37fd5949 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -7,9 +7,22 @@ import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../ */ export interface QueueProcessingEc2ServiceProps extends QueueProcessingServiceBaseProps { /** - * The minimum number of CPU units to reserve for the container. + * The number of cpu units used by the task. + * Valid values, which determines your range of valid values for the memory parameter: * - * @default - No minimum CPU units reserved. + * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * + * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * + * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * + * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * + * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments + * + * This default is set in the underlying FargateTaskDefinition construct. + * + * @default none */ readonly cpu?: number; @@ -41,7 +54,7 @@ export interface QueueProcessingEc2ServiceProps extends QueueProcessingServiceBa } /** - * Class to create a queue processing Ec2 service + * Class to create a queue processing EC2 service. */ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts index b5365616cbdd5..feeb5ee43740b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts @@ -41,7 +41,7 @@ export interface ScheduledEc2TaskProps extends ScheduledTaskBaseProps { } /** - * A scheduled Ec2 task that will be initiated off of cloudwatch events. + * A scheduled EC2 task that will be initiated off of cloudwatch events. */ export class ScheduledEc2Task extends ScheduledTaskBase { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts index a7b80e888976f..57af79c5e39ae 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/load-balanced-fargate-service.ts @@ -8,11 +8,17 @@ import { LoadBalancedServiceBase, LoadBalancedServiceBaseProps } from '../base/l export interface LoadBalancedFargateServiceProps extends LoadBalancedServiceBaseProps { /** * The number of cpu units used by the task. + * * Valid values, which determines your range of valid values for the memory parameter: + * * 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB + * * 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB + * * 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB + * * 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments + * * 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments * * This default is set in the underlying FargateTaskDefinition construct. @@ -27,15 +33,15 @@ export interface LoadBalancedFargateServiceProps extends LoadBalancedServiceBase * This field is required and you must use one of the following values, which determines your range of valid values * for the cpu parameter: * - * 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU) + * 512 (0.5 GB), 1024 (1 GB), 2048 (2 GB) - Available cpu values: 256 (.25 vCPU) * - * 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU) + * 1024 (1 GB), 2048 (2 GB), 3072 (3 GB), 4096 (4 GB) - Available cpu values: 512 (.5 vCPU) * - * 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU) + * 2048 (2 GB), 3072 (3 GB), 4096 (4 GB), 5120 (5 GB), 6144 (6 GB), 7168 (7 GB), 8192 (8 GB) - Available cpu values: 1024 (1 vCPU) * - * Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU) + * Between 4096 (4 GB) and 16384 (16 GB) in increments of 1024 (1 GB) - Available cpu values: 2048 (2 vCPU) * - * Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU) + * Between 8192 (8 GB) and 30720 (30 GB) in increments of 1024 (1 GB) - Available cpu values: 4096 (4 vCPU) * * This default is set in the underlying FargateTaskDefinition construct. * @@ -45,12 +51,12 @@ export interface LoadBalancedFargateServiceProps extends LoadBalancedServiceBase } /** - * A Fargate service running on an ECS cluster fronted by a load balancer + * A Fargate service running on an ECS cluster fronted by a load balancer. */ export class LoadBalancedFargateService extends LoadBalancedServiceBase { /** - * The Fargate service in this construct + * The Fargate service in this construct. */ public readonly service: FargateService; diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index bee0698be7f63..11dc3eb1abdc7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -323,6 +323,11 @@ export class ContainerDefinition extends cdk.Construct { */ constructor(scope: cdk.Construct, id: string, private readonly props: ContainerDefinitionProps) { super(scope, id); + if (props.memoryLimitMiB !== undefined && props.memoryReservationMiB !== undefined) { + if (props.memoryLimitMiB < props.memoryReservationMiB) { + throw new Error(`MemoryLimitMiB should not be less than MemoryReservationMiB.`); + } + } this.essential = props.essential !== undefined ? props.essential : true; this.taskDefinition = props.taskDefinition; this.memoryLimitSpecified = props.memoryLimitMiB !== undefined || props.memoryReservationMiB !== undefined; diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 96cf8e57b5bfd..49615cd008f33 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -190,7 +190,7 @@ export class Ec2Service extends BaseService implements IEc2Service, elb.ILoadBal throw new Error("Cannot use a Classic Load Balancer if NetworkMode is Bridge. Use Host or AwsVpc instead."); } if (this.taskDefinition.networkMode === NetworkMode.NONE) { - throw new Error("Cannot use a load balancer if NetworkMode is None. Use Host or AwsVpc instead."); + throw new Error("Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or AwsVpc instead."); } this.loadBalancers.push({ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index ed504a74f0cef..eb59e9f20bc91 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -1,6 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import elb = require('@aws-cdk/aws-elasticloadbalancing'); +import elbv2 = require("@aws-cdk/aws-elasticloadbalancingv2"); import cloudmap = require('@aws-cdk/aws-servicediscovery'); import cdk = require('@aws-cdk/core'); import { Test } from 'nodeunit'; @@ -10,7 +11,7 @@ import { LaunchType } from '../../lib/base/base-service'; import { PlacementConstraint, PlacementStrategy } from '../../lib/placement'; export = { - "When creating an ECS Service": { + "When creating an EC2 Service": { "with only required properties set, it correctly sets default properties"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -50,6 +51,145 @@ export = { test.done(); }, + "with all properties set"(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('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: NetworkMode.AWS_VPC + }); + + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE + }); + + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512, + }); + + // WHEN + const service = new ecs.Ec2Service(stack, "Ec2Service", { + cluster, + taskDefinition, + desiredCount: 2, + assignPublicIp: true, + cloudMapOptions: { + name: "myapp", + dnsRecordType: cloudmap.DnsRecordType.A, + dnsTtl: cdk.Duration.seconds(50), + failureThreshold: 20 + }, + daemon: false, + healthCheckGracePeriod: cdk.Duration.seconds(60), + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroup: new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bob', + vpc, + }), + serviceName: "bonjour", + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC } + }); + + service.addPlacementConstraints(PlacementConstraint.memberOf("attribute:ecs.instance-type =~ t2.*")); + service.addPlacementStrategies(PlacementStrategy.spreadAcross(BuiltInAttributes.AVAILABILITY_ZONE)); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + TaskDefinition: { + Ref: "Ec2TaskDef0226F28C" + }, + Cluster: { + Ref: "EcsCluster97242B84" + }, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 55 + }, + DesiredCount: 2, + LaunchType: LaunchType.EC2, + LoadBalancers: [], + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "ENABLED", + SecurityGroups: [ + { + "Fn::GetAtt": [ + "SecurityGroup1F554B36F", + "GroupId" + ] + } + ], + Subnets: [ + { + Ref: "MyVpcPublicSubnet1SubnetF6608456" + }, + { + Ref: "MyVpcPublicSubnet2Subnet492B6BFB" + } + ] + } + }, + PlacementConstraints: [ + { + Expression: "attribute:ecs.instance-type =~ t2.*", + Type: "memberOf" + } + ], + PlacementStrategies: [ + { + Field: "attribute:ecs.availability-zone", + Type: "spread" + } + ], + SchedulingStrategy: "REPLICA", + ServiceName: "bonjour", + ServiceRegistries: [ + { + RegistryArn: { + "Fn::GetAtt": [ + "Ec2ServiceCloudmapService45B52C0F", + "Arn" + ] + } + } + ] + })); + + test.done(); + }, + + "throws when task definition is not EC2 compatible"(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.TaskDefinition(stack, 'FargateTaskDef', { + compatibility: ecs.Compatibility.FARGATE, + cpu: "256", + memoryMiB: "512" + }); + taskDefinition.addContainer('BaseContainer', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryReservationMiB: 10, + }); + + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, "Ec2Service", { + cluster, + taskDefinition, + }); + }, /Supplied TaskDefinition is not configured for compatibility with EC2/); + + test.done(); + }, + "errors if daemon and desiredCount both specified"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -398,6 +538,38 @@ export = { test.done(); }, + "with spreadAcross container instances strategy"(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('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.Ec2Service(stack, "Ec2Service", { + cluster, + taskDefinition, + }); + + // WHEN + service.addPlacementStrategies(PlacementStrategy.spreadAcrossInstances()); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + PlacementStrategies: [{ + Field: "instanceId", + Type: "spread" + }] + })); + + test.done(); + }, + "with spreadAcross placement strategy"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -429,6 +601,51 @@ export = { test.done(); }, + "can turn PlacementStrategy into json format"(test: Test) { + // THEN + test.deepEqual(PlacementStrategy.spreadAcross(BuiltInAttributes.AVAILABILITY_ZONE).toJson(), [{ + type: 'spread', + field: 'attribute:ecs.availability-zone' + }]); + + test.done(); + }, + + "can turn PlacementConstraints into json format"(test: Test) { + // THEN + test.deepEqual(PlacementConstraint.distinctInstances().toJson(), [{ + type: 'distinctInstance' + }]); + + test.done(); + }, + + "errors when spreadAcross with no input"(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('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + const service = new ecs.Ec2Service(stack, "Ec2Service", { + cluster, + taskDefinition, + }); + + // THEN + test.throws(() => { + service.addPlacementStrategies(PlacementStrategy.spreadAcross()); + }, 'spreadAcross: give at least one field to spread by'); + + test.done(); + }, + "errors with spreadAcross placement strategy if daemon specified"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -456,6 +673,59 @@ export = { test.done(); }, + "with no placement constraints"(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('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.Ec2Service(stack, "Ec2Service", { + cluster, + taskDefinition, + }); + + // THEN + expect(stack).notTo(haveResource("AWS::ECS::Service", { + PlacementConstraints: [] + })); + + test.done(); + }, + + "with no placement strategy if daemon specified"(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('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 512 + }); + + new ecs.Ec2Service(stack, "Ec2Service", { + cluster, + taskDefinition, + daemon: true + }); + + // THEN + expect(stack).notTo(haveResource("AWS::ECS::Service", { + PlacementStrategies: [] + })); + + test.done(); + }, + "with random placement strategy"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -634,6 +904,228 @@ export = { } }, + "attachToClassicLB": { + "allows network mode of task definition to be host"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.HOST }); + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ containerPort: 808 }); + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); + + // THEN + const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); + service.attachToClassicLB(lb); + + test.done(); + }, + + 'allows network mode of task definition to be AwsVpc'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.AWS_VPC }); + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ containerPort: 808 }); + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); + + // THEN + const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); + service.attachToClassicLB(lb); + + test.done(); + }, + + 'throws when network mode of task definition is bridge'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.BRIDGE }); + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ containerPort: 808 }); + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); + + // THEN + const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); + test.throws(() => { + service.attachToClassicLB(lb); + }, /Cannot use a Classic Load Balancer if NetworkMode is Bridge. Use Host or AwsVpc instead./); + + test.done(); + }, + + 'throws when network mode of task definition is none'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.NONE }); + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + }); + container.addPortMappings({ containerPort: 808 }); + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); + + // THEN + const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); + test.throws(() => { + service.attachToClassicLB(lb); + }, /Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or AwsVpc instead./); + + test.done(); + } + }, + + "attachToApplicationTargetGroup": { + "allows network mode of task definition to be other than none"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: ecs.NetworkMode.AWS_VPC }); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); + + const lb = new elbv2.ApplicationLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + const targetGroup = listener.addTargets("target", { + port: 80, + }); + + // THEN + service.attachToApplicationTargetGroup(targetGroup); + + test.done(); + }, + + "throws when network mode of task definition is none"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: ecs.NetworkMode.NONE }); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); + + const lb = new elbv2.ApplicationLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + const targetGroup = listener.addTargets("target", { + port: 80, + }); + + // THEN + test.throws(() => { + service.attachToApplicationTargetGroup(targetGroup); + }, /Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + + test.done(); + } + }, + + "attachToNetworkTargetGroup": { + "allows network mode of task definition to be other than none"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: ecs.NetworkMode.AWS_VPC }); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); + + const lb = new elbv2.NetworkLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + const targetGroup = listener.addTargets("target", { + port: 80, + }); + + // THEN + service.attachToNetworkTargetGroup(targetGroup); + + test.done(); + }, + + "throws when network mode of task definition is none"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: ecs.NetworkMode.NONE }); + const container = taskDefinition.addContainer('MainContainer', { + image: ContainerImage.fromRegistry('hello'), + }); + container.addPortMappings({ containerPort: 8000 }); + + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); + + const lb = new elbv2.NetworkLoadBalancer(stack, "lb", { vpc }); + const listener = lb.addListener("listener", { port: 80 }); + const targetGroup = listener.addTargets("target", { + port: 80, + }); + + // THEN + test.throws(() => { + service.attachToNetworkTargetGroup(targetGroup); + }, /Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + + test.done(); + } + }, + 'classic ELB': { 'can attach to classic ELB'(test: Test) { // GIVEN @@ -647,7 +1139,10 @@ export = { memoryLimitMiB: 1024, }); container.addPortMappings({ containerPort: 808 }); - const service = new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition}); + const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition + }); // WHEN const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); 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 e0e8ebcc7a494..229c1750168e9 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 @@ -2,8 +2,11 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import { Protocol } from '@aws-cdk/aws-ec2'; import { Repository } from '@aws-cdk/aws-ecr'; import iam = require('@aws-cdk/aws-iam'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); +import ssm = require('@aws-cdk/aws-ssm'); import cdk = require('@aws-cdk/core'); import { Test } from 'nodeunit'; +import path = require('path'); import ecs = require('../../lib'); export = { @@ -26,6 +29,92 @@ export = { test.done(); }, + "with all properties set"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + executionRole: new iam.Role(stack, 'ExecutionRole', { + path: '/', + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal("ecs.amazonaws.com"), + new iam.ServicePrincipal("ecs-tasks.amazonaws.com") + ) + }), + family: "ecs-tasks", + networkMode: ecs.NetworkMode.AWS_VPC, + placementConstraints: [ecs.PlacementConstraint.memberOf("attribute:ecs.instance-type =~ t2.*")], + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + volumes: [{ + host: { + sourcePath: "/tmp/cache", + }, + name: "scratch" + }] + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [], + ExecutionRoleArn: { + "Fn::GetAtt": [ + "ExecutionRole605A040B", + "Arn" + ] + }, + Family: "ecs-tasks", + NetworkMode: "awsvpc", + PlacementConstraints: [ + { + Expression: "attribute:ecs.instance-type =~ t2.*", + Type: "memberOf" + } + ], + RequiresCompatibilities: [ + "EC2" + ], + TaskRoleArn: { + "Fn::GetAtt": [ + "TaskRole30FC0FBB", + "Arn" + ] + }, + Volumes: [ + { + Host: { + SourcePath: "/tmp/cache" + }, + Name: "scratch" + } + ] + })); + + test.done(); + }, + + "correctly sets placement constraint"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + // WHEN + taskDefinition.addPlacementConstraint(ecs.PlacementConstraint.memberOf("attribute:ecs.instance-type =~ t2.*")); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + PlacementConstraints: [ + { + Expression: "attribute:ecs.instance-type =~ t2.*", + Type: "memberOf" + } + ], + + })); + + test.done(); + }, + "correctly sets network mode"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -117,18 +206,201 @@ export = { test.done(); }, - "correctly sets containers from ECR repository"(test: Test) { + "all container definition options defined"(test: Test) { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { + parameterName: '/name', + version: 1 + }); taskDefinition.addContainer("web", { - image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, "myECRImage")), + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 2048, + cpu: 256, + disableNetworking: true, + command: ['CMD env'], + dnsSearchDomains: ['0.0.0.0'], + dnsServers: ['1.1.1.1'], + dockerLabels: {LABEL: 'label'}, + dockerSecurityOptions: ['ECS_SELINUX_CAPABLE=true'], + entryPoint: ["/app/node_modules/.bin/cdk"], + environment: {TEST_ENVIRONMENT_VARIABLE: "test environment variable value"}, + essential: true, + extraHosts: {EXTRAHOST: 'extra host'}, + healthCheck: { + command: ["curl localhost:8000"], + interval: cdk.Duration.seconds(20), + retries: 5, + startPeriod: cdk.Duration.seconds(10) + }, + hostname: "webHost", + linuxParameters: new ecs.LinuxParameters(stack, 'LinuxParameters', { + initProcessEnabled: true, + sharedMemorySize: 1024, + }), + logging: new ecs.AwsLogDriver({ streamPrefix: 'prefix' }), + memoryReservationMiB: 1024, + privileged: true, + readonlyRootFilesystem: true, + secrets: { + SECRET: ecs.Secret.fromSecretsManager(secret), + PARAMETER: ecs.Secret.fromSsmParameter(parameter), + }, + user: "amazon", + workingDirectory: "app/" + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "Ec2TaskDef", + ContainerDefinitions: [ + { + Command: [ + "CMD env" + ], + Cpu: 256, + DisableNetworking: true, + DnsSearchDomains: [ + "0.0.0.0" + ], + DnsServers: [ + "1.1.1.1" + ], + DockerLabels: { + LABEL: "label" + }, + DockerSecurityOptions: [ + "ECS_SELINUX_CAPABLE=true" + ], + EntryPoint: [ + "/app/node_modules/.bin/cdk" + ], + Environment: [ + { + Name: "TEST_ENVIRONMENT_VARIABLE", + Value: "test environment variable value" + } + ], + Essential: true, + ExtraHosts: [ + { + Hostname: "EXTRAHOST", + IpAddress: "extra host" + } + ], + HealthCheck: { + Command: [ + "CMD-SHELL", + "curl localhost:8000" + ], + Interval: 20, + Retries: 5, + StartPeriod: 10, + Timeout: 5 + }, + Hostname: "webHost", + Image: "amazon/amazon-ecs-sample", + LinuxParameters: { + Capabilities: { + Add: [], + Drop: [] + }, + Devices: [], + InitProcessEnabled: true, + SharedMemorySize: 1024, + Tmpfs: [] + }, + LogConfiguration: { + LogDriver: "awslogs", + Options: { + "awslogs-group": { + Ref: "Ec2TaskDefwebLogGroup7F786C6B" + }, + "awslogs-stream-prefix": "prefix", + "awslogs-region": { + Ref: "AWS::Region" + } + } + }, + Memory: 2048, + MemoryReservation: 1024, + Name: "web", + Privileged: true, + ReadonlyRootFilesystem: true, + Secrets: [ + { + Name: "SECRET", + ValueFrom: { + Ref: "SecretA720EF05" + } + }, + { + Name: "PARAMETER", + ValueFrom: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/name" + ] + ] + } + } + ], + User: "amazon", + WorkingDirectory: "app/" + } + ], + })); + + test.done(); + }, + + "correctly sets containers from ECR repository using all props"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, "myECRImage", { + lifecycleRegistryId: '123456789101', + lifecycleRules: [{ + rulePriority: 10, + tagPrefixList: ['abc'], + maxImageCount: 1 + }], + removalPolicy: cdk.RemovalPolicy.DESTROY, + repositoryName: 'project-a/amazon-ecs-sample' + })), memoryLimitMiB: 512 }); // THEN + expect(stack).to(haveResource('AWS::ECR::Repository', { + LifecyclePolicy: { + // tslint:disable-next-line:max-line-length + LifecyclePolicyText: "{\"rules\":[{\"rulePriority\":10,\"selection\":{\"tagStatus\":\"tagged\",\"tagPrefixList\":[\"abc\"],\"countType\":\"imageCountMoreThan\",\"countNumber\":1},\"action\":{\"type\":\"expire\"}}]}", + RegistryId: "123456789101" + }, + RepositoryName: "project-a/amazon-ecs-sample" + })); + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { Family: "Ec2TaskDef", ContainerDefinitions: [{ @@ -190,6 +462,170 @@ export = { test.done(); }, + "correctly sets containers from ECR repository using default props"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + // WHEN + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, "myECRImage")), + memoryLimitMiB: 512 + }); + + // THEN + expect(stack).notTo(haveResource('AWS::ECR::Repository', {})); + + test.done(); + }, + + "correctly sets containers from asset using default props"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + // WHEN + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromAsset(path.join(__dirname, '..', 'demo-image')), + memoryLimitMiB: 512 + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::TaskDefinition", { + Family: "Ec2TaskDef", + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ecr:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "Ec2TaskDefwebAssetImageAdoptRepositoryEA698962", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ecr:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":repository/", + { + "Fn::GetAtt": [ + "Ec2TaskDefwebAssetImageAdoptRepositoryEA698962", + "RepositoryName" + ] + } + ] + ] + } + ] + } + ] + }, + ".", + { + Ref: "AWS::URLSuffix" + }, + "/", + { + "Fn::GetAtt": [ + "Ec2TaskDefwebAssetImageAdoptRepositoryEA698962", + "RepositoryName" + ] + }, + "@sha256:", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "@sha256:", + { + Ref: "Ec2TaskDefwebAssetImageImageNameCBACAA57" + } + ] + } + ] + } + ] + ] + }, + Name: "web" + }], + })); + + test.done(); + }, + + "correctly sets containers from asset using all props"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromAsset(path.join(__dirname, '..', 'demo-image'), { + buildArgs: {HTTP_PROXY: 'http://10.20.30.2:1234'} + }), + memoryLimitMiB: 512 + }); + + test.done(); + }, + "correctly sets scratch space"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -513,6 +949,19 @@ export = { test.done(); }, + "automatically sets taskRole by default"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + // THEN + expect(stack).to(haveResourceLike("AWS::ECS::TaskDefinition", { + TaskRoleArn: stack.resolve(taskDefinition.taskRole.roleArn) + })); + + test.done(); + }, + "correctly sets dockerVolumeConfiguration"(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 93f3582f02ce6..11929be069e6c 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -84,6 +84,123 @@ export = { test.done(); }, + "with all properties set"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE + }); + + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + taskDefinition.addContainer("web", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + }); + + new ecs.FargateService(stack, "FargateService", { + cluster, + taskDefinition, + desiredCount: 2, + assignPublicIp: true, + cloudMapOptions: { + name: "myapp", + dnsRecordType: cloudmap.DnsRecordType.A, + dnsTtl: cdk.Duration.seconds(50), + failureThreshold: 20 + }, + healthCheckGracePeriod: cdk.Duration.seconds(60), + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroup: new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bob', + vpc, + }), + serviceName: "bonjour", + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC } + }); + + // THEN + expect(stack).to(haveResource("AWS::ECS::Service", { + TaskDefinition: { + Ref: "FargateTaskDefC6FB60B4" + }, + Cluster: { + Ref: "EcsCluster97242B84" + }, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 55 + }, + DesiredCount: 2, + HealthCheckGracePeriodSeconds: 60, + LaunchType: LaunchType.FARGATE, + LoadBalancers: [], + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: "ENABLED", + SecurityGroups: [ + { + "Fn::GetAtt": [ + "SecurityGroup1F554B36F", + "GroupId" + ] + } + ], + Subnets: [ + { + Ref: "MyVpcPublicSubnet1SubnetF6608456" + }, + { + Ref: "MyVpcPublicSubnet2Subnet492B6BFB" + } + ] + } + }, + ServiceName: "bonjour", + ServiceRegistries: [ + { + RegistryArn: { + "Fn::GetAtt": [ + "FargateServiceCloudmapService9544B753", + "Arn" + ] + } + } + ] + })); + + test.done(); + }, + + "throws when task definition is not Fargate compatible"(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.TaskDefinition(stack, 'Ec2TaskDef', { + compatibility: ecs.Compatibility.EC2, + }); + taskDefinition.addContainer('BaseContainer', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryReservationMiB: 10, + }); + + // THEN + test.throws(() => { + new ecs.FargateService(stack, "FargateService", { + cluster, + taskDefinition, + }); + }, /Supplied TaskDefinition is not configured for compatibility with Fargate/); + + test.done(); + }, + "errors when no container specified on task definition"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -515,64 +632,6 @@ export = { test.done(); }, - "allow adding a load balancing target to an application target group"(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'MyVpc', {}); - const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); - const container = taskDefinition.addContainer('MainContainer', { - image: ContainerImage.fromRegistry('hello'), - }); - container.addPortMappings({ containerPort: 8000 }); - - const service = new ecs.FargateService(stack, 'Service', { - cluster, - taskDefinition - }); - - const lb = new elbv2.ApplicationLoadBalancer(stack, "lb", { vpc }); - const listener = lb.addListener("listener", { port: 80 }); - const targetGroup = listener.addTargets("target", { - port: 80, - }); - - // WHEN - targetGroup.addTarget(service); - - const capacity = service.autoScaleTaskCount({ maxCapacity: 10, minCapacity: 1 }); - capacity.scaleOnRequestCount("ScaleOnRequests", { - requestsPerTarget: 1000, - targetGroup - }); - - // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { - MaxCapacity: 10, - MinCapacity: 1, - ResourceId: { - "Fn::Join": [ - "", - [ - "service/", - { - Ref: "EcsCluster97242B84" - }, - "/", - { - "Fn::GetAtt": [ - "ServiceD69D759B", - "Name" - ] - } - ] - ] - }, - })); - - test.done(); - }, - 'When enabling service discovery': { 'throws if namespace has not been added to cluster'(test: Test) { // GIVEN diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts index 75ca6c0920b0d..3ea513bae5ed7 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-task-definition.ts @@ -1,4 +1,5 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; +import iam = require('@aws-cdk/aws-iam'); import cdk = require('@aws-cdk/core'); import { Test } from 'nodeunit'; import ecs = require('../../lib'); @@ -21,8 +22,81 @@ export = { Memory: "512", })); - // test error if no container defs? test.done(); }, + + "with all properties set"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { + cpu: 128, + executionRole: new iam.Role(stack, 'ExecutionRole', { + path: '/', + assumedBy: new iam.CompositePrincipal( + new iam.ServicePrincipal("ecs.amazonaws.com"), + new iam.ServicePrincipal("ecs-tasks.amazonaws.com") + ) + }), + family: "myApp", + memoryLimitMiB: 1024, + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }) + }); + + taskDefinition.addVolume({ + host: { + sourcePath: "/tmp/cache", + }, + name: "scratch" + }); + + // THEN + expect(stack).to(haveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [], + Cpu: "128", + ExecutionRoleArn: { + "Fn::GetAtt": [ + "ExecutionRole605A040B", + "Arn" + ] + }, + Family: "myApp", + Memory: "1024", + NetworkMode: "awsvpc", + RequiresCompatibilities: [ + ecs.LaunchType.FARGATE + ], + TaskRoleArn: { + "Fn::GetAtt": [ + "TaskRole30FC0FBB", + "Arn" + ] + }, + Volumes: [ + { + Host: { + SourcePath: "/tmp/cache" + }, + Name: "scratch" + } + ] + })); + + test.done(); + }, + + 'throws when adding placement constraint'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + // THEN + test.throws(() => { + taskDefinition.addPlacementConstraint(ecs.PlacementConstraint.memberOf("attribute:ecs.instance-type =~ t2.*")); + }, /Cannot set placement constraints on tasks that run on Fargate/); + + test.done(); + } } }; diff --git a/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts index f33b3de7731d4..10f94c2e88173 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts @@ -104,6 +104,10 @@ export = { }); // THEN + expect(stack).to(haveResource('AWS::Logs::LogGroup', { + RetentionInDays: logs.RetentionDays.TWO_YEARS + })); + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { @@ -122,6 +126,21 @@ export = { test.done(); }, + 'without a defined log group'(test: Test) { + // GIVEN + td.addContainer('Container', { + image, + logging: new ecs.AwsLogDriver({ + streamPrefix: 'hello', + }) + }); + + // THEN + expect(stack).notTo(haveResource('AWS::Logs::LogGroup', {})); + + test.done(); + }, + 'throws when specifying log retention and log group'(test: Test) { // GIVEN const logGroup = new logs.LogGroup(stack, 'LogGroup'); diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index dd531d624a813..fdcd422b2fe0e 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -7,9 +7,52 @@ import ecs = require('../lib'); export = { "When creating a Task Definition": { - // Validating portMapping inputs + "add a container using default props"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + new ecs.ContainerDefinition(stack, "Container", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + taskDefinition, + memoryLimitMiB: 2048, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Essential: true, + Image: "/aws/aws-example-app", + Memory: 2048, + Name: "Container" + } + ] + })); + + test.done(); + }, + + "throws when MemoryLimit is less than MemoryReservationLimit"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // THEN + test.throws(() => { + new ecs.ContainerDefinition(stack, "Container", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + taskDefinition, + memoryLimitMiB: 512, + memoryReservationMiB: 1024, + }); + }, /MemoryLimitMiB should not be less than MemoryReservationMiB./); + + test.done(); + }, + "With network mode AwsVpc": { - "Host port should be the same as container port"(test: Test) { + "throws when Host port is different from container port"(test: Test) { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { @@ -32,6 +75,27 @@ export = { test.done(); }, + "Host port is the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AWS_VPC, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + container.addPortMappings({ + containerPort: 8080, + hostPort: 8080 + }); + + // THEN no exception raised + test.done(); + }, + "Host port can be empty "(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -55,7 +119,7 @@ export = { }, "With network mode Host ": { - "Host port should be the same as container port"(test: Test) { + "throws when Host port is different from container port"(test: Test) { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { @@ -78,6 +142,27 @@ export = { test.done(); }, + "when host port is the same as container port"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.HOST, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + container.addPortMappings({ + containerPort: 8080, + hostPort: 8080 + }); + + // THEN no exception raised + test.done(); + }, + "Host port can be empty "(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -126,6 +211,47 @@ export = { }, "With network mode Bridge": { + "when Host port is empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + container.addPortMappings({ + containerPort: 8080, + }); + + // THEN no exception raises + test.done(); + }, + + "when Host port is not empty "(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + container.addPortMappings({ + containerPort: 8080, + hostPort: 8084 + }); + + // THEN no exception raises + test.done(); + }, + "allows adding links"(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -151,6 +277,58 @@ export = { } }, + "Container Port": { + "should return the first container port in PortMappings"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AWS_VPC, + }); + + const container = taskDefinition.addContainer("Container", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + memoryLimitMiB: 2048, + }); + + // WHEN + container.addPortMappings({ + containerPort: 8080, + }); + + container.addPortMappings({ + containerPort: 8081, + }); + const actual = container.containerPort; + + // THEN + const expected = 8080; + test.equal(actual, expected, "containerPort should return the first container port in PortMappings"); + test.done(); + }, + + "throws when calling containerPort with no PortMappings"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AWS_VPC, + }); + + const container = taskDefinition.addContainer("MyContainer", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + memoryLimitMiB: 2048 + }); + + // THEN + test.throws(() => { + const actual = container.containerPort; + const expected = 8080; + test.equal(actual, expected); + }, /Container MyContainer hasn't defined any ports. Call addPortMappings()./); + + test.done(); + }, + }, + "Ingress Port": { "With network mode AwsVpc": { "Ingress port should be the same as container port"(test: Test) { @@ -176,6 +354,28 @@ export = { test.equal(actual, expected, "Ingress port should be the same as container port"); test.done(); }, + + "throws when calling ingressPort with no PortMappings"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', { + networkMode: ecs.NetworkMode.AWS_VPC, + }); + + const container = taskDefinition.addContainer("MyContainer", { + image: ecs.ContainerImage.fromRegistry("/aws/aws-example-app"), + memoryLimitMiB: 2048 + }); + + // THEN + test.throws(() => { + const actual = container.ingressPort; + const expected = 8080; + test.equal(actual, expected); + }, /Container MyContainer hasn't defined any ports. Call addPortMappings()./); + + test.done(); + }, }, "With network mode Host ": { @@ -201,7 +401,7 @@ export = { const expected = 8080; test.equal(actual, expected); test.done(); - }, + } }, "With network mode Bridge": { @@ -252,7 +452,7 @@ export = { const expected = 0; test.equal(actual, expected); test.done(); - }, + } }, }, @@ -437,6 +637,7 @@ export = { test.done(); }, + 'can set Health Check with defaults'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -469,6 +670,39 @@ export = { test.done(); }, + 'throws when setting Health Check with no commands'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + healthCheck: { + command: [] + } + }); + + // THEN + test.throws(() => { + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + HealthCheck: { + Command: [], + Interval: 30, + Retries: 3, + Timeout: 5 + }, + } + ] + })); + }, /At least one argument must be supplied for health check command./); + + test.done(); + }, + 'can specify Health Check values in shell form'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -620,7 +854,79 @@ export = { test.done(); }, + '_linkContainer works properly': { + 'when the props passed in is an essential container'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + const container = taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + essential: true + }); + + // THEN + test.equal(taskDefinition.defaultContainer, container); + + test.done(); + }, + + 'when the props passed in is not an essential container'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + essential: false + }); + + // THEN + test.equal(taskDefinition.defaultContainer, undefined); + + test.done(); + } + }, + 'Can specify linux parameters': { + 'with only required properties set, it correctly sets default properties'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + const linuxParameters = new ecs.LinuxParameters(stack, 'LinuxParameters'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + linuxParameters, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Image: 'test', + LinuxParameters: { + Capabilities: { + Add: [], + Drop: [] + }, + Devices: [], + Tmpfs: [] + } + } + ] + })); + + test.done(); + }, + 'before calling addContainer'(test: Test) { // GIVEN const stack = new cdk.Stack(); 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 3713422d3dd1e..bac88409973e7 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -338,6 +338,109 @@ export = { RoleARN: { "Fn::GetAtt": [ "EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B", "Arn" ] } })); + expect(stack).to(haveResource('AWS::Lambda::Function', { + Timeout: 310, + Environment: { + Variables: { + CLUSTER: { + Ref: "EcsCluster97242B84" + } + } + }, + Handler: "index.lambda_handler" + })); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + 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" + } + ] + })); + test.done(); }, @@ -838,6 +941,57 @@ export = { test.done(); }, + "allows specifying drain time"(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('t2.micro'), + taskDrainTime: cdk.Duration.minutes(1) + }); + + // THEN + expect(stack).to(haveResource("AWS::AutoScaling::LifecycleHook", { + HeartbeatTimeout: 60 + })); + + test.done(); + }, + + "allows containers access to instance metadata service"(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('t2.micro'), + canContainersAccessInstanceRole: 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" + ] + ] + } + } + })); + + test.done(); + }, + "allows adding default service discovery namespace"(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts index 63c8c2396e494..a473b67765787 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts @@ -13,21 +13,21 @@ export class PolicyStatement { public sid?: string; public effect: Effect; - private action = new Array(); - private notaction = new Array(); - private principal: { [key: string]: any[] } = {}; - private resource = new Array(); - private notresource = new Array(); - private condition: { [key: string]: any } = { }; + private readonly action = new Array(); + private readonly notAction = new Array(); + private readonly principal: { [key: string]: any[] } = {}; + private readonly resource = new Array(); + private readonly notResource = new Array(); + private readonly condition: { [key: string]: any } = { }; constructor(props: PolicyStatementProps = {}) { this.effect = props.effect || Effect.ALLOW; this.addActions(...props.actions || []); - this.addNotActions(...props.notactions || []); + this.addNotActions(...props.notActions || []); this.addPrincipals(...props.principals || []); this.addResources(...props.resources || []); - this.addNotResources(...props.notresources || []); + this.addNotResources(...props.notResources || []); if (props.conditions !== undefined) { this.addConditions(props.conditions); } @@ -38,11 +38,17 @@ export class PolicyStatement { // public addActions(...actions: string[]) { + if (actions.length > 0 && this.notAction.length > 0) { + throw new Error(`Cannot add 'Actions' to policy statement if 'NotActions' have been added`); + } this.action.push(...actions); } - public addNotActions(...notactions: string[]) { - this.notaction.push(...notactions); + public addNotActions(...notActions: string[]) { + if (notActions.length > 0 && this.action.length > 0) { + throw new Error(`Cannot add 'NotActions' to policy statement if 'Actions' have been added`); + } + this.notAction.push(...notActions); } // @@ -103,11 +109,17 @@ export class PolicyStatement { // public addResources(...arns: string[]) { + if (arns.length > 0 && this.notResource.length > 0) { + throw new Error(`Cannot add 'Resources' to policy statement if 'NotResources' have been added`); + } this.resource.push(...arns); } public addNotResources(...arns: string[]) { - this.notresource.push(...arns); + if (arns.length > 0 && this.resource.length > 0) { + throw new Error(`Cannot add 'NotResources' to policy statement if 'Resources' have been added`); + } + this.notResource.push(...arns); } /** @@ -154,12 +166,12 @@ export class PolicyStatement { public toStatementJson(): any { return noUndef({ Action: _norm(this.action), - NotAction: _norm(this.notaction), + NotAction: _norm(this.notAction), Condition: _norm(this.condition), Effect: _norm(this.effect), Principal: _normPrincipal(this.principal), Resource: _norm(this.resource), - NotResource: _norm(this.notresource), + NotResource: _norm(this.notResource), Sid: _norm(this.sid), }); @@ -246,9 +258,9 @@ export interface PolicyStatementProps { /** * List of not actions to add to the statement * - * @default - no actions + * @default - no not-actions */ - readonly notactions?: string[]; + readonly notActions?: string[]; /** * List of principals to add to the statement @@ -267,9 +279,9 @@ export interface PolicyStatementProps { /** * NotResource ARNs to add to the statement * - * @default - no resources + * @default - no not-resources */ - readonly notresources?: string[]; + readonly notResources?: string[]; /** * Conditions to add to the statement diff --git a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts index f61fdb680e4a8..26758a5393e35 100644 --- a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts +++ b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts @@ -42,7 +42,6 @@ export = { const doc = new PolicyDocument(); const p1 = new PolicyStatement(); p1.addActions('sqs:SendMessage'); - p1.addResources('*'); p1.addNotResources('arn:aws:sqs:us-east-1:123456789012:forbidden_queue'); const p2 = new PolicyStatement(); @@ -60,13 +59,35 @@ export = { test.deepEqual(stack.resolve(doc), { Version: '2012-10-17', Statement: - [{ Effect: 'Allow', Action: 'sqs:SendMessage', Resource: '*', NotResource: 'arn:aws:sqs:us-east-1:123456789012:forbidden_queue' }, + [{ Effect: 'Allow', Action: 'sqs:SendMessage', NotResource: 'arn:aws:sqs:us-east-1:123456789012:forbidden_queue' }, { Effect: 'Deny', Action: 'cloudformation:CreateStack' }, { Effect: 'Allow', NotAction: 'cloudformation:UpdateTerminationProtection' } ] }); test.done(); }, + 'Cannot combine Actions and NotActions'(test: Test) { + test.throws(() => { + new PolicyStatement({ + actions: ['abc'], + notActions: ['def'], + }); + }, /Cannot add 'NotActions' to policy statement if 'Actions' have been added/); + + test.done(); + }, + + 'Cannot combine Resources and NotResources'(test: Test) { + test.throws(() => { + new PolicyStatement({ + resources: ['abc'], + notResources: ['def'], + }); + }, /Cannot add 'NotResources' to policy statement if 'Resources' have been added/); + + test.done(); + }, + 'Permission allows specifying multiple actions upon construction'(test: Test) { const stack = new Stack(); const perm = new PolicyStatement(); diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 924b4377a172d..ed3ce1ca5bab8 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,23 @@ +# CloudFormation Resource Specification v5.2.0 + +## New Resource Types + +* AWS::SSM::MaintenanceWindowTarget +* AWS::SageMaker::Workteam + +## Attribute Changes + + +## Property Changes + +* AWS::DMS::ReplicationTask CdcStartPosition (__added__) +* AWS::DMS::ReplicationTask CdcStopPosition (__added__) + +## Property Type Changes + +* AWS::AppSync::GraphQLApi.LogConfig ExcludeVerboseContent (__added__) + + # CloudFormation Resource Specification v5.1.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index da7d64a990593..0b7f2be5d3885 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -1951,6 +1951,12 @@ "Required": false, "UpdateType": "Mutable" }, + "ExcludeVerboseContent": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-logconfig.html#cfn-appsync-graphqlapi-logconfig-excludeverbosecontent", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "FieldLogLevel": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-logconfig.html#cfn-appsync-graphqlapi-logconfig-fieldloglevel", "PrimitiveType": "String", @@ -23853,6 +23859,24 @@ } } }, + "AWS::SSM::MaintenanceWindowTarget.Targets": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtarget-targets.html", + "Properties": { + "Key": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtarget-targets.html#cfn-ssm-maintenancewindowtarget-targets-key", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Values": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtarget-targets.html#cfn-ssm-maintenancewindowtarget-targets-values", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::SSM::MaintenanceWindowTask.LoggingInfo": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-maintenancewindowtask-logginginfo.html", "Properties": { @@ -24258,6 +24282,51 @@ } } }, + "AWS::SageMaker::Workteam.CognitoMemberDefinition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-workteam-cognitomemberdefinition.html", + "Properties": { + "CognitoClientId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-workteam-cognitomemberdefinition.html#cfn-sagemaker-workteam-cognitomemberdefinition-cognitoclientid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "CognitoUserGroup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-workteam-cognitomemberdefinition.html#cfn-sagemaker-workteam-cognitomemberdefinition-cognitousergroup", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "CognitoUserPool": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-workteam-cognitomemberdefinition.html#cfn-sagemaker-workteam-cognitomemberdefinition-cognitouserpool", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::SageMaker::Workteam.MemberDefinition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-workteam-memberdefinition.html", + "Properties": { + "CognitoMemberDefinition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-workteam-memberdefinition.html#cfn-sagemaker-workteam-memberdefinition-cognitomemberdefinition", + "Required": true, + "Type": "CognitoMemberDefinition", + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::Workteam.NotificationConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-workteam-notificationconfiguration.html", + "Properties": { + "NotificationTopicArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-workteam-notificationconfiguration.html#cfn-sagemaker-workteam-notificationconfiguration-notificationtopicarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::SecretsManager::RotationSchedule.RotationRules": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-rotationschedule-rotationrules.html", "Properties": { @@ -25211,7 +25280,7 @@ } } }, - "ResourceSpecificationVersion": "5.1.0", + "ResourceSpecificationVersion": "5.2.0", "ResourceTypes": { "AWS::AmazonMQ::Broker": { "Attributes": { @@ -30932,12 +31001,24 @@ "AWS::DMS::ReplicationTask": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-replicationtask.html", "Properties": { + "CdcStartPosition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-replicationtask.html#cfn-dms-replicationtask-cdcstartposition", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "CdcStartTime": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-replicationtask.html#cfn-dms-replicationtask-cdcstarttime", "PrimitiveType": "Double", "Required": false, "UpdateType": "Mutable" }, + "CdcStopPosition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-replicationtask.html#cfn-dms-replicationtask-cdcstopposition", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "MigrationType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-replicationtask.html#cfn-dms-replicationtask-migrationtype", "PrimitiveType": "String", @@ -43009,6 +43090,48 @@ } } }, + "AWS::SSM::MaintenanceWindowTarget": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtarget.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtarget.html#cfn-ssm-maintenancewindowtarget-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtarget.html#cfn-ssm-maintenancewindowtarget-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "OwnerInformation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtarget.html#cfn-ssm-maintenancewindowtarget-ownerinformation", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ResourceType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtarget.html#cfn-ssm-maintenancewindowtarget-resourcetype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Targets": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtarget.html#cfn-ssm-maintenancewindowtarget-targets", + "ItemType": "Targets", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "WindowId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtarget.html#cfn-ssm-maintenancewindowtarget-windowid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::SSM::MaintenanceWindowTask": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtask.html", "Properties": { @@ -43524,6 +43647,48 @@ } } }, + "AWS::SageMaker::Workteam": { + "Attributes": { + "WorkteamName": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-workteam.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-workteam.html#cfn-sagemaker-workteam-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MemberDefinitions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-workteam.html#cfn-sagemaker-workteam-memberdefinitions", + "ItemType": "MemberDefinition", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "NotificationConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-workteam.html#cfn-sagemaker-workteam-notificationconfiguration", + "Required": false, + "Type": "NotificationConfiguration", + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-workteam.html#cfn-sagemaker-workteam-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "WorkteamName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-workteam.html#cfn-sagemaker-workteam-workteamname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::SecretsManager::ResourcePolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-resourcepolicy.html", "Properties": { diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java index 592a64e45b173..21cd8d3437333 100644 --- a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloConstructProps.java @@ -26,7 +26,7 @@ public static HelloConstructPropsBuilder aHelloConstructProps() { return new HelloConstructPropsBuilder(); } - public HelloConstructPropsBuilder withBucketCount(int bucketCount) { + public HelloConstructPropsBuilder bucketCount(int bucketCount) { this.bucketCount = bucketCount; return this; } diff --git a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java index eaff7d77c131a..cd3fe94c1d32f 100644 --- a/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java +++ b/packages/aws-cdk/lib/init-templates/app/java/src/main/java/com/myorg/HelloStack.java @@ -21,17 +21,17 @@ public HelloStack(final Construct parent, final String id, final StackProps prop super(parent, id, props); Queue queue = new Queue(this, "MyFirstQueue", QueueProps.builder() - .withVisibilityTimeout(Duration.seconds(300)) + .visibilityTimeout(Duration.seconds(300)) .build()); Topic topic = new Topic(this, "MyFirstTopic", TopicProps.builder() - .withDisplayName("My First Topic Yeah") + .displayName("My First Topic Yeah") .build()); topic.addSubscription(new SqsSubscription(queue)); HelloConstruct hello = new HelloConstruct(this, "Buckets", HelloConstructProps.builder() - .withBucketCount(5) + .bucketCount(5) .build()); User user = new User(this, "MyUser", UserProps.builder().build());