Andrew Vo-Nguyen
January 24, 2023
6 min read
Early last year I worked on a personal full stack project using AWS Amplify. I had abandoned the project since, and only recently decided to revive it. The issue that I ran into was that I had already deleted the existing resources off AWS and the Amplify codebase was still referencing those references. After jumping through many hoops and a lot of googling, I could not successfully spin up the project again and re-deploy it on AWS Amplify. My only choice was to use the Amplify CLI to create each resource 1 by 1 (DynamoDB, Lambdas, Cognito Pool, Federated Signin configurations etc.).
Through my full time job we were in the process of moving one of our projects from Serverless to AWS CDK and boy was that a game changer. To fully appreciate the evolution of infrastructure as code we must take it back to the beginning.
The classic AWS web interface. Besides from having one of the most hideous UI designs on the internet, it's super easy quickly search for an AWS service and quickly create a resource. This is of course a very manual method of creating resources and is not feasibly for large projects that require hundreds of resources. Imagine creating and uploading Lambdas one by one for each of your environments.
The Cloud Formation template is the purest form of infrastructure as code for AWS. It is in JSON format and can easily be moved from one environment to another to replicate resources. Unfortunately the JSON file is not very easy to write without memorising all the valid keys and value options beforehand.
The Amplify CLI I used to spin up a full stack environment as an alternative to something like Firebase. It extracts away the need to know how to write a CloudFormation template and generates the template for you as your use the CLI to create resources, one by one. This method works well for small scale applications, but if you have a larger application that has over 30 lambdas, multiple S3 triggers etc, the creation on resources could get real tedious via the CLI prompts. Not to mention that Amplify is a large abstraction over many other AWS services which gives you less flexibility on how you want to configure the resources.
Serverless is framework designed to implement a common language that can spin up resources for cloud providers. It supports AWS, GCP, Azure and many more. Serverless for the most part does well in AWS resource creation and handling multiple environments (dev, staging, uat, prod etc). It has a large plugin ecosystem (this can be seen as a pro or a con) that helps with additional functionality such as packaging (e.g. serverless-webpack
) or handling of environment variables (e.g. serverless-dotenv-plugin
). The plugins are written by the Serverless community which sometimes has bugs or very limited support. My main criticisms on using Serverless is the .yml file feels clumsy and is prone to mistakes (much like writing your own CloudFormation template), lack of type safety or IDE autocomplete and that it does not handle CloudFormation stack limitations of 500 resources well. For small to mid projects, Serverless is a fine option.
My newest favourite kid on the block. From the official docs:
The AWS CDK lets you build reliable, scalable, cost-effective applications in the cloud with the considerable expressive power of a programming language.
The fact that you can describe your infrastructure using your language of choice, whether it's TypeScript, JavaScript, Java, Go, Python or C#, it means that your dev team does not need to learn a new language to add resources.
As a TypeScript developer, I am able to take full advantage of type safety, IDE autocomplete support, use of enums to restrict invalid values, utilize object oriented paradigms, re-use variables and functions and use any external library from the node ecosystem. Here is an example of how a Lambda could be described:
1import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
2
3import { RetentionDays } from 'aws-cdk-lib/aws-logs';
4import { Duration } from 'aws-cdk-lib';
5import { Runtime } from 'aws-cdk-lib/aws-lambda';
6
7const lambda = new NodejsFunction(this, id, {
8 entry: path.join(__dirname, '..', '..', 'lambdas', 'src', 'functions', 'send-message.ts'),
9 environment: { WEB_SOCKETS_TABLE: webSocketsTable.tableName },
10 initialPolicy: [
11 new PolicyStatement({
12 effect: Effect.ALLOW,
13 actions: ['dynamodb:GetItem'],
14 resources: [webSocketsTable.tableArn],
15 }),
16 ],
17 timeout: Duration.seconds(10),
18 runtime: Runtime.NODEJS_16_X,
19 handler: 'handler',
20 logRetention: RetentionDays.THREE_DAYS,
21});
path
package to form a path to the lambda.aws-cdk-lib
package to restrict allowed values for those keys.I can even go one step further and create a custom Lambda construct class to promote re-usability and consistency:
1import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
2
3import { Construct } from 'constructs';
4import { Duration } from 'aws-cdk-lib';
5import { Runtime } from 'aws-cdk-lib/aws-lambda';
6
7type Props = Pick<NodejsFunctionProps, 'entry' | 'environment' | 'initialPolicy' | 'timeout'>;
8
9export class LambdaConstruct extends Construct {
10 instance: NodejsFunction;
11 constructor(scope: Construct, id: string, props: Props) {
12 super(scope, id);
13
14 const lambda = new NodejsFunction(this, id, {
15 entry: props.entry,
16 environment: props.environment,
17 initialPolicy: props.initialPolicy,
18 timeout: props.timeout ?? Duration.seconds(10),
19 runtime: Runtime.NODEJS_16_X,
20 handler: 'handler',
21 });
22 this.instance = lambda;
23 }
24}
Now everytime I need to spin up a lambda, I just create a new instance of LambdaConstruct
and I know the parameters will be consistent with every other Lambda created previously.
Another bonus of using CDK is that my Lambdas are written in TypeScript and CDK automatically compiles them, bundles them using esbuild and uploads them for me. No plugins like serverless-webpack
required.
In the end when running cdk synth
in the CDK cli, it creates a Cloud Formation template and running cdk deploy
will push the resources to the cloud. A simple cdk destroy
command can quickly tear down resources if they are no longer required.