Logo

dev-resources.site

for different kinds of informations.

AWS Lambda in Deno or Bun

Published at
7/4/2024
Categories
deno
bunjs
aws
awslambda
Author
begoon
Categories
4 categories in total
deno
open
bunjs
open
aws
open
awslambda
open
Author
6 person written this
begoon
open
AWS Lambda in Deno or Bun

The article describes creating the AWS Lambda using purely Deno or Bun Javascript runtimes with zero external dependencies. We will use Deno by default, but the switch to Bun can be made via the RUNTIME variable (see Makefile).

Usually, to create an AWS lambda in Typescript, the code must be compiled in Javascript because AWS Lambda does not natively support Deno and Bun, only Node.

Some projects offer the flexibility of using Typescript directly in AWS Lambda, such as Deno Lambda.

However, we will implement our own custom AWS Lambda runtime to run Typescript by Deno or Bun and use AWS Lambda API directly.

The project comprises a Makefile for automation and clarity, Dockerfiles for containerization, and the lambda.ts file for the AWS Lambda function. That's all you need.

We will be building Docker images-based AWS Lambda deployment.

We start by explaining how to prepare AWS resources (image repository, role and policies, and lambda deployment).

We will use AWS CLI.

Part 1

You can skip this part of the article entirely if you are comfortable creating an AWS Elastic Container Repository named $(FUNCTION_NAME) (refer to Makefile) to prepare the lambda function named $(FUNCTION_NAME) to be created from the image, which we will build later.

You need to have .env file:

AWS_PROFILE=<YOU AWS PROFILE NAME>
AWS_ACCOUNT=<YOU AWS ACCOUNT>
AWS_REGION=<YOU AWS REGION>
Enter fullscreen mode Exit fullscreen mode

The profile name allows AWS CLI to find your AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY.

The .env file is included at the beginning of Makefile:

include .env
export

FUNCTION_NAME=lambda-ts-container
REPO = $(AWS_ACCOUNT).dkr.ecr.$(AWS_REGION).amazonaws.com
Enter fullscreen mode Exit fullscreen mode

Once again, instead of using the Makefile file below, you can create the repository and manually prepare the AWS Lambda creation via AWS Console.

Create the depository:

create-repo:
  aws ecr create-repository \
  --profile $(AWS_PROFILE) \
  --repository-name $(FUNCTION_NAME)
Enter fullscreen mode Exit fullscreen mode

make create-repo

Login docker to the repository:

ecr-login:
  aws ecr get-login-password --region $(AWS_REGION) \
  --profile $(AWS_PROFILE) \
  | docker login --username AWS --password-stdin $(REPO)
Enter fullscreen mode Exit fullscreen mode

make ecr-login

Build, tag and push the image:

build-tag-push: build tag-push

build:
  docker build -t $(FUNCTION_NAME) \
  --platform linux/amd64 \
  -f Dockerfile-$(RUNTIME) .

tag-push:
  docker tag $(FUNCTION_NAME):latest \
  $(REPO)/$(FUNCTION_NAME):latest \
  docker push $(REPO)/$(FUNCTION_NAME):latest
Enter fullscreen mode Exit fullscreen mode

make build-tag-push

Before creating the AWS Lambda, we need to create a role:

create-lambda-role:
  aws iam create-role \
  --profile $(AWS_PROFILE) \
  --role-name $(FUNCTION_NAME)-role \
  --assume-role-policy-document \
  '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'
  aws iam attach-role-policy \
  --profile $(AWS_PROFILE)
  --role-name $(FUNCTION_NAME)-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Enter fullscreen mode Exit fullscreen mode

make create-lambda-role

The role uses the AWSLambdaBasicExecutionRole policy, which allows the lambda to write logs for CloudWatch.

Finally, we create the lambda function:

create-lambda:
  aws lambda create-function \
  --function-name $(FUNCTION_NAME) \
  --role arn:aws:iam::$(AWS_ACCOUNT):role/$(FUNCTION_NAME)-role \
  --package-type Image \
  --code ImageUri=$(REPO)/$(FUNCTION_NAME):latest \
  --architectures x86_64 \
  --profile $(AWS_PROFILE) | cat
Enter fullscreen mode Exit fullscreen mode

make create-lambda

We must create the AWS lambda URL and allow unauthenticated access to complete the lambda creation.

create-lambda-url:
  aws lambda create-function-url-config \
  --profile $(AWS_PROFILE) \
  --function-name $(FUNCTION_NAME) \
  --auth-type NONE 

