From fb55002017385b61db8178c8bcb4839bab8647a9 Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Wed, 19 Jan 2022 09:24:48 -0500 Subject: [PATCH 01/11] Add Cluster.fromClusterArn to support cluster import --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 24 ++++++++++++++++++- .../@aws-cdk/aws-ecs/test/cluster.test.ts | 15 ++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 1dc50ac97ae49..cb9c4dc95bfa3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -6,7 +6,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; -import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, IConstruct } from '@aws-cdk/core'; +import { Duration, Lazy, IResource, Resource, Stack, Aspects, IAspect, IConstruct, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { BottleRocketImage, EcsOptimizedAmi } from './amis'; import { InstanceDrainHook } from './drain-hook/instance-drain-hook'; @@ -105,6 +105,28 @@ export class Cluster extends Resource implements ICluster { return new ImportedCluster(scope, id, attrs); } + /** + * Import an existing cluster to the stack from the cluster ARN. + */ + public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { + const stack = Stack.of(scope); + const clusterName = stack.splitArn(clusterArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName as string; + class Import extends Resource implements ICluster { + public readonly clusterArn = clusterArn; + public readonly clusterName = clusterName; + public readonly hasEc2Capacity = false; + public readonly connections = new ec2.Connections({ + securityGroups: [], + }); + get vpc(): ec2.IVpc { + throw new Error('vpc is not available for a Cluster imported using fromClusterArn(), please use fromClusterAttributes() instead.'); + } + } + return new Import(scope, id, { + environmentFromArn: clusterArn, + }); + } + /** * Manage the allowed network connections for the cluster with Security Groups. */ diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index f6b34f76439c0..1f21ff9ec062b 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -2140,6 +2140,21 @@ describe('cluster', () => { }); + + test('When importing ECS Cluster via Arn', () => { + // GIVEN + const stack = new cdk.Stack(); + const clusterName = 'my-cluster'; + const region = 'service-region'; + const account = 'service-account'; + const clusterArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}`; + const cluster = ecs.Cluster.fromClusterArn(stack, 'Cluster', clusterArn); + + // THEN + expect(cluster.clusterName).toEqual(clusterName); + expect(cluster.env.region).toEqual(region); + expect(cluster.env.account).toEqual(account); + }); }); test('can add ASG capacity via Capacity Provider by not specifying machineImageType', () => { From 17211440f86cc4b85356a7323784891916abaa0a Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Wed, 19 Jan 2022 09:35:06 -0500 Subject: [PATCH 02/11] throw errors on connections and hasEc2Capacity as well --- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index cb9c4dc95bfa3..ee52b05f6905d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -111,15 +111,19 @@ export class Cluster extends Resource implements ICluster { public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { const stack = Stack.of(scope); const clusterName = stack.splitArn(clusterArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName as string; + const errorSuffix = 'is not available for a Cluster imported using fromClusterArn(), please use fromClusterAttributes() instead.'; + class Import extends Resource implements ICluster { public readonly clusterArn = clusterArn; public readonly clusterName = clusterName; - public readonly hasEc2Capacity = false; - public readonly connections = new ec2.Connections({ - securityGroups: [], - }); + get hasEc2Capacity(): boolean { + throw new Error(`hasEc2Capacity ${errorSuffix}`); + } + get connections(): ec2.Connections { + throw new Error(`connections ${errorSuffix}`); + } get vpc(): ec2.IVpc { - throw new Error('vpc is not available for a Cluster imported using fromClusterArn(), please use fromClusterAttributes() instead.'); + throw new Error(`vpc ${errorSuffix}`); } } return new Import(scope, id, { From aa35e12cec163716d8c16610764310b8dbe51c6f Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Wed, 19 Jan 2022 10:29:18 -0500 Subject: [PATCH 03/11] update readme with fromClusterArn --- packages/@aws-cdk/aws-ecs/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index f1e50e28ea909..fb810d0f749d3 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -96,6 +96,15 @@ const cluster = new ecs.Cluster(this, 'Cluster', { }); ``` +The following code imports an existing cluster using the ARN which can be used to +import an Amazon ECS service either EC2 or Fargate. + +```ts +const clusterArn = 'arn:aws:ecs:us-east-1:012345678910:service/clusterName'; + +const cluster = ecs.Cluster.fromClusterArn(this, 'Cluster', clusterArn); +``` + To use tasks with Amazon EC2 launch-type, you have to add capacity to the cluster in order for tasks to be scheduled on your instances. Typically, you add an AutoScalingGroup with instances running the latest From 6dfaf318fa53094a71a1fd432ad4ad9314ac0979 Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Thu, 20 Jan 2022 08:51:21 -0500 Subject: [PATCH 04/11] fix clusterArn and fix test for resource name --- packages/@aws-cdk/aws-ecs/README.md | 2 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 10 ++++++++-- packages/@aws-cdk/aws-ecs/test/cluster.test.ts | 12 +++++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index fb810d0f749d3..d99b830aad6dd 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -100,7 +100,7 @@ The following code imports an existing cluster using the ARN which can be used t import an Amazon ECS service either EC2 or Fargate. ```ts -const clusterArn = 'arn:aws:ecs:us-east-1:012345678910:service/clusterName'; +const clusterArn = 'arn:aws:ecs:us-east-1:012345678910:cluster/clusterName'; const cluster = ecs.Cluster.fromClusterArn(this, 'Cluster', clusterArn); ``` diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index ee52b05f6905d..e147a696171c4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -110,12 +110,18 @@ export class Cluster extends Resource implements ICluster { */ public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { const stack = Stack.of(scope); - const clusterName = stack.splitArn(clusterArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName as string; + const arn = stack.splitArn(clusterArn, ArnFormat.SLASH_RESOURCE_NAME); + const clusterName = arn.resourceName; + + if (!clusterName) { + throw new Error(`Missing required Cluster Name from Cluster ARN: ${clusterArn}`); + } + const errorSuffix = 'is not available for a Cluster imported using fromClusterArn(), please use fromClusterAttributes() instead.'; class Import extends Resource implements ICluster { public readonly clusterArn = clusterArn; - public readonly clusterName = clusterName; + public readonly clusterName = clusterName!; get hasEc2Capacity(): boolean { throw new Error(`hasEc2Capacity ${errorSuffix}`); } diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index 1f21ff9ec062b..c80f36c2a143a 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -2147,7 +2147,7 @@ describe('cluster', () => { const clusterName = 'my-cluster'; const region = 'service-region'; const account = 'service-account'; - const clusterArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}`; + const clusterArn = `arn:aws:ecs:${region}:${account}:cluster/${clusterName}`; const cluster = ecs.Cluster.fromClusterArn(stack, 'Cluster', clusterArn); // THEN @@ -2155,6 +2155,16 @@ describe('cluster', () => { expect(cluster.env.region).toEqual(region); expect(cluster.env.account).toEqual(account); }); + + test('throws error when import ECS Cluster without resource name in arn', () => { + // GIVEN + const stack = new cdk.Stack(); + const clusterArn = 'arn:aws:ecs:service-region:service-account:cluster'; + // THEN + expect(() => { + ecs.Cluster.fromClusterArn(stack, 'Cluster', clusterArn); + }).toThrowError(/Missing required Cluster Name from Cluster ARN: /); + }); }); test('can add ASG capacity via Capacity Provider by not specifying machineImageType', () => { From fc96ff4d4a35d7784652eb5ad5e3d0364f716aab Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Thu, 20 Jan 2022 16:52:17 -0500 Subject: [PATCH 05/11] add BaseService.fromServiceArnWithCluster --- .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 42 ++++++++++++++- .../aws-ecs/test/base-service.test.ts | 54 +++++++++++++++++++ .../aws-ecs/test/ec2/ec2-service.test.ts | 49 +++++++++++++++++ .../test/fargate/fargate-service.test.ts | 50 +++++++++++++++++ 4 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-ecs/test/base-service.test.ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 1809000064f12..b5ccb5b061482 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -5,10 +5,10 @@ import * as elb from '@aws-cdk/aws-elasticloadbalancing'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as iam from '@aws-cdk/aws-iam'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; -import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/task-definition'; -import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging } from '../cluster'; +import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging, Cluster } from '../cluster'; import { ContainerDefinition, Protocol } from '../container-definition'; import { CfnService } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; @@ -315,6 +315,44 @@ export interface IBaseService extends IService { */ export abstract class BaseService extends Resource implements IBaseService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { + /** + * Import an existing ECS/Fargate Service using the service cluster format. + */ + public static fromServiceArnWithCluster(scope: Construct, id: string, serviceArn: string): IBaseService { + const stack = Stack.of(scope); + const arn = stack.splitArn(serviceArn, ArnFormat.SLASH_RESOURCE_NAME); + const resourceName = arn.resourceName; + if (!resourceName) { + throw new Error('Missing resource Name from service ARN: ${serviceArn}'); + } + + if (resourceName.split('/').length !== 2) { + throw new Error(`resource name ${resourceName} from service ARN: ${serviceArn} is not using the ARN cluster format`); + } + const clusterName = resourceName.split('/')[0]; + const serviceName = resourceName.split('/')[1]; + + const clusterArn = Stack.of(scope).formatArn({ + partition: arn.partition, + region: arn.region, + account: arn.account, + service: 'ecs', + resource: 'cluster', + resourceName: clusterName, + }); + + const cluster = Cluster.fromClusterArn(scope, `${id}Cluster`, clusterArn); + + class Import extends Resource implements IBaseService { + public readonly serviceArn = serviceArn; + public readonly serviceName = serviceName; + public readonly cluster = cluster; + + } + return new Import(scope, id, { + environmentFromArn: serviceArn, + }); + } /** * The security groups which manage the allowed network traffic for the service. diff --git a/packages/@aws-cdk/aws-ecs/test/base-service.test.ts b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts new file mode 100644 index 0000000000000..d2c43a5b16b5e --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts @@ -0,0 +1,54 @@ +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../lib'; + +describe('When import an ECS Service', () => { + test('with serviceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const clusterName = 'cluster-name'; + const serviceName = 'my-http-service'; + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; + + // WHEN + const service = ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', serviceArn); + + // THEN + expect(service.serviceArn).toEqual(serviceArn); + expect(service.serviceName).toEqual(serviceName); + expect(service.env.account).toEqual(account); + expect(service.env.region).toEqual(region); + + expect(service.cluster.clusterName).toEqual(clusterName); + expect(service.cluster.env.account).toEqual(account); + expect(service.cluster.env.region).toEqual(region); + }); + + test('throws an expection if no resourceName provided on fromServiceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service`; + + //THEN + expect(() => { + ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', serviceArn); + }).toThrowError(/Missing resource Name from service ARN/); + }); + + test('throws an expection if not using cluster arn format on fromServiceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const region = 'service-region'; + const account = 'service-account'; + const serviceName = 'my-http-service'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${serviceName}`; + + //THEN + expect(() => { + ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', serviceArn); + }).toThrowError(`resource name ${serviceName} from service ARN`); + }); +}); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index 85c5d68568b22..b328ee4ee3df6 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -3333,5 +3333,54 @@ describe('ec2 service', () => { }); + test('with serviceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const clusterName = 'cluster-name'; + const serviceName = 'my-http-service'; + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; + + // WHEN + const service = ecs.Ec2Service.fromServiceArnWithCluster(stack, 'EcsService', serviceArn); + + // THEN + expect(service.serviceArn).toEqual(serviceArn); + expect(service.serviceName).toEqual(serviceName); + expect(service.env.account).toEqual(account); + expect(service.env.region).toEqual(region); + + expect(service.cluster.clusterName).toEqual(clusterName); + expect(service.cluster.env.account).toEqual(account); + expect(service.cluster.env.region).toEqual(region); + }); + + test('throws an expection if no resourceName provided on fromServiceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service`; + + //THEN + expect(() => { + ecs.Ec2Service.fromServiceArnWithCluster(stack, 'EcsService', serviceArn); + }).toThrowError(/Missing resource Name from service ARN/); + }); + + test('throws an expection if not using cluster arn format on fromServiceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const region = 'service-region'; + const account = 'service-account'; + const serviceName = 'my-http-service'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${serviceName}`; + + //THEN + expect(() => { + ecs.Ec2Service.fromServiceArnWithCluster(stack, 'EcsService', serviceArn); + }).toThrowError(`resource name ${serviceName} from service ARN`); + }); }); }); 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 3780c43903284..5b7b3b5210cdc 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 @@ -2212,6 +2212,56 @@ describe('fargate service', () => { }); + test('with serviceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const clusterName = 'cluster-name'; + const serviceName = 'my-http-service'; + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; + + // WHEN + const service = ecs.FargateService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); + + // THEN + expect(service.serviceArn).toEqual(serviceArn); + expect(service.serviceName).toEqual(serviceName); + expect(service.env.account).toEqual(account); + expect(service.env.region).toEqual(region); + + expect(service.cluster.clusterName).toEqual(clusterName); + expect(service.cluster.env.account).toEqual(account); + expect(service.cluster.env.region).toEqual(region); + }); + + test('throws an expection if no resourceName provided on fromServiceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service`; + + //THEN + expect(() => { + ecs.FargateService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); + }).toThrowError(/Missing resource Name from service ARN/); + }); + + test('throws an expection if not using cluster arn format on fromServiceArnWithCluster', () => { + // GIVEN + const stack = new cdk.Stack(); + const region = 'service-region'; + const account = 'service-account'; + const serviceName = 'my-http-service'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${serviceName}`; + + //THEN + expect(() => { + ecs.FargateService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); + }).toThrowError(`resource name ${serviceName} from service ARN`); + }); + test('allows setting enable execute command', () => { // GIVEN const stack = new cdk.Stack(); From 9160bc0dc8b6cb50a6b91c308efcd87920cc9974 Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Fri, 21 Jan 2022 08:58:33 -0500 Subject: [PATCH 06/11] remove fargate and ec2 tests, fix docs, add test and readme in codepipeline actions --- .../aws-codepipeline-actions/README.md | 25 ++++++ .../test/ecs/ecs-deploy-action.test.ts | 78 +++++++++++++++++++ .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 12 +-- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 3 + .../aws-ecs/test/base-service.test.ts | 28 +++---- .../@aws-cdk/aws-ecs/test/cluster.test.ts | 7 +- .../aws-ecs/test/ec2/ec2-service.test.ts | 49 ------------ .../test/fargate/fargate-service.test.ts | 50 ------------ 8 files changed, 125 insertions(+), 127 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index d3569b4ea5872..6c011cb91748f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -764,6 +764,31 @@ const deployStage = pipeline.addStage({ [image definition file]: https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions +#### Deploying ECS applications with existing ECS service ARN + +CodePipeline can deploy an ECS service which [cluster formatted ECS service ARN](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids) this can be deploy across region and accounts as well using the ARN. + +```ts +import * as ecs from `@aws-cdk/aws-ecs`; + +const service = ecs.BaseService.fromServiceArnWithCluster(this, 'EcsService', + 'arn:aws:ecs:us-east-1:123456789012:service/myClusterName}/myServiceName' +); +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const buildOutput = new codepipeline.Artifact(); +const deployStage = pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new codepipeline_actions.EcsDeployAction({ + actionName: 'DeployAction', + service: service, + input: buildOutput, + }), + ], +}); +``` + + #### Deploying ECS applications stored in a separate source code repository The idiomatic CDK way of deploying an ECS application is to have your Dockerfiles and your CDK code in the same source code repository, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts index 63927d5832ec8..da8e957c994fe 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts @@ -196,6 +196,84 @@ describe('ecs deploy action', () => { }); + + test('can be created by existing service with cluster ARN format', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'PipelineStack', { + env: { + region: 'pipeline-region', account: 'pipeline-account', + }, + }); + const clusterName = 'cluster-name'; + const serviceName = 'service-name'; + const region = 'service-region'; + const account = 'service-account'; + const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; + const service = ecs.BaseService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); + + const artifact = new codepipeline.Artifact('Artifact'); + const bucket = new s3.Bucket(stack, 'PipelineBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + const source = new cpactions.S3SourceAction({ + actionName: 'Source', + output: artifact, + bucket, + bucketKey: 'key', + }); + const action = new cpactions.EcsDeployAction({ + actionName: 'ECS', + service: service, + input: artifact, + }); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [source], + }, + { + stageName: 'Deploy', + actions: [action], + }, + ], + }); + + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + {}, + { + Actions: [ + { + Name: 'ECS', + ActionTypeId: { + Category: 'Deploy', + Provider: 'ECS', + }, + Configuration: { + ClusterName: clusterName, + ServiceName: serviceName, + }, + Region: 'service-region', + RoleArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::service-account:role/pipelinestack-support-serloyecsactionrole49867f847238c85af7c0', + ], + ], + }, + }, + ], + }, + ], + }); + }); }); }); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index b5ccb5b061482..486c95c9f3ef4 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -317,6 +317,8 @@ export abstract class BaseService extends Resource implements IBaseService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { /** * Import an existing ECS/Fargate Service using the service cluster format. + * The format is the "new" format "arn:aws:ecs:region:aws_account_id:service/cluster-name/service-name" + * see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids */ public static fromServiceArnWithCluster(scope: Construct, id: string, serviceArn: string): IBaseService { const stack = Stack.of(scope); @@ -325,12 +327,12 @@ export abstract class BaseService extends Resource if (!resourceName) { throw new Error('Missing resource Name from service ARN: ${serviceArn}'); } - - if (resourceName.split('/').length !== 2) { + const resourceNameParts = resourceName.split('/'); + if (resourceNameParts.length !== 2) { throw new Error(`resource name ${resourceName} from service ARN: ${serviceArn} is not using the ARN cluster format`); } - const clusterName = resourceName.split('/')[0]; - const serviceName = resourceName.split('/')[1]; + const clusterName = resourceNameParts[0]; + const serviceName = resourceNameParts[1]; const clusterArn = Stack.of(scope).formatArn({ partition: arn.partition, @@ -347,8 +349,8 @@ export abstract class BaseService extends Resource public readonly serviceArn = serviceArn; public readonly serviceName = serviceName; public readonly cluster = cluster; - } + return new Import(scope, id, { environmentFromArn: serviceArn, }); diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index e147a696171c4..23116a6e1e215 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -107,6 +107,8 @@ export class Cluster extends Resource implements ICluster { /** * Import an existing cluster to the stack from the cluster ARN. + * This does not provide access to the vpc, hasEc2Capacity, or connections use fromClusterAttributes to + * access those properties. */ public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { const stack = Stack.of(scope); @@ -132,6 +134,7 @@ export class Cluster extends Resource implements ICluster { throw new Error(`vpc ${errorSuffix}`); } } + return new Import(scope, id, { environmentFromArn: clusterArn, }); diff --git a/packages/@aws-cdk/aws-ecs/test/base-service.test.ts b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts index d2c43a5b16b5e..6e3b563cf4e1e 100644 --- a/packages/@aws-cdk/aws-ecs/test/base-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/base-service.test.ts @@ -1,10 +1,15 @@ import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; +let stack: cdk.Stack; + +beforeEach(() => { + stack = new cdk.Stack(); +}); + describe('When import an ECS Service', () => { test('with serviceArnWithCluster', () => { // GIVEN - const stack = new cdk.Stack(); const clusterName = 'cluster-name'; const serviceName = 'my-http-service'; const region = 'service-region'; @@ -26,29 +31,14 @@ describe('When import an ECS Service', () => { }); test('throws an expection if no resourceName provided on fromServiceArnWithCluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const region = 'service-region'; - const account = 'service-account'; - const serviceArn = `arn:aws:ecs:${region}:${account}:service`; - - //THEN expect(() => { - ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', serviceArn); + ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', 'arn:aws:ecs:service-region:service-account:service'); }).toThrowError(/Missing resource Name from service ARN/); }); test('throws an expection if not using cluster arn format on fromServiceArnWithCluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const region = 'service-region'; - const account = 'service-account'; - const serviceName = 'my-http-service'; - const serviceArn = `arn:aws:ecs:${region}:${account}:service/${serviceName}`; - - //THEN expect(() => { - ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', serviceArn); - }).toThrowError(`resource name ${serviceName} from service ARN`); + ecs.BaseService.fromServiceArnWithCluster(stack, 'Service', 'arn:aws:ecs:service-region:service-account:service/my-http-service'); + }).toThrowError(/is not using the ARN cluster format/); }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index c80f36c2a143a..d167c30989ded 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -2147,8 +2147,7 @@ describe('cluster', () => { const clusterName = 'my-cluster'; const region = 'service-region'; const account = 'service-account'; - const clusterArn = `arn:aws:ecs:${region}:${account}:cluster/${clusterName}`; - const cluster = ecs.Cluster.fromClusterArn(stack, 'Cluster', clusterArn); + const cluster = ecs.Cluster.fromClusterArn(stack, 'Cluster', `arn:aws:ecs:${region}:${account}:cluster/${clusterName}`); // THEN expect(cluster.clusterName).toEqual(clusterName); @@ -2159,10 +2158,10 @@ describe('cluster', () => { test('throws error when import ECS Cluster without resource name in arn', () => { // GIVEN const stack = new cdk.Stack(); - const clusterArn = 'arn:aws:ecs:service-region:service-account:cluster'; + // THEN expect(() => { - ecs.Cluster.fromClusterArn(stack, 'Cluster', clusterArn); + ecs.Cluster.fromClusterArn(stack, 'Cluster', 'arn:aws:ecs:service-region:service-account:cluster'); }).toThrowError(/Missing required Cluster Name from Cluster ARN: /); }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index b328ee4ee3df6..85c5d68568b22 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -3333,54 +3333,5 @@ describe('ec2 service', () => { }); - test('with serviceArnWithCluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const clusterName = 'cluster-name'; - const serviceName = 'my-http-service'; - const region = 'service-region'; - const account = 'service-account'; - const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; - - // WHEN - const service = ecs.Ec2Service.fromServiceArnWithCluster(stack, 'EcsService', serviceArn); - - // THEN - expect(service.serviceArn).toEqual(serviceArn); - expect(service.serviceName).toEqual(serviceName); - expect(service.env.account).toEqual(account); - expect(service.env.region).toEqual(region); - - expect(service.cluster.clusterName).toEqual(clusterName); - expect(service.cluster.env.account).toEqual(account); - expect(service.cluster.env.region).toEqual(region); - }); - - test('throws an expection if no resourceName provided on fromServiceArnWithCluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const region = 'service-region'; - const account = 'service-account'; - const serviceArn = `arn:aws:ecs:${region}:${account}:service`; - - //THEN - expect(() => { - ecs.Ec2Service.fromServiceArnWithCluster(stack, 'EcsService', serviceArn); - }).toThrowError(/Missing resource Name from service ARN/); - }); - - test('throws an expection if not using cluster arn format on fromServiceArnWithCluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const region = 'service-region'; - const account = 'service-account'; - const serviceName = 'my-http-service'; - const serviceArn = `arn:aws:ecs:${region}:${account}:service/${serviceName}`; - - //THEN - expect(() => { - ecs.Ec2Service.fromServiceArnWithCluster(stack, 'EcsService', serviceArn); - }).toThrowError(`resource name ${serviceName} from service ARN`); - }); }); }); 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 5b7b3b5210cdc..3780c43903284 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 @@ -2212,56 +2212,6 @@ describe('fargate service', () => { }); - test('with serviceArnWithCluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const clusterName = 'cluster-name'; - const serviceName = 'my-http-service'; - const region = 'service-region'; - const account = 'service-account'; - const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/${serviceName}`; - - // WHEN - const service = ecs.FargateService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); - - // THEN - expect(service.serviceArn).toEqual(serviceArn); - expect(service.serviceName).toEqual(serviceName); - expect(service.env.account).toEqual(account); - expect(service.env.region).toEqual(region); - - expect(service.cluster.clusterName).toEqual(clusterName); - expect(service.cluster.env.account).toEqual(account); - expect(service.cluster.env.region).toEqual(region); - }); - - test('throws an expection if no resourceName provided on fromServiceArnWithCluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const region = 'service-region'; - const account = 'service-account'; - const serviceArn = `arn:aws:ecs:${region}:${account}:service`; - - //THEN - expect(() => { - ecs.FargateService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); - }).toThrowError(/Missing resource Name from service ARN/); - }); - - test('throws an expection if not using cluster arn format on fromServiceArnWithCluster', () => { - // GIVEN - const stack = new cdk.Stack(); - const region = 'service-region'; - const account = 'service-account'; - const serviceName = 'my-http-service'; - const serviceArn = `arn:aws:ecs:${region}:${account}:service/${serviceName}`; - - //THEN - expect(() => { - ecs.FargateService.fromServiceArnWithCluster(stack, 'FargateService', serviceArn); - }).toThrowError(`resource name ${serviceName} from service ARN`); - }); - test('allows setting enable execute command', () => { // GIVEN const stack = new cdk.Stack(); From 25a0e6bf7c9d63e08a8c3b9722defa9bfa899c0d Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Fri, 21 Jan 2022 09:50:41 -0500 Subject: [PATCH 07/11] fix test after merge from master --- .../aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts index 264c9202d5a36..6b9f494837a8e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts @@ -240,7 +240,7 @@ describe('ecs deploy action', () => { ], }); - expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ {}, { From d7a7ef7f477023bbf32221256f720b23e365093e Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Fri, 21 Jan 2022 10:40:28 -0500 Subject: [PATCH 08/11] fix typo in readme --- packages/@aws-cdk/aws-codepipeline-actions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index 6c011cb91748f..a144a16302bb2 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -769,7 +769,7 @@ const deployStage = pipeline.addStage({ CodePipeline can deploy an ECS service which [cluster formatted ECS service ARN](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids) this can be deploy across region and accounts as well using the ARN. ```ts -import * as ecs from `@aws-cdk/aws-ecs`; +import * as ecs from '@aws-cdk/aws-ecs'; const service = ecs.BaseService.fromServiceArnWithCluster(this, 'EcsService', 'arn:aws:ecs:us-east-1:123456789012:service/myClusterName}/myServiceName' From 359f1eff23fcdb3181019e49cd14bdc0d8c9699a Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Fri, 21 Jan 2022 13:18:55 -0500 Subject: [PATCH 09/11] remove extra '}' in readme --- packages/@aws-cdk/aws-codepipeline-actions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index a144a16302bb2..e7edb5268d814 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -772,7 +772,7 @@ CodePipeline can deploy an ECS service which [cluster formatted ECS service ARN] import * as ecs from '@aws-cdk/aws-ecs'; const service = ecs.BaseService.fromServiceArnWithCluster(this, 'EcsService', - 'arn:aws:ecs:us-east-1:123456789012:service/myClusterName}/myServiceName' + 'arn:aws:ecs:us-east-1:123456789012:service/myClusterName/myServiceName' ); const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const buildOutput = new codepipeline.Artifact(); From f84c6aef71908d84e78d7575a951157efb65afe6 Mon Sep 17 00:00:00 2001 From: Toby Tipton Date: Fri, 21 Jan 2022 17:23:51 -0500 Subject: [PATCH 10/11] add note able role to readme --- packages/@aws-cdk/aws-codepipeline-actions/README.md | 4 ++++ .../test/ecs/ecs-deploy-action.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index e7edb5268d814..4e16e1d0bb9b8 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -788,6 +788,10 @@ const deployStage = pipeline.addStage({ }); ``` +When deploying across accounts especially in a CDK self mutating pipeline it is recommended to provide +the `role` on the `EcsDeployAction`. The role will need to have permissions assigned to it for +ECS deployment see [EcsDeployAction](https://github.com/aws/aws-cdk/blob/d735ce4bc9049b68b830641d68c97f4be565968d/packages/%40aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts#L85-L110) for proper permissions to apply to the role. + #### Deploying ECS applications stored in a separate source code repository diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts index 6b9f494837a8e..0841d47946a23 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts @@ -255,7 +255,7 @@ describe('ecs deploy action', () => { ClusterName: clusterName, ServiceName: serviceName, }, - Region: 'service-region', + Region: region, RoleArn: { 'Fn::Join': [ '', @@ -264,7 +264,7 @@ describe('ecs deploy action', () => { { Ref: 'AWS::Partition', }, - ':iam::service-account:role/pipelinestack-support-serloyecsactionrole49867f847238c85af7c0', + `:iam::${account}:role/pipelinestack-support-serloyecsactionrole49867f847238c85af7c0`, ], ], }, From 3b7afce52d6039a16e8c9d231df0b5bb0a36cd4d Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 21 Jan 2022 15:29:32 -0800 Subject: [PATCH 11/11] Applu doc wording suggestions --- .../@aws-cdk/aws-codepipeline-actions/README.md | 16 ++++++++++------ .../@aws-cdk/aws-ecs/lib/base/base-service.ts | 4 ++-- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index 4e16e1d0bb9b8..ff43850561651 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -764,9 +764,11 @@ const deployStage = pipeline.addStage({ [image definition file]: https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-create.html#pipelines-create-image-definitions -#### Deploying ECS applications with existing ECS service ARN +#### Deploying ECS applications to existing services -CodePipeline can deploy an ECS service which [cluster formatted ECS service ARN](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids) this can be deploy across region and accounts as well using the ARN. +CodePipeline can deploy to an existing ECS service which uses the +[ECS service ARN format that contains the Cluster name](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids). +This also works if the service is in a different account and/or region than the pipeline: ```ts import * as ecs from '@aws-cdk/aws-ecs'; @@ -776,6 +778,7 @@ const service = ecs.BaseService.fromServiceArnWithCluster(this, 'EcsService', ); const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); const buildOutput = new codepipeline.Artifact(); +// add source and build stages to the pipeline as usual... const deployStage = pipeline.addStage({ stageName: 'Deploy', actions: [ @@ -788,10 +791,11 @@ const deployStage = pipeline.addStage({ }); ``` -When deploying across accounts especially in a CDK self mutating pipeline it is recommended to provide -the `role` on the `EcsDeployAction`. The role will need to have permissions assigned to it for -ECS deployment see [EcsDeployAction](https://github.com/aws/aws-cdk/blob/d735ce4bc9049b68b830641d68c97f4be565968d/packages/%40aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts#L85-L110) for proper permissions to apply to the role. - +When deploying across accounts, especially in a CDK Pipelines self-mutating pipeline, +it is recommended to provide the `role` property to the `EcsDeployAction`. +The Role will need to have permissions assigned to it for ECS deployment. +See [the CodePipeline documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/how-to-custom-role.html#how-to-update-role-new-services) +for the permissions needed. #### Deploying ECS applications stored in a separate source code repository diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index df8b18b383ba4..4e5de4c90eacf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -317,8 +317,8 @@ export abstract class BaseService extends Resource implements IBaseService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { /** * Import an existing ECS/Fargate Service using the service cluster format. - * The format is the "new" format "arn:aws:ecs:region:aws_account_id:service/cluster-name/service-name" - * see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids + * The format is the "new" format "arn:aws:ecs:region:aws_account_id:service/cluster-name/service-name". + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids */ public static fromServiceArnWithCluster(scope: Construct, id: string, serviceArn: string): IBaseService { const stack = Stack.of(scope); diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 23116a6e1e215..8e48e2be59cec 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -107,8 +107,8 @@ export class Cluster extends Resource implements ICluster { /** * Import an existing cluster to the stack from the cluster ARN. - * This does not provide access to the vpc, hasEc2Capacity, or connections use fromClusterAttributes to - * access those properties. + * This does not provide access to the vpc, hasEc2Capacity, or connections - + * use the `fromClusterAttributes` method to access those properties. */ public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { const stack = Stack.of(scope);