AWS Developer Tools Blog

Contributing to the AWS Cloud Development Kit

This blog was authored by Mike Cowgill, Principal Engineer at Intuit (maker of TurboTax and QuickBooks) and active collaborator in the AWS Cloud Development Kit open source community.

What is exciting about the AWS CDK?

First, if you don’t know what the AWS Cloud Development Kit (CDK) is, check out the GitHub project or CDK’s introductory AWS blog.

The AWS CDK solves an important problem by providing a true software language interface to AWS CloudFormation. From my perspective, AWS CloudFormation has limitations that are better solved in a programming language. Infrastructure as code has been around for a few years now, but is AWS CloudFormation really code? How do you share a standard AWS WAF configuration with your company? The answer is, you can’t do this well because AWS CloudFormation is more configuration and less code.

With the AWS CDK, developers can now:

  • Version and release infrastructure code to the same repositories as application code (Nexus, NPM, PyPi, RubyGems, Artifactory, NuGet, GitHub).
  • Use CDK support for programming languages, so code constructs like functions, inheritance, or conditional flow are no longer an odd notation in YAML or JSON, but are in the actual programming language the developers are using for application code. CDK supports TypeScript, .NET, Java, and JavaScript today. More languages are coming, and I expect to see Golang, Python, and Ruby join the list.
  • Reuse and consume CDK components, known as constructs, by using the dependency management tools already native to the developer’s programming language of choice.

These features really enable a company to finally compose reusable AWS CloudFormation components that are tailored to the specific business needs of the company.

A quick example can illustrate this best. Let’s build a VPC:

import ec2 = require('@aws-cdk/aws-ec2');
const vpc = new ec2.VpcNetwork(this, 'VPC');

The result is a ready-to-use VPC with subnets, internet gateways, NAT gateways, and route tables. Clearly, the best part of the above code is all the components you don’t see. Practically, these defaults may not fully suit your needs, and in fact, when I first launched CDK, the default options didn’t meet mine. However, because CDK is open source, I decided to propose an improvement. If you’re not familiar with CDK concepts, take a minute to review them because we’ll reference these as we explore further.

Contributing to CDK

I’ll now cover how I submitted my contribution to the Amazon EC2 library. CDK comes packaged with a pretty good developer flow, and I’ll summarize my approach.

CDK is organized as a Lerna project, but you can get started without a lot of Lerna experience. Because CDK is a Lerna project, one repository contains all the modules for the AWS services that CDK supports. CDK’s authors provide guidelines for how to contribute in the repo. However, this is under active development and the information in here can quickly become outdated. I skimmed through this document to get an understanding of expectations from the contributors, and I’ll highlight a shorter path to development with Node.js and TypeScript.

At Intuit, we needed Node.js version 8.11.0 or later to develop against CDK. If you work using Node but don’t have this version installed, you can follow the setup guides at https://nodejs.org. As an alternative, you can use the official Node.js Docker container.

To begin, we pull down CDK source code, and then build and test the @aws-cdk/aws-ec2 module. This would work the same way for any modules in the @aws-cdk directory.

git clone https://github.com/awslabs/aws-cdk.git
cd aws-cdk/
npm install -g lerna
./install.sh
export PATH=$PATH:$PWD/scripts/
cd packages/@aws-cdk/aws-ec2/
buildup
lerna run --stream --scope $(node -p "require(\"./package.json\").name") test

The output should look similar to the following:

… snip … 
@aws-cdk/aws-ec2: ✔ export/import - simple VPC
@aws-cdk/aws-ec2: ✔ export/import - multiple subnets of the same type
@aws-cdk/aws-ec2: ✔ export/import - can select isolated subnets by type
@aws-cdk/aws-ec2: ✔ export/import - can select isolated subnets by name
@aws-cdk/aws-ec2: OK: 60 assertions (1726ms)
@aws-cdk/aws-ec2: =============================== Coverage summary ===============================
@aws-cdk/aws-ec2: Statements   : 81.38% ( 494/607 )
@aws-cdk/aws-ec2: Branches     : 64.5% ( 129/200 )
@aws-cdk/aws-ec2: Functions    : 65.29% ( 111/170 )
@aws-cdk/aws-ec2: Lines        : 82.74% ( 484/585 )
@aws-cdk/aws-ec2: ================================================================================
@aws-cdk/aws-ec2: Verifying integ.vpc.js against integ.vpc.expected.json... OK.
@aws-cdk/aws-ec2: Tests successful. Total time (9.6s) | detectChanges (5.0s) | nyc (3.7s) | cdk-integ-assert (0.9s)
lerna success run Ran npm script 'test' in 1 package:
lerna success - @aws-cdk/aws-ec2

