Running Bun as AWS Lambda function

2023/10/19

#aws #lambda #bun

Table of contents

LLaMA

What is Bun?

Bun is a toolkit for JavaScript and TypeScript applications, packaged as a single executable named “bun.” It acts as a runtime, manages packages, bundles code, and runs tests, ensuring a smooth and effective development workflow. Developed in Zig and powered by JavaScriptCore, it drastically cuts down startup times and memory consumption.

Three things stand out to me:

  1. All-in-One Solution: Bun provides a toolkit, including a runtime, package manager, bundler, and test runner. If you prefer having a cohesive and integrated development environment without the need to integrate multiple tools separately, Bun can be a convenient choice.

  2. Performance Optimization: Bun is designed with a focus on performance. If you are working on projects where performance optimization is a critical concern, and you need a runtime and development environment that is tuned for speed, Bun might offer advantages over the default Node.js setup.

  3. Simplified Development: Bun aims to simplify the development process, making it more straightforward and efficient. If you are looking for a tool that abstracts away complexities and provides a more streamlined workflow, Bun could be a suitable option.

Let’s continue with the installation and setup for the AWS Lambda environment.

Installing Bun

For having the bun cli tool installed we can use Brew or something similar on different operating systems.

❯ brew install bun
==> Downloading https://formulae.brew.sh/api/formula.jws.json
################ 100.0%
==> Downloading https://formulae.brew.sh/api/cask.jws.json
################ 100.0%
==> Fetching oven-sh/bun/bun
==> Summary
🍺  /opt/homebrew/Cellar/bun/1.0.6: 7 files, 48.8MB, built in 2 seconds
==> Running `brew cleanup bun`...

Running it without any parameter:

❯ bun
Bun: a fast JavaScript runtime, package manager, bundler and test runner. (1.0.6)

  run       ./my-script.ts       Run JavaScript with Bun, a package.json script, or a bin
  test                           Run unit tests with Bun
  x         bun-repl             Install and execute a package bin (bunx)
  repl                           Start a REPL session with Bun

  init                           Start an empty Bun project from a blank template
  create    astro                Create a new project from a template (bun c)

  install                        Install dependencies for a package.json (bun i)
  add       zod                  Add a dependency to package.json (bun a)
  remove    redux                Remove a dependency from package.json (bun rm)
  update    tailwindcss          Update outdated dependencies
  link                           Link an npm package globally
  unlink                         Globally unlink an npm package
  pm                             More commands for managing packages

  build     ./a.ts ./b.jsx       Bundle TypeScript & JavaScript into a single file

  upgrade                        Get the latest version of Bun
  bun --help                     Show all supported flags and commands

  Learn more about Bun:          https://bun.sh/docs
  Join our Discord community:    https://bun.sh/discord

Getting Bun source code

This step is needed to build a Lambda layer.

❯ git clone git@github.com:oven-sh/bun.git
Cloning into 'bun'...
❯ cd bun/
❯ git checkout bun-v1.0.6
Note: switching to 'bun-v1.0.6'.
HEAD is now at 969da088f fix(install): re-evaluate overrides when removed
bun (969da08) via 🍞 v1.0.1 via ↯ v0.10.1

Installing lambda dependencies

There are a few dependencies that need to be installed before we can create a Lambda layer.

❯ cd packages/bun-lambda

❯ bun install
bun install v1.0.6 (969da088)
 + @oclif/plugin-plugins@3.9.1
 + bun-types@0.7.3
 + jszip@3.10.1
 + oclif@3.6.5
 + prettier@2.8.4
 + aws4fetch@1.0.17

 686 packages installed [6.68s]

❯ bun install @oclif/plugin-plugins
bun add v1.0.6 (969da088)

 installed @oclif/plugin-plugins@3.9.1

 4 packages installed [110.00ms]

Building and publishing the layer

With the dependencies installed we can create the Lambda layer for arm64.

❯ bun run build-layer -- \
        --arch aarch64   \
        --release latest \
        --output ./bun-lambda-layer.zip
$ bun scripts/build-layer.ts --arch aarch64 --release latest --output ./bun-lambda-layer.zip
Downloading... https://bun.sh/download/latest/linux/aarch64?avx2=true
Extracting...
Saving... ./bun-lambda-layer.zip
Saved

Publishing the layer is also very simple.

❯ bun run publish-layer --         \
  --layer bun-lambda-layer         \
  --arch aarch64                   \
  --release latest                 \
  --output ./bun-lambda-layer.zip  \
  --region eu-west-1

$ bun scripts/publish-layer.ts --layer bun-lambda-layer --arch aarch64 --release latest --output ./bun-lambda-layer.zip --region eu-west-1
Downloading... https://bun.sh/download/latest/linux/aarch64?avx2=true
Extracting...
Saving... ./bun-lambda-layer.zip
Saved
Publishing...
Published arn:aws:lambda:eu-west-1:651831719661:layer:bun-lambda-layer:1
Done

Checking the layer:

