Skip to content

Commit

Permalink
chore(release): 2.52.0 (aws#23106)
Browse files Browse the repository at this point in the history
See [CHANGELOG.v2.md](https://github.com/aws/aws-cdk/compare/v2-release...2.52.0?expand=1#diff-9e3d5c5da890b65a67bf632f93416744d5b76f6eb239e42ab714c7768429ff99)

----

### All Submissions:

* [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
mergify[bot] authored Nov 27, 2022
2 parents 3d30cdb + d04a359 commit 096d2e0
Show file tree
Hide file tree
Showing 23 changed files with 3,418 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.v2.alpha.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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.

## [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)

## [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)

## [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)
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

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.

## [2.52.0](https://github.com/aws/aws-cdk/compare/v2.51.1...v2.52.0) (2022-11-27)


### Features

* **ecs:** enable Amazon ECS Service Connect ([96ec613](https://github.com/aws/aws-cdk/commit/96ec6139e1ad7637466e95b71e824965b081154f))

## [2.51.1](https://github.com/aws/aws-cdk/compare/v2.51.0...v2.51.1) (2022-11-18)


Expand Down
76 changes: 76 additions & 0 deletions packages/@aws-cdk/aws-ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1233,3 +1233,79 @@ const cluster = new ecs.Cluster(this, 'Cluster', {
},
});
```

## Amazon ECS Service Connect

Service Connect is a managed AWS mesh network offering. It simplifies DNS queries and inter-service communication for
ECS Services by allowing customers to set up simple DNS aliases for their services, which are accessible to all
services that have enabled Service Connect.

To enable Service Connect, you must have created a CloudMap namespace. The CDK can infer your cluster's default CloudMap namespace,
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.

```ts
declare const cluster: ecs.Cluster;
declare const taskDefinition: ecs.TaskDefinition;
declare const container: ecs.ContainerDefinition;

container.addPortMappings({
name: 'api',
containerPort: 8080,
});

taskDefinition.addContainer(container);

cluster.addDefaultCloudMapNamespace({
name: 'local',
});

const service = new ecs.FargateService(this, 'Service', {
cluster,
taskDefinition,
serviceConnectConfiguration: {
services: [
{
portMappingName: 'api',
dnsName: 'http-api',
port: 80,
},
],
},
});
```

Service Connect-enabled services may now reach this service at `http-api:80`. Traffic to this endpoint will
be routed to the container's port 8080.

To opt a service into using service connect without advertising a port, simply call the 'enableServiceConnect' method on an initialized service.

```ts
const service = new ecs.FargateService(this, 'Service', {
cluster,
taskDefinition
)
service.enableServiceConnect();
```

Service Connect also allows custom logging, Service Discovery name, and configuration of the port where service connect traffic is received.

```ts
const customService = new ecs.FargateService(this, 'CustomizedService', {
cluster,
taskDefinition,
serviceConnectConfiguration: {
logDriver: ecs.LogDrivers.awslogs({
streamPrefix: 'sc-traffic',
}),
services: [
{
portMappingName: 'api',
dnsName: 'customized-api',
port: 80,
ingressPortOverride: 20040,
discoveryName: 'custom',
},
],
},
});
```
222 changes: 222 additions & 0 deletions packages/@aws-cdk/aws-ecs/lib/base/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/
import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging, Cluster } from '../cluster';
import { ContainerDefinition, Protocol } from '../container-definition';
import { CfnService } from '../ecs.generated';
import { LogDriver, LogDriverConfig } from '../log-drivers/log-driver';
import { ScalableTaskCount } from './scalable-task-count';

/**
Expand Down Expand Up @@ -104,6 +105,76 @@ export interface EcsTarget {
export interface IEcsLoadBalancerTarget extends elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget {
}

/**
* Interface for Service Connect configuration.
*/
export interface ServiceConnectProps {
/**
* The cloudmap namespace to register this service into.
*
* @default the cloudmap namespace specified on the cluster.
*/
readonly namespace?: string;

/**
* The list of Services, including a port mapping, terse client alias, and optional intermediate DNS name.
*
* This property may be left blank if the current ECS service does not need to advertise any ports via Service Connect.
*
* @default none
*/
readonly services?: ServiceConnectService[];

/**
* The log driver configuration to use for the Service Connect agent logs.
*
* @default - none
*/
readonly logDriver?: LogDriver;
}

/**
* Interface for service connect Service props.
*/
export interface ServiceConnectService {
/**
* portMappingName specifies which port and protocol combination should be used for this
* service connect service.
*/
readonly portMappingName: string;

/**
* Optionally specifies an intermediate dns name to register in the CloudMap namespace.
* This is required if you wish to use the same port mapping name in more than one service.
*
* @default - port mapping name
*/
readonly discoveryName?: string;

/**
* The terse DNS alias to use for this port mapping in the service connect mesh.
* Service Connect-enabled clients will be able to reach this service at
* http://dnsName:port.
*
* @default - No alias is created. The service is reachable at `portMappingName.namespace:port`.
*/
readonly dnsName?: string;

/**
The port for clients to use to communicate with this service via Service Connect.
*
* @default the container port specified by the port mapping in portMappingName.
*/
readonly port?: number;

/**
* Optional. The port on the Service Connect agent container to use for traffic ingress to this service.
*
* @default - none
*/
readonly ingressPortOverride?: number;
}

/**
* The properties for the base Ec2Service or FargateService service.
*/
Expand Down Expand Up @@ -216,6 +287,14 @@ export interface BaseServiceOptions {
* @default - undefined
*/
readonly enableExecuteCommand?: boolean;

/**
* Configuration for Service Connect.
*
* @default No ports are advertised via Service Connect on this service, and the service
* cannot make requests to other services via Service Connect.
*/
readonly serviceConnectConfiguration?: ServiceConnectProps;
}

/**
Expand Down Expand Up @@ -367,6 +446,9 @@ export abstract class BaseService extends Resource
});
}

private static MIN_PORT = 1;
private static MAX_PORT = 65535;

/**
* The security groups which manage the allowed network traffic for the service.
*/
Expand Down Expand Up @@ -417,6 +499,12 @@ export abstract class BaseService extends Resource
*/
protected serviceRegistries = new Array<CfnService.ServiceRegistryProperty>();

/**
* The service connect configuration for this service.
* @internal
*/
protected _serviceConnectConfig?: CfnService.ServiceConnectConfigurationProperty;

private readonly resource: CfnService;
private scalableTaskCount?: ScalableTaskCount;

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

Expand Down Expand Up @@ -502,6 +591,10 @@ export abstract class BaseService extends Resource
this.enableCloudMap(props.cloudMapOptions);
}

if (props.serviceConnectConfiguration) {
this.enableServiceConnect(props.serviceConnectConfiguration);
}

if (props.enableExecuteCommand) {
this.enableExecuteCommand();

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

/** * Enable Service Connect
*/
public enableServiceConnect(config?: ServiceConnectProps) {
if (this._serviceConnectConfig) {
throw new Error('Service connect configuration cannot be specified more than once.');
}

this.validateServiceConnectConfiguration(config);

let cfg = config || {};

/**
* Namespace already exists as validated in validateServiceConnectConfiguration.
* Resolve which namespace to use by picking:
* 1. The namespace defined in service connect config.
* 2. The namespace defined in the cluster's defaultCloudMapNamespace property.
*/
let namespace;
if (this.cluster.defaultCloudMapNamespace) {
namespace = this.cluster.defaultCloudMapNamespace.namespaceName;
}

if (cfg.namespace) {
namespace = cfg.namespace;
}

/**
* Map services to CFN property types. This block manages:
* 1. Finding the correct port.
* 2. Client alias enumeration
*/
const services = cfg.services?.map(svc => {
const containerPort = this.taskDefinition.findPortMappingByName(svc.portMappingName)?.containerPort;
if (!containerPort) {
throw new Error(`Port mapping with name ${svc.portMappingName} does not exist.`);
}
const alias = {
port: svc.port || containerPort,
dnsName: svc.dnsName,
};

return {
portName: svc.portMappingName,
discoveryName: svc.discoveryName,
ingressPortOverride: svc.ingressPortOverride,
clientAliases: [alias],
} as CfnService.ServiceConnectServiceProperty;
});

let logConfig: LogDriverConfig | undefined;
if (cfg.logDriver && this.taskDefinition.defaultContainer) {
// Default container existence is validated in validateServiceConnectConfiguration.
// We only need the default container so that bind() can get the task definition from the container definition.
logConfig = cfg.logDriver.bind(this, this.taskDefinition.defaultContainer);
}

this._serviceConnectConfig = {
enabled: true,
logConfiguration: logConfig,
namespace: namespace,
services: services,
};
};

/**
* Validate Service Connect Configuration
*/
private validateServiceConnectConfiguration(config?: ServiceConnectProps) {
if (!this.taskDefinition.defaultContainer) {
throw new Error('Task definition must have at least one container to enable service connect.');
}

// 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.
if ((!config || !config.namespace) && !this.cluster.defaultCloudMapNamespace) {
throw new Error('Namespace must be defined either in serviceConnectConfig or cluster.defaultCloudMapNamespace');
}

// When config isn't specified, return.
if (!config) {
return;
}

if (!config.services) {
return;
}
let portNames = new Map<string, string[]>();
config.services.forEach(serviceConnectService => {
// port must exist on the task definition
if (!this.taskDefinition.findPortMappingByName(serviceConnectService.portMappingName)) {
throw new Error(`Port Mapping '${serviceConnectService.portMappingName}' does not exist on the task definition.`);
};

// Check that no two service connect services use the same discovery name.
const discoveryName = serviceConnectService.discoveryName || serviceConnectService.portMappingName;
if (portNames.get(serviceConnectService.portMappingName)?.includes(discoveryName)) {
throw new Error(`Cannot create multiple services with the discoveryName '${discoveryName}'.`);
}

let currentDiscoveries = portNames.get(serviceConnectService.portMappingName);
if (!currentDiscoveries) {
portNames.set(serviceConnectService.portMappingName, [discoveryName]);
} else {
currentDiscoveries.push(discoveryName);
portNames.set(serviceConnectService.portMappingName, currentDiscoveries);
}

// IngressPortOverride should be within the valid port range if it exists.
if (serviceConnectService.ingressPortOverride && !this.isValidPort(serviceConnectService.ingressPortOverride)) {
throw new Error(`ingressPortOverride ${serviceConnectService.ingressPortOverride} is not valid.`);
}

// clientAlias.port should be within the valid port range
if (serviceConnectService.port &&
!this.isValidPort(serviceConnectService.port)) {
throw new Error(`Client Alias port ${serviceConnectService.port} is not valid.`);
}
});
}

/**
* Determines if a port is valid
*
* @param port: The port number
* @returns boolean whether the port is valid
*/
private isValidPort(port?: number): boolean {
return !!(port && Number.isInteger(port) && port >= BaseService.MIN_PORT && port <= BaseService.MAX_PORT);
}

/**
* The CloudMap service created for this service, if any.
*/
Expand Down
Loading

0 comments on commit 096d2e0

Please sign in to comment.