From 5e0e90ec0f9b3f7a28b6ef84840963366594c5ca Mon Sep 17 00:00:00 2001 From: Alexander Johnson Date: Thu, 1 Apr 2021 17:26:08 -0700 Subject: [PATCH] Implement Outlier Detection for Virtual Nodes issue https://github.com/aws/aws-cdk/issues/11648 --- packages/@aws-cdk/aws-appmesh/README.md | 27 +++++++++ .../aws-appmesh/lib/shared-interfaces.ts | 26 +++++++++ .../aws-appmesh/lib/virtual-node-listener.ts | 36 ++++++++++-- .../aws-appmesh/test/test.virtual-node.ts | 55 +++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 678bbe22a2c20..fe3973e71149e 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -279,6 +279,33 @@ const gateway = new appmesh.VirtualGateway(this, 'gateway', { }); ``` +## Adding outlier detection to a Virtual Node listener + +The `outlierDetection` property can be added to a Virtual Node listener to add outlier detection. The 4 parameters +(`baseEjectionDuration`, `interval`, `maxEjectionPercent`, `maxServerErrors`) are required. + +```typescript +// Cloud Map service discovery is currently required for host ejection by outlier detection +const vpc = new ec2.Vpc(stack, 'vpc'); +const namespace = new servicediscovery.PrivateDnsNamespace(this, 'test-namespace', { + vpc, + name: 'domain.local', +}); +const service = namespace.createService('Svc'); + +const node = mesh.addVirtualNode('virtual-node', { + serviceDiscovery: appmesh.ServiceDiscovery.cloudMap({ + service: service, + }), + outlierDetection: { + baseEjectionDuration: cdk.Duration.seconds(10), + interval: cdk.Duration.seconds(30), + maxEjectionPercent: 50, + maxServerErrors: 5, + }, +}); +``` + ## Adding a Route A `route` is associated with a virtual router, and it's used to match requests for a virtual router and distribute traffic accordingly to its associated virtual nodes. diff --git a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index 007f67c4a7a9b..c06a0f40e5a28 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -124,6 +124,32 @@ export interface HealthCheck { readonly unhealthyThreshold?: number; } +/** + * Represents the outlier detection for a listener. + */ +export interface OutlierDetection { + /** + * The base amount of time for which a host is ejected. + */ + readonly baseEjectionDuration: cdk.Duration; + + /** + * The time interval between ejection sweep analysis. + */ + readonly interval: cdk.Duration; + + /** + * Maximum percentage of hosts in load balancing pool for upstream service that can be ejected. Will eject at + * least one host regardless of the value. + */ + readonly maxEjectionPercent: number; + + /** + * Number of consecutive 5xx errors required for ejection. + */ + readonly maxServerErrors: number; +} + /** * All Properties for Envoy Access logs for mesh endpoints */ diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts index 883d05583d5c7..9ff768d07294c 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts @@ -1,7 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { CfnVirtualNode } from './appmesh.generated'; import { validateHealthChecks } from './private/utils'; -import { HealthCheck, Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces'; +import { HealthCheck, Protocol, HttpTimeout, GrpcTimeout, TcpTimeout, OutlierDetection } from './shared-interfaces'; import { TlsCertificate, TlsCertificateConfig } from './tls-certificate'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -42,6 +42,13 @@ interface VirtualNodeListenerCommonOptions { * @default - none */ readonly tlsCertificate?: TlsCertificate; + + /** + * Represents the configuration for enabling outlier detection + * + * @default - none + */ + readonly outlierDetection?: OutlierDetection; } /** @@ -88,28 +95,28 @@ 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, props.tlsCertificate); + return new VirtualNodeListenerImpl(Protocol.HTTP, props.healthCheck, props.timeout, props.port, props.tlsCertificate, props.outlierDetection); } /** * Returns an HTTP2 Listener for a VirtualNode */ public static http2(props: HttpVirtualNodeListenerOptions = {}): VirtualNodeListener { - return new VirtualNodeListenerImpl(Protocol.HTTP2, props.healthCheck, props.timeout, props.port, props.tlsCertificate); + return new VirtualNodeListenerImpl(Protocol.HTTP2, props.healthCheck, props.timeout, props.port, props.tlsCertificate, props.outlierDetection); } /** * Returns an GRPC Listener for a VirtualNode */ public static grpc(props: GrpcVirtualNodeListenerOptions = {}): VirtualNodeListener { - return new VirtualNodeListenerImpl(Protocol.GRPC, props.healthCheck, props.timeout, props.port, props.tlsCertificate); + return new VirtualNodeListenerImpl(Protocol.GRPC, props.healthCheck, props.timeout, props.port, props.tlsCertificate, props.outlierDetection); } /** * Returns an TCP Listener for a VirtualNode */ public static tcp(props: TcpVirtualNodeListenerOptions = {}): VirtualNodeListener { - return new VirtualNodeListenerImpl(Protocol.TCP, props.healthCheck, props.timeout, props.port, props.tlsCertificate); + return new VirtualNodeListenerImpl(Protocol.TCP, props.healthCheck, props.timeout, props.port, props.tlsCertificate, props.outlierDetection); } /** @@ -124,7 +131,8 @@ class VirtualNodeListenerImpl extends VirtualNodeListener { private readonly healthCheck: HealthCheck | undefined, private readonly timeout: HttpTimeout | undefined, private readonly port: number = 8080, - private readonly tlsCertificate: TlsCertificate | undefined) { super(); } + private readonly tlsCertificate: TlsCertificate | undefined, + private readonly outlierDetection: OutlierDetection | undefined) { super(); } public bind(scope: Construct): VirtualNodeListenerConfig { const tlsConfig = this.tlsCertificate?.bind(scope); @@ -137,6 +145,7 @@ class VirtualNodeListenerImpl extends VirtualNodeListener { healthCheck: this.healthCheck ? this.renderHealthCheck(this.healthCheck) : undefined, timeout: this.timeout ? this.renderTimeout(this.timeout) : undefined, tls: tlsConfig ? this.renderTls(tlsConfig) : undefined, + outlierDetection: this.outlierDetection ? this.renderOutlierDetection(this.outlierDetection) : undefined, }, }; } @@ -191,5 +200,20 @@ class VirtualNodeListenerImpl extends VirtualNodeListener { }, }); } + + private renderOutlierDetection(outlierDetection: OutlierDetection): CfnVirtualNode.OutlierDetectionProperty { + return { + baseEjectionDuration: { + unit: 'ms', + value: outlierDetection.baseEjectionDuration.toMilliseconds(), + }, + interval: { + unit: 'ms', + value: outlierDetection.interval.toMilliseconds(), + }, + maxEjectionPercent: outlierDetection.maxEjectionPercent, + maxServerErrors: outlierDetection.maxServerErrors, + }; + } } diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts index c09bdef5badbd..c6992ff9cc065 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -257,6 +257,61 @@ export = { }, }, + 'when a listener is added with outlier detection with user defined props': { + 'should add a listener outlier detection to the resource'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = new appmesh.VirtualNode(stack, 'test-node', { + mesh, + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + }); + + node.addListener(appmesh.VirtualNodeListener.tcp({ + port: 80, + outlierDetection: { + baseEjectionDuration: cdk.Duration.seconds(10), + interval: cdk.Duration.seconds(30), + maxEjectionPercent: 50, + maxServerErrors: 5, + }, + })); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualNode', { + Spec: { + Listeners: [ + { + OutlierDetection: { + BaseEjectionDuration: { + Unit: 'ms', + Value: 10000, + }, + Interval: { + Unit: 'ms', + Value: 30000, + }, + MaxEjectionPercent: 50, + MaxServerErrors: 5, + }, + PortMapping: { + Port: 80, + Protocol: 'tcp', + }, + }, + ], + }, + })); + + test.done(); + }, + }, + 'when a default backend is added': { 'should add a backend default to the resource'(test: Test) { // GIVEN