Skip to content

Commit b2d0a66

Browse files
authored
2 parents de1adf0 + b50a5e2 commit b2d0a66

22 files changed

+3263
-9
lines changed

CHANGELOG.v2.alpha.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
## [2.52.0-alpha.0](https://github.com/aws/aws-cdk/compare/v2.51.1-alpha.0...v2.52.0-alpha.0) (2022-11-27)
6+
57
## [2.51.1-alpha.0](https://github.com/aws/aws-cdk/compare/v2.51.0-alpha.0...v2.51.1-alpha.0) (2022-11-18)
68

79
## [2.51.0-alpha.0](https://github.com/aws/aws-cdk/compare/v2.50.0-alpha.0...v2.51.0-alpha.0) (2022-11-18)

CHANGELOG.v2.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
## [2.52.0](https://github.com/aws/aws-cdk/compare/v2.51.1...v2.52.0) (2022-11-27)
6+
7+
8+
### Features
9+
10+
* **ecs:** enable Amazon ECS Service Connect ([96ec613](https://github.com/aws/aws-cdk/commit/96ec6139e1ad7637466e95b71e824965b081154f))
11+
512
## [2.51.1](https://github.com/aws/aws-cdk/compare/v2.51.0...v2.51.1) (2022-11-18)
613

714

packages/@aws-cdk/aws-ecs/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,3 +1233,79 @@ const cluster = new ecs.Cluster(this, 'Cluster', {
12331233
},
12341234
});
12351235
```
1236+
1237+
## Amazon ECS Service Connect
1238+
1239+
Service Connect is a managed AWS mesh network offering. It simplifies DNS queries and inter-service communication for
1240+
ECS Services by allowing customers to set up simple DNS aliases for their services, which are accessible to all
1241+
services that have enabled Service Connect.
1242+
1243+
To enable Service Connect, you must have created a CloudMap namespace. The CDK can infer your cluster's default CloudMap namespace,
1244+
or you can specify a custom namespace. You must also have created a named port mapping on at least one container in your Task Definition.
1245+
1246+
```ts
1247+
declare const cluster: ecs.Cluster;
1248+
declare const taskDefinition: ecs.TaskDefinition;
1249+
declare const container: ecs.ContainerDefinition;
1250+
1251+
container.addPortMappings({
1252+
name: 'api',
1253+
containerPort: 8080,
1254+
});
1255+
1256+
taskDefinition.addContainer(container);
1257+
1258+
cluster.addDefaultCloudMapNamespace({
1259+
name: 'local',
1260+
});
1261+
1262+
const service = new ecs.FargateService(this, 'Service', {
1263+
cluster,
1264+
taskDefinition,
1265+
serviceConnectConfiguration: {
1266+
services: [
1267+
{
1268+
portMappingName: 'api',
1269+
dnsName: 'http-api',
1270+
port: 80,
1271+
},
1272+
],
1273+
},
1274+
});
1275+
```
1276+
1277+
Service Connect-enabled services may now reach this service at `http-api:80`. Traffic to this endpoint will
1278+
be routed to the container's port 8080.
1279+
1280+
To opt a service into using service connect without advertising a port, simply call the 'enableServiceConnect' method on an initialized service.
1281+
1282+
```ts
1283+
const service = new ecs.FargateService(this, 'Service', {
1284+
cluster,
1285+
taskDefinition
1286+
)
1287+
service.enableServiceConnect();
1288+
```
1289+
1290+
Service Connect also allows custom logging, Service Discovery name, and configuration of the port where service connect traffic is received.
1291+
1292+
```ts
1293+
const customService = new ecs.FargateService(this, 'CustomizedService', {
1294+
cluster,
1295+
taskDefinition,
1296+
serviceConnectConfiguration: {
1297+
logDriver: ecs.LogDrivers.awslogs({
1298+
streamPrefix: 'sc-traffic',
1299+
}),
1300+
services: [
1301+
{
1302+
portMappingName: 'api',
1303+
dnsName: 'customized-api',
1304+
port: 80,
1305+
ingressPortOverride: 20040,
1306+
discoveryName: 'custom',
1307+
},
1308+
],
1309+
},
1310+
});
1311+
```

packages/@aws-cdk/aws-ecs/lib/base/base-service.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/
2323
import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging, Cluster } from '../cluster';
2424
import { ContainerDefinition, Protocol } from '../container-definition';
2525
import { CfnService } from '../ecs.generated';
26+
import { LogDriver, LogDriverConfig } from '../log-drivers/log-driver';
2627
import { ScalableTaskCount } from './scalable-task-count';
2728

2829
/**
@@ -104,6 +105,76 @@ export interface EcsTarget {
104105
export interface IEcsLoadBalancerTarget extends elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget {
105106
}
106107

108+
/**
109+
* Interface for Service Connect configuration.
110+
*/
111+
export interface ServiceConnectProps {
112+
/**
113+
* The cloudmap namespace to register this service into.
114+
*
115+
* @default the cloudmap namespace specified on the cluster.
116+
*/
117+
readonly namespace?: string;
118+
119+
/**
120+
* The list of Services, including a port mapping, terse client alias, and optional intermediate DNS name.
121+
*
122+
* This property may be left blank if the current ECS service does not need to advertise any ports via Service Connect.
123+
*
124+
* @default none
125+
*/
126+
readonly services?: ServiceConnectService[];
127+
128+
/**
129+
* The log driver configuration to use for the Service Connect agent logs.
130+
*
131+
* @default - none
132+
*/
133+
readonly logDriver?: LogDriver;
134+
}
135+
136+
/**
137+
* Interface for service connect Service props.
138+
*/
139+
export interface ServiceConnectService {
140+
/**
141+
* portMappingName specifies which port and protocol combination should be used for this
142+
* service connect service.
143+
*/
144+
readonly portMappingName: string;
145+
146+
/**
147+
* Optionally specifies an intermediate dns name to register in the CloudMap namespace.
148+
* This is required if you wish to use the same port mapping name in more than one service.
149+
*
150+
* @default - port mapping name
151+
*/
152+
readonly discoveryName?: string;
153+
154+
/**
155+
* The terse DNS alias to use for this port mapping in the service connect mesh.
156+
* Service Connect-enabled clients will be able to reach this service at
157+
* http://dnsName:port.
158+
*
159+
* @default - No alias is created. The service is reachable at `portMappingName.namespace:port`.
160+
*/
161+
readonly dnsName?: string;
162+
163+
/**
164+
The port for clients to use to communicate with this service via Service Connect.
165+
*
166+
* @default the container port specified by the port mapping in portMappingName.
167+
*/
168+
readonly port?: number;
169+
170+
/**
171+
* Optional. The port on the Service Connect agent container to use for traffic ingress to this service.
172+
*
173+
* @default - none
174+
*/
175+
readonly ingressPortOverride?: number;
176+
}
177+
107178
/**
108179
* The properties for the base Ec2Service or FargateService service.
109180
*/
@@ -216,6 +287,14 @@ export interface BaseServiceOptions {
216287
* @default - undefined
217288
*/
218289
readonly enableExecuteCommand?: boolean;
290+
291+
/**
292+
* Configuration for Service Connect.
293+
*
294+
* @default No ports are advertised via Service Connect on this service, and the service
295+
* cannot make requests to other services via Service Connect.
296+
*/
297+
readonly serviceConnectConfiguration?: ServiceConnectProps;
219298
}
220299

221300
/**
@@ -367,6 +446,9 @@ export abstract class BaseService extends Resource
367446
});
368447
}
369448

449+
private static MIN_PORT = 1;
450+
private static MAX_PORT = 65535;
451+
370452
/**
371453
* The security groups which manage the allowed network traffic for the service.
372454
*/
@@ -417,6 +499,12 @@ export abstract class BaseService extends Resource
417499
*/
418500
protected serviceRegistries = new Array<CfnService.ServiceRegistryProperty>();
419501

502+
/**
503+
* The service connect configuration for this service.
504+
* @internal
505+
*/
506+
protected _serviceConnectConfig?: CfnService.ServiceConnectConfigurationProperty;
507+
420508
private readonly resource: CfnService;
421509
private scalableTaskCount?: ScalableTaskCount;
422510

@@ -469,6 +557,7 @@ export abstract class BaseService extends Resource
469557
/* role: never specified, supplanted by Service Linked Role */
470558
networkConfiguration: Lazy.any({ produce: () => this.networkConfiguration }, { omitEmptyArray: true }),
471559
serviceRegistries: Lazy.any({ produce: () => this.serviceRegistries }, { omitEmptyArray: true }),
560+
serviceConnectConfiguration: Lazy.any({ produce: () => this._serviceConnectConfig }, { omitEmptyArray: true }),
472561
...additionalProps,
473562
});
474563

@@ -502,6 +591,10 @@ export abstract class BaseService extends Resource
502591
this.enableCloudMap(props.cloudMapOptions);
503592
}
504593

594+
if (props.serviceConnectConfiguration) {
595+
this.enableServiceConnect(props.serviceConnectConfiguration);
596+
}
597+
505598
if (props.enableExecuteCommand) {
506599
this.enableExecuteCommand();
507600

@@ -517,6 +610,135 @@ export abstract class BaseService extends Resource
517610
this.node.defaultChild = this.resource;
518611
}
519612

613+
/** * Enable Service Connect
614+
*/
615+
public enableServiceConnect(config?: ServiceConnectProps) {
616+
if (this._serviceConnectConfig) {
617+
throw new Error('Service connect configuration cannot be specified more than once.');
618+
}
619+
620+
this.validateServiceConnectConfiguration(config);
621+
622+
let cfg = config || {};
623+
624+
/**
625+
* Namespace already exists as validated in validateServiceConnectConfiguration.
626+
* Resolve which namespace to use by picking:
627+
* 1. The namespace defined in service connect config.
628+
* 2. The namespace defined in the cluster's defaultCloudMapNamespace property.
629+
*/
630+
let namespace;
631+
if (this.cluster.defaultCloudMapNamespace) {
632+
namespace = this.cluster.defaultCloudMapNamespace.namespaceName;
633+
}
634+
635+
if (cfg.namespace) {
636+
namespace = cfg.namespace;
637+
}
638+
639+
/**
640+
* Map services to CFN property types. This block manages:
641+
* 1. Finding the correct port.
642+
* 2. Client alias enumeration
643+
*/
644+
const services = cfg.services?.map(svc => {
645+
const containerPort = this.taskDefinition.findPortMappingByName(svc.portMappingName)?.containerPort;
646+
if (!containerPort) {
647+
throw new Error(`Port mapping with name ${svc.portMappingName} does not exist.`);
648+
}
649+
const alias = {
650+
port: svc.port || containerPort,
651+
dnsName: svc.dnsName,
652+
};
653+
654+
return {
655+
portName: svc.portMappingName,
656+
discoveryName: svc.discoveryName,
657+
ingressPortOverride: svc.ingressPortOverride,
658+
clientAliases: [alias],
659+
} as CfnService.ServiceConnectServiceProperty;
660+
});
661+
662+
let logConfig: LogDriverConfig | undefined;
663+
if (cfg.logDriver && this.taskDefinition.defaultContainer) {
664+
// Default container existence is validated in validateServiceConnectConfiguration.
665+
// We only need the default container so that bind() can get the task definition from the container definition.
666+
logConfig = cfg.logDriver.bind(this, this.taskDefinition.defaultContainer);
667+
}
668+
669+
this._serviceConnectConfig = {
670+
enabled: true,
671+
logConfiguration: logConfig,
672+
namespace: namespace,
673+
services: services,
674+
};
675+
};
676+
677+
/**
678+
* Validate Service Connect Configuration
679+
*/
680+
private validateServiceConnectConfiguration(config?: ServiceConnectProps) {
681+
if (!this.taskDefinition.defaultContainer) {
682+
throw new Error('Task definition must have at least one container to enable service connect.');
683+
}
684+
685+
// Check the implicit enable case; when config isn't specified or namespace isn't specified, we need to check that there is a namespace on the cluster.
686+
if ((!config || !config.namespace) && !this.cluster.defaultCloudMapNamespace) {
687+
throw new Error('Namespace must be defined either in serviceConnectConfig or cluster.defaultCloudMapNamespace');
688+
}
689+
690+
// When config isn't specified, return.
691+
if (!config) {
692+
return;
693+
}
694+
695+
if (!config.services) {
696+
return;
697+
}
698+
let portNames = new Map<string, string[]>();
699+
config.services.forEach(serviceConnectService => {
700+
// port must exist on the task definition
701+
if (!this.taskDefinition.findPortMappingByName(serviceConnectService.portMappingName)) {
702+
throw new Error(`Port Mapping '${serviceConnectService.portMappingName}' does not exist on the task definition.`);
703+
};
704+
705+
// Check that no two service connect services use the same discovery name.
706+
const discoveryName = serviceConnectService.discoveryName || serviceConnectService.portMappingName;
707+
if (portNames.get(serviceConnectService.portMappingName)?.includes(discoveryName)) {
708+
throw new Error(`Cannot create multiple services with the discoveryName '${discoveryName}'.`);
709+
}
710+
711+
let currentDiscoveries = portNames.get(serviceConnectService.portMappingName);
712+
if (!currentDiscoveries) {
713+
portNames.set(serviceConnectService.portMappingName, [discoveryName]);
714+
} else {
715+
currentDiscoveries.push(discoveryName);
716+
portNames.set(serviceConnectService.portMappingName, currentDiscoveries);
717+
}
718+
719+
// IngressPortOverride should be within the valid port range if it exists.
720+
if (serviceConnectService.ingressPortOverride && !this.isValidPort(serviceConnectService.ingressPortOverride)) {
721+
throw new Error(`ingressPortOverride ${serviceConnectService.ingressPortOverride} is not valid.`);
722+
}
723+
724+
// clientAlias.port should be within the valid port range
725+
if (serviceConnectService.port &&
726+
!this.isValidPort(serviceConnectService.port)) {
727+
throw new Error(`Client Alias port ${serviceConnectService.port} is not valid.`);
728+
}
729+
});
730+
}
731+
732+
/**
733+
* Determines if a port is valid
734+
*
735+
* @param port: The port number
736+
* @returns boolean whether the port is valid
737+
*/
738+
private isValidPort(port?: number): boolean {
739+
return !!(port && Number.isInteger(port) && port >= BaseService.MIN_PORT && port <= BaseService.MAX_PORT);
740+
}
741+
520742
/**
521743
* The CloudMap service created for this service, if any.
522744
*/

0 commit comments

Comments
 (0)