The AWS CDK for software-defined deployments
Dreaming of Clouds
Modularity
Having just talked about reusability, it's hard to shake the notion that this sounds a lot like reusability by another name: modularity. Although they are related concepts, they are not the same. Essentially, reuse is an outcome of modular code. As you structure code, focus on modularity and let the reuse flow naturally from it. I wanted to introduce the concept formally, though, because I'll be teasing apart the VPC and networking aspects of the stack from the application-specific resources by creating modules.
The other critical piece in designing a good, robust module is ensuring consistent and clean interfaces that hide (or abstract, if you prefer the academic term) the inner workings of the module that aren't necessary for the consumer of the module. The primary focus will be the construction of two modules: one for shared services (i.e., the networking elements), which I'll cover in this article, and one for a web application running behind a load balancer, to be covered in a future article.
Next, I'll look at how to orchestrate these individual modules to produce all of the infrastructure needed for the application, and finally, I'll look at some lifecycle considerations, while considering the tightly knit relationship between CloudFormation, the CDK code, and CDK resources.
VPC from the Ground Up, CDK Style
To keep your code not only logically but also physically modular, create a new file in the lib
directory that will contain the code for the networking resources:
$ touch lib/hello-cdk-base.ts
Listing 4 contains the code that should go in this file [20]. When the time comes to build, you'll need the @aws-cdk/aws-ec2
npm module installed, so you should take care of that now:
$ npm install --save @aws-cdk/aws-ec2
Listing 4
lib/hello-cdk-base.ts
01 import cdk = require('@aws-cdk/cdk'); 02 import ec2 = require('@aws-cdk/aws-ec2'); 03 04 export class HelloCdkBase extends cdk.Stack { 05 public readonly vpc: ec2.VpcNetworkRefProps; 06 07 constructor(parent: cdk.App, name: string, props?: cdk.StackProps, env?: string) { 08 super(parent, name, props); 09 10 const maxZones = this.getContext('max_azs')[`${env}`] 11 12 const helloCdkVpc = new ec2.VpcNetwork(this, 'VPC', { 13 cidr: this.getContext('cidr_by_env')[`${env}`], 14 natGateways: maxZones, 15 enableDnsHostnames: true, 16 enableDnsSupport: true, 17 maxAZs: maxZones, 18 natGatewayPlacement: { subnetName: 'DMZ' }, 19 subnetConfiguration: [ 20 { 21 cidrMask: 24, 22 name: 'Web', 23 subnetType: ec2.SubnetType.Public, 24 }, 25 { 26 cidrMask: 24, 27 name: 'App', 28 subnetType: ec2.SubnetType.Private, 29 }, 30 { 31 cidrMask: 27, 32 name: 'Data', 33 subnetType: ec2.SubnetType.Isolated, 34 }, 35 ], 36 tags: { 37 'stack': 'HelloCdkCommon', 38 'env': `${env}`, 39 'costCenter': 'Shared', 40 'deleteBy': 'NEVER', 41 }, 42 }); 43 44 this.vpc = helloCdkVpc.export(); 45 } 46 }
Although I won't cover the complexities of VPCs, subnets, and all of the accompanying network elements that accompany them in depth, I will mention them for the sake of discussing how they appear in the CDK code.
Subnets, AZs, and NAT Gateways
A look through the VPC code in Listing 4 shows a build-out of a reference three-tier networking architecture with dedicated subnets for a front end, where resources like load balancers, proxies, or ingress controllers live. Accordingly, these subnets are designated Web
(line 22). The ec2.SubnetType.Public
designation used for the subnetType
parameter for these particular subnets tells the CDK that this subnet will receive public traffic.
The natGateways
parameter (line 14) tells the VpcNetwork
constructor function how many AWS NAT Gateways you'd like to provision. By changing the number of availability zones (AZs) that are being used according to environment type (e.g., the "dev"
environment will only use two AZs, compared with three for the "qa"
and "prod"
environments; see the "How Context Works in the AWS CDK" box for more details on specifying context), you're limiting cost in lower environments by restricting the number of AZs into which to deploy resources. Accordingly, the maxAZs
parameter is what tells the constructor function the number of AZs you want to use; omitting this parameter will cause the aforementioned default behavior of utilizing all AZs available for a given region.
How Context Works in the AWS CDK
One thing I like about the CDK is its notion of context. This is certainly a familiar concept to any programmer who has dealt with environment variables or framework configuration files or with passing in run-time variables to something like CloudFormation or Terraform. The CDK gives you a few ways to define context; in this article, I'll leverage CLI context variables for dynamic values and the use of static values stored in a file special to the CDK: cdk.json
.
By default, the cdk.json
file sits in the root of the project directory and is created with the project by the cdk init
command. At this moment (unless you've done some experimentation outside of linearly working through this article), your cdk.json
file should have the content:
{ "app": "node bin/hello-cdk.js" }
To dress it up a bit, overwrite the content of the file with the code in Listing 5.
Here, you've set a few static parameters for your VPC and networking resources – namely, how many AZs you'd like to use for each type of environment and what base classless interdomain routing (CIDR) to use for the VPC. In lib/hello-cdk-base.ts
(Listing 4), you referenced these values in two lines of code:
this.getContext('cidr_by_env')[`${env}`] this.getContext('max_azs')[`${env}`]
The ${env}
in these instances is a placeholder for another dynamic context variable that defines the type of environment in which you are working (e.g., "dev"
, "qa"
, or "prod"
). By default, the CDK will use all AZs available in a region when provisioning subnets for a VPC. The maximum number of AZs available differs between AWS regions, so by setting a maximum, you're ensuring consistency between environments, regardless of the region in which they are built.
Dynamic Context
As you'll see from the use of many of cdk
commands, the CDK exposes a way to pass in run-time variables dynamically; for example:
cdk -c VAR=value
The -c
signals to the command that a variable and accompanying value follow. More than a single variable/value pair can be passed, à la:
cdk -c VAR1=value1 -c VAR2=value2
This mechanism provides a great deal of power and flexibility, particularly when considering the use of CDK scripts within the context of a continuous integration/continuous delivery server or when being leveraged from some other script or orchestrator.
Listing 5
cdk.json
01 { 02 "app": "node bin/hello-cdk.js", 03 "context": { 04 "cidr_by_env": { 05 "dev": "10.100.0.0/16", 06 "qa": "10.200.0.0/16", 07 "prod": "10.300.0.0/16" 08 }, 09 "max_azs": { 10 "dev": 2, 11 "qa": 3, 12 "prod": 3 13 } 14 } 15 }
From the code in the subnetConfiguration
block, you can see three types of subnets: Public
, Private
, and Isolated
. As mentioned, the Web
subnets are of the Public
type. The Web
and App
subnets are provisioned with /24 netmasks, meaning you have 251 usable IP addresses (256 addresses total minus five AWS-reserved addresses; see the "AWS Networking Primer" box for more details on networking within AWS). For the Data
subnets (e.g., where databases and other data-related services might reside), smaller subnets are used: a /27, yielding 27 usable addresses per subnet (32 theoretical available addresses minus five AWS-reserved addresses).
AWS Networking Primer
Most networking aficionados would expect to see two addresses (network and broadcast) reserved with a CIDR block or subnet. That AWS has five is a bit surprising, and you might be wondering what AWS reserves them for. Considering a 10.200.0.0/24 subnet, addresses are reserved as follows:
- 10.200.0.0 is a reserved network address, as in traditional networking setups.
- 10.200.0.1 is reserved for the VPC router.
- 10.200.0.2: puts the VPC DNS resolver at this address, if it is the first subnet of the entire VPC CIDR block; otherwise, it's just a reserved address.
- 10.200.0.3 is reserved for future use.
- 10.200.0.255 is the broadcast address reserved by AWS, as in traditional networking. However, AWS only supports unicast and not multicast or broadcast.
For more information, see the AWS VPCs and subnets documentation [21].
For subnet and subnet types, the natGatewayPlacement
parameter tells the CDK where you want it to place your NAT gateways when you create your VPC. The DNS tags
parameters (lines 36-41) give additional flexibility for the use of custom internal DNS domains within your VPC. The tags
parameter, while providing conventional AWS tag sets on all the networking resources (which are abundantly useful in and of themselves within the AWS ecosystem for a multitude of reasons), also take on additional functionality within the context of the CDK, which I will cover in future discussions.
Buy this article as PDF
(incl. VAT)