Have you ever tried to publish a CDK construct that was using a Lambda function, for example to create a custom resource or provide a REST API endpoint? It’s relatively easy to publish your construct if your Lambda function is just using the AWS SDK. But it gets more complicated as soon as other dependencies are involved as well. This post will present you five different ways to bundle your Lambda function within a CDK construct and tells you about the advantages and disadvantages of each option.
Problem Context
Recently I started digging more into the AWS CDK world and wanted to build a simple single page application. I had some special requirement for which I couldn’t find an existing CDK construct available in TypeScript. So I thought let’s create it myself and publish it later to NPM. I wanted to include a Lambda function in this CDK construct that was using an external dependency apart from the AWS SDK. The problem is that if you require other dependencies in an AWS Lambda function, you need to bundle them with your function (the AWS SDK is always available for Node.js runtimes). This means, you have to create an artifact that includes all required dependencies. However, I wanted to avoid that my resulting CDK construct package gets too big. I had some ideas in my mind but also asked CDK experts on Twitter for their opinion and experiences. Below are the results of my ideas and their suggestions!
You have another idea how to include a Lambda function in a CDK construct? Please comment below ⬇️ or let me know via Twitter @seeebiii. If you’re curious about developing AWS Lambda functions in general, I can recommend you my article about best practices developing AWS Lambda functions.
Inline Code in CDK Construct
The easiest solution is to write some inline code within the CDK code. It usually looks like this when using the Function
construct from the @aws-cdk/aws-lambda
package:
new Function(this, 'MyFunction', {
handler: 'index.handler',
code: Code.fromInline(`
exports.handler = async (event) => {
console.log('event: ', event)
};
`),
runtime: Runtime.NODEJS_12_X
});
This code will create a Lambda function with a very basic implementation. You can of course extend it further. However, you are a bit limited in terms of which dependencies you can include. For example, you can only refer to external dependencies like the AWS SDK and regular Node.js modules like path
or fs
. Also, this approach only works for runtimes that interpret text files, like Python or Node.js. It does not work for languages like Java.
Advantages:
- Quick and easy way to write a Lambda function
- No extra bundle steps necessary
Disadvantages
- You are very limited what your function can do
- No IDE support while writing your code
- No testing possible, only manual tests
Separate File(s) in CDK Construct
Instead of providing inline code, you can also move your Lambda function code outside of the CDK code into a separate file. Then, you just link to your file from the CDK construct your using. For example:
new Function(this, 'MyFunction', {
runtime: Runtime.NODEJS_12_X,
handler: 'index.handler',
code: Code.fromAsset(`${path.resolve(__dirname)}/lambda`)
});
The code
property is referencing an external asset which points to the file of your Lambda function. It assumes the following folder structure:
root/
- my-stack.js
- lambda/
- index.js
When deploying a stack with this function code, the CDK will simply take the index
file of your Lambda function and use it as your Lambda function’s code. You can do something similar using other runtimes like Java or Python. Just reference the appropriate artifact like a JAR or Python file. Although this approach is much more preferred than writing inline code, it still has the drawback that you can not simply include other dependencies apart from the AWS SDK, at least for Node.js. You could of course zip your index.js
file together with your node_modules
folder and use that as your artifact. However, this approach is not recommended because it unnecessarily slows down your Lambda function due to a bigger artifact size. You’re just carrying around code which you’re not using.
Advantages
- Keep CDK stack code and Lambda function code separated
- You can test your Lambda function’s code using automated tests
- IDE support while writing your Lambda function’s code
Disadvantages
- External dependencies (apart from AWS SDK) not supported
Bundle Lambda Function Before Publishing
If you want to use other external dependencies, you need to make sure that those dependencies are available when your Lambda function is executed. Therefore, the next logical step is to bundle your Lambda function’s code and generate a code artifact with all the dependencies included. This artifact is used by your CDK construct and in the end used by the users of your construct. In your CDK construct you still use the same Function
definition as above where you include the code asset. However, you have to make sure to bundle your code before you publish your CDK construct to any registry like NPM. For example, if you’re writing a TypeScript Lambda function, you can use esbuild (or webpack or similar) to compile and bundle it to “native” Node.js code that your Lambda function understands:
esbuild lambda/index.ts --bundle --platform=node --target=node12 --external:aws-sdk --outfile=lambda/build/index.js
This command creates an index.js
file with all dependencies included, except the AWS SDK since this is already provided by the Lambda runtime. If you want to speed up your Lambda function even more, you can append --minify
to use minification and reduce the output size. Here the output file is created under lambda/build
, so take care to adjust the Function
definition. For example:
new Function(this, 'MyFunction', {
runtime: Runtime.NODEJS_12_X,
handler: 'index.handler',
code: Code.fromAsset(`${path.resolve(__dirname)}/lambda/build`)
});
In order to include the Lambda function’s code in your published CDK construct, consider the following:
- Make sure the
lambda/build
folder is not ignored by NPM (this is usually configured in.npmignore
) - Before publishing the construct, you have to bundle the Lambda function first – otherwise your published construct is missing the code for your Lambda function
- If you are using
projen
to configure your construct project, you can use the following code to execute any command before building your construct:
const construct = new AwsCdkConstructLibrary(...)
// append command execution
construct.buildTask.exec(...)
// prepend command execution
construct.buildTask.prependExec(...)
💡If you don’t know what projen
is, take a look at my step-by-step getting started tutorial about projen and jsii. I can recommend to check it out! Maybe you even want to migrate your existing CDK construct to it?
Advantages
- Everything from the previous section about having separate files
- You don’t make any assumptions about the environment of the users that use your construct (will be important in the following sections)
- You throw out all unnecessary code by only bundling the relevant code and maybe even minifying it
Disadvantages
- It takes another build step and slightly increases the size of your CDK construct
[mailjet_subscribe widget_id=”3″]
Bundle Lambda Function Before Deploying
Instead of bundling the code before publishing your CDK construct, you can also bundle your Lambda function code before the construct is deployed to AWS. The AWS CDK provides a construct for Node.js Lambda functions called NodejsFunction
from the @aws-cdk/aws-lambda-nodejs
package. This construct will build the Lambda function as soon as your CDK construct is deployed within a stack. The NodejsFunction
construct is using esbuild to do that or a Docker container if esbuild is not available (read more about it in the documentation). Using it in your construct is similar to how you define a regular Function
– however, it already defines some useful defaults. An example definition can look like this:
new NodejsFunction(this, 'MyFunction', {
entry: `${path.resolve(__dirname)}/lambda/index.js`
});
As you can see, it’s pretty simple and short. Unfortunately the big disadvantage is that you make assumptions about the environment where your construct is deployed. If they don’t have esbuild or Docker available, it won’t work. Therefore it only makes sense to use NodejsFunction
in an constructs where you control the environment or if you let your users know about this requirement.
Note: Take care which file you’re referencing for your Lambda function. If you’re using TypeScript, then you need to reference index.ts
for local execution of e.g. tests. However, if someone is using your construct, they usually won’t have the TypeScript files available but only the compiled JavaScript files. The following code snippet can help you:
import * as fs from 'fs';
import * as path from 'path';
const lambdaFile = 'lambda/index';
const extension = fs.existsSync(lambdaFile + '.ts') ? '.ts' : '.js';
const entry = path.join(__dirname, `${lambdaFile}${extension}`)
Advantages
- Everything from the previous section about having separate files
- You throw out all unnecessary code by only bundling the relevant code and maybe even minifying it
Disadvantages
- You make assumptions about the environment of the users of your construct
- It takes another build step and slightly increases the size of your CDK construct
Publish Lambda Function to Serverless Application Repository or Using Docker
A completely different option compared to the ones above is to use the Serverless Application Repository. It’s a repository for serverless application that you can build and publish to AWS using the SAM CLI. Then you can use this application in other stacks using the CloudFormation SAM type AWS::Serverless::Application
. The CDK equivalent is CfnApplication from the @aws-cdk/aws-sam package. Since those applications can be made public to everyone, you have a neat way to host your Lambda function outside of your CDK construct, i.e. without bundling it inside your CDK construct. You could even share your AWS Lambda Layer in the same way and reference that instead of a full serverless application (see how to use Layers in AWS CDK here). This has the advantage that you can still use the Function
construct as explained above and just add a LayerVersion
construct. Similarly, you can publish your Lambda function as a container nowadays and reference that in your CDK construct. The CDK provides a DockerImageFunction
for this case.
Although these options sound like a good idea because you are much more flexible in how your Lambda function is built, the solution has two disadvantages: First, you are referencing an unknown external stack or dependency that you should make your users aware of so they can verify it. Second, it adds much more complexity than often necessary. Especially if you’re using a Node.js runtime, none of these step should be necessary for bundling a Lambda function within your CDK construct. It’s much easier to use one of the other options above.
Advantages
- Separation of concerns
- Flexibility of which dependencies you need and how you provide them
Disadvantages
- More complexity
- Potential concerns by users
Conclusion
In most cases having separate files is totally enough. Especially if you just use the AWS SDK in your Lambda function, there’s often no need to over-optimize your code. However, as soon as you use other external dependencies, you have to do more to bundle a Lambda function within a CDK construct. My recommended approach is to bundle your Lambda function before you publish your CDK construct. Then, in your CDK construct just use the bundled artifact. As mentioned, the advantage is that you don’t make any assumptions about the (build) environments that the users of your CDK constructs have.