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 bin
to 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.
Top comments (1)
Hi Jason, I tried to view the GitHub repo but am getting a 404. Are you able to make the repo accessible?