Skip to content

Commit

Permalink
docs: add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
tomwwright committed May 11, 2023
1 parent 4cb6082 commit ac79325
Show file tree
Hide file tree
Showing 18 changed files with 687 additions and 51 deletions.
369 changes: 342 additions & 27 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions bin/appmesh.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env node
import "source-map-support/register";
import { Peer } from "aws-cdk-lib/aws-ec2";
import { AppMeshApp } from "../lib/appmesh";
import { externalAccess } from "../lib/ip";

new AppMeshApp({
namespaceName: "appmesh",
externalAccess: Peer.ipv4("139.130.21.126/32"),
externalAccess,
});
14 changes: 12 additions & 2 deletions bin/bluegreen.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
#!/usr/bin/env node
import "source-map-support/register";
import { Peer } from "aws-cdk-lib/aws-ec2";
import { BlueGreenApp } from "../lib/bluegreen";
import { App } from "aws-cdk-lib";
import { injectBlueGreenState as injectBlueGreenState } from "../lib/bluegreen/deployment";
import { externalAccess } from "../lib/ip";

async function main() {
/**
* as constructors cannot be async and retrieving the current
* state from AWS Parameter Store is async it has to be done
* here
*/

const app = new App();
await injectBlueGreenState(app); // needs to be performed out here because async unsupported in constructors

/**
* having populated the state we can now enter the construct tree
*/

new BlueGreenApp(app, "bluegreen", {
namespaceName: "bluegreen",
externalAccess: Peer.ipv4("139.130.21.126/32"),
externalAccess,
});
}

Expand Down
3 changes: 2 additions & 1 deletion bin/serviceconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import "source-map-support/register";
import { Peer } from "aws-cdk-lib/aws-ec2";
import { ServiceConnectApp } from "../lib/serviceconnect";
import { externalAccess } from "../lib/ip";

