Skip to content

Commit

Permalink
feat(appmesh): add listener timeout to Virtual Nodes (#10793)
Browse files Browse the repository at this point in the history
Adds listener timeout to Virtual Nodes.

BREAKING CHANGE: `IVirtualNode` no longer has the `addBackends()` method. A backend can be added to `VirtualNode` using the `addBackend()` method which accepts a single `IVirtualService`
* **appmesh**: `IVirtualNode` no longer has the `addListeners()` method. A listener can be added to `VirtualNode` using the `addListener()` method which accepts a single `VirtualNodeListener`
* **appmesh**: `VirtualNode` no longer has a default listener. It is valid to have a `VirtualNode` without any listeners
* **appmesh**: the construction property `listener` of `VirtualNode` has been renamed to `listeners`, and its type changed to an array of listeners
* **appmesh**: the struct `VirtualNodeListener` has been removed. To create Virtual Node listeners, use the static factory methods of the `VirtualNodeListener` class
----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
sshver authored Nov 11, 2020
1 parent 8c17a35 commit 62baa7b
Show file tree
Hide file tree
Showing 12 changed files with 524 additions and 256 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -259,17 +259,28 @@ export class AppMeshExtension extends ServiceExtension {
throw new Error('You must add a CloudMap namespace to the ECS cluster in order to use the AppMesh extension');
}

function addListener(protocol: appmesh.Protocol, port: number): appmesh.VirtualNodeListener {
switch (protocol) {
case appmesh.Protocol.HTTP :
return appmesh.VirtualNodeListener.http({ port });

case appmesh.Protocol.HTTP2 :
return appmesh.VirtualNodeListener.http2({ port });

case appmesh.Protocol.GRPC :
return appmesh.VirtualNodeListener.grpc({ port });

case appmesh.Protocol.TCP :
return appmesh.VirtualNodeListener.tcp({ port });
}
}

// Create a virtual node for the name service
this.virtualNode = new appmesh.VirtualNode(this.scope, `${this.parentService.id}-virtual-node`, {
mesh: this.mesh,
virtualNodeName: this.parentService.id,
cloudMapService: service.cloudMapService,
listener: {
portMapping: {
port: containerextension.trafficPort,
protocol: this.protocol,
},
},
listeners: [addListener(this.protocol, containerextension.trafficPort)],
});

// Create a virtual router for this service. This allows for retries
Expand Down Expand Up @@ -326,7 +337,7 @@ export class AppMeshExtension extends ServiceExtension {
// Next update the app mesh config so that the local Envoy
// proxy on this service knows how to route traffic to
// nodes from the other service.
this.virtualNode.addBackends(otherAppMesh.virtualService);
this.virtualNode.addBackend(otherAppMesh.virtualService);
}

private virtualRouterListener(port: number): appmesh.VirtualRouterListener {
Expand Down
25 changes: 11 additions & 14 deletions packages/@aws-cdk/aws-appmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,8 @@ const service = namespace.createService('Svc');

const node = mesh.addVirtualNode('virtual-node', {
cloudMapService: service,
listener: {
portMapping: {
port: 8081,
protocol: Protocol.HTTP,
},
listeners: [appmesh.VirtualNodeListener.httpNodeListener({
port: 8081,
healthCheck: {
healthyThreshold: 3,
interval: Duration.seconds(5), // minimum
Expand All @@ -153,9 +150,9 @@ const node = mesh.addVirtualNode('virtual-node', {
timeout: Duration.seconds(2), // minimum
unhealthyThreshold: 2,
},
},
})],
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
})
});
```

Create a `VirtualNode` with the the constructor and add tags.
Expand All @@ -164,11 +161,8 @@ Create a `VirtualNode` with the the constructor and add tags.
const node = new VirtualNode(this, 'node', {
mesh,
cloudMapService: service,
listener: {
portMapping: {
port: 8080,
protocol: Protocol.HTTP,
},
listeners: [appmesh.VirtualNodeListener.httpNodeListener({
port: 8080,
healthCheck: {
healthyThreshold: 3,
interval: Duration.seconds(5), // min
Expand All @@ -177,15 +171,18 @@ const node = new VirtualNode(this, 'node', {
protocol: Protocol.HTTP,
timeout: Duration.seconds(2), // min
unhealthyThreshold: 2,
},
timeout: {
idle: cdk.Duration.seconds(5),
},
},
})],
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
});

cdk.Tag.add(node, 'Environment', 'Dev');
```

The listeners property can be left blank and added later with the `node.addListeners()` method. The `healthcheck` property is optional but if specifying a listener, the `portMappings` must contain at least one property.
The `listeners` property can be left blank and added later with the `node.addListener()` method. The `healthcheck` and `timeout` properties are optional but if specifying a listener, the `port` must be added.

## Adding a Route

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-appmesh/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './virtual-node';
export * from './virtual-router';
export * from './virtual-router-listener';
export * from './virtual-service';
export * from './virtual-node-listener';
export * from './virtual-gateway';
export * from './virtual-gateway-listener';
export * from './gateway-route';
Expand Down
38 changes: 0 additions & 38 deletions packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,44 +68,6 @@ export interface HealthCheck {
readonly unhealthyThreshold?: number;
}

/**
* Port mappings for resources that require these attributes, such as VirtualNodes and Routes
*/
export interface PortMapping {
/**
* Port mapped to the VirtualNode / Route
*
* @default 8080
*/
readonly port: number;

/**
* Protocol for the VirtualNode / Route, only GRPC, HTTP, HTTP2, or TCP is supported
*
* @default HTTP
*/
readonly protocol: Protocol;
}

/**
* Represents the properties needed to define healthy and active listeners for nodes
*/
export interface VirtualNodeListener {
/**
* Array of PortMappingProps for the listener
*
* @default - HTTP port 8080
*/
readonly portMapping?: PortMapping;

/**
* Health checking strategy upstream nodes should use when communicating with the listener
*
* @default - no healthcheck
*/
readonly healthCheck?: HealthCheck;
}

/**
* All Properties for Envoy Access logs for mesh endpoints
*/
Expand Down
219 changes: 219 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import * as cdk from '@aws-cdk/core';
import { CfnVirtualNode } from './appmesh.generated';
import { validateHealthChecks } from './private/utils';
import { HealthCheck, Protocol } from './shared-interfaces';

/**
* Properties for a VirtualNode listener
*/
export interface VirtualNodeListenerConfig {
/**
* Single listener config for a VirtualNode
*/
readonly listener: CfnVirtualNode.ListenerProperty,
}

/**
* Represents the properties needed to define a Listeners for a VirtualNode
*/
interface VirtualNodeListenerCommonOptions {
/**
* Port to listen for connections on
*
* @default - 8080
*/
readonly port?: number

/**
* The health check information for the listener
*
* @default - no healthcheck
*/
readonly healthCheck?: HealthCheck;
}

/**
* Represent the HTTP Node Listener prorperty
*/
export interface HttpVirtualNodeListenerOptions extends VirtualNodeListenerCommonOptions {
/**
* Timeout for HTTP protocol
*
* @default - None
*/
readonly timeout?: HttpTimeout;
}

/**
* Represent the GRPC Node Listener prorperty
*/
export interface GrpcVirtualNodeListenerOptions extends VirtualNodeListenerCommonOptions {
/**
* Timeout for GRPC protocol
*
* @default - None
*/
readonly timeout?: GrpcTimeout;
}

/**
* Represent the TCP Node Listener prorperty
*/
export interface TcpVirtualNodeListenerOptions extends VirtualNodeListenerCommonOptions {
/**
* Timeout for TCP protocol
*
* @default - None
*/
readonly timeout?: TcpTimeout;
}

/**
* Represents timeouts for HTTP protocols.
*/
export interface HttpTimeout {
/**
* Represents an idle timeout. The amount of time that a connection may be idle.
*
* @default - none
*/
readonly idle?: cdk.Duration;

/**
* Represents per request timeout.
*
* @default - 15 s
*/
readonly perRequest?: cdk.Duration;
}

/**
* Represents timeouts for GRPC protocols.
*/
export interface GrpcTimeout {
/**
* Represents an idle timeout. The amount of time that a connection may be idle.
*
* @default - none
*/
readonly idle?: cdk.Duration;

/**
* Represents per request timeout.
*
* @default - 15 s
*/
readonly perRequest?: cdk.Duration;
}

/**
* Represents timeouts for TCP protocols.
*/
export interface TcpTimeout {
/**
* Represents an idle timeout. The amount of time that a connection may be idle.
*
* @default - none
*/
readonly idle?: cdk.Duration;
}

/**
* Defines listener for a VirtualNode
*/
export abstract class VirtualNodeListener {
/**
* Returns an HTTP Listener for a VirtualNode
*/
public static http(props: HttpVirtualNodeListenerOptions = {}): VirtualNodeListener {
return new VirtualNodeListenerImpl(Protocol.HTTP, props.healthCheck, props.timeout, props.port);
}

/**
* Returns an HTTP2 Listener for a VirtualNode
*/
public static http2(props: HttpVirtualNodeListenerOptions = {}): VirtualNodeListener {
return new VirtualNodeListenerImpl(Protocol.HTTP2, props.healthCheck, props.timeout, props.port);
}

/**
* Returns an GRPC Listener for a VirtualNode
*/
public static grpc(props: GrpcVirtualNodeListenerOptions = {}): VirtualNodeListener {
return new VirtualNodeListenerImpl(Protocol.GRPC, props.healthCheck, props.timeout, props.port);
}

/**
* Returns an TCP Listener for a VirtualNode
*/
public static tcp(props: TcpVirtualNodeListenerOptions = {}): VirtualNodeListener {
return new VirtualNodeListenerImpl(Protocol.TCP, props.healthCheck, props.timeout, props.port);
}

/**
* Binds the current object when adding Listener to a VirtualNode
*/
public abstract bind(scope: cdk.Construct): VirtualNodeListenerConfig;

}

class VirtualNodeListenerImpl extends VirtualNodeListener {
constructor(private readonly protocol: Protocol,
private readonly healthCheck: HealthCheck | undefined,
private readonly timeout: HttpTimeout | undefined,
private readonly port: number = 8080) { super(); }

public bind(_scope: cdk.Construct): VirtualNodeListenerConfig {
return {
listener: {
portMapping: {
port: this.port,
protocol: this.protocol,
},
healthCheck: this.healthCheck ? this.renderHealthCheck(this.healthCheck) : undefined,
timeout: this.timeout ? this.renderTimeout(this.timeout) : undefined,
},
};
}

private renderHealthCheck(hc: HealthCheck): CfnVirtualNode.HealthCheckProperty | undefined {
if (hc === undefined) { return undefined; }

if (hc.protocol === Protocol.TCP && hc.path) {
throw new Error('The path property cannot be set with Protocol.TCP');
}

if (hc.protocol === Protocol.GRPC && hc.path) {
throw new Error('The path property cannot be set with Protocol.GRPC');
}

const healthCheck: CfnVirtualNode.HealthCheckProperty = {
healthyThreshold: hc.healthyThreshold || 2,
intervalMillis: (hc.interval || cdk.Duration.seconds(5)).toMilliseconds(), // min
path: hc.path || (hc.protocol === Protocol.HTTP ? '/' : undefined),
port: hc.port || this.port,
protocol: hc.protocol || this.protocol,
timeoutMillis: (hc.timeout || cdk.Duration.seconds(2)).toMilliseconds(),
unhealthyThreshold: hc.unhealthyThreshold || 2,
};

validateHealthChecks(healthCheck);

return healthCheck;
}

private renderTimeout(timeout: HttpTimeout): CfnVirtualNode.ListenerTimeoutProperty {
return ({
[this.protocol]: {
idle: timeout?.idle !== undefined ? {
unit: 'ms',
value: timeout?.idle.toMilliseconds(),
} : undefined,
perRequest: timeout?.perRequest !== undefined ? {
unit: 'ms',
value: timeout?.perRequest.toMilliseconds(),
} : undefined,
},
});
}
}
Loading

0 comments on commit 62baa7b

Please sign in to comment.