Skip to content

Commit

Permalink
feat(appmesh): add Virtual Gateways and Gateway Routes (#10879)
Browse files Browse the repository at this point in the history
----

This is a draft PR to resolve #9533

Takes an approach for creating protocol specific Gateway Routes as described in #10793 

This is a draft as I am seeking feedback on the implementation and approach for creating per protocol variants of App Mesh Resources.

Before merging:

- [x] Approach for per protocol variants defined
- [x] Update Gateway Listeners to follow the same pattern
- [x] Add more integ tests

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Dominic Fezzie authored Oct 29, 2020
1 parent 87887a3 commit 79200e7
Show file tree
Hide file tree
Showing 15 changed files with 1,640 additions and 66 deletions.
101 changes: 71 additions & 30 deletions packages/@aws-cdk/aws-appmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@ mesh.addVirtualService('virtual-service', {

A `virtual node` acts as a logical pointer to a particular task group, such as an Amazon ECS service or a Kubernetes deployment.

![Virtual node logical diagram](https://docs.aws.amazon.com/app-mesh/latest/userguide/images/virtual_node.png)

When you create a `virtual node`, you must specify the DNS service discovery hostname for your task group. Any inbound traffic that your `virtual node` expects should be specified as a listener. Any outbound traffic that your `virtual node` expects to reach should be specified as a backend.

The response metadata for your new `virtual node` contains the Amazon Resource Name (ARN) that is associated with the `virtual node`. Set this value (either the full ARN or the truncated resource name) as the APPMESH_VIRTUAL_NODE_NAME environment variable for your task group's Envoy proxy container in your task definition or pod spec. For example, the value could be mesh/default/virtualNode/simpleapp. This is then mapped to the node.id and node.cluster Envoy parameters.
Expand All @@ -144,7 +142,6 @@ const namespace = new servicediscovery.PrivateDnsNamespace(this, 'test-namespace
const service = namespace.createService('Svc');

const node = mesh.addVirtualNode('virtual-node', {
dnsHostName: 'node-a',
cloudMapService: service,
listener: {
portMapping: {
Expand All @@ -170,7 +167,6 @@ Create a `VirtualNode` with the the constructor and add tags.
```typescript
const node = new VirtualNode(this, 'node', {
mesh,
dnsHostName: 'node-1',
cloudMapService: service,
listener: {
portMapping: {
Expand All @@ -193,7 +189,7 @@ const node = new VirtualNode(this, 'node', {
cdk.Tag.add(node, 'Environment', 'Dev');
```

The listeners property can be left blank and added later with the `mesh.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.addListeners()` method. The `healthcheck` property is optional but if specifying a listener, the `portMappings` must contain at least one property.

## Adding a Route

Expand Down Expand Up @@ -235,34 +231,79 @@ router.addRoute('route', {
});
```

Multiple routes may also be added at once to different applications or targets.
## Adding a Virtual Gateway

A _virtual gateway_ allows resources outside your mesh to communicate to resources that are inside your mesh.
The virtual gateway represents an Envoy proxy running in an Amazon ECS task, in a Kubernetes service, or on an Amazon EC2 instance.
Unlike a virtual node, which represents an Envoy running with an application, a virtual gateway represents Envoy deployed by itself.

A virtual gateway is similar to a virtual node in that it has a listener that accepts traffic for a particular port and protocol (HTTP, HTTP2, GRPC).
The traffic that the virtual gateway receives, is directed to other services in your mesh,
using rules defined in gateway routes which can be added to your virtual gateway.

Create a virtual gateway with the constructor:

```typescript
ratingsRouter.addRoutes(
['route1', 'route2'],
[
{
routeTargets: [
{
virtualNode,
weight: 1,
},
],
prefix: `/path-to-app`,
routeType: RouteType.HTTP,
const gateway = new appmesh.VirtualGateway(stack, 'gateway', {
mesh: mesh,
listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({
port: 443,
healthCheck: {
interval: cdk.Duration.seconds(10),
},
{
routeTargets: [
{
virtualNode: virtualNode2,
weight: 1,
},
],
prefix: `/path-to-app2`,
routeType: RouteType.HTTP,
})],
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
virtualGatewayName: 'virtualGateway',
});
```

Add a virtual gateway directly to the mesh:

```typescript
const gateway = mesh.addVirtualGateway('gateway', {
accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'),
virtualGatewayName: 'virtualGateway',
listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({
port: 443,
healthCheck: {
interval: cdk.Duration.seconds(10),
},
})],
});
```

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.

## Adding a Gateway Route

A _gateway route_ is attached to a virtual gateway and routes traffic to an existing virtual service.
If a route matches a request, it can distribute traffic to a target virtual service.

For HTTP based routes, the match field can be used to match on a route prefix.
By default, an HTTP based route will match on `/`. All matches must start with a leading `/`.

```typescript
gateway.addGatewayRoute('gateway-route-http', {
routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({
routeTarget: virtualService,
match: {
prefixMatch: '/',
},
]
);
}),
});
```

The number of `route ids` and `route targets` must match as each route needs to have a unique name per router.
For GRPC based routes, the match field can be used to match on service names.
You cannot omit the field, and must specify a match for these routes.

```typescript
gateway.addGatewayRoute('gateway-route-grpc', {
routeSpec: appmesh.GatewayRouteSpec.grpcRouteSpec({
routeTarget: virtualService,
match: {
serviceName: 'my-service.default.svc.cluster.local',
},
}),
});
```
209 changes: 209 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import * as cdk from '@aws-cdk/core';
import { CfnGatewayRoute } from './appmesh.generated';
import { Protocol } from './shared-interfaces';
import { IVirtualService } from './virtual-service';

/**
* The criterion for determining a request match for this GatewayRoute
*/
export interface HttpGatewayRouteMatch {
/**
* Specifies the path to match requests with.
* This parameter must always start with /, which by itself matches all requests to the virtual service name.
* You can also match for path-based routing of requests. For example, if your virtual service name is my-service.local
* and you want the route to match requests to my-service.local/metrics, your prefix should be /metrics.
*/
readonly prefixPath: string;
}

/**
* The criterion for determining a request match for this GatewayRoute
*/
export interface GrpcGatewayRouteMatch {
/**
* The fully qualified domain name for the service to match from the request
*/
readonly serviceName: string;
}

/**
* Properties specific for HTTP Based GatewayRoutes
*/
export interface HttpRouteSpecProps {
/**
* The criterion for determining a request match for this GatewayRoute
*
* @default - matches on '/'
*/
readonly match?: HttpGatewayRouteMatch;

/**
* The VirtualService this GatewayRoute directs traffic to
*/
readonly routeTarget: IVirtualService;
}

/**
* Properties specific for a GRPC GatewayRoute
*/
export interface GrpcRouteSpecProps {
/**
* The criterion for determining a request match for this GatewayRoute
*/
readonly match: GrpcGatewayRouteMatch;

/**
* The VirtualService this GatewayRoute directs traffic to
*/
readonly routeTarget: IVirtualService;
}

/**
* All Properties for GatewayRoute Specs
*/
export interface GatewayRouteSpecConfig {
/**
* The spec for an http gateway route
*
* @default - no http spec
*/
readonly httpSpecConfig?: CfnGatewayRoute.HttpGatewayRouteProperty;

/**
* The spec for an http2 gateway route
*
* @default - no http2 spec
*/
readonly http2SpecConfig?: CfnGatewayRoute.HttpGatewayRouteProperty;

/**
* The spec for a grpc gateway route
*
* @default - no grpc spec
*/
readonly grpcSpecConfig?: CfnGatewayRoute.GrpcGatewayRouteProperty;
}

/**
* Used to generate specs with different protocols for a GatewayRoute
*/
export abstract class GatewayRouteSpec {
/**
* Creates an HTTP Based GatewayRoute
*
* @param props - no http gateway route
*/
public static httpRouteSpec(props: HttpRouteSpecProps): GatewayRouteSpec {
return new HttpGatewayRouteSpec(props, Protocol.HTTP);
}

/**
* Creates an HTTP2 Based GatewayRoute
*
* @param props - no http2 gateway route
*/
public static http2RouteSpec(props: HttpRouteSpecProps): GatewayRouteSpec {
return new HttpGatewayRouteSpec(props, Protocol.HTTP2);
}

/**
* Creates an GRPC Based GatewayRoute
*
* @param props - no grpc gateway route
*/
public static grpcRouteSpec(props: GrpcRouteSpecProps): GatewayRouteSpec {
return new GrpcGatewayRouteSpec(props);
}

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

class HttpGatewayRouteSpec extends GatewayRouteSpec {
/**
* The criterion for determining a request match for this GatewayRoute.
*
* @default - matches on '/'
*/
readonly match?: HttpGatewayRouteMatch;

/**
* The VirtualService this GatewayRoute directs traffic to
*/
readonly routeTarget: IVirtualService;

/**
* Type of route you are creating
*/
readonly routeType: Protocol;

constructor(props: HttpRouteSpecProps, protocol: Protocol.HTTP | Protocol.HTTP2) {
super();
this.routeTarget = props.routeTarget;
this.routeType = protocol;
this.match = props.match;
}

public bind(_scope: cdk.Construct): GatewayRouteSpecConfig {
const prefixPath = this.match ? this.match.prefixPath : '/';
if (prefixPath[0] != '/') {
throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`);
}
const httpConfig: CfnGatewayRoute.HttpGatewayRouteProperty = {
match: {
prefix: prefixPath,
},
action: {
target: {
virtualService: {
virtualServiceName: this.routeTarget.virtualServiceName,
},
},
},
};
return {
httpSpecConfig: this.routeType === Protocol.HTTP ? httpConfig : undefined,
http2SpecConfig: this.routeType === Protocol.HTTP2 ? httpConfig : undefined,
};
}
}

class GrpcGatewayRouteSpec extends GatewayRouteSpec {
/**
* The criterion for determining a request match for this GatewayRoute.
*
* @default - no default
*/
readonly match: GrpcGatewayRouteMatch;

/**
* The VirtualService this GatewayRoute directs traffic to
*/
readonly routeTarget: IVirtualService;

constructor(props: GrpcRouteSpecProps) {
super();
this.match = props.match;
this.routeTarget = props.routeTarget;
}

public bind(_scope: cdk.Construct): GatewayRouteSpecConfig {
return {
grpcSpecConfig: {
action: {
target: {
virtualService: {
virtualServiceName: this.routeTarget.virtualServiceName,
},
},
},
match: {
serviceName: this.match.serviceName,
},
},
};
}
}
Loading

0 comments on commit 79200e7

Please sign in to comment.