Introduction
This tutorial shows how to use AWS Lambda's custom runtime to run TypeScript in Deno. We will also use AWS CDK as a practical example for deploying Lambda functions.
There are other tools for generating AWS CloudFormation, such as serverless framework and SAM, but in this case, we will use AWS CDK. Unfortunately, at the time of writing this article, these tools are not available with Deno. For example, aws-cdk only provides the CommonJS module format, so it cannot be run in Deno runtime. For more information, please check Information for Requesting Deno Support #17386.
For this reason, we will introduce a method that uses a Deno-style code base for Lambda functions and a Node.js-style code base for the AWS stack. In the future, it will be possible to completely change to Deno, but I hope this will be helpful as a bridge until then.
Also, please refer to bit-history for an actual project in operation.
Project structure
In the case of a mixed project of Deno and Node.js, it is often the case that they cannot use each other's code. This is because Deno's module resolution algorithm is different from Node.js. Therefore, the code needs to be independent of each other.
Fortunately, Lambda functions and the AWS stack are written completely independently, so this should not be a problem.
Based on this assumption, we recommend the following directory structure:
Place the Lambda functions under the api
directory. The api
naming convention is familiar from vercel.
In the api
directory, we also put .vscode
. Here, we enable VSCode extension for Deno.
If you have a mixed project of Deno and Node.js, you need to be careful about the scope of the VSCode extension for Deno.
Also, place the AWS stack under the app
directory.
You can also use templates with cdk init app --language=typescript
and so on.
We will proceed under the assumption that we use the template for TypeScript.
The entry point of the stack will look like this.
Deno and Lambda functions
There is a convenient type definition for the Lambda function definition. We'll give priority to deployment first, and define the appropriate function.
Custom runtime and AWS CDK
The AWS stack, on the other hand, is defined as follows.
External modules such as @aws-cdk/aws-sam
and @aws-cdk/aws-lambda
should be installed accordingly for use in Node.js environment.
The Deno runtime uses deno-lambda. It is available as SAR application, so we use it in the Lambda layer.
The above will create a minimal Deno runtime environment. You can also change the version of Deno by changing the semanticVersion
.
Finally, deploy it with cdk deploy
.
When you run this Lambda function, you will get the following output.
The part with the warning will be described later. Anyway, I was able to run it successfully with Deno runtime.
Advanced Logging
In the previous example, we used console.log
to output the log.
The deno-lambda runtime provides a log template feature, so you can customize the log output.
If you set the DENO_PREFIX
environment variable, you can output it as the log prefix.
Since we are going to do this, let's use AWS CDK to set the environment variable.
Modify the Lambda stack.
In the example above, each log is prefixed with the log level, request ID, and line number. Including the line number in the stack trace in the log makes debugging a little easier.
Note that if you are setting this up from the AWS CDK, you will need to escape it, like \\n
.
If you deploy and run this, you will get the following output:
Deno and caching
Here is the log from the previous Lambda run.
This indicates that an external module was fetched at runtime. Deno also transpiles TypeScript.
Deno is a TypeScript runtime, but it does not execute TypeScript as is. It uses the V8 engine to execute JavaScript.
Internally, TypeScript is transpiled to JavaScript using swc, and then executed in V8. Normally, Deno does these processes at runtime, caching external modules and transpiled JavaScript.
To verify this, let's run the following code.
We will use the AWS SSM client as an external module. We are fetching values from the parameter store. If you are using Lambda, the environment variables will be set automatically, otherwise, set them accordingly.
You can run it with the following command:
You can also set the parameter store from the AWS console, or use the following command:
Now, I think we have successfully obtained the parameter.
Next, set the environment variable to DENO_DIR
.
When you run it again, two directories will be created directly under deno-dir
.
Let's take a look at a few of these.
/deps
Under $DENO_DIR/deps
, files fetched via remote URL import will be saved.
The location to be saved is determined based on the URL scheme and the domain name.
For example, the URL path above will create a subdirectory with the https
URL scheme and the deno.land
domain name.
Note that the actual file name will be replaced by the hash value.
/gen
Under $DENO_DIR/gen
, JavaScript files transpiled from TypeScript files will be saved.
In the case of local files, they will be saved under the file
directory with an absolute path.
If you run the above file, it will be saved under the path
and to
directories of the file
directory.
Note that this is based on the directory structure at caching runtime.
Deno also has a command that only does caching.
Lambda and caching
These caches are used as long as the source files have not been modified. This prevents unnecessary recompilation.
If these processes are done at runtime of Lambda, it will affect the execution time. Since Lambda often reuses contexts, this may not happen every time, but it can be quite significant depending on the size of the external module.
Here is a way to solve this problem.
The basic idea is to avoid this by caching at a different time than runtime, such as when deploying. There are several possible ways to do this.
- Pre-bundle the JavaScript file
- pre-generate the cache file and deploy it including it. Change
DENO_DIR
to refer to the cache directory. - Generate an executable file and run it.
In this article, only 1 will be presented.
Pre-bundle with JavaScript files
This was a common strategy when we used Node.js. Bundle all external modules into a single JavaScript file. Instead of preparing a cache, bundle it into a JavaScript file that doesn't need to be cached, so that you don't have to think about anything else.
In this way, the deployment flow remains simple and performance is improved.
The downside is that you will be deploying a JavaScript file. The code you see from the AWS console is different from the actual source code.
One of the advantages of using Deno was the ability to run TypeScript as-is. Since you will be throwing away this advantage, you should consult with your actual operation before deciding whether to adopt it or not.
In my case, I am currently adopting this strategy since I will only be debugging from the AWS console.
Now, I will use a code that uses an external module. Let's consider an using the AWS SSM client with Lambda to get parameters.
The Lambda function looks like this:
The parameter retrieval is done outside of the Lambda export function. This way, the parameters are retrieved only when the container is initialized, not every time Lambda is executed.
On the other hand, due to the use of SSM, it is necessary to grant IAM roles. Add the IAM role to the AWS stack and attach it to the Lambda function.
Now, what happens when you run this deployed function? You will see that the external module is fetched at runtime.
When you run the function again, you will notice that the execution speed is faster than the first time.
Pre-bundling with containers
Now, we will make a change to prevent the cold start from happening the first time. The AWS CDK Lambda stack has a bundle hook that we can use.
You can define the pre-bundling process in the bundling
field of the Lambda stack.
You can use the official deno image as a base image.
The bundling process is simple: the deno bundle
command is applied to the export file of a Lambda function.
This will generate a JavaScript file including external modules.
However, the deno-lambda runtime recognizes .ts
files as Lambda functions by default.
This behavior can be changed by changing the HANDLER_EXT
environment variable.
Since we want to treat bundled .js
files as Lambda functions, we will set HANDLER_EXT
to js
.
Now, when you deploy this, module resolution will be performed at deployment time and the bundling process will be executed.
Finally, here is the entire definition of the AWS stack used in the example.
Other options
As mentioned above, there are several ways to prevent cold start. In the second method above, for example, the cache file is simply prepared in advance, so there is no divergence between the AWS console and the source file like in the banding method.
However, the size of the cache file will be larger than that of the bundle, which may consume a large amount of code storage.
Since the code storage for Lambda is 75GB, there was a concern that it would not fit in the storage as the number of Lambda functions increased.
It was also a bit more complicated to do in the bundling
step.
On the other hand, the third method described above could be done using deno compile
.
deno compile
is a command that can generate an executable script, which is unstable at the time of writing.
This article focuses on using the Deno runtime with Lambda, so I won't go into it in depth, but I think it's a good option to know about.
Edit this page on GitHub