Getting Started with AWS CDK

Start building infrastructure as code with AWS CDK best practices

We've covered the AWS Cloud Development Kit in the past. CDK is a tool for declaring your infrastructure as code, with all the benefits that entails. But now that you're sold on it, where do you start?

Bootstrapping the Stack


The AWS CDK Toolkit is a command-line tool for working with CDK projects. Installing it is usually as simple as running npm install -g aws-cdk. (You'll also need the AWS CLI installed, if you don't have it already.)

Initializing the Project


We can now use cdk to initialize our project. We'll use Typescript for this demo, but CDK supports several languages, so you can use one that you're familiar with:

cdk init app --language=typescript

This creates a Typescript app, complete with package.json, that is ready to deploy - apart from having no components yet! We have a bin/aws-cdk-demo.ts file, which sets up the stack with environment parameters, and a lib/aws-cdk-demo-stack.ts file which defines the stack and its resources.

In addition, there's a cdk.json file with some context parameters. We'll circle back to this in a bit.

Creating Resources


Be careful when copying code from guides online about CDK resources! There are (in June 2022) two different versions of CDK, v1 and v2, and these have different npm packages. Unless you know what you're doing, use aws-cdk-lib packages and not @aws-cdk packages. For more details about the differences, check out the v2 migration guide.

For this demo, we'll create a couple Lambdas and a supporting database. You can check out the API reference for details about the resources we're creating; for right now, let's focus on a few important details.

For this example, we're putting resources in a VPC. Here, we're creating the VPC, but you could just as easily look up one that already exists with ec2.Vpc.fromLookup().

We're also setting up connections via security groups between the database and lambdas:

const securityGroup = new ec2.SecurityGroup(this, 'lambda-security-group', {
    vpc,
    allowAllOutbound: true,
    description: 'Security group shared by lambdas'
});
// Allow Postgres connection from all lambdas
securityGroup.connections.allowTo(cluster, ec2.Port.tcp(5432));

CDK will automatically wire up security group rules for you with the connections object. In this case, the security group is shared by multiple lambdas, so we can set the rule once; but this applies to any connectable resource, so you could just as easily set this up from the lambda to the database directly:

appLambda.connections.allowTo(cluster, ec2.Port.tcp(5432));

The security groups on the lambda and the cluster will be created and the rules will be generated automatically from this single line of code.

There's one other useful technique to note here, the migrations custom resource - we'll get to that in a minute!

Setting Context for Different Environments


We often deploy the same stack to different regions - to have a dev, QA, and production version, for example. We might also have devs to spin up their own copy of the stack to test changes for a particular branch. This can be configured by setting context variables. In our bin/aws-cdk-demo.ts, we'll set an env context variable:

const app = new cdk.App({
  context: {
    // May be overridden by cdk.json or the --context parameter
    env: userInfo().username
  }
});
// Set the stack name based on the environment
new AwsCdkDemoStack(app, `AwsCdkDemoStack-${app.node.tryGetContext('env')}`, {
  /* Deploys this stack to the AWS Account and Region that are 
   * implied by the current CLI configuration. */
  env: { 
    account: process.env.CDK_DEFAULT_ACCOUNT, 
    region: process.env.CDK_DEFAULT_REGION 
  },
});

This gives the stack a suffix which defaults to the current user's username. If another value is specified in cdk.json or a command line parameter to cdk deploy, that default will be overridden.

We can also reference this elswhere in the stack: we might want to deploy more database instances to production than dev, for instance. We can use tryGetContext anywhere in the stack to check this value.

To create a different stack for each environment, we can just pass in the appropriate value:

cdk deploy --context env=dev
cdk deploy --context env=prod

You can use this technique to pass in API keys or other environment-specific details as well. If you have a lot of variables in your command line, it may be easier to have your CI pipeline replace the default cdk.json with an environment-specific one.

Advanced Techniques

As you build more complex applications, CDK enables some patterns to make your life easier.

Shared Constructs


If you find yourself reusing certain architecture patterns in your CDK builds, you can extract those common components. You might want a FunctionWithBucket that sets up a lambda and s3 bucket with the connections and permissions pre-configured, for example. You can create your own Constructs to encapsulate these components for reuse.

Custom Resources


Custom resources are a useful way to run steps as part of the deployment process. If you're deploying code and infrastructure changes together, then it can make sense to run your database migrations as part of the process. This is surprisingly easy to implement, but there are some gotchas.

const migrationsProvider = new cr.Provider(this, 'migrationsProvider', {
    onEventHandler: migrationsLambda
});

new core.CustomResource(this, 'migrations', {
    serviceToken: migrationsProvider.serviceToken,
    properties: {
        // Run migrations every time
        migrationsRun: new Date().toString()
    }
})

The Custom Resource will execute the associated lambda when it is created, when its properties are updated, or when it's deleted. If you want to make sure it runs every time the stack deploys, set a property that will be updated on each deployment. Here we're just using the current date.

export function runMigrations(event: CdkCustomResourceEvent, context: Context) {
    console.log('Pretending to connect to', process.env.RDS_HOST, 'to run migrations!');
    send(event, context, 'SUCCESS');
}

This handler uses the send method from the AWS cfn-response module. Depending how you're deploying your functions, you may be able to import it the module; otherwise, you could paste the code into your build (as we've shown here) or use another library like axios to make the call.

The important thing is that every time the lambda is called, whether it succeeds or fails, it must make the callback to notify CloudFormation. Otherwise, your stack will hang for an hour, waiting for a response. Make sure you have robust error handling, so that if your process fails the callback still gets sent.

If your custom resource is getting triggered too soon, before some resource is ready, you can add a dependency manually:

migration.node.addDependency(cluster);

In most cases, fortunately, CDK will set up these dependencies correctly.


An Easier Way to Build


Infrastructure as code comes with many benefits for repeatability and resiliency, and CDK adds a layer of abstraction that makes it much friendlier for developers to work with.

The JBS Quick Launch Lab

Free Qualified Assessment

Quantify what it will take to implement your next big idea!

Our assessment session will deliver tangible timelines, costs, high-level requirements, and recommend architectures that will work best. Let JBS prove to you and your team why over 24 years of experience matters.

Get Your Assessment