loading...
Cover image for AWS CDK - aws-lambda-nodejs Module

AWS CDK - aws-lambda-nodejs Module

elthrasher profile image Matt Morgan ・6 min read

I wrote some articles about AWS Cloud Developer Kit earlier this year. I was attracted to CDK immediately upon hearing of it due to the ability to write infrastructure as code in TypeScript. I really like writing code in TypeScript and CDK seemed almost too good to be true.

Table of Contents

A Missed Opportunity?

CDK is a new technology and that means that it doesn't necessarily cover every use case yet. What I found as I worked through official examples was that somebody had written CDK code in TypeScript but the accompanying Lambda code was written in JavaScript! This struck me as a missed opportunity. It turns out it wasn't a missed opportunity but one that just hadn't landed yet.

Lambda in CDK

To explain a bit better for those who aren't really in the transpilation game, TypeScript code is usually transpiled into JavaScript before being shipped into a runtime, be that runtime a web server, NodeJS or Lambda. That's because (leaving deno aside for now), there's no TypeScript execution environment. I say usually because there is actually a pretty cool project called ts-node that lets you execute TypeScript code in NodeJS without transpiling the code ahead of time. ts-node is a great tool to save developers a step in development flows. It's debatable whether you should use it in production or not (I don't). That said, it's totally appropriate to use ts-node with CDK. This lets you shorten the code=>build=>deploy cycle to code=>deploy. That's great!

But this doesn't work with Lambda Functions. CDK turns my TypeScript infrastructure constructs into CloudFormation. It doesn't do anything special with my Lambda code - or at least it didn't until the aws-lambda-nodejs module landed in CDK.

aws-lambda-nodejs

The aws-lambda-nodejs module is an extension of aws-lambda. Really the only thing it adds is an automatic transpilation step using Parcel. Whenever you run a cdk deploy or cdk synth, this module will bundle your Lambda functions and stick the result in special .cache and .build directories (which you will probably want to gitignore). Then the deploy process will stage the bundles in S3 and provide them to Lambda - all with no extra config required. It's quite impressive!

An interesting thing about this module is it actually does your Parcel build in Docker, which will let you build for a different runtime (NodeJS version) than you are running locally. You could even have multiple functions with different runtimes if you needed to for some reason. This does mean you need to have Docker installed to use the module which might give you grief if you're running CDK in some CD pipeline that doesn't have Docker available.

Parcel

I actually haven't used Parcel before. I remember it arriving on the scene a couple of years back, but I have already paid the "Webpack tax" (meaning I have spent enough time with Webpack that I can be productive without creating a complete mess) so I never got around to looking at Parcel. This is pretty cool. I my have to rethink my approach to SAM.

Refactoring #1

Okay, so let's update my projects to use aws-lambda-nodejs! I'll start with my Step Functions example: https://github.com/elthrasher/cdk-step-functions-example. This should be pretty simple since the functions are incredibly basic with no dependencies.

First I update all my dependencies. No reason to be working with old versions. In fact when I wrote this code back in December 2019, the latest version of CDK was 1.19.0 and aws-lambda-nodejs didn't exist yet. Today, May 29, 2020, the latest version of CDK is 1.41.0. You get a lot of advantages by staying current which is why my repo is set up with dependabot and github actions. Anyway, I'm current so now I can npm i @aws-cdk/aws-lambda-nodejs and then modify some code!

My old code using the aws-lambda function looked like this:

import { AssetCode, Function, Runtime } from '@aws-cdk/aws-lambda';

const lambdaPath = `${__dirname}/lambda`;

const assignCaseLambda = new Function(this, 'assignCaseFunction', {
  code: new AssetCode(lambdaPath),
  handler: 'assign-case.handler',
  runtime: Runtime.NODEJS_12_X,
});

The assumption here is that some other process (in my case just simple tsc) will drop the transpiled lambda code (assign-case.js) in the right place. Here's my refactor:

import { Runtime } from '@aws-cdk/aws-lambda';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';

const lambdaPath = `${__dirname}/lambda`;

const assignCaseLambda = new NodejsFunction(this, 'assignCaseFunction', {
  entry: `${lambdaPath}/assign-case.ts`,
  handler: 'handler',
  runtime: Runtime.NODEJS_12_X,
});

I'm now using the entry key to specify the TypeScript file that has my function handler. I'm still specifying the runtime, but maybe I don't have to. I kind of like being really explicit about my runtime. Will maybe play around with having that derived later on.

That's basically it! Everything else in my PR is either a dependency update or removing the unneeded build system. I deployed this and it works just fine. I checked out the code in the Lambda console and it looks good too. Check out my PR diff for this refactor.

Refactoring #2

My other CDK example has dependencies in aws-cli (DynamoDB) and the faker library. Let's see how Parcel handles those. The code change required is trivial. Here's my PR diff.

Now let's see how Parcel handled bundling my function. It produced an index.js file weighing in at 11.6 MB. That seems kind of big, considering this is a sample project. Inspecting the file, it seems that Parcel brought in all of aws-sdk. It doesn't look like proper tree-shaking is happening here and there's no way to declare aws-sdk as an external module in Parcel 1. Well, that's a weakness for sure.

Fortunately there is a config option for minify. Let's try that and see if it helps.

const initDBLambda = new NodejsFunction(this, 'initDBFunction', {
  entry: `${lambdaPath}/init-db.ts`,
  handler: 'handler',
  memorySize: 3000,
  minify: true,
  runtime: Runtime.NODEJS_12_X,
  timeout: Duration.minutes(15),
});

The minified build is now 6.3 MB. That's a good reduction in size, but if we could remove the non-DynamoDB dependencies from aws-sdk, it would be a heck of a lot smaller. It doesn't look like Parcel 1 allows that unfortunately. Parcel 2 should add some more of these quality of life issues and will definitely be worth a look. I recommend watching this issue to see that unfold.

To be clear, if your million dollar app has a 6 MB Lambda in it, you are probably quite happy. This isn't a fatal flaw by any means, but it's certainly an area for improvement.

Next Steps

I should mention that at the time of this writing, this module is marked experimental, which means the API could change at some point in the near future. I'm sure the CDK team will want to switch to Parcel 2 when that's available and this module will improve. Whether or not to use this for production will depend on the workload. Given the rate CDK is moving, I would consider using this module for an active development situation where the tooling can evolve, but it's maybe not ideal for a case where we want to ship something and expect stability.

Cover: The Beagle Laid Ashore drawn by Conrad Martens (1834) and engraved by Thomas Landseer (1838)

Posted on May 12 by:

elthrasher profile

Matt Morgan

@elthrasher

TypeScript, Lambda, Serverless, IoC, Cloud Native, make it faster!

Discussion

markdown guide