new ServiceConnectApp({
namespaceName: "serviceconnect",
externalAccess: Peer.ipv4("139.130.21.126/32"),
externalAccess,
});
3 changes: 3 additions & 0 deletions docs/appmesh-components.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/appmesh.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/bluegreen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/serviceconnect.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 59 additions & 2 deletions lib/appmesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,20 @@ export class AppMeshApp extends App {

const { namespaceName, externalAccess } = props;

// create ourselves an empty stack to hold our resources

const stack = new Stack(this, namespaceName);

/**
* create our ECS Cluster that is configured for App Mesh
*
* the key difference is that the namespace is backed by Route 53
*
* `cluster` our ECS Cluster with underlying default VPC
* `namespace` our Cloud Map namespace created by ourselves
* `securityGroup` security group accessible from our IP to make things easy
**/

const { cluster, namespace, securityGroup } = new AppMeshCluster(
stack,
"Cluster",
Expand All @@ -36,6 +48,15 @@ export class AppMeshApp extends App {
}
);

/**
* configure App Mesh components
*
* `mesh` the App Mesh mesh that services will be added to as nodes,
* routes, services, etc.
* `gateway` the App Mesh Gateway, running on ECS Fargate, that will
* receive external traffic and route it into the service mesh
*/

const { mesh, gateway } = new AppMesh(stack, "Mesh", {
cluster,
securityGroup,
Expand All @@ -49,6 +70,21 @@ export class AppMeshApp extends App {
securityGroup,
};

/**
* create two ECS Services, "blue" and "green", running our sample Express.js
* application that are configured for App Mesh
*
* the details of configuring for App Mesh are quite gory -- take a look
* inside the Construct!
*
* thankfull this is precisely the sort of thing that AWS CDK excels at,
* abstracting away gory details
*
* we can use the /downstream endpoint of our sample application to explore
* how these services are able to discover and route traffic to each other
* as they have been connected to the same namespace
**/

const blue = new AppMeshExpress(stack, "Blue", {
serviceName: "blue",
...meshThings,
Expand All @@ -59,10 +95,22 @@ export class AppMeshApp extends App {
...meshThings,
});

/**
* configure each service as a "backend" of the other's node so that
* traffic can be routed between them via the mesh
*/

blue.virtualNode.addBackend(Backend.virtualService(green.virtualService));
green.virtualNode.addBackend(Backend.virtualService(blue.virtualService));

// configure gateway routes for the services
/**
* configure a route on the gateway for each service so that
* traffic entering the mesh is routed to that service
*
* this sets up path-based routing:
* - all traffic with path starting with '/blue' -> blue
* - all traffic with path starting with '/green' -> green
*/

gateway.addGatewayRoute("blue", {
routeSpec: GatewayRouteSpec.http({
Expand All @@ -82,7 +130,10 @@ export class AppMeshApp extends App {
}),
});

// configure fancy routing based on path
/**
* configure a router that divides traffic equally between our
* "blue" and "green" nodes
*/

const router = new VirtualRouter(stack, "Router", {
mesh,
Expand All @@ -106,6 +157,12 @@ export class AppMeshApp extends App {
}),
});

/**
* wrap our router in a service so that it can be the destination of
* a gateway route:
* - all traffic with path starting with '/split' -> router
*/

const routerService = new VirtualService(stack, "RouterService", {
virtualServiceProvider: VirtualServiceProvider.virtualRouter(router),
virtualServiceName: "router",
Expand Down
17 changes: 17 additions & 0 deletions lib/appmesh/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,27 @@ export class AppMeshCluster extends Construct {

const { namespaceName } = props;

/**
* Vanilla ECS Cluster with underlying default VPC
*/

this.cluster = new Cluster(this, `Cluster`, {
clusterName: namespaceName,
});

/**
* create our own Cloud Map namespace for service discovery
*
* notably, this is a Private DNS namespace so Cloud Map creates
* an associated Route 53 private hosted zone and automates
* managing the records in it
*
* this differs from ECS Service Connect in that it uses an HTTP-Only
* namespace with no backing Route 53 DNS records
*
* see additional notes in the README
*/

this.namespace = new PrivateDnsNamespace(this, "Namespace", {
name: namespaceName,
vpc: this.cluster.vpc,
Expand Down
54 changes: 48 additions & 6 deletions lib/appmesh/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ export class AppMeshExpress extends Construct {

const { cluster, mesh } = props;

/**
* our task definition needs to have proxy configuration applied
*
* this is almost entirely boilerplate except for 'appPort' which
* indicates which port our running service will be on
*
* (app mesh needs to know this port so that it can 'hijack' it
* for the proxying)
*/

const taskDefinition = new FargateTaskDefinition(this, "TaskDefinition", {
proxyConfiguration: new AppMeshProxyConfiguration({
containerName: "proxy",
Expand All @@ -56,6 +66,14 @@ export class AppMeshExpress extends Construct {
}),
});

/**
* configure and add our application -- note there is nothing
* "service meshy" about it!
*
* unbeknownst to our little application, all sort of whacky
* stuff is happening elsewhere...
*/

const expressContainer = taskDefinition.addContainer("expressjs", {
image: ContainerImage.fromAsset("expressjs"),
command: ["serve:js"],
Expand Down Expand Up @@ -86,6 +104,10 @@ export class AppMeshExpress extends Construct {
assignPublicIp: true,
});

/**
* register our service for service discovery with Cloud Map
*/

const cloudMapService = new Service(this, "ServiceDiscovery", {
name: props.serviceName,
namespace: props.namespace,
Expand All @@ -99,6 +121,16 @@ export class AppMeshExpress extends Construct {
service: cloudMapService,
});

/**
* create a logical node and service in our service mesh for our
* Express.js application
*
* note how service discovery is "wired up" on the virtual node
* to our Cloud Map service that we just registered
*
* ECS -> Cloud Map -> App Mesh
*/

const virtualNode = new VirtualNode(this, "VirtualNode", {
mesh,
serviceDiscovery: ServiceDiscovery.cloudMap(cloudMapService),
Expand All @@ -113,6 +145,22 @@ export class AppMeshExpress extends Construct {
virtualNode.grantStreamAggregatedResources(taskDefinition.taskRole);
this.virtualNode = virtualNode;

const virtualService = new VirtualService(this, "VirtualService", {
virtualServiceName: `${props.serviceName}.${props.namespace.namespaceName}`,
virtualServiceProvider: VirtualServiceProvider.virtualNode(virtualNode),
});
this.virtualService = virtualService;

/**
* where the magic happens -- configure an Envoy proxy sidecar in our task
* definition
*
* note again the use of APPMESH_RESOURCE_ARN to configure this proxy
* as the virtual node we added
*
* the rest is _entirely_ boilerplate :sweat:
*/

const proxyContainer = taskDefinition.addContainer("proxy", {
image: ContainerImage.fromRegistry(
"public.ecr.aws/appmesh/aws-appmesh-envoy:v1.24.0.0-prod"
Expand Down Expand Up @@ -156,11 +204,5 @@ export class AppMeshExpress extends Construct {
expressContainer.addContainerDependencies({
container: proxyContainer,
});

const virtualService = new VirtualService(this, "VirtualService", {
virtualServiceName: `${props.serviceName}.${props.namespace.namespaceName}`,
virtualServiceProvider: VirtualServiceProvider.virtualNode(virtualNode),
});
this.virtualService = virtualService;
}
}
38 changes: 38 additions & 0 deletions lib/appmesh/mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,22 @@ export class AppMesh extends Construct {

const { cluster, securityGroup } = props;

/**
* create the App Mesh mesh
*/

const mesh = new Mesh(this, "Mesh", {
egressFilter: MeshFilterType.DROP_ALL,
});
this.mesh = mesh;

/**
* configure the gateway within the mesh
*
* at this stage this is just a logical component of the mesh
* with no corresponding running infrastructure
*/

const gateway = new VirtualGateway(this, "VirtualGateway", {
mesh,
listeners: [
Expand All @@ -53,6 +64,21 @@ export class AppMesh extends Construct {
});
this.gateway = gateway;

/**
* configure a task definition that runs only the
* Envoy proxy
*
* we "wire this up" to the gateway in the mesh using the
* APPMESH_RESOURCE_ARN environment variable
*
* the rest is essentially boilerplate -- wordy!
*
* note: for nodes in the mesh, it would instead be our application
* with the Envoy proxy as a sidecar -- but a gateway is
* just the Envoy itself expecting to receive traffic directly
* and forward it
*/

const gatewayTaskDefinition = new FargateTaskDefinition(
this,
"GatewayTaskDefinition"
Expand Down Expand Up @@ -91,6 +117,11 @@ export class AppMesh extends Construct {
}),
});

/**
* ensure that the Envoy when it starts has permissions
* to connect to AppMesh and download the state of the mesh
*/

gatewayTaskDefinition.addToTaskRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
Expand All @@ -99,6 +130,13 @@ export class AppMesh extends Construct {
})
);

/**
* alright some actual infrastructure!
*
* fire up a Fargate service behind an NLB to act as the "front door"
* of our service mesh -- external traffic comes in here
*/

const gatewayService = new NetworkLoadBalancedFargateService(
this,
"GatewayService",
Expand Down
Loading

0 comments on commit ac79325

Please sign in to comment.