❯ aws lambda list-layers 
| jq '.Layers[].LayerArn' 
| xargs -I {} bash -c "aws lambda list-layer-versions --layer-name {} | jq '.LayerVersions[].LayerVersionArn'" 
| egrep bun
"arn:aws:lambda:eu-west-1:651831719661:layer:bun-lambda-layer:1"

Creating Lambda execution role

First we need a file with the assume role policy document.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

We can create the role called bun-lambda-role referencing the assume role policy.

❯ aws iam create-role --role-name bun-lambda-role --assume-role-policy-document file://role.policy.json
{
    "Role": {
        "Path": "/",
        "RoleName": "bun-lambda-role",
        "RoleId": "AROAZPRBSW3W7DL5YG6HZ",
        "Arn": "arn:aws:iam::651831719661:role/bun-lambda-role",
        "CreateDate": "2023-10-19T10:16:11+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

Creating the Lambda function

Creating the lambda function that handles the requests.

export default {
  async handler(request: Request): Promise<Response> {
    console.log(request.headers.get("x-amzn-function-arn"));
    return new Response(JSON.stringify({"Hello": "from Bun runtime"}), {
      status: 200,
      headers: {
        "Content-Type": "application/json",
      },
    });
  },
};

Saving this as index.ts and then creating a zip file.

zip bun-function.zip index.ts

Finally, we can create the function:

❯ aws lambda create-function \
          --function-name bun-lambda-function \
          --runtime provided.al2 \
          --architectures arm64 \
          --layers arn:aws:lambda:eu-west-1:651831719661:layer:bun-layer:1 \
          --zip-file fileb://bun-function.zip \
          --handler index.handler \
          --role arn:aws:iam::651831719661:role/bun-lambda-role

The response from the AWS API has all the details of the lambda function we just created:

{
    "FunctionName": "bun-lambda-function",
    "FunctionArn": "arn:aws:lambda:eu-west-1:651831719661:function:bun-lambda-function",
    "Runtime": "provided.al2",
    "Role": "arn:aws:iam::651831719661:role/bun-lambda-role",
    "Handler": "index.handler",
    "CodeSize": 365,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2023-10-19T10:16:58.106+0000",
    "CodeSha256": "yNUNBaGkJuist/ZoQkZtnToNNXIU/taYQHqZrwCu3Mk=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "c8edb420-aeeb-4568-8289-ea7b0b845d00",
    "Layers": [
        {
            "Arn": "arn:aws:lambda:eu-west-1:651831719661:layer:bun-layer:1",
            "CodeSize": 33260946
        }
    ],
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "arm64"
    ],
    "EphemeralStorage": {
        "Size": 512
    },
    "SnapStart": {
        "ApplyOn": "None",
        "OptimizationStatus": "Off"
    },
    "RuntimeVersionConfig": {
        "RuntimeVersionArn": "arn:aws:lambda:eu-west-1::runtime:dce29199fb5887a2c4fceaa2f34d395ba43a74a6895b381cb9383b1c7f3b5875"
    }
}

To make the Lambda function accessible without any additional services we can enable function URL.

❯ aws lambda create-function-url-config \
  --function-name bun-lambda-function \
  --auth-type NONE
{
    "FunctionUrl": "https://wbjmhbkwx5ikumwxkhka4lcgle0nvjgs.lambda-url.eu-west-1.on.aws/",
    "FunctionArn": "arn:aws:lambda:eu-west-1:651831719661:function:bun-lambda-function",
    "AuthType": "NONE",
    "CreationTime": "2023-10-19T10:18:56.838382Z"
}

Testing the newly created function

curl -vIX GET https://wbjmhbkwx5ikumwxkhka4lcgle0nvjgs.lambda-url.eu-west-1.on.aws/
{"Message":"Forbidden"}

It seems something is missing. Digging through the possible reasons I found the following:

Your function URL auth type is NONE, but is missing permissions required 
for public access. To allow unauthenticated requests, choose the Permissions 
tab and create a resource-based policy that grants lambda:invokeFunctionUrl 
permissions to all principals (*). Alternatively, you can update your function 
URL auth type to AWS_IAM to use IAM authentication.

Let’s create the missing permission:

Function URL permission

The JSON version of the policy:

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "FunctionURLAllowPublicAccess",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "lambda:InvokeFunctionUrl",
      "Resource": "arn:aws:lambda:eu-west-1:651831719661:function:bun-lambda-function",
      "Condition": {
        "StringEquals": {
          "lambda:FunctionUrlAuthType": "NONE"
        }
      }
    }
  ]
}

Finally, we can invoke the function:

❯ curl -iX GET https://wbjmhbkwx5ikumwxkhka4lcgle0nvjgs.lambda-url.eu-west-1.on.aws/
HTTP/1.1 200 OK
Date: Thu, 19 Oct 2023 12:01:29 GMT
Content-Type: application/json
Content-Length: 28
Connection: keep-alive
x-amzn-RequestId: cd204d16-01c9-4468-a02e-9f6d7d3ee2e1
X-Amzn-Trace-Id: root=1-65311a98-3ead96f16ebd86bc6366d05b;sampled=0;lineage=fc7d7573:0

{"Hello":"from Bun runtime"}

Latency looks reasonable:

Bun latency

Bun latency

That’s all folks!