AWS CDK: Boilerplating to Build Node.js Apps in TypeScript

jasonlyy

JasonLy

Posted on June 27, 2020

AWS CDK: Boilerplating to Build Node.js Apps in TypeScript

AWS CDK: Boilerplating to Build Node.js Apps in TypeScript

AWS Cloud Development Kit (CDK) enables provisioning of infrastructure using traditional programming languages you're famiilar with including TypeScript and Python. When I first started building in CDK, I remember looking for some quick start boilerplate code to quickly get started and build a serverless application in Node.js (TypeScript).

However, some issues I encountered trying gettings started includes:

  • There wasn't many templates to start with
  • Many code examples were in JavaScript and not TypeScript
  • There were also some unique issues around bundling and deployment which I wished I knew about earlier.

Here's how my projects are setup to get me up and running quickly. Here's the GitHub for a TL;DR and a basic example for what will be described below.

Getting Started

Run this command inside a empty directory to setup a CDK project in that folder:

cdk init --language typescript

While we are here, lets setup some Linting and Formatting using ESLint and Prettier (Credits)

npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier

Lets update package.json for linting by adding "lint": "eslint '*/**/*.{js,ts}' --quiet --fix" to scripts.

Lets also create a .eslintrc.js file:

module.exports = {
    parser: "@typescript-eslint/parser", // Specifies the ESLint parser
    parserOptions: {
      ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
      sourceType: "module", // Allows for the use of imports
    },
    extends: [
      "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
      "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
      "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
    ],
    rules: {
      // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
      // e.g. "@typescript-eslint/explicit-function-return-type": "off",
    },
};

And finally, lets create a .prettierrc.js file:

module.exports = {
    semi: true,
    trailingComma: "all",
    singleQuote: true,
    printWidth: 120,
    tabWidth: 4
};

Quick Project Refactor

Now I personally don't like how the CDK project is initially structured so I refactored it to distinctly seperate infrastructure code and actual code.

Lets create a new folder called infra which will hold all the infrastructure code. We will move the file in binto infra (I also renamed it to app.ts) and the file in lib to infra. Once we've done that we can remove both the bin and lib folder.

Go to cdk.json and update "app": "npx ts-node bin/[your_orignal_file_in_bin].ts" to "app": "npx ts-node infra/app.ts".

Now the actual code and other non-infrastructure related files will be located in src. So lets create a src folder. Inside that src folder, lets also create a folder called lambda which will hold all lambda functions (if the project uses Lambda). Each Lambda will be represented inside that folder as lambda_name/index.ts.

While we are here, lets also update the .gitignore for later by removing *.js, and adding dist.

Setting Up CDK Deploy in package.json

CDK will throw you an error if you try to deploy with mismatching versions of aws-cdk or if CDK dependencies you use have different versions (e.g. "@aws-cdk/aws-sns": "^1.47.0" and "@aws-cdk/aws-lambda": "^1.46.0"). This can cause some issues in some CI/CD pipelines where you have to run npm install -g aws-cdk.

A quick fix to those issues is to add something like this to your package.json file: "deploy": "cdk deploy your_stack_name" to ensure that it uses the CDK defined in your package.json already. Also everytime you deploy, you must ensure that any CDK dependencies are also of the same version.

CDK Bundling

Although CDK intrastructure is built in TypeScript, it is actually executed using ts-node to generate the infrastructure CloudFormation. However, CDK by default does not transpile TypeScript to JavaScript or bundles code together (even if you're just using JavaScript). This can become a problem if you're building your Lambdas in TypeScript or your Lambda may reference other files in different directories or require other files.

Recently, CDK released the aws-lambda-nodejs construct which solves the exact problem described above. However, this module is still considered as 'experimental' and under 'active development'. Another solution is to use Webpack to transpile and bundle TypeScript.

To get started with Webpack lets install some dependencies. To get started lets run:

npm install --save-dev webpack webpack-cli rimraf builtin-modules ts-loader

rimraf is used to delete transpiled files and other CDK generated files which we might want everytime we run npm run build. builtin-modules will be used later to reduce deployment sizes. ts-loader is used to transpile TypeScript.

Now add this line below to your package.json scripts:

"build": "rimraf dist && webpack"

This will remove dist where the transpiled files will go to later via webpack before running webpack.

Now lets create a webpack.config.js file with the following settings:

const path = require('path');
const fs = require('fs');
const nodeBuiltins = require('builtin-modules');

const lambdaDir = 'src/lambda';
const lambdaNames = fs.readdirSync(path.join(__dirname, lambdaDir));

const entry = lambdaNames.reduce((entryPoints, lambdaName) => {
    const tsPath = path.join(__dirname, lambdaDir, `${lambdaName}/index.ts`);
    const jsPath = path.join(__dirname, lambdaDir, `${lambdaName}/index.js`);

    const isTsFile = fs.existsSync(tsPath);
    const isJsFile = fs.existsSync(jsPath);
    if (isTsFile) {
        entryPoints[lambdaName] = tsPath;
    } else if (isJsFile) {
        entryPoints[lambdaName] = jsPath;
    }

    return entryPoints;
}, {});

const externals = ['aws-sdk'].concat(nodeBuiltins).reduce((externals, moduleName) => {
    externals[moduleName] = moduleName;
    return externals;
}, {});

module.exports = {
    entry,
    externals,
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: {
                    loader: 'ts-loader',
                    options: { onlyCompileBundledFiles: true },
                },
                exclude: /node_modules/,
            },
        ],
    },
    output: {
        path: path.join(__dirname, 'dist', lambdaDir),
        libraryTarget: 'commonjs',
        filename: '[name]/index.js',
    },
    target: 'node',
    optimization: {
        minimize: false,
    },
    devtool: 'inline-cheap-module-source-map',
};

This webpack is configured to bundle for each Lambda (the entry point) based on the following pattern: src/lambda/[lamba_name]/index.ts. It will also support Lambdas which are not TypeScript by also looking for src/lambda/[lamba_name]/index.js. TypeScript Lambdas will be bundled via ts-loader.

In the Lambda Execution Environment, it will already have aws-sdk and other built in node modules so they will be excluded from the bundle (to reduce deployment size).

The bundle will be outputted to the dist folder in the exact same pattern as above. Also minimisation is disabled and source mapping using inline-cheap-module-source-map is enabled (to map transpiled code back to TypeScript lines) as it is running on the backend.

To get Lambda to work with source mapping you must include source map support in every Lambda To do this, you first must install source-map-support by running:

npm install source-map-support

Then for each Lambda you must add import 'source-map-support/register'; to the top of every file.

Conclusion

As you develop with CDK, you'll find that your project components and structure varies depending on your needs. I hope this gives you a good starting point to get something running when working with a relatively new tool like CDK.

💖 💪 🙅 🚩
jasonlyy
JasonLy

Posted on June 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related