Introduction Link to heading

In Getting Started with CDK, I wrote “I noticed that cdk has a pattern of using a lambdas directory and then having sub-directories for every lambda”. Well, I was wrong, or maybe things have just changed. Either way, we’re going to update hugo-cdk to follow the new pattern.

The New Pattern Link to heading

Recently, cdk was updated to add bundling support. We didn’t need bundling with our previous lambda, because it doesn’t depend on any libraries. The new pattern is still a more elegant approach, so lets take a look at the fancy but plainly named aws-lambda-nodejs.

Before:

this.fn = new lambda.Function(this, 'RedirectLambda', {
  functionName: 'cloudfront-redirect',
  runtime: lambda.Runtime.NODEJS_12_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset(path.join(__dirname, 'lambdas', 'cloudfront-redirect')),
  role: role,
});

After:

this.fn = new lambda.Function(this, 'cloudfront-redirect', {
  runtime: lambda.Runtime.NODEJS_12_X, // not strictly require, but...
  role: role,
});

That looks like it will let me delete more than half the code! Very promising! “Convention over configuration” is pretty persuasive, so…

Preparation Link to heading

Under the hood, the bundler is going to use parcel. If you don’t have a local install of it, cdk will cleverly use docker to just make things work. Unfortunately, this is slow beyond words if you happen to be on a mac. Fortunately, cdk will detect and use a local install, which is much faster.

npm install --save-dev --save-exact parcel@2.0.0-beta.1

Implementation Link to heading

As usual, we start by upgrading cdk. Next, we’ll need to pull in the new bundler module:

npm install @aws-cdk/aws-lambda-nodejs

We’ll start by renaming the lambda to it’s new standard location.

cd lib;
git mv lambdas/cloudfront-redirect/index.ts cloudfront-redirect.cloudfront-redirect.ts

And then updating the code. Here’s a diff to make it easier to see the change.

[andrew.hammond@andrew-hammond-C02XKGFMJG5H-SM personal hugo-cdk (master $%)]$ git show -p
commit b070d451df8a52d09db1e03e7496647cc2dc17d6 (HEAD -> master)
Author: Andrew Hammond <andrew.hammond@helix.com>
Date:   Sun Oct 4 18:54:35 2020 -0700

    lambda-nodejs pattern

diff --git a/lib/lambdas/cloudfront-redirect/index.ts b/lib/cloudfront-redirect.cloudfront-redirect.ts
similarity index 95%
rename from lib/lambdas/cloudfront-redirect/index.ts
rename to lib/cloudfront-redirect.cloudfront-redirect.ts
index a716a4d..1363b87 100644
--- a/lib/lambdas/cloudfront-redirect/index.ts
+++ b/lib/cloudfront-redirect.cloudfront-redirect.ts
@@ -1,4 +1,4 @@
-import awsLambda = require('aws-lambda');
+import * as awsLambda from 'aws-lambda';

 function generateRedirectResponse(value: string): awsLambda.CloudFrontRequestResult {
   return {
diff --git a/lib/cloudfront-redirect.ts b/lib/cloudfront-redirect.ts
index ebbec1d..df102cb 100644
--- a/lib/cloudfront-redirect.ts
+++ b/lib/cloudfront-redirect.ts
@@ -1,7 +1,7 @@
 import cdk = require('@aws-cdk/core');
 import iam = require('@aws-cdk/aws-iam');
 import lambda = require('@aws-cdk/aws-lambda');
-import path = require('path');
+import lambdaNodejs = require('@aws-cdk/aws-lambda-nodejs');

 export class CloudfrontRedirect extends cdk.Construct {
   public readonly fn: lambda.Function;
@@ -21,12 +21,16 @@ export class CloudfrontRedirect extends cdk.Construct {
       }),
     );

-    this.fn = new lambda.Function(this, 'RedirectLambda', {
-      functionName: 'cloudfront-redirect',
-      runtime: lambda.Runtime.NODEJS_12_X,
-      handler: 'index.handler',
-      code: lambda.Code.fromAsset(path.join(__dirname, 'lambdas', 'cloudfront-redirect')),
+    this.fn = new lambdaNodejs.NodejsFunction(this, 'cloudfront-redirect', {
+      runtime: lambda.Runtime.NODEJS_12_X, // Default would work, but let's go with latest greatest.
       role: role,
+      parcelEnvironment: {
+        NODE_ENV: 'production',
+      },
+      externalModules: [
+        'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
+      ],
+      nodeModules: ['@types/aws-lambda'],
     });
     new cdk.CfnOutput(this, 'TheRedirectLambda', { value: this.fn.functionArn });

And finally I set the parcelEnvironment, which… I’m totally guessing is important. I like this new pattern.

Things worth noting:

  • I did indeed get to remove a bunch of boilerplate (the functionName, code, and handler) in favor of convention.
  • I also made explicit the sourcing of the modules being used. This code was taking advantage of the fact that aws-sdk is already present, and that the aws-lambda code is only used for type information. Now we can reference out to other code
  • We had to get rid of the import foo = require('...'); in favor of import * as foo from '...';. I haven’t found a clear explanation of the difference between these two import patterns. So I’ve been using the require() pattern because… uh… it looked more elegant to me. The fact that parcel doesn’t like it though? That’s a strong enough argument for me to switch.
  • This will change the name of the resource we have used to deploy the lambda, so we’ll need to re-deploy both of our stacks.

Cleanup Link to heading

I see that I have a new directory in my hugo-cdk path:

[andrew.hammond@andrew-hammond-C02XKGFMJG5H-SM master hugo-cdk (master $%)]$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.
  (use "git push" to publish your local commits)

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.parcel-cache/

A quick browse of the parcel docs tells me that this belongs in .gitignore

echo '.parcel-cache/' >> .gitignore
git add .gitignore
git commit -m "ignore the .parcel-cache"

Conclusion Link to heading

I like this new pattern. I LOVE that it will simplify the deployment of lambdas which require bundled libraries. Catch you next post!