diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 0b61cddb999f4..bf67ee5437d26 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -703,6 +703,37 @@ There are two higher-level constructs available which include a load balancer fo - `LoadBalancedFargateService` - `LoadBalancedEc2Service` +### Import existing services + +`Ec2Service` and `FargateService` provide methods to import existing EC2/Fargate services. +The ARN of the existing service has to be specified to import the service. + +Since AWS has changed the [ARN format for ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids), +feature flag `@aws-cdk/aws-ecs:arnFormatIncludesClusterName` must be enabled to use the new ARN format. +The feature flag changes behavior for the entire CDK project. Therefore it is not possible to mix the old and the new format in one CDK project. + +```tss +declare const cluster: ecs.Cluster; + +// Import service from EC2 service attributes +const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', + cluster, +}); + +// Import service from EC2 service ARN +const service = ecs.Ec2Service.fromEc2ServiceArn(stack, 'EcsService', 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + +// Import service from Fargate service attributes +const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', + cluster, +}); + +// Import service from Fargate service ARN +const service = ecs.FargateService.fromFargateServiceArn(stack, 'EcsService', 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); +``` + ## Task Auto-Scaling You can configure the task count of a service to match demand. Task auto-scaling is diff --git a/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts b/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts index 7a9cbc0d28563..ad135288179cb 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/from-service-attributes.ts @@ -1,4 +1,5 @@ -import { ArnFormat, Resource, Stack } from '@aws-cdk/core'; +import { ArnFormat, FeatureFlags, Fn, Resource, Stack, Token } from '@aws-cdk/core'; +import { ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME } from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { IBaseService } from '../base/base-service'; import { ICluster } from '../cluster'; @@ -32,22 +33,25 @@ export function fromServiceAttributes(scope: Construct, id: string, attrs: Servi throw new Error('You can only specify either serviceArn or serviceName.'); } + const newArnFormat = FeatureFlags.of(scope).isEnabled(ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME); + const stack = Stack.of(scope); let name: string; let arn: string; if (attrs.serviceName) { name = attrs.serviceName as string; + const resourceName = newArnFormat ? `${attrs.cluster.clusterName}/${attrs.serviceName}` : attrs.serviceName as string; arn = stack.formatArn({ partition: stack.partition, service: 'ecs', region: stack.region, account: stack.account, resource: 'service', - resourceName: name, + resourceName, }); } else { arn = attrs.serviceArn as string; - name = stack.splitArn(arn, ArnFormat.SLASH_RESOURCE_NAME).resourceName as string; + name = extractServiceNameFromArn(scope, arn); } class Import extends Resource implements IBaseService { public readonly serviceArn = arn; @@ -58,3 +62,22 @@ export function fromServiceAttributes(scope: Construct, id: string, attrs: Servi environmentFromArn: arn, }); } + +export function extractServiceNameFromArn(scope: Construct, arn: string): string { + const newArnFormat = FeatureFlags.of(scope).isEnabled(ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME); + const stack = Stack.of(scope); + + if (Token.isUnresolved(arn)) { + if (newArnFormat) { + const components = Fn.split(':', arn); + const lastComponents = Fn.split('/', Fn.select(5, components)); + return Fn.select(2, lastComponents); + } else { + return stack.splitArn(arn, ArnFormat.SLASH_RESOURCE_NAME).resourceName as string; + } + } else { + const resourceName = stack.splitArn(arn, ArnFormat.SLASH_RESOURCE_NAME).resourceName as string; + const resourceNameSplit = resourceName.split('/'); + return resourceNameSplit.length === 1 ? resourceName : resourceNameSplit[1]; + } +} \ No newline at end of file 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 3c35c3ef90ceb..12e8a44b6cf8b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -1,8 +1,8 @@ import * as ec2 from '@aws-cdk/aws-ec2'; -import { ArnFormat, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { BaseService, BaseServiceOptions, DeploymentControllerType, IBaseService, IService, LaunchType } from '../base/base-service'; -import { fromServiceAttributes } from '../base/from-service-attributes'; +import { fromServiceAttributes, extractServiceNameFromArn } from '../base/from-service-attributes'; import { NetworkMode, TaskDefinition } from '../base/task-definition'; import { ICluster } from '../cluster'; import { CfnService } from '../ecs.generated'; @@ -128,7 +128,7 @@ export class Ec2Service extends BaseService implements IEc2Service { public static fromEc2ServiceArn(scope: Construct, id: string, ec2ServiceArn: string): IEc2Service { class Import extends Resource implements IEc2Service { public readonly serviceArn = ec2ServiceArn; - public readonly serviceName = Stack.of(scope).splitArn(ec2ServiceArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName as string; + public readonly serviceName = extractServiceNameFromArn(this, ec2ServiceArn); } return new Import(scope, id); } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index a1ae858d0be61..f0fcedbebe1f3 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -1,9 +1,8 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { BaseService, BaseServiceOptions, DeploymentControllerType, IBaseService, IService, LaunchType } from '../base/base-service'; -import { fromServiceAttributes } from '../base/from-service-attributes'; +import { fromServiceAttributes, extractServiceNameFromArn } from '../base/from-service-attributes'; import { TaskDefinition } from '../base/task-definition'; import { ICluster } from '../cluster'; @@ -105,7 +104,7 @@ export class FargateService extends BaseService implements IFargateService { public static fromFargateServiceArn(scope: Construct, id: string, fargateServiceArn: string): IFargateService { class Import extends cdk.Resource implements IFargateService { public readonly serviceArn = fargateServiceArn; - public readonly serviceName = cdk.Stack.of(scope).splitArn(fargateServiceArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName as string; + public readonly serviceName = extractServiceNameFromArn(this, fargateServiceArn); } return new Import(scope, id); } 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 aaea8043709b5..43df7f990f040 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 @@ -9,6 +9,8 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; +import { App } from '@aws-cdk/core'; +import { ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME } from '@aws-cdk/cx-api'; import * as ecs from '../../lib'; import { DeploymentControllerType, LaunchType, PropagatedTagSource } from '../../lib/base/base-service'; import { PlacementConstraint, PlacementStrategy } from '../../lib/placement'; @@ -3227,7 +3229,103 @@ describe('ec2 service', () => { }); describe('When import an EC2 Service', () => { - test('with serviceArn', () => { + test('fromEc2ServiceArn old format', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceArn(stack, 'EcsService', 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + + // THEN + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + expect(service.serviceName).toEqual('my-http-service'); + }); + + test('fromEc2ServiceArn new format', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceArn(stack, 'EcsService', 'arn:aws:ecs:us-west-2:123456789012:service/my-cluster-name/my-http-service'); + + // THEN + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-cluster-name/my-http-service'); + expect(service.serviceName).toEqual('my-http-service'); + }); + + describe('fromEc2ServiceArn tokenized ARN', () => { + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is disabled, use old ARN format', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceArn(stack, 'EcsService', new cdk.CfnParameter(stack, 'ARN').valueAsString); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual({ Ref: 'ARN' }); + expect(stack.resolve(service.serviceName)).toEqual({ + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }, + ], + }, + ], + }); + }); + + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is enabled, use new ARN format', () => { + // GIVEN + const app = new App({ + context: { + [ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME]: true, + }, + }); + + const stack = new cdk.Stack(app); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceArn(stack, 'EcsService', new cdk.CfnParameter(stack, 'ARN').valueAsString); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual({ Ref: 'ARN' }); + expect(stack.resolve(service.serviceName)).toEqual({ + 'Fn::Select': [ + 2, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }, + ], + }, + ], + }); + }); + }); + + test('with serviceArn old format', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -3247,23 +3345,189 @@ describe('ec2 service', () => { }); - test('with serviceName', () => { + test('with serviceArn new format', () => { // GIVEN const stack = new cdk.Stack(); - const pseudo = new cdk.ScopedAws(stack); const cluster = new ecs.Cluster(stack, 'EcsCluster'); // WHEN const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { - serviceName: 'my-http-service', + serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-cluster-name/my-http-service', cluster, }); // THEN - expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-cluster-name/my-http-service'); expect(service.serviceName).toEqual('my-http-service'); + expect(service.env.account).toEqual('123456789012'); + expect(service.env.region).toEqual('us-west-2'); + }); + + describe('with serviceArn tokenized ARN', () => { + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is disabled, use old ARN format', () => { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + serviceArn: new cdk.CfnParameter(stack, 'ARN').valueAsString, + cluster, + }); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual({ Ref: 'ARN' }); + expect(stack.resolve(service.serviceName)).toEqual({ + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }, + ], + }, + ], + }); + + expect(stack.resolve(service.env.account)).toEqual({ + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }); + expect(stack.resolve(service.env.region)).toEqual({ + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }); + }); + + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is enabled, use new ARN format', () => { + // GIVEN + const app = new App({ + context: { + [ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME]: true, + }, + }); + const stack = new cdk.Stack(app); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + serviceArn: new cdk.CfnParameter(stack, 'ARN').valueAsString, + cluster, + }); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual({ Ref: 'ARN' }); + expect(stack.resolve(service.serviceName)).toEqual({ + 'Fn::Select': [ + 2, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }, + ], + }, + ], + }); + + expect(stack.resolve(service.env.account)).toEqual({ + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }); + expect(stack.resolve(service.env.region)).toEqual({ + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }); + }); + }); + + describe('with serviceName', () => { + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is disabled, use old ARN format', () => { + // GIVEN + const stack = new cdk.Stack(); + const pseudo = new cdk.ScopedAws(stack); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + serviceName: 'my-http-service', + cluster, + }); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + expect(service.serviceName).toEqual('my-http-service'); + }); + + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is enabled, use new ARN format', () => { + // GIVEN + const app = new App({ + context: { + [ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME]: true, + }, + }); + const stack = new cdk.Stack(app); + const pseudo = new cdk.ScopedAws(stack); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { + serviceName: 'my-http-service', + cluster, + }); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/${cluster.clusterName}/my-http-service`)); + expect(service.serviceName).toEqual('my-http-service'); + }); }); test('throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes', () => { 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 c3f36b13f22cb..6d6c433d12f07 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 @@ -10,6 +10,8 @@ import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; +import { App } from '@aws-cdk/core'; +import { ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME } from '@aws-cdk/cx-api'; import * as ecs from '../../lib'; import { DeploymentControllerType, LaunchType, PropagatedTagSource } from '../../lib/base/base-service'; import { addDefaultCapacityProvider } from '../util'; @@ -2083,7 +2085,102 @@ describe('fargate service', () => { }); describe('When import a Fargate Service', () => { - test('with serviceArn', () => { + test('fromFargateServiceArn old format', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const service = ecs.FargateService.fromFargateServiceArn(stack, 'EcsService', 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + + // THEN + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + expect(service.serviceName).toEqual('my-http-service'); + }); + + test('fromFargateServiceArn new format', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const service = ecs.FargateService.fromFargateServiceArn(stack, 'EcsService', 'arn:aws:ecs:us-west-2:123456789012:service/my-cluster-name/my-http-service'); + + // THEN + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-cluster-name/my-http-service'); + expect(service.serviceName).toEqual('my-http-service'); + }); + + describe('fromFargateServiceArn tokenized ARN', () => { + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is disabled, use old ARN format', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const service = ecs.FargateService.fromFargateServiceArn(stack, 'EcsService', new cdk.CfnParameter(stack, 'ARN').valueAsString); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual({ Ref: 'ARN' }); + expect(stack.resolve(service.serviceName)).toEqual({ + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }, + ], + }, + ], + }); + }); + + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is enabled, use new ARN format', () => { + // GIVEN + const app = new App({ + context: { + [ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME]: true, + }, + }); + const stack = new cdk.Stack(app); + + // WHEN + const service = ecs.FargateService.fromFargateServiceArn(stack, 'EcsService', new cdk.CfnParameter(stack, 'ARN').valueAsString); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual({ Ref: 'ARN' }); + expect(stack.resolve(service.serviceName)).toEqual({ + 'Fn::Select': [ + 2, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }, + ], + }, + ], + }); + }); + }), + + test('with serviceArn old format', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -2103,21 +2200,190 @@ describe('fargate service', () => { }); - test('with serviceName', () => { + test('with serviceArn new format', () => { // GIVEN const stack = new cdk.Stack(); - const pseudo = new cdk.ScopedAws(stack); const cluster = new ecs.Cluster(stack, 'EcsCluster'); // WHEN const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { - serviceName: 'my-http-service', + serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-cluster-name/my-http-service', cluster, }); // THEN - expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-cluster-name/my-http-service'); expect(service.serviceName).toEqual('my-http-service'); + + expect(service.env.account).toEqual('123456789012'); + expect(service.env.region).toEqual('us-west-2'); + }); + + describe('with serviceArn tokenized ARN', () => { + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is disabled, use old ARN format', () => { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + serviceArn: new cdk.CfnParameter(stack, 'ARN').valueAsString, + cluster, + }); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual({ Ref: 'ARN' }); + expect(stack.resolve(service.serviceName)).toEqual({ + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }, + ], + }, + ], + }); + + expect(stack.resolve(service.env.account)).toEqual({ + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }); + expect(stack.resolve(service.env.region)).toEqual({ + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }); + }); + + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is enabled, use new ARN format', () => { + // GIVEN + const app = new App({ + context: { + [ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME]: true, + }, + }); + const stack = new cdk.Stack(app); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + serviceArn: new cdk.CfnParameter(stack, 'ARN').valueAsString, + cluster, + }); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual({ Ref: 'ARN' }); + expect(stack.resolve(service.serviceName)).toEqual({ + 'Fn::Select': [ + 2, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }, + ], + }, + ], + }); + + expect(stack.resolve(service.env.account)).toEqual({ + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }); + expect(stack.resolve(service.env.region)).toEqual({ + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { Ref: 'ARN' }, + ], + }, + ], + }); + }); + }); + + describe('with serviceName', () => { + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is disabled, use old ARN format', () => { + // GIVEN + const stack = new cdk.Stack(); + const pseudo = new cdk.ScopedAws(stack); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + serviceName: 'my-http-service', + cluster, + }); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + expect(service.serviceName).toEqual('my-http-service'); + }); + + test('when @aws-cdk/aws-ecs:arnFormatIncludesClusterName is enabled, use new ARN format', () => { + // GIVEN + const app = new App({ + context: { + [ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME]: true, + }, + }); + + const stack = new cdk.Stack(app); + const pseudo = new cdk.ScopedAws(stack); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + + // WHEN + const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { + serviceName: 'my-http-service', + cluster, + }); + + // THEN + expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/${cluster.clusterName}/my-http-service`)); + expect(service.serviceName).toEqual('my-http-service'); + }); }); test('with circuit breaker', () => { diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 06e4b8e66e87a..422015c8a0e5b 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -243,6 +243,19 @@ export const ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER = '@aws-cdk-contai */ export const EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME = '@aws-cdk/aws-ec2:uniqueImdsv2TemplateName'; +/** + * ARN format used by ECS. In the new ARN format, the cluster name is part + * of the resource ID. + * + * If this flag is not set, the old ARN format (without cluster name) for ECS is used. + * If this flag is set, the new ARN format (with cluster name) for ECS is used. + * + * This is a feature flag as the old format is still valid for existing ECS clusters. + * + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids + */ +export const ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME = '@aws-cdk/aws-ecs:arnFormatIncludesClusterName'; + /** * Minimize IAM policies by combining Principals, Actions and Resources of two * Statements in the policies, as long as it doesn't change the meaning of the @@ -328,6 +341,7 @@ export const FUTURE_FLAGS: { [key: string]: boolean } = { [EC2_UNIQUE_IMDSV2_LAUNCH_TEMPLATE_NAME]: true, [CHECK_SECRET_USAGE]: true, [IAM_MINIMIZE_POLICIES]: true, + [ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME]: true, [VALIDATE_SNAPSHOT_REMOVAL_POLICY]: true, [CODEPIPELINE_CROSS_ACCOUNT_KEY_ALIAS_STACK_SAFE_RESOURCE_NAME]: true, [S3_CREATE_DEFAULT_LOGGING_POLICY]: true,