Skip to content

Commit

Permalink
feat(appmesh): add ClientPolicy to VirtualNode, VirtualGateway and Vi…
Browse files Browse the repository at this point in the history
…rtualService (aws#11563)

Adds backend defaults to Virtual Node and Virtual Gateways.
Adds validation context for the backend defined on the Virtual Node.

Before merging (TODO):

- [ ] Update README

----

*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 Dec 7, 2020
1 parent cb703c7 commit bfee58c
Show file tree
Hide file tree
Showing 14 changed files with 427 additions and 7 deletions.
33 changes: 33 additions & 0 deletions packages/@aws-cdk/aws-acmpca/lib/certificate-authority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';

/**
* Interface which all CertificateAuthority based class must implement
*/
export interface ICertificateAuthority extends cdk.IResource {
/**
* The Amazon Resource Name of the Certificate
*
* @attribute
*/
readonly certificateAuthorityArn: string;
}

/**
* Defines a Certificate for ACMPCA
*
* @resource AWS::ACMPCA::CertificateAuthority
*/
export class CertificateAuthority {
/**
* Import an existing Certificate given an ARN
*/
public static fromCertificateAuthorityArn(scope: Construct, id: string, certificateAuthorityArn: string): ICertificateAuthority {
return new class extends cdk.Resource implements ICertificateAuthority {
readonly certificateAuthorityArn = certificateAuthorityArn;
}(scope, id);
}

private constructor() {
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-acmpca/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// AWS::ACMPCA CloudFormation Resources:
export * from './acmpca.generated';
export * from './certificate-authority';
53 changes: 52 additions & 1 deletion packages/@aws-cdk/aws-appmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const node = mesh.addVirtualNode('virtual-node', {
});
```

Create a `VirtualNode` with the the constructor and add tags.
Create a `VirtualNode` with the constructor and add tags.

```ts
const node = new VirtualNode(this, 'node', {
Expand All @@ -184,14 +184,57 @@ const node = new VirtualNode(this, 'node', {
idle: cdk.Duration.seconds(5),
},
})],
backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({
certificateChain: '/keys/local_cert_chain.pem',
}),
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
});

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

Create a `VirtualNode` with the constructor and add backend virtual service.

```ts
const node = new VirtualNode(this, 'node', {
mesh,
cloudMapService: service,
listeners: [appmesh.VirtualNodeListener.httpNodeListener({
port: 8080,
healthCheck: {
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),
},
})],
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
});

const virtualService = new appmesh.VirtualService(stack, 'service-1', {
virtualServiceName: 'service1.domain.local',
mesh,
clientPolicy: appmesh.ClientPolicy.fileTrust({
certificateChain: '/keys/local_cert_chain.pem',
ports: [8080, 8081],
}),
});

node.addBackend(virtualService);
```

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.

The `backends` property can be added with `node.addBackend()`. We define a virtual service and add it to the virtual node to allow egress traffic to other node.

The `backendsDefaultClientPolicy` property are added to the node while creating the virtual node. These are virtual node's service backends client policy defaults.

## 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 Expand Up @@ -269,6 +312,8 @@ using rules defined in gateway routes which can be added to your virtual gateway
Create a virtual gateway with the constructor:

```ts
const certificateAuthorityArn = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012';

const gateway = new appmesh.VirtualGateway(stack, 'gateway', {
mesh: mesh,
listeners: [appmesh.VirtualGatewayListener.http({
Expand All @@ -277,6 +322,10 @@ const gateway = new appmesh.VirtualGateway(stack, 'gateway', {
interval: cdk.Duration.seconds(10),
},
})],
backendsDefaultClientPolicy: appmesh.ClientPolicy.acmTrust({
certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)],
ports: [8080, 8081],
}),
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
virtualGatewayName: 'virtualGateway',
});
Expand All @@ -300,6 +349,8 @@ const gateway = mesh.addVirtualGateway('gateway', {
The listeners field can be omitted which will default to an HTTP Listener on port 8080.
A gateway route can be added using the `gateway.addGatewayRoute()` method.

The `backendsDefaultClientPolicy` property are added to the node while creating the virtual gateway. These are virtual gateway's service backends client policy defaults.

## Adding a Gateway Route

A _gateway route_ is attached to a virtual gateway and routes traffic to an existing virtual service.
Expand Down
110 changes: 110 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/client-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as acmpca from '@aws-cdk/aws-acmpca';
import * as cdk from '@aws-cdk/core';
import { CfnVirtualNode } from './appmesh.generated';

enum CertificateType {
ACMPCA = 'acm',
FILE = 'file',
}

/**
* Properties of TLS Client Policy
*/
export interface ClientPolicyConfig {
/**
* Represents single Client Policy property
*/
readonly clientPolicy: CfnVirtualNode.ClientPolicyProperty;
}

/**
* Represents the property needed to define a Client Policy
*/
export interface ClientPolicyOptions {
/**
* TLS is enforced on the ports specified here.
* If no ports are specified, TLS will be enforced on all the ports.
*
* @default - none
*/
readonly ports?: number[];
}

/**
* ACM Trust Properties
*/
export interface AcmTrustOptions extends ClientPolicyOptions {
/**
* Contains information for your private certificate authority
*/
readonly certificateAuthorities: acmpca.ICertificateAuthority[];
}

/**
* File Trust Properties
*/
export interface FileTrustOptions extends ClientPolicyOptions {
/**
* Path to the Certificate Chain file on the file system where the Envoy is deployed.
*/
readonly certificateChain: string;
}

/**
* Defines the TLS validation context trust.
*/
export abstract class ClientPolicy {
/**
* Tells envoy where to fetch the validation context from
*/
public static fileTrust(props: FileTrustOptions): ClientPolicy {
return new ClientPolicyImpl(props.ports, CertificateType.FILE, props.certificateChain, undefined);
}

/**
* TLS validation context trust for ACM Private Certificate Authority (CA).
*/
public static acmTrust(props: AcmTrustOptions): ClientPolicy {
return new ClientPolicyImpl(props.ports, CertificateType.ACMPCA, undefined, props.certificateAuthorities);
}

/**
* Returns Trust context based on trust type.
*/
public abstract bind(scope: cdk.Construct): ClientPolicyConfig;

}

class ClientPolicyImpl extends ClientPolicy {
constructor (private readonly ports: number[] | undefined,
private readonly certificateType: CertificateType,
private readonly certificateChain: string | undefined,
private readonly certificateAuthorityArns: acmpca.ICertificateAuthority[] | undefined) { super(); }

public bind(_scope: cdk.Construct): ClientPolicyConfig {
if (this.certificateType === CertificateType.ACMPCA && this.certificateAuthorityArns?.map(certificateArn =>
certificateArn.certificateAuthorityArn).length === 0) {
throw new Error('You must provide at least one Certificate Authority when creating an ACM Trust ClientPolicy');
} else {
return {
clientPolicy: {
tls: {
ports: this.ports,
validation: {
trust: {
[this.certificateType]: this.certificateType === CertificateType.FILE
? {
certificateChain: this.certificateChain,
}
: {
certificateAuthorityArns: this.certificateAuthorityArns?.map(certificateArn =>
certificateArn.certificateAuthorityArn),
},
},
},
},
},
};
}
}
}
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 @@ -13,3 +13,4 @@ export * from './virtual-gateway';
export * from './virtual-gateway-listener';
export * from './gateway-route';
export * from './gateway-route-spec';
export * from './client-policy';
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,5 @@ class FileAccessLog extends AccessLog {
},
};
}
}
}

10 changes: 9 additions & 1 deletion packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnVirtualGateway } from './appmesh.generated';
import { ClientPolicy } from './client-policy';
import { GatewayRoute, GatewayRouteBaseProps } from './gateway-route';

import { IMesh, Mesh } from './mesh';
import { AccessLog } from './shared-interfaces';
import { VirtualGatewayListener, VirtualGatewayListenerConfig } from './virtual-gateway-listener';
Expand Down Expand Up @@ -60,6 +60,13 @@ export interface VirtualGatewayBaseProps {
* @default - no access logging
*/
readonly accessLog?: AccessLog;

/**
* Default Configuration Virtual Node uses to communicate with Virtual Service
*
* @default - No Config
*/
readonly backendsDefaultClientPolicy?: ClientPolicy;
}

/**
Expand Down Expand Up @@ -173,6 +180,7 @@ export class VirtualGateway extends VirtualGatewayBase {
meshName: this.mesh.meshName,
spec: {
listeners: this.listeners.map(listener => listener.listener),
backendDefaults: props.backendsDefaultClientPolicy?.bind(this),
logging: accessLogging !== undefined ? {
accessLog: accessLogging.virtualGatewayAccessLog,
} : undefined,
Expand Down
14 changes: 12 additions & 2 deletions packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as cloudmap from '@aws-cdk/aws-servicediscovery';
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnVirtualNode } from './appmesh.generated';
import { ClientPolicy } from './client-policy';
import { IMesh, Mesh } from './mesh';
import { AccessLog } from './shared-interfaces';
import { VirtualNodeListener, VirtualNodeListenerConfig } from './virtual-node-listener';
Expand Down Expand Up @@ -94,6 +95,13 @@ export interface VirtualNodeBaseProps {
* @default - No access logging
*/
readonly accessLog?: AccessLog;

/**
* Default Configuration Virtual Node uses to communicate with Virtual Service
*
* @default - No Config
*/
readonly backendsDefaultClientPolicy?: ClientPolicy;
}

/**
Expand Down Expand Up @@ -193,8 +201,9 @@ export class VirtualNode extends VirtualNodeBase {
virtualNodeName: this.physicalName,
meshName: this.mesh.meshName,
spec: {
backends: cdk.Lazy.any({ produce: () => this.backends }, { omitEmptyArray: true }),
listeners: cdk.Lazy.any({ produce: () => this.listeners.map(listener => listener.listener) }, { omitEmptyArray: true }),
backends: cdk.Lazy.anyValue({ produce: () => this.backends }, { omitEmptyArray: true }),
listeners: cdk.Lazy.anyValue({ produce: () => this.listeners.map(listener => listener.listener) }, { omitEmptyArray: true }),
backendDefaults: props.backendsDefaultClientPolicy?.bind(this),
serviceDiscovery: {
dns: props.dnsHostName !== undefined ? { hostname: props.dnsHostName } : undefined,
awsCloudMap: props.cloudMapService !== undefined ? {
Expand Down Expand Up @@ -231,6 +240,7 @@ export class VirtualNode extends VirtualNodeBase {
this.backends.push({
virtualService: {
virtualServiceName: virtualService.virtualServiceName,
clientPolicy: virtualService.clientPolicy?.bind(this).clientPolicy,
},
});
}
Expand Down
Loading

0 comments on commit bfee58c

Please sign in to comment.