create-lambda-invoke-permission:
  aws lambda add-permission \
  --profile $(AWS_PROFILE) \
  --function-name $(FUNCTION_NAME) \
  --action lambda:InvokeFunctionUrl \
  --statement-id FunctionURLAllowPublicAccess \
  --principal "*" \
  --function-url-auth-type NONE 
Enter fullscreen mode Exit fullscreen mode

make create-lambda-url create-lambda-invoke-permission

At this point, the AWS should be successfully created and deployed.

If you change the lambda source and want to deploy the update, you call:

deploy: build-tag-push update-image wait

update-image:
  SHA=$(shell make last-tag) && \
  echo "SHA=$(WHITE)$$SHA$(NC)" && \
  aws lambda update-function-code \
  --profile $(AWS_PROFILE) \
  --function-name $(FUNCTION_NAME) \
  --image $(REPO)/$(FUNCTION_NAME)@$$SHA \
  | jq -r '.CodeSha256'

status:
  @aws lambda get-function \
  --function-name $(FUNCTION_NAME) \
  --profile $(AWS_PROFILE) \
  | jq -r .Configuration.LastUpdateStatus

wait:
  @while [ "$$(make status)" != "Successful" ]; do \
    echo "wait a moment for AWS to update the function..."; \
    sleep 10; \
  done
  @echo "lambda function update complete"
Enter fullscreen mode Exit fullscreen mode

make deploy

This command builds, tags and deploys a new image.

Let's invoke the function:

lambda-url:
  @aws lambda get-function-url-config \
  --function-name $(FUNCTION_NAME) \
  | jq -r '.FunctionUrl | rtrimstr("/")'

get:
  @HOST=$(shell make lambda-url) && \
  http GET "$$HOST/call?a=1"
Enter fullscreen mode Exit fullscreen mode

make get

This command calls the lambda function via its public URL. The URL path is /call but can be anything with some query parameters. The path and query parameters will be provided to the function code and other standard HTTP-related information.

Other examples in Makefile invoke the function in different ways. For example, put- calls the data function in the request body.

put-json:
  @HOST=$(shell make lambda-url) && \
  http -b PUT "$$HOST/call?q=1" a=1 b="message"

put-text:
  @HOST=$(shell make lambda-url) && \
  http -b PUT "$$HOST/call?q=1" --raw='plain data'

get-418:
  @HOST=$(shell make lambda-url) && \
  http GET "$$HOST/call?a=1&status=418"
Enter fullscreen mode Exit fullscreen mode

The code uses the http command from httpie.

Part 2

Let's look at the most exciting part -- the function's source code.

As I promised, we do not use any libraries. Instead, we use AWS Lambda API directly.

The AWS lambda lifecycle is a simple loop. The code below fetches the next function invocation event from the AWS API, passes it to the handler, and then sends the response to the AWS Lambda API response endpoint. That is it!

import process from "node:process";

const env = process.env;

const AWS_LAMBDA_RUNTIME_API = env.AWS_LAMBDA_RUNTIME_API || "?";
console.log("AWS_LAMBDA_RUNTIME_API", AWS_LAMBDA_RUNTIME_API);

const API = `http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation`;

while (true) {
    const event = await fetch(API + "/next");

    const REQUEST_ID = event.headers.get("Lambda-Runtime-Aws-Request-Id");
    console.log("REQUEST_ID", REQUEST_ID);

    const response = await handler(await event.json());

    await fetch(API + `/${REQUEST_ID}/response`, {
        method: "POST",
        body: JSON.stringify(response),
    });
}

// This is a simplified version of the AWS Lambda runtime API.
// The full specification can be found at:
// https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html

type APIGatewayProxyEvent = {
    queryStringParameters?: Record<string, string>;
    requestContext: { http: { method: string; path: string } };
    body?: string;
};

async function handler(event: APIGatewayProxyEvent) {
    const { method, path } = event.requestContext.http;

    const echo = {
        method,
        path,
        status: "200",
        queryStringParameters: {},
        runtime: runtime(),
        env: {
            ...env,
            AWS_SESSION_TOKEN: "REDACTED",
            AWS_SECRET_ACCESS_KEY: "REDACTED",
        },
        format: "",
        body: "",
    };

    if (event.queryStringParameters) {
        echo.queryStringParameters = event.queryStringParameters;
        echo.status = event.queryStringParameters.status || "200";
    }

    if (event.body) {
        try {
            echo.body = JSON.parse(event.body);
            echo.format = "json";
        } catch {
            echo.body = event.body;
            echo.format = "text";
        }
    }

    return {
        statusCode: echo.status,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(echo),
    };
}

