diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 2293c129ee245..5edbd314482fb 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -523,3 +523,44 @@ new QueueProcessingFargateService(stack, 'QueueProcessingService', { image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')), }); ``` + +### Deploy application and metrics sidecar + +The following is an example of deploying an application along with a metrics sidecar container that utilizes `dockerLabels` for discovery: + +```ts +const service = new ApplicationLoadBalancedFargateService(stack, 'Service', { + cluster, + vpc, + desiredCount: 1, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + }, + dockerLabels: { + 'application.label.one': 'first_label' + 'application.label.two': 'second_label' + } +}); + +service.taskDefinition.addContainer('Sidecar', { + image: ContainerImage.fromRegistry('example/metrics-sidecar') +} +``` + +### Select specific load balancer name ApplicationLoadBalancedFargateService + +```ts +const loadBalancedFargateService = new ApplicationLoadBalancedFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + desiredCount: 1, + cpu: 512, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + }, + vpcSubnets: { + subnets: [ec2.Subnet.fromSubnetId(stack, 'subnet', 'VpcISOLATEDSubnet1Subnet80F07FA0')], + }, + loadBalancerName: 'application-lb-name', +}); +``` diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index bef5df841df19..4f7cd20220387 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -240,6 +240,13 @@ export interface ApplicationLoadBalancedServiceBaseProps { */ readonly circuitBreaker?: DeploymentCircuitBreaker; + /** + * Name of the load balancer + * + * @default - Automatically generated name. + */ + readonly loadBalancerName?: string; + } export interface ApplicationLoadBalancedTaskImageOptions { @@ -321,6 +328,13 @@ export interface ApplicationLoadBalancedTaskImageOptions { * @default - Automatically generated name. */ readonly family?: string; + + /** + * A key/value map of labels to add to the container. + * + * @default - No labels. + */ + readonly dockerLabels?: { [key: string]: string }; } /** @@ -400,6 +414,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { const lbProps = { vpc: this.cluster.vpc, + loadBalancerName: props.loadBalancerName, internetFacing, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index e801b8797d13b..aae6421c621c9 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -184,6 +184,13 @@ export interface ApplicationLoadBalancedTaskImageProps { * @default - Automatically generated name. */ readonly family?: string; + + /** + * A key/value map of labels to add to the container. + * + * @default - No labels. + */ + readonly dockerLabels?: { [key: string]: string }; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index f04ca581f14cb..7ecd4efabfd9b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -263,6 +263,13 @@ export interface NetworkLoadBalancedTaskImageOptions { * @default - Automatically generated name. */ readonly family?: string; + + /** + * A key/value map of labels to add to the container. + * + * @default - No labels. + */ + readonly dockerLabels?: { [key: string]: string }; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts index a6ad1a6d3a53f..0162a6e825c67 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts @@ -182,6 +182,13 @@ export interface NetworkLoadBalancedTaskImageProps { * @default - Automatically generated name. */ readonly family?: string; + + /** + * A key/value map of labels to add to the container. + * + * @default - No labels. + */ + readonly dockerLabels?: { [key: string]: string }; } /** @@ -448,4 +455,4 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends Construct { }); } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts index ed606bb72c907..6d1809a68d18b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts @@ -110,6 +110,7 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, logging: logDriver, + dockerLabels: taskImageOptions.dockerLabels, }); container.addPortMappings({ containerPort: taskImageOptions.containerPort || 80, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts index 90f4afdd5fa8f..f77e7e4dccdc3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts @@ -104,6 +104,7 @@ export class ApplicationMultipleTargetGroupsEc2Service extends ApplicationMultip environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, logging: this.logDriver, + dockerLabels: taskImageOptions.dockerLabels, }); if (taskImageOptions.containerPorts) { for (const containerPort of taskImageOptions.containerPorts) { @@ -151,4 +152,4 @@ export class ApplicationMultipleTargetGroupsEc2Service extends ApplicationMultip cloudMapOptions: props.cloudMapOptions, }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts index 4bae918ca67fc..b8862dfbed338 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts @@ -108,6 +108,7 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, logging: logDriver, + dockerLabels: taskImageOptions.dockerLabels, }); container.addPortMappings({ containerPort: taskImageOptions.containerPort || 80, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts index f0d3b0a1571ce..12d5b25ce67fd 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts @@ -103,6 +103,7 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, logging: this.logDriver, + dockerLabels: taskImageOptions.dockerLabels, }); if (taskImageOptions.containerPorts) { for (const containerPort of taskImageOptions.containerPorts) { @@ -151,4 +152,4 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget cloudMapOptions: props.cloudMapOptions, }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts index 326a68529272b..19a2501355223 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts @@ -146,6 +146,7 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc logging: logDriver, environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, + dockerLabels: taskImageOptions.dockerLabels, }); container.addPortMappings({ containerPort: taskImageOptions.containerPort || 80, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts index 6759e8e001376..8052e0483b16a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts @@ -136,6 +136,7 @@ export class ApplicationMultipleTargetGroupsFargateService extends ApplicationMu logging: this.logDriver, environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, + dockerLabels: taskImageOptions.dockerLabels, }); if (taskImageOptions.containerPorts) { for (const containerPort of taskImageOptions.containerPorts) { @@ -184,4 +185,4 @@ export class ApplicationMultipleTargetGroupsFargateService extends ApplicationMu platformVersion: props.platformVersion, }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts index 1f2618bbca314..9095be5cf2cd1 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts @@ -133,6 +133,7 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic logging: logDriver, environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, + dockerLabels: taskImageOptions.dockerLabels, }); container.addPortMappings({ containerPort: taskImageOptions.containerPort || 80, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts index 4a4974af7cce3..1a185f642a558 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts @@ -136,6 +136,7 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa logging: this.logDriver, environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, + dockerLabels: taskImageOptions.dockerLabels, }); if (taskImageOptions.containerPorts) { for (const containerPort of taskImageOptions.containerPorts) { @@ -184,4 +185,4 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa platformVersion: props.platformVersion, }); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts index 77f171425c88a..0946123907fe2 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts @@ -107,6 +107,7 @@ export = { taskRole: new Role(stack, 'TaskRole', { assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), }), + dockerLabels: { label1: 'labelValue1', label2: 'labelValue2' }, }, cpu: 256, desiredCount: 3, @@ -214,6 +215,10 @@ export = { Protocol: 'tcp', }, ], + DockerLabels: { + label1: 'labelValue1', + label2: 'labelValue2', + }, }, ], ExecutionRoleArn: { @@ -967,6 +972,7 @@ export = { taskRole: new Role(stack, 'TaskRole', { assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), }), + dockerLabels: { label1: 'labelValue1', label2: 'labelValue2' }, }, cpu: 256, desiredCount: 3, @@ -1079,6 +1085,10 @@ export = { Protocol: 'tcp', }, ], + DockerLabels: { + label1: 'labelValue1', + label2: 'labelValue2', + }, }, ], ExecutionRoleArn: { @@ -1532,4 +1542,4 @@ export = { test.done(); }, }, -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index a028d34c22157..5ef253d71bdb1 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -28,6 +28,7 @@ export = { TEST_ENVIRONMENT_VARIABLE1: 'test environment variable 1 value', TEST_ENVIRONMENT_VARIABLE2: 'test environment variable 2 value', }, + dockerLabels: { label1: 'labelValue1', label2: 'labelValue2' }, }, desiredCount: 2, }); @@ -54,6 +55,10 @@ export = { }, ], Memory: 1024, + DockerLabels: { + label1: 'labelValue1', + label2: 'labelValue2', + }, }, ], })); @@ -389,6 +394,7 @@ export = { TEST_ENVIRONMENT_VARIABLE1: 'test environment variable 1 value', TEST_ENVIRONMENT_VARIABLE2: 'test environment variable 2 value', }, + dockerLabels: { label1: 'labelValue1', label2: 'labelValue2' }, }, desiredCount: 2, }); @@ -416,6 +422,10 @@ export = { 'awslogs-region': { Ref: 'AWS::Region' }, }, }, + DockerLabels: { + label1: 'labelValue1', + label2: 'labelValue2', + }, }, ], })); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service-v2.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service-v2.ts index 81f1711c961c0..cad68a5a270ae 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service-v2.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service-v2.ts @@ -4,7 +4,7 @@ import * as ecs from '@aws-cdk/aws-ecs'; import { CompositePrincipal, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { ApplicationMultipleTargetGroupsFargateService, NetworkMultipleTargetGroupsFargateService } from '../../lib'; +import { ApplicationMultipleTargetGroupsFargateService, NetworkMultipleTargetGroupsFargateService, ApplicationLoadBalancedFargateService } from '../../lib'; export = { 'When Application Load Balancer': { @@ -114,6 +114,7 @@ export = { taskRole: new Role(stack, 'TaskRole', { assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), }), + dockerLabels: { label1: 'labelValue1', label2: 'labelValue2' }, }, cpu: 256, assignPublicIp: true, @@ -223,6 +224,10 @@ export = { Protocol: 'tcp', }, ], + DockerLabels: { + label1: 'labelValue1', + label2: 'labelValue2', + }, }, ], Cpu: '256', @@ -304,6 +309,83 @@ export = { test.done(); }, + + 'test Fargate loadbalancer construct with application load balancer name set'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ApplicationLoadBalancedFargateService(stack, 'Service', { + cluster, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + loadBalancerName: 'alb-test-load-balancer', + }); + + // THEN - stack contains a load balancer and a service + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::LoadBalancer', { + Name: 'alb-test-load-balancer', + })); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: 1, + LaunchType: 'FARGATE', + LoadBalancers: [ + { + ContainerName: 'web', + ContainerPort: 80, + TargetGroupArn: { + Ref: 'ServiceLBPublicListenerECSGroup0CC8688C', + }, + }, + ], + })); + + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Image: 'test', + LogConfiguration: { + LogDriver: 'awslogs', + Options: { + 'awslogs-group': { + Ref: 'ServiceTaskDefwebLogGroup2A898F61', + }, + 'awslogs-stream-prefix': 'Service', + 'awslogs-region': { + Ref: 'AWS::Region', + }, + }, + }, + Name: 'web', + PortMappings: [ + { + ContainerPort: 80, + Protocol: 'tcp', + }, + ], + }, + ], + Cpu: '256', + ExecutionRoleArn: { + 'Fn::GetAtt': [ + 'ServiceTaskDefExecutionRole919F7BE3', + 'Arn', + ], + }, + Family: 'ServiceTaskDef79D79521', + Memory: '512', + NetworkMode: 'awsvpc', + RequiresCompatibilities: [ + 'FARGATE', + ], + })); + + test.done(); + }, }, 'When Network Load Balancer': { @@ -413,6 +495,7 @@ export = { taskRole: new Role(stack, 'TaskRole', { assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), }), + dockerLabels: { label1: 'labelValue1', label2: 'labelValue2' }, }, cpu: 256, assignPublicIp: true, @@ -517,6 +600,10 @@ export = { Protocol: 'tcp', }, ], + DockerLabels: { + label1: 'labelValue1', + label2: 'labelValue2', + }, }, ], Cpu: '256', @@ -599,4 +686,4 @@ export = { test.done(); }, }, -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts index a8c588d1187ef..b0f8d0d573d7f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts @@ -1012,4 +1012,65 @@ export = { }, + 'test ALB load balanced service with docker labels defined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', { + cluster, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + dockerLabels: { label1: 'labelValue1', label2: 'labelValue2' }, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Image: '/aws/aws-example-app', + DockerLabels: { + label1: 'labelValue1', + label2: 'labelValue2', + }, + }, + ], + })); + + test.done(); + }, + + 'test Network load balanced service with docker labels defined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedFargateService(stack, 'Service', { + cluster, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + dockerLabels: { label1: 'labelValue1', label2: 'labelValue2' }, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Image: '/aws/aws-example-app', + DockerLabels: { + label1: 'labelValue1', + label2: 'labelValue2', + }, + }, + ], + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 0f85d2e308dfd..b7e07a2978350 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -122,14 +122,9 @@ export class Cluster extends Resource implements ICluster { public readonly clusterName: string; /** - * The cluster-level (FARGATE, FARGATE_SPOT) capacity providers. + * The names of both ASG and Fargate capacity providers associated with the cluster. */ - private _fargateCapacityProviders: string[] = []; - - /** - * The EC2 Auto Scaling Group capacity providers associated with the cluster. - */ - private _asgCapacityProviders: AsgCapacityProvider[] = []; + private _capacityProviderNames: string[] = []; /** * The AWS Cloud Map namespace to associate with the cluster. @@ -169,7 +164,7 @@ export class Cluster extends Resource implements ICluster { clusterSettings = [{ name: 'containerInsights', value: props.containerInsights ? ContainerInsights.ENABLED : ContainerInsights.DISABLED }]; } - this._fargateCapacityProviders = props.capacityProviders ?? []; + this._capacityProviderNames = props.capacityProviders ?? []; if (props.enableFargateCapacityProviders) { this.enableFargateCapacityProviders(); } @@ -185,7 +180,6 @@ export class Cluster extends Resource implements ICluster { const cluster = new CfnCluster(this, 'Resource', { clusterName: this.physicalName, clusterSettings, - capacityProviders: Lazy.list({ produce: () => this._fargateCapacityProviders }, { omitEmpty: true }), configuration: this._executeCommandConfiguration && this.renderExecuteCommandConfiguration(), }); @@ -212,7 +206,7 @@ export class Cluster extends Resource implements ICluster { // since it's harmless, but we'd prefer not to add unexpected new // resources to the stack which could surprise users working with // brown-field CDK apps and stacks. - Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id, this._asgCapacityProviders)); + Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id, this._capacityProviderNames)); } /** @@ -220,8 +214,8 @@ export class Cluster extends Resource implements ICluster { */ public enableFargateCapacityProviders() { for (const provider of ['FARGATE', 'FARGATE_SPOT']) { - if (!this._fargateCapacityProviders.includes(provider)) { - this._fargateCapacityProviders.push(provider); + if (!this._capacityProviderNames.includes(provider)) { + this._capacityProviderNames.push(provider); } } } @@ -325,7 +319,7 @@ export class Cluster extends Resource implements ICluster { */ public addAsgCapacityProvider(provider: AsgCapacityProvider, options: AddAutoScalingGroupCapacityOptions = {}) { // Don't add the same capacity provider more than once. - if (this._asgCapacityProviders.includes(provider)) { + if (this._capacityProviderNames.includes(provider.capacityProviderName)) { return; } @@ -336,7 +330,7 @@ export class Cluster extends Resource implements ICluster { taskDrainTime: provider.enableManagedTerminationProtection ? Duration.seconds(0) : options.taskDrainTime, }); - this._asgCapacityProviders.push(provider); + this._capacityProviderNames.push(provider.capacityProviderName); } /** @@ -458,8 +452,8 @@ export class Cluster extends Resource implements ICluster { throw new Error('CapacityProvider not supported'); } - if (!this._fargateCapacityProviders.includes(provider)) { - this._fargateCapacityProviders.push(provider); + if (!this._capacityProviderNames.includes(provider)) { + this._capacityProviderNames.push(provider); } } @@ -1336,22 +1330,24 @@ export class AsgCapacityProvider extends Construct { class MaybeCreateCapacityProviderAssociations implements IAspect { private scope: Construct; private id: string; - private capacityProviders: AsgCapacityProvider[] + private capacityProviders: string[] + private resource?: CfnClusterCapacityProviderAssociations - constructor(scope: Construct, id: string, capacityProviders: AsgCapacityProvider[]) { + constructor(scope: Construct, id: string, capacityProviders: string[] ) { this.scope = scope; this.id = id; this.capacityProviders = capacityProviders; } + public visit(node: IConstruct): void { if (node instanceof Cluster) { - const providers = this.capacityProviders.map(p => p.capacityProviderName).filter(p => p !== 'FARGATE' && p !== 'FARGATE_SPOT'); - if (providers.length > 0) { - new CfnClusterCapacityProviderAssociations(this.scope, this.id, { + if (this.capacityProviders.length > 0 && !this.resource) { + const resource = new CfnClusterCapacityProviderAssociations(this.scope, this.id, { cluster: node.clusterName, defaultCapacityProviderStrategy: [], - capacityProviders: Lazy.list({ produce: () => providers }), + capacityProviders: Lazy.list({ produce: () => this.capacityProviders }), }); + this.resource = resource; } } } diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index fa74b98350ce5..2c7e814d61619 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -1710,6 +1710,10 @@ nodeunitShim({ // THEN expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ABSENT, + })); + + expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE_SPOT'], })); @@ -1728,6 +1732,10 @@ nodeunitShim({ // THEN expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ABSENT, + })); + + expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], })); @@ -1745,6 +1753,10 @@ nodeunitShim({ // THEN expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ABSENT, + })); + + expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], })); @@ -1763,6 +1775,10 @@ nodeunitShim({ // THEN expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ABSENT, + })); + + expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE'], })); @@ -1781,6 +1797,10 @@ nodeunitShim({ // THEN expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ABSENT, + })); + + expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE'], })); @@ -1929,7 +1949,6 @@ nodeunitShim({ enableManagedTerminationProtection: false, }); - // These should not be added at the association level cluster.enableFargateCapacityProviders(); // Ensure not added twice @@ -1942,6 +1961,8 @@ nodeunitShim({ Ref: 'EcsCluster97242B84', }, CapacityProviders: [ + 'FARGATE', + 'FARGATE_SPOT', { Ref: 'providerD3FF4D3A', }, diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.expected.json index d4e008750fe93..555d9acc46764 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.expected.json @@ -362,6 +362,8 @@ "Type": "AWS::ECS::ClusterCapacityProviderAssociations", "Properties": { "CapacityProviders": [ + "FARGATE", + "FARGATE_SPOT", { "Ref": "EC2CapacityProvider5A2E35CD" } @@ -582,7 +584,6 @@ "LaunchConfigurationName": { "Ref": "ASGLaunchConfigC00AF12B" }, - "NewInstancesProtectedFromScaleIn": true, "Tags": [ { "Key": "Name", @@ -605,6 +606,292 @@ } } }, + "ASGDrainECSHookFunctionServiceRoleC12963BB": { + "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": "integ-ec2-capacity-provider/ASG" + } + ] + } + }, + "ASGDrainECSHookFunctionServiceRoleDefaultPolicy16848A27": { + "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": "ASG46ED3070" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EC2CPClusterD5F0FD32", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EC2CPClusterD5F0FD32", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "EC2CPClusterD5F0FD32", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ASGDrainECSHookFunctionServiceRoleDefaultPolicy16848A27", + "Roles": [ + { + "Ref": "ASGDrainECSHookFunctionServiceRoleC12963BB" + } + ] + } + }, + "ASGDrainECSHookFunction5F24CF4D": { + "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 task_arns = container_instance_task_arns(cluster, instance_arn)\n \n if task_arns:\n print('Instance ARN %s has task ARNs %s' % (instance_arn, ', '.join(task_arns)))\n\n while has_tasks(cluster, instance_arn, task_arns):\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\ndef container_instance_task_arns(cluster, instance_arn):\n \"\"\"Fetch tasks for a container instance ARN.\"\"\"\n arns = ecs.list_tasks(cluster=cluster, containerInstance=instance_arn)['taskArns']\n return arns\n\ndef has_tasks(cluster, instance_arn, task_arns):\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 task_count = None\n\n if task_arns:\n # Fetch details for tasks running on the container instance\n tasks = ecs.describe_tasks(cluster=cluster, tasks=task_arns)['tasks']\n if tasks:\n # Consider any non-stopped tasks as running\n task_count = sum(task['lastStatus'] != 'STOPPED' for task in tasks) + instance['pendingTasksCount']\n \n if not task_count:\n # Fallback to instance task counts if detailed task information is unavailable\n task_count = instance['runningTasksCount'] + instance['pendingTasksCount']\n \n print('Instance %s has %s tasks' % (instance_arn, task_count))\n\n return task_count > 0\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" + }, + "Role": { + "Fn::GetAtt": [ + "ASGDrainECSHookFunctionServiceRoleC12963BB", + "Arn" + ] + }, + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "EC2CPClusterD5F0FD32" + } + } + }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/ASG" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "ASGDrainECSHookFunctionServiceRoleDefaultPolicy16848A27", + "ASGDrainECSHookFunctionServiceRoleC12963BB" + ] + }, + "ASGDrainECSHookFunctionAllowInvokeintegec2capacityproviderASGLifecycleHookDrainHookTopic4714B3C1EB63E78F": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ASGDrainECSHookFunction5F24CF4D", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "ASGLifecycleHookDrainHookTopicA8AD4ACB" + } + } + }, + "ASGDrainECSHookFunctionTopicD6FC59F7": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "ASGLifecycleHookDrainHookTopicA8AD4ACB" + }, + "Endpoint": { + "Fn::GetAtt": [ + "ASGDrainECSHookFunction5F24CF4D", + "Arn" + ] + } + } + }, + "ASGLifecycleHookDrainHookRoleD640316C": { + "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": "integ-ec2-capacity-provider/ASG" + } + ] + } + }, + "ASGLifecycleHookDrainHookRoleDefaultPolicy3EEFDE57": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "ASGLifecycleHookDrainHookTopicA8AD4ACB" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ASGLifecycleHookDrainHookRoleDefaultPolicy3EEFDE57", + "Roles": [ + { + "Ref": "ASGLifecycleHookDrainHookRoleD640316C" + } + ] + } + }, + "ASGLifecycleHookDrainHookTopicA8AD4ACB": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-ec2-capacity-provider/ASG" + } + ] + } + }, + "ASGLifecycleHookDrainHookFE4AFEBE": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "ASG46ED3070" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "ASGLifecycleHookDrainHookTopicA8AD4ACB" + }, + "RoleARN": { + "Fn::GetAtt": [ + "ASGLifecycleHookDrainHookRoleD640316C", + "Arn" + ] + } + }, + "DependsOn": [ + "ASGLifecycleHookDrainHookRoleDefaultPolicy3EEFDE57", + "ASGLifecycleHookDrainHookRoleD640316C" + ] + }, "EC2CapacityProvider5A2E35CD": { "Type": "AWS::ECS::CapacityProvider", "Properties": { @@ -616,7 +903,7 @@ "Status": "ENABLED", "TargetCapacity": 100 }, - "ManagedTerminationProtection": "ENABLED" + "ManagedTerminationProtection": "DISABLED" } } }, diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.ts index f82ce6a9f9f56..3cc93ebaf2eb2 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.capacity-provider.ts @@ -10,6 +10,7 @@ const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); const cluster = new ecs.Cluster(stack, 'EC2CPCluster', { vpc, + enableFargateCapacityProviders: true, }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); @@ -27,6 +28,8 @@ const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'ASG', { const cp = new ecs.AsgCapacityProvider(stack, 'EC2CapacityProvider', { autoScalingGroup, + // This is to allow cdk destroy to work; otherwise deletion will hang bc ASG cannot be deleted + enableManagedTerminationProtection: false, }); cluster.addAsgCapacityProvider(cp); @@ -43,4 +46,3 @@ new ecs.Ec2Service(stack, 'EC2Service', { }); app.synth(); - diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index 3c383971844e8..df681647f45ab 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -148,6 +148,10 @@ nodeunitShim({ // THEN expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ABSENT, + })); + + expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], })); @@ -234,6 +238,10 @@ nodeunitShim({ // THEN expect(stack).to(haveResource('AWS::ECS::Cluster', { + CapacityProviders: ABSENT, + })); + + expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], })); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.capacity-providers.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.capacity-providers.expected.json index c0281dc8e14ae..7eb0eada4f244 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.capacity-providers.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.capacity-providers.expected.json @@ -356,12 +356,19 @@ } }, "FargateCPCluster668E71F2": { - "Type": "AWS::ECS::Cluster", + "Type": "AWS::ECS::Cluster" + }, + "FargateCPClusterBFD66A36": { + "Type": "AWS::ECS::ClusterCapacityProviderAssociations", "Properties": { "CapacityProviders": [ "FARGATE", "FARGATE_SPOT" - ] + ], + "Cluster": { + "Ref": "FargateCPCluster668E71F2" + }, + "DefaultCapacityProviderStrategy": [] } }, "TaskDefTaskRole1EDB4A67": { diff --git a/packages/@aws-cdk/aws-lambda-python/README.md b/packages/@aws-cdk/aws-lambda-python/README.md index 77c9fee5a6f7b..ffd19568aa5dc 100644 --- a/packages/@aws-cdk/aws-lambda-python/README.md +++ b/packages/@aws-cdk/aws-lambda-python/README.md @@ -44,6 +44,16 @@ If `requirements.txt` or `Pipfile` exists at the entry path, the construct will all required modules in a [Lambda compatible Docker container](https://gallery.ecr.aws/sam/build-python3.7) according to the `runtime`. +Python bundles are only recreated and published when a file in a source directory has changed. +Therefore (and as a general best-practice), it is highly recommended to commit a lockfile with a +list of all transitive dependencies and their exact versions. +This will ensure that when any dependency version is updated, the bundle asset is recreated and uploaded. + +To that end, we recommend using [`pipenv`] or [`poetry`] which has lockfile support. + +[`pipenv`]: https://pipenv-fork.readthedocs.io/en/latest/basics.html#example-pipfile-lock +[`poetry`]: https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control + **Lambda with a requirements.txt** ```plaintext diff --git a/packages/@aws-cdk/custom-resources/README.md b/packages/@aws-cdk/custom-resources/README.md index 4f13a0f63cbf6..ccf5765dea13e 100644 --- a/packages/@aws-cdk/custom-resources/README.md +++ b/packages/@aws-cdk/custom-resources/README.md @@ -268,8 +268,8 @@ This module includes a few examples for custom resource implementations: Provisions an object in an S3 bucket with textual contents. See the source code for the -[construct](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/custom-resources/test/provider-framework/integration-test-fixtures/s3-file.ts) and -[handler](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/custom-resources/test/provider-framework/integration-test-fixtures/s3-file-handler/index.ts). +[construct](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/custom-resources/test/provider-framework/integration-test-fixtures/s3-assert.ts) and +[handler](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/custom-resources/test/provider-framework/integration-test-fixtures/s3-assert-handler/index.py). The following example will create the file `folder/file1.txt` inside `myBucket` with the contents `hello!`. diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index a99dba1630f3c..d59446e8b8b7e 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -815,6 +815,26 @@ After turning on `privilegedMode: true`, you will need to do a one-time manual c pipeline to get it going again (as with a broken 'synth' the pipeline will not be able to self update to the right state). +### S3 error: Access Denied + +Some constructs, such as EKS clusters, generate nested stacks. When CloudFormation tries +to deploy those stacks, it may fail with this error: + +```console +S3 error: Access Denied For more information check http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html +``` + +This happens because the pipeline is not self-mutating and, as a consequence, the `FileAssetX` +build projects get out-of-sync with the generated templates. To fix this, make sure the +`selfMutating` property is set to `true`: + +```typescript +const pipeline = new CdkPipeline(this, 'MyPipeline', { + selfMutating: true, + ... +}); +``` + ## Current Limitations Limitations that we are aware of and will address: