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): change HealthChecks to use protocol-specific union-like classes #14432

Merged
merged 14 commits into from
May 7, 2021
Merged
26 changes: 10 additions & 16 deletions packages/@aws-cdk/aws-appmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,13 @@ const node = mesh.addVirtualNode('virtual-node', {
}),
listeners: [appmesh.VirtualNodeListener.httpNodeListener({
port: 8081,
healthCheck: {
healthCheck: appmesh.HealthCheck.http({
healthyThreshold: 3,
interval: Duration.seconds(5), // minimum
path: `/health-check-path`,
port: 8080,
protocol: Protocol.HTTP,
timeout: Duration.seconds(2), // minimum
unhealthyThreshold: 2,
},
}),
})],
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
});
Expand All @@ -173,15 +171,13 @@ const node = new VirtualNode(this, 'node', {
}),
listeners: [appmesh.VirtualNodeListener.httpNodeListener({
port: 8080,
healthCheck: {
healthCheck: appmesh.HealthCheck.http({
healthyThreshold: 3,
interval: Duration.seconds(5), // min
path: '/ping',
port: 8080,
protocol: Protocol.HTTP,
timeout: Duration.seconds(2), // min
unhealthyThreshold: 2,
},
}),
timeout: {
idle: cdk.Duration.seconds(5),
},
Expand All @@ -207,15 +203,13 @@ const node = new VirtualNode(this, 'node', {
}),
listeners: [appmesh.VirtualNodeListener.httpNodeListener({
port: 8080,
healthCheck: {
healthCheck: appmesh.HealthCheck.http({
healthyThreshold: 3,
interval: Duration.seconds(5), // min
path: '/ping',
port: 8080,
protocol: Protocol.HTTP,
timeout: Duration.seconds(2), // min
unhealthyThreshold: 2,
},
}),
timeout: {
idle: cdk.Duration.seconds(5),
},
Expand Down Expand Up @@ -494,9 +488,9 @@ const gateway = new appmesh.VirtualGateway(stack, 'gateway', {
mesh: mesh,
listeners: [appmesh.VirtualGatewayListener.http({
port: 443,
healthCheck: {
healthCheck: appmesh.HealthCheck.http({
interval: cdk.Duration.seconds(10),
},
}),
})],
backendDefaults: {
clientPolicy: appmesh.ClientPolicy.acmTrust({
Expand All @@ -517,9 +511,9 @@ const gateway = mesh.addVirtualGateway('gateway', {
virtualGatewayName: 'virtualGateway',
listeners: [appmesh.VirtualGatewayListener.http({
port: 443,
healthCheck: {
healthCheck: appmesh.HealthCheck.http({
interval: cdk.Duration.seconds(10),
},
}),
})],
});
```
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CfnGatewayRoute } from './appmesh.generated';
import { Protocol } from './shared-interfaces';
import { Protocol } from './private/utils';
import { IVirtualService } from './virtual-service';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
Expand Down
52 changes: 10 additions & 42 deletions packages/@aws-cdk/aws-appmesh/lib/private/utils.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,3 @@
import * as cdk from '@aws-cdk/core';
import { CfnVirtualGateway, CfnVirtualNode } from '../appmesh.generated';

type AppMeshHealthCheck = CfnVirtualNode.HealthCheckProperty | CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty

/**
* Validates health check properties, throws an error if they are misconfigured.
*
* @param healthCheck Healthcheck property from a Virtual Node or Virtual Gateway
*/
export function validateHealthChecks(healthCheck: AppMeshHealthCheck) {
(Object.keys(healthCheck) as Array<keyof AppMeshHealthCheck>)
.filter((key) =>
HEALTH_CHECK_PROPERTY_THRESHOLDS[key] &&
typeof healthCheck[key] === 'number' &&
!cdk.Token.isUnresolved(healthCheck[key]),
).map((key) => {
const [min, max] = HEALTH_CHECK_PROPERTY_THRESHOLDS[key]!;
const value = healthCheck[key]!;

if (value < min) {
throw new Error(`The value of '${key}' is below the minimum threshold (expected >=${min}, got ${value})`);
}
if (value > max) {
throw new Error(`The value of '${key}' is above the maximum threshold (expected <=${max}, got ${value})`);
}
});
}

/**
* Minimum and maximum thresholds for HeathCheck numeric properties
*
* @see https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HealthCheckPolicy.html
*/
const HEALTH_CHECK_PROPERTY_THRESHOLDS: {[key in (keyof AppMeshHealthCheck)]?: [number, number]} = {
healthyThreshold: [2, 10],
intervalMillis: [5000, 300000],
port: [1, 65535],
timeoutMillis: [2000, 60000],
unhealthyThreshold: [2, 10],
};

/**
* Generated Connection pool config
*/
Expand All @@ -64,4 +22,14 @@ export interface ConnectionPoolConfig {
* @default - none
*/
readonly maxRequests?: number;
}

/**
* Enum of supported AppMesh protocols
*/
export enum Protocol {
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
HTTP = 'http',
TCP = 'tcp',
HTTP2 = 'http2',
GRPC = 'grpc',
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-appmesh/lib/route-spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as cdk from '@aws-cdk/core';
import { CfnRoute } from './appmesh.generated';
import { Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces';
import { Protocol } from './private/utils';
import { HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces';
import { IVirtualNode } from './virtual-node';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
Expand Down
159 changes: 125 additions & 34 deletions packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as cdk from '@aws-cdk/core';
import { CfnVirtualGateway, CfnVirtualNode } from './appmesh.generated';
import { ClientPolicy } from './client-policy';
import { Protocol } from './private/utils';
import { IVirtualService } from './virtual-service';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
Expand Down Expand Up @@ -58,70 +59,160 @@ export interface TcpTimeout {
}

/**
* Enum of supported AppMesh protocols
* Properties used to define healthchecks.
*/
export enum Protocol {
HTTP = 'http',
TCP = 'tcp',
HTTP2 = 'http2',
GRPC = 'grpc',
}

/**
* Properties used to define healthchecks when creating virtual nodes.
* All values have a default if only specified as {} when creating.
* If property not set, then no healthchecks will be defined.
*/
export interface HealthCheck {
export interface HealthCheckCommonOptions {
/**
* Number of successful attempts before considering the node UP
* The number of consecutive successful health checks that must occur before declaring listener healthy.
*
* @default 2
* @default 5
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly healthyThreshold?: number;

/**
* Interval in milliseconds to re-check
* The time period in milliseconds between each health check execution.
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
*
* @default 5 seconds
* @default Duration.seconds(30)
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly interval?: cdk.Duration;

/**
* The path where the application expects any health-checks, this can also be the application path.
* The amount of time to wait when receiving a response from the health check, in milliseconds.
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
*
* @default /
* @default Duration.seconds(5)
*/
readonly path?: string;
readonly timeout?: cdk.Duration;

/**
* The TCP port number for the healthcheck
* The number of consecutive failed health checks that must occur before declaring a listener unhealthy.
*
* @default - same as corresponding port mapping
* @default - 2
*/
readonly port?: number;
readonly unhealthyThreshold?: number;
}

/**
* Properties used to define healthchecks.
*/
export interface HttpHealthCheckOptions extends HealthCheckCommonOptions {
/**
* The protocol to use for the healthcheck, for convinience a const enum has been defined.
* Protocol.HTTP or Protocol.TCP
* The destination path for the health check request.
*
* @default - same as corresponding port mapping
* @default /
*/
readonly protocol?: Protocol;
readonly path?: string;
}

/**
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
* All Properties for Health Checks for mesh endpoints
*/
export interface HealthCheckConfig {
/**
* Timeout in milli-seconds for the healthcheck to be considered a fail.
* VirtualNode CFN configuration for Health Checks
*
* @default 2 seconds
* @default - no health checks
*/
readonly timeout?: cdk.Duration;
readonly virtualNodeHealthCheck?: CfnVirtualNode.HealthCheckProperty;

/**
* Number of failed attempts before considering the node DOWN.
* VirtualGateway CFN configuration for Health Checks
*
* @default 2
* @default - no health checks
*/
readonly unhealthyThreshold?: number;
readonly virtualGatewayHealthCheck?: CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty;
}

/**
* Contains static factory methods for creating health checks for different protocols
*/
export abstract class HealthCheck {
/**
* Construct a HTTP health check
*/
public static http(options: HttpHealthCheckOptions = {}): HealthCheck {
return new HealthCheckImpl(Protocol.HTTP, options.healthyThreshold, options.unhealthyThreshold, options.interval, options.timeout, options.path);
}

/**
* Construct a HTTP2 health check
*/
public static http2(options: HttpHealthCheckOptions = {}): HealthCheck {
return new HealthCheckImpl(Protocol.HTTP2, options.healthyThreshold, options.unhealthyThreshold, options.interval, options.timeout, options.path);
}

/**
* Construct a GRPC health check
*/
public static grpc(options: HealthCheckCommonOptions= {}): HealthCheck {
return new HealthCheckImpl(Protocol.GRPC, options.healthyThreshold, options.unhealthyThreshold, options.interval, options.timeout);
}

/**
* Construct a TCP health check
*/
public static tcp(options: HealthCheckCommonOptions = {}): HealthCheck {
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
return new HealthCheckImpl(Protocol.TCP, options.healthyThreshold, options.unhealthyThreshold, options.interval, options.timeout);
}

/**
* Called when the AccessLog type is initialized. Can be used to enforce
* mutual exclusivity with future properties
*/
public abstract bind(scope: Construct): HealthCheckConfig;
}

class HealthCheckImpl extends HealthCheck {
constructor(
private readonly protocol: Protocol,
private readonly healthyThreshold: number = 5,
private readonly unhealthyThreshold: number = 2,
private readonly interval: cdk.Duration = cdk.Duration.seconds(30),
private readonly timeout: cdk.Duration = cdk.Duration.seconds(5),
private readonly path?: string) {
super();
if (healthyThreshold < 2 || healthyThreshold > 10) {
throw new Error('healthyThreshold must be between 2 and 10');
}

if (unhealthyThreshold < 2 || unhealthyThreshold > 10) {
throw new Error('unhealthyThreshold must be between 2 and 10');
}

if (interval.toMilliseconds() < 5000 || interval.toMilliseconds() > 300_000) {
throw new Error('interval must be more than 5 seconds and less than 300 seconds');
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
}

if (timeout.toMilliseconds() < 2000 || timeout.toMilliseconds() > 60_000) {
throw new Error('timeout must be more than 2 seconds and less than 60 seconds');
}

// Default to / for HTTP Health Checks
if (path === undefined && (protocol === Protocol.HTTP || protocol === Protocol.HTTP2)) {
this.path = '/';
}
}

public bind(_scope: Construct): HealthCheckConfig {
return {
virtualNodeHealthCheck: {
protocol: this.protocol,
healthyThreshold: this.healthyThreshold,
unhealthyThreshold: this.unhealthyThreshold,
intervalMillis: this.interval.toMilliseconds(),
timeoutMillis: this.timeout.toMilliseconds(),
path: this.path,
},
virtualGatewayHealthCheck: {
protocol: this.protocol,
healthyThreshold: this.healthyThreshold,
unhealthyThreshold: this.unhealthyThreshold,
intervalMillis: this.interval.toMilliseconds(),
timeoutMillis: this.timeout.toMilliseconds(),
path: this.path,
},
};
dfezzie marked this conversation as resolved.
Show resolved Hide resolved
}

}

/**
Expand Down
Loading