Levels of Infrastructure Deployment


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.

Level 1 - AWS Console

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.

Level 2 - Cloud Formation Template

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.

Level 3 - Amplify CLI

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.

Level 4 - Serverless

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.

Level 5 - AWS CDK

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';
3import { RetentionDays } from 'aws-cdk-lib/aws-logs';
4import { Duration } from 'aws-cdk-lib';
5import { Runtime } from 'aws-cdk-lib/aws-lambda';
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,
  • Line 8 uses the native NodeJS path package to form a path to the lambda.
  • Line 9 extracts the table name from a DynamoDB instance, instead of manually typing the table name in multiple places and possibly mis-spelling it.
  • Line 11 takes advantage of a class instance that describeds an AWS policy statement.
  • Line 14 dynamically interpolates the DynamoDB ARN as this ARN does not exist yet.
  • Lines 17, 18 and 20 take advantage of enums from the 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';
3import { Construct } from 'constructs';
4import { Duration } from 'aws-cdk-lib';
5import { Runtime } from 'aws-cdk-lib/aws-lambda';
7type Props = Pick<NodejsFunctionProps, 'entry' | 'environment' | 'initialPolicy' | 'timeout'>;
9export class LambdaConstruct extends Construct {
10  instance: NodejsFunction;
11  constructor(scope: Construct, id: string, props: Props) {
12    super(scope, id);
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  }

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.