I want to highlight a couple of things from this code. We export a scripts folder in our path that contains very useful build commands. We invoke buildup. Buildup builds only the dependencies in CDK repo that our module needs. This saves us significant time from building the entire CDK. There is also a builddown, which we can use later to build our downstream dependencies. From the root of the project, we can also invoke install.sh and build.sh to trigger the entire build process. Finally, we use the Lerna run command, which is key. This is the primary call to run tests, and you likely want to alias this method, as CDK’s contribution guidance recommends:

# add to your ~/.zshrc or ~/.bashrc
alias lr='lerna run --stream --scope $(node -p "require(\"./package.json\").name")'
# more sugar
alias lw='lr watch &'
alias lt='lr test'

At this point, I’m prepared to start making my source code changes. Typically, I start watch as a background job in a split terminal. After watch is successful, I periodically run tests as I develop.

Enhancing the VPC AWS construct

On first use, the VPC was missing the following key features:

  • Control over network CIDR block definitions without needing to be a network expert
  • Configurable subnets for three types: public, private, and isolated
  • Ability to specify NAT gateway configurations

Before going too much further, I should note that as an AWS construct, the VPC has some special meaning we should highlight. An AWS construct is commonly called an “L2 construct”. An AWS CloudFormation construct is called an “L1 construct”. Each module uses the AWS CloudFormation specification to generate the L1 constructs, then AWS and CDK contributors build a more intent-based and user-friendly L2 construct.

To configure the network space, CDK needed classes to support the understanding of networking concepts. I added the NetworkBuilder and CidrBlock classes, as well as tests. The builder is composed of the network CIDR block and potentially many subnet CIDR blocks. To keep this interface clean to consumers, I wanted to support a simple subnet configuration syntax:

export interface SubnetConfiguration {
    /**
     * The CIDR Mask or the number of leading 1 bits in the routing mask
     *
     * Valid values are 16 - 28
     */
    cidrMask?: number;

    /**
     * The type of subnet to configure.
     *
     * The subnet type will control the ability to route and connect to the
     * internet.
     */
    subnetType: SubnetType;
    
    /**
     * The common logical name for the `VpcSubnet`
     *
     * This name will be suffixed with an integer correlating to a specific
     * Availability Zone.
     */
    name: string;
    /**
     * The AWS resource tags to associate with the resource.
     */
    tags?: cdk.Tags;
}

With this in place, a VPC designer needs to know the CIDR block for the entire VPC, the masks, and types of subnets. The order in which the subnet configurations are passed dictates the order in which the CIDR blocks are allocated.

Subnet types are also created and those also convey specific intents:

export enum SubnetType {    
    /**
     * Isolated subnets don’t route outbound traffic
     *
     * This can be good for subnets with Amazon RDS or
     * Amazon ElastiCache endpoints
     */
    Isolated = 1,
    /**
     * Subnet that allow outbound traffic to the internet via a NAT Gateway.
     *
     * Outbound traffic will be routed via a NAT gateway. Preference being in
     * the same Availability Zone, but if not available will use another Availability Zone (control by
     * specifying `natGateways` on VpcNetwork). This might be used for
     * experimental cost conscious accounts, or accounts where HA outbound
     * traffic is not needed.
     */
    Private = 2,
    /**
     * Subnet connected to the internet
     *
     * Instances in a public subnet can connect to the internet, and can be
     * connected to from the internet, if they are launched with public
     * IPs (controlled on the EC2 Auto Scaling group or other constructs that launch
     * instances).
     *
     * Public subnets route outbound traffic via an internet gateway.
     */
    Public = 3
}

By default, you will get one NAT gateway per Availability Zone, which is the most resilient design. However, I wanted the ability to trade cost savings for resiliency by reducing the number of NAT gateways. This is now possible using the natGateways property.

Intuit has a concept of “learning accounts”. If developers want to learn or experiment with AWS services, they can request an account at any time. The account is delivered within 15 minutes and has a VPC already configured, in addition to our security automation capabilities. This account will also be cleaned up after three months. NAT gateways are the only component with a cost associated during setup. Because these are learning accounts, high availability is not a requirement and we vend these VPCs with a single gateway instead of three. CDK enables this cost savings use-case with significantly less complication than traditional AWS CloudFormation conditions.

