Skip to content

Commit

Permalink
feat(ecs): add tls property to a ServiceConnectService (#32605)
Browse files Browse the repository at this point in the history
### Issue # (if applicable)

Closes #32583 

### Reason for this change

ServiceConnectService in ECS did not support the `tls` property.

### Description of changes

- Added `tls` property to ServiceConnectService(interface) in ECS(BaseService)
- modified implementation to allow specifying ServiceConnectService tls in the `enableServiceConnect` method

### Description of how you validated changes

Added unit  tests.

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
ren-yamanashi authored Jan 31, 2025
1 parent 7b5f5a5 commit d32baf6
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 0 deletions.
37 changes: 37 additions & 0 deletions packages/aws-cdk-lib/aws-ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1938,3 +1938,40 @@ taskDefinition.addContainer('TheContainer', {
}],
});
```

## Service Connect TLS

Service Connect TLS is a feature that allows you to secure the communication between services using TLS.

You can specify the `tls` option in the `services` array of the `serviceConnectConfiguration` property.

The `tls` property is an object with the following properties:

- `role`: The IAM role that's associated with the Service Connect TLS.
- `awsPcaAuthorityArn`: The ARN of the certificate root authority that secures your service.
- `kmsKey`: The KMS key used for encryption and decryption.

```ts
declare const cluster: ecs.Cluster;
declare const taskDefinition: ecs.TaskDefinition;
declare const kmsKey: kms.IKey;
declare const role: iam.IRole;

const service = new ecs.FargateService(this, 'FargateService', {
cluster,
taskDefinition,
serviceConnectConfiguration: {
services: [
{
tls: {
role,
kmsKey,
awsPcaAuthorityArn: 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/123456789012',
},
portMappingName: 'api',
},
],
namespace: 'sample namespace',
},
});
```
49 changes: 49 additions & 0 deletions packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as ec2 from '../../../aws-ec2';
import * as elb from '../../../aws-elasticloadbalancing';
import * as elbv2 from '../../../aws-elasticloadbalancingv2';
import * as iam from '../../../aws-iam';
import * as kms from '../../../aws-kms';
import * as cloudmap from '../../../aws-servicediscovery';
import {
Annotations,
Expand Down Expand Up @@ -254,6 +255,39 @@ export interface ServiceConnectService {
* @default - Duration.seconds(15)
*/
readonly perRequestTimeout?: Duration;

/**
* A reference to an object that represents a Transport Layer Security (TLS) configuration.
*
* @default - none
*/
readonly tls?: ServiceConnectTlsConfiguration;
}

/**
* TLS configuration for Service Connect service
*/
export interface ServiceConnectTlsConfiguration {
/**
* The ARN of the certificate root authority that secures your service.
*
* @default - none
*/
readonly awsPcaAuthorityArn?: string;

/**
* The KMS key used for encryption and decryption.
*
* @default - none
*/
readonly kmsKey?: kms.IKey;

/**
* The IAM role that's associated with the Service Connect TLS.
*
* @default - none
*/
readonly role?: iam.IRole;
}

/**
Expand Down Expand Up @@ -920,12 +954,21 @@ export abstract class BaseService extends Resource
dnsName: svc.dnsName,
};

const tls: CfnService.ServiceConnectTlsConfigurationProperty | undefined = svc.tls ? {
issuerCertificateAuthority: {
awsPcaAuthorityArn: svc.tls.awsPcaAuthorityArn,
},
kmsKey: svc.tls.kmsKey?.keyArn,
roleArn: svc.tls.role?.roleArn,
} : undefined;

return {
portName: svc.portMappingName,
discoveryName: svc.discoveryName,
ingressPortOverride: svc.ingressPortOverride,
clientAliases: [alias],
timeout: this.renderTimeout(svc.idleTimeout, svc.perRequestTimeout),
tls,
} as CfnService.ServiceConnectServiceProperty;
});

Expand Down Expand Up @@ -996,6 +1039,12 @@ export abstract class BaseService extends Resource
!this.isValidPort(serviceConnectService.port)) {
throw new Error(`Client Alias port ${serviceConnectService.port} is not valid.`);
}

// tls.awsPcaAuthorityArn should be an ARN
const awsPcaAuthorityArn = serviceConnectService.tls?.awsPcaAuthorityArn;
if (awsPcaAuthorityArn && !Token.isUnresolved(awsPcaAuthorityArn) && !awsPcaAuthorityArn.startsWith('arn:')) {
throw new Error(`awsPcaAuthorityArn must start with "arn:" and have at least 6 components; received ${awsPcaAuthorityArn}`);
}
});
}

Expand Down
95 changes: 95 additions & 0 deletions packages/aws-cdk-lib/aws-ecs/test/base-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Template, Match } from '../../assertions';
import * as ec2 from '../../aws-ec2';
import * as iam from '../../aws-iam';
import * as kms from '../../aws-kms';
import * as cdk from '../../core';
import { App, Stack } from '../../core';
import * as cxapi from '../../cx-api';
Expand Down Expand Up @@ -79,6 +80,100 @@ describe('When import an ECS Service', () => {
],
});
});

test('should add tls configuration to service connect service', () => {
// GIVEN
const vpc = new ec2.Vpc(stack, 'Vpc');
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef');
const kmsKey = new kms.Key(stack, 'KmsKey');
const role = new iam.Role(stack, 'Role', {
assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'),
});
taskDefinition.addContainer('Web', {
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
portMappings: [
{
name: 'api',
containerPort: 80,
},
],
});
const service = new ecs.FargateService(stack, 'Service', {
cluster,
taskDefinition,
});

// WHEN
service.enableServiceConnect({
services: [
{
tls: {
awsPcaAuthorityArn:
'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/123456789012',
kmsKey,
role,
},
portMappingName: 'api',
},
],
namespace: 'test namespace',
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', {
ServiceConnectConfiguration: {
Services: [
{
Tls: {
IssuerCertificateAuthority: {
AwsPcaAuthorityArn:
'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/123456789012',
},
KmsKey: stack.resolve(kmsKey.keyArn),
RoleArn: stack.resolve(role.roleArn),
},
},
],
},
});
});

test('throws an error when awsPcaAuthorityArn is not an ARN', () => {
// GIVEN
const vpc = new ec2.Vpc(stack, 'Vpc');
const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef');
taskDefinition.addContainer('Web', {
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
portMappings: [
{
name: 'api',
containerPort: 80,
},
],
});

// WHEN
const createFargateService = () => new ecs.FargateService(stack, 'Service', {
cluster,
taskDefinition,
serviceConnectConfiguration: {
services: [
{
tls: {
awsPcaAuthorityArn: 'invalid-arn',
},
portMappingName: 'api',
},
],
namespace: 'test namespace',
},
});

// THEN
expect(() => createFargateService()).toThrow(/awsPcaAuthorityArn must start with "arn:" and have at least 6 components; received invalid-arn/);
});
});

describe('For alarm-based rollbacks', () => {
Expand Down

0 comments on commit d32baf6

Please sign in to comment.