function runtime() {
    return typeof Deno !== "undefined"
        ? "deno " + Deno.version.deno
        : typeof Bun !== "undefined"
        ? "bun " + Bun.version
        : "maybe node";
}
Enter fullscreen mode Exit fullscreen mode

For demonstration purposes, the handler returns the input data as the response.

NOTE: There are essential moments in the Dockerfile where we must configure the location for temporary files. The AWS Lambda container execution environment file system is read-only, and only the /tmp directory can be used for writing.

Let's discuss Dockerfiles to build the image.

FROM denoland/deno as deno

FROM public.ecr.aws/lambda/provided:al2

COPY --from=deno /usr/bin/deno /usr/bin/deno

# We need to set the DENO_DIR to /tmp because the AWS lambda filesystem 
# is read-only except for /tmp. Deno may need to write to its cache.
ENV DENO_DIR=/tmp

COPY lambda.ts /var/task/

ENTRYPOINT [ "/usr/bin/deno" ]
CMD [ "run", "-A", "--no-lock", "/var/task/lambda.ts"]
Enter fullscreen mode Exit fullscreen mode

Dockerfile uses the official AWS base image public.ecr.aws/lambda/provided:al2.

This image comes with the AWS Lambda Runtime Client preinstalled. This client runs in the background and proxies the requests from the lambda function loop to AWS endpoints. The AWS_LAMBDA_RUNTIME_API variable points to localhost with a port on which the Lambda Runtime Client listens.

This concludes the article.

By default, Makefile uses Deno (RUNTIME=deno). The RUNTIME variable can be set to bun as a drop-in change, so no other changes are required.

For convenience, the handler reports what runtime it is running on in the runtime field.

Resources

The links to the sources of the files from this article:

awslambda Article's
30 articles in total
Favicon
Build a Crypto Price Alert System with Telegram and AWS Lambda
Favicon
Leveraging Docker with AWS Lambda for Custom Runtimes and Large Deployments
Favicon
Docker for Serverless: Customizing Functions and Scaling Flexibly
Favicon
Inventory Management with AWS Lambda λ
Favicon
Lambda Power Tuning: Una comparativa entre arquitecturas x86_64 y arm64
Favicon
A Beginners Guide to Serverless API Gateway Authentication with Lambda Authorizer
Favicon
Serverless Functions: Unlocking the Power of AWS Lambda, Azure Functions, and More
Favicon
Mastering Serverless and Event-Driven Architectures with AWS: Innovations in Lambda, EventBridge, and Beyond
Favicon
Parse UserParameters sent from AWS CodePipeline to AWS Lambda in Go
Favicon
Leveraging Amazon Connect for Real-Time Incident Response Calls
Favicon
Lambda Code Execution Freeze/Thaw
Favicon
Efficiently Delete Inactive User Data Using TypeScript and AWS Lambda
Favicon
Unlocking Serverless: Build Your First Python AWS Lambda Function
Favicon
Lamba LLRT(Low Latency Runtime Javascript)
Favicon
Building Scalable Microservices with AWS Lambda and Serverless
Favicon
Serverless Architecture Best Practices
Favicon
Deep Dive on Writing and Reading Data to DynamoDB Table from Lambda Functions Using AWS Cloud Map Service Discovery
Favicon
AWS Lambda in Deno or Bun
Favicon
Lambda extension to cache SSM and Secrets Values for PHP Lambda on CDK
Favicon
Create a Fast Node.js Serverless Backend Using AWS Lambda and DynamoDB
Favicon
30-day Learning Challenge: Day 2— Learning AWS S3
Favicon
AWS Lambda Functions Failure Management
Favicon
Understanding Load Balancers: How They Work, Types, Algorithms, and Use Cases
Favicon
How to Deploy Dart Functions to AWS Lambda
Favicon
Using Custom Authorization - Request based for AWS Lambda
Favicon
How to generate a presigned url to upload images to S3
Favicon
Create an AppSync API using Terraform
Favicon
Creating a Cognito Trigger using CDK and TypeScript
Favicon
API Gateway REST API with Lambda Integration
Favicon
AWS Lambda Runtime debate

Featured ones: