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(appmesh): Implement Outlier Detection for Virtual Nodes #13952

Merged
merged 2 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 27 additions & 0 deletions packages/@aws-cdk/aws-appmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
36 changes: 30 additions & 6 deletions packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -42,6 +42,13 @@ interface VirtualNodeListenerCommonOptions {
* @default - none
*/
readonly tlsCertificate?: TlsCertificate;

/**
* Represents the configuration for enabling outlier detection
*
* @default - none
*/
readonly outlierDetection?: OutlierDetection;
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
Expand All @@ -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,
},
};
}
Expand Down Expand Up @@ -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,
};
}
}

55 changes: 55 additions & 0 deletions packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down