You can review the entire process I went through in pull request 250.

Now that we can create VPCs, let’s look at an example that combines this ability with other CDK constructs. I have created a getting started Amazon EKS example repository. The example creates two stacks and closely follows the AWS getting started documentation.

VPC resources are still inherently complex. If this L2 construct doesn’t meet your needs, you still might need to create your own. In the next section, we’ll discuss some challenges with L2s. Going forward, I’m looking forward to a change from VpcRef constructs to a VPC interface object. This will allow users in the future to more easily implement their own custom L2 constructs for VPCs. You can follow the status in this issue.

Challenges in using the AWS CDK

The AWS CDK is not quite ready for general use and there are still some rough edges.

The AWS constructs (L2) can hide features supported by AWS CloudFormation (L1) from CDK users. In the early days of CDK, this was a real problem because you had to nearly rewrite the entire L2 for your use-case. However, the community opened this issue and developed an improved method for accessing the L1 layer. Although CDK has created this mechanism, users are still encouraged to open issues when they need to use overrides. The intent of CDK is to address these issues correctly in the L2 layer. For example, this issue was opened when overrides would have worked, but the team addressed the gap by enabling the L2 APIs. Regardless, there will still be issues where you might temporarily or permanently need to use this feature.

Logical IDs are generated or the user has to completely manage all logical IDs. The root problem here is that AWS CloudFormation has a flat namespace for all logical IDs in a stack. CDK developers have been smart to create an MD5 hash to ensure uniqueness based on the construct tree path. The problem is that if you change the tree during an update, AWS CloudFormation wants to delete and recreate your resources. CDK developers have attempted to minimize deletion and recreation, but they can still happen. CDK does support renaming. Understanding the construct tree and renaming has so far been sufficient. The alternate approach is that the developer manages all the logical IDs.

Support for multiple languages is achieved through jsii modules. The magic is really achieved by running a JavaScript runtime and using proxy classes in the target language. Although I don’t currently see any major problems, this is definitely a key project that is directly connected to CDK’s success. The architecture behind this approach will remove some features that are not ubiquitous across languages, such as generics. This isn’t really a pain point, but definitely something worth acknowledging.

Next things I’m exploring

CDK is still in the beta phase and there are many potential benefits still in development. Here are a few that I’m looking into for the short term, and hopefully AWS and the community will develop many more:

  • CDK L2 constructs for AWS CodePipeline look very promising. Can we create a standard pipeline to deploy and manage CDK stacks? AWS has already opened an issue to explore this.
  • Policy enforcement before execution. The framework provides the ability to traverse the graph of resources, which will enable a team to develop better pre-execution validation checks. AWS has been tracking this issue for the idea, long before I mentioned it.
  • The difference calculation is awesome, and with an extension to programmatically review it, we can now build conditional automated approvals. For example: If this is an update only, then apply. If this contains only a LaunchConfig replacement or Amazon EC2 Auto Scaling group replacement, automatically apply. If this deletes any persistent data store (Amazon RDS, Amazon S3, Amazon DynamoDB, Amazon Redshift), wait for approval. We’ve opened this issue to track the concept.
  • Context providers enable CDK developers to leverage the AWS APIs. This is a powerful feature to look up resources outside of AWS CloudFormation and pass decoupled inputs between stacks. How does this impact or change the practice around cross-stack references?

Conclusion

The AWS CDK is a major step for developers to control and automate AWS infrastructure. The current status of CDK is still beta, and there are missing AWS constructs. We’ll likely see breaking changes in the framework as the team receives feedback from the community. The speed at which I can develop and iterate using CDK is significantly faster than using AWS CloudFormation alone. The ability to share and distribute AWS CloudFormation patterns using my company’s native software tools increases the consistency and standardization across all Intuit accounts. Even more importantly, AWS has made this project open source. This finally enables customers to directly contribute to and enhance the future of AWS CloudFormation.

About the Author

 Mike Cowgill is a Principal Engineer at Intuit, maker of TurboTax and QuickBooks. During his three years at Intuit, Mike has focused on Intuit’s adoption of AWS, contributing to the successful migration of multiple core components of Intuit’s TurboTax property and key internal systems. Mike is part of a central Cloud Engineering organization that develops and harvests the best practices in cloud migration for all of Intuit.

Disclaimer

The content and opinions in this post are those of the third-party author and AWS is not responsible for the content or accuracy of this post.