HTTP server runs on AWS Lambda

MORIYA Hiroyuki
3 min readFeb 19, 2023

--

TL;DR

With aws-lambda-adapter, you can easily run an HTTP server on a container image on AWS Lambda.

# Dockerfile

...
+ COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.6.1 /lambda-adapter /opt/extensions/lambda-adapter
...

Introduction

I read a Japanese article about how to convert a web app to serverless.
https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/

I‘m interested in Rust, I tried to run an HTTP server using Rust web framework on AWS Lambda.

Example repository

https://github.com/mryhryki/example-rust-server-on-lambda

HTTP server example (Rust — axum)

I implemented a HTTP Server by Rust and axum. This code is mostly sample code from axum repository.

use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
// initialize tracing
tracing_subscriber::fmt::init();
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.route("/", get(root))
// `POST /users` goes to `create_user`
.route("/users", post(create_user));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
tracing::debug!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

// basic handler that responds with a static string
async fn root() -> &'static str {
"Hello, World!"
}

async fn create_user(
// this argument tells axum to parse the request body
// as JSON into a `CreateUser` type
Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
// insert your application logic here
let user = User {
id: 1337,
username: payload.username,
};
// this will be converted into a JSON response
// with a status code of `201 Created`
(StatusCode::CREATED, Json(user))
}

// the input to our `create_user` handler
#[derive(Deserialize)]
struct CreateUser {
username: String,
}

// the output to our `create_user` handler
#[derive(Serialize)]
struct User {
id: u64,
username: String,
}

Build container image

Prepare a Dockerfile

This Dockerfile is normal for a web application.

# Dockerfile
FROM rust:1-slim-buster as builder
WORKDIR /app
COPY ./Cargo.toml /app/Cargo.toml
COPY ./Cargo.lock /app/Cargo.lock
COPY ./src /app/src
RUN cargo build --release

FROM debian:buster-slim
WORKDIR /app
COPY --from=builder "/app/target/release/example-rust-server-on-lambda" "/app/example-rust-server-on-lambda"
EXPOSE 8080
ENTRYPOINT ["/app/example-rust-server-on-lambda"]

Add to copy “aws-lambda-adapter” step

Add a step to the Dockerfile to copy aws-lambda-adapter. Just added this step, and the container image will be able to run on AWS Lambda.

...
FROM debian:buster-slim
+ COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.6.1 /lambda-adapter /opt/extensions/lambda-adapter
WORKDIR /app
COPY --from=builder "/app/target/release/example-rust-server-on-lambda" "/app/example-rust-server-on-lambda"
EXPOSE 8080
...

Note: aws-lambda-adapter default port is “8080”. If your web application uses other port, you have to set environment variables.

Build

Execute the following command to build the container image.

# Login to Amazon ECR Public
$ aws ecr-public get-login-password --region us-east-1 |
docker login --username AWS --password-stdin public.ecr.aws

# Build container image
$ export DOCKER_TAG="IMAGE_NAME:latest"
$ docker build --tag "${DOCKER_TAG}" .

Push to Amazon ECR

Execute the following command to push to the your ECR repository.

$ export DOCKER_TAG="IMAGE_NAME:latest"
$ export ECR_URI="${YOUR_AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/IMAGE_NAME:latest"

$ aws ecr get-login-password --region ap-northeast-1 |
docker login --username AWS --password-stdin "${ECR_URI}"
$ docker tag "${DOCKER_TAG}" "${ECR_URI}"
$ docker push "${ECR_URI}"

Deploy to AWS Lambda with Pulumi

I deployed an axum HTTP server on AWS using Pulumi. The reason I used Pulumi is because I heard it is the modern IaC tool. However, you can deploy it any way you like, such as AWS console, Terraform, etc.

I wrote the following TypeScript code, but I’m a Pulumi beginner, so this code may not be the best way.

import * as aws from "@pulumi/aws";

const lambdaRole = new aws.iam.Role("example-rust-server-on-lambda-role", {
assumeRolePolicy: {
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Principal: {
Service: "lambda.amazonaws.com",
},
Effect: "Allow",
Sid: "",
}],
},
});
new aws.iam.RolePolicyAttachment(
"example-rust-server-on-lambda-policy-attachment",
{
role: lambdaRole,
policyArn: aws.iam.ManagedPolicies.AWSLambdaExecute,
},
);

const lambdaFunction = new aws.lambda.Function(
"example-rust-server-on-lambda",
{
packageType: "Image",
imageUri: process.env.ECR_URI,
role: lambdaRole.arn,
},
);

new aws.lambda.FunctionUrl("example-rust-server-on-lambda", {
functionName: lambdaFunction.name,
authorizationType: "NONE",
}).functionUrl.apply(console.log);

Using Lambda function URLs

In this example, I use the Lambda function URLs. It provides an HTTP endpoint without using other services such as API Gateway.

Result

Accessing HTTP endpoints via cURL and returns 200 Success.

# GET /
$ curl https://2svshuw3qrunpbd7zcl4uhd44a0mpifl.lambda-url.ap-northeast-1.on.aws/
Hello, World!

# POST /users
$ curl -XPOST \
-H 'Content-Type: application/json' \
-d '{"username":"mryhryki"}' \
https://2svshuw3qrunpbd7zcl4uhd44a0mpifl.lambda-url.ap-northeast-1.on.aws/users
{"id":1337,"username":"mryhryki"}

--

--