Simpler single-file AWS CDK deployments

I am checking out the AWS CDK to deploy serverless applications.

But having used Serverless Framework a lot, I find the AWS CDK very noisy and invasive in some of my projects. It doesn't have to be though.

Here are my notes on how to use the AWS CDK in a single file, without polluting your existing projects.

Disclaimer: what I am describing is only interesting is some projects, where the CDK is just "a deployment tool". I understand that some other projects can use the CDK as a framework for the whole app and the "invasiveness" is a feature.

The standard AWS CDK setup

If you have an existing project that you want to deploy using the AWS CDK, tough luck. You can't easily "introduce" the CDK in an existing project: cdk init only works on an empty directory.

But let's move past that. Let's create a new TypeScript CDK project:

cdk init app --language=typescript myapp

Here is what we have:

bin/
    cdk-myapp.ts
lib/
    cdk-myapp-stack.ts
test/
    cdk-myapp.test.ts
cdk.json
README.md
package.json
package-lock.json
tsconfig.json
jest.config.js

With that setup, we can deploy with npx cdk deploy.

Trimming the fat

If we look only at what makes the CDK actually work, we have:

bin/
    cdk-myapp.ts
lib/
    cdk-myapp-stack.ts
cdk.json
tsconfig.json

When cdk deploy runs, it executes the command listed in cdk.json:

{
    "app": "npx ts-node --prefer-ts-exts bin/cdk-myapp.ts"
}

Cool, it uses ts-node to compile TypeScript on the fly. At least we don't have to run a build step and deal with compiled files. It also means we can get rid of tsconfig.json if we don't need it for the rest of our app.

It also means we can change the bin/cdk-myapp.ts file. Let's move it to the root and rename it to cdk.ts:

lib/
    cdk-myapp-stack.ts
cdk.ts
cdk.json

Now let's look at cdk.ts:

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkMyappStack } from '../lib/cdk-myapp-stack';

const app = new cdk.App();
new CdkMyappStack(app, 'CdkMyappStack', {
  /* loads of comments here... */
});

And lib/cdk-myapp-stack.ts:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkMyappStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
  }
}

Let's inline all the CDK code in cdk.ts:

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';

class CdkMyappStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // The code that defines your stack goes here
    }
}

const app = new cdk.App();
new CdkMyappStack(app, 'CdkMyappStack', {
  /* loads of comments here... */
});

Finally, let's inline the stack class:

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';

const app = new cdk.App();

new class extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // The code that defines your stack goes here
    }
}(app, 'my-app', {
  /* stack options */
});

Final result

In the end, the AWS CDK only requires 2 files in our project:

cdk.ts
cdk.json

Those are easy to add to an existing project! We also still deploy with npx cdk deploy because cdk.json points to cdk.ts:

{
    "app": "npx ts-node --prefer-ts-exts cdk.ts"
}

And finally, all our infrastructure code is defined in cdk.ts:

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';

const app = new cdk.App();

new class extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // The code that defines your stack goes here
    }
}(app, 'my-app', {
  /* stack options */
});

Going further

We could imagine a helper function for simple apps:

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';

cdkStack('my-app', (scope: Construct, id: string, props?: cdk.StackProps) => {
    // The code that defines your stack goes here
});

That cdkStack helper would create the App and the Stack for us.

I'm tempted to it as an open-source package… Let me know what you think of the idea!