Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ecs): add tls property to a ServiceConnectService #32605

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading