├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── examples └── main.rs ├── src └── lib.rs └── test-lambda-runtime ├── Dockerfile ├── requirements.txt └── test_lambda_runtime.py /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /target 3 | 4 | # https://doc.rust-lang.org/cargo/faq.html#why-do-binaries-have-cargolock-in-version-control-but-not-libraries 5 | /Cargo.lock 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.10.0] 9 | 10 | ### Changed 11 | 12 | - Upgrade lambda_http to 0.14 13 | - Upgrade axum to 0.8 14 | 15 | ## [0.9.0] 16 | 17 | ### Changed 18 | 19 | - Upgrade to tower 0.5 20 | 21 | ## [0.8.0] 22 | 23 | ### Changed 24 | 25 | - Upgrade lambda_http to 0.13 26 | 27 | ## [0.7.0] 28 | 29 | ### Changed 30 | 31 | - Upgrade lambda_http to 0.11 32 | 33 | ## [0.6.0] 34 | 35 | ### Changed 36 | 37 | - Upgrade axum to 0.7, hyper to 1.0, and lambda_http to 0.9 versions 38 | 39 | ## [0.5.1] 40 | 41 | ### Changed 42 | 43 | - Bumped dependencies to http v1.0 and bytes v1.5 44 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-aws-lambda" 3 | version = "0.10.0" 4 | edition = "2021" 5 | authors = ["Michael Lazear"] 6 | description = "Tower Layer for compatibility between Axum and AWS Lambda Runtime" 7 | readme = "README.md" 8 | homepage = "https://github.com/lazear/axum-aws-lambda" 9 | repository = "https://github.com/lazear/axum-aws-lambda" 10 | license = "MIT" 11 | keywords = ["axum", "lambda", "tower", "aws"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | lambda_http = "0.14" 17 | axum = "0.8" 18 | hyper = "1.0" 19 | bytes = "1.5" 20 | http = "1" 21 | tower = "0.5" 22 | tower-service = "0.3" 23 | futures-util = "0.3" 24 | http-body-util = "0.1" 25 | 26 | [dev-dependencies] 27 | tokio = { version = "1.0", features = ["rt"] } 28 | tower-http = { version = "0.5.0", features = [ 29 | "cors", 30 | "compression-gzip", 31 | "compression-deflate", 32 | "trace", 33 | ] } 34 | tracing = "0.1" 35 | tracing-subscriber = { version = "0.3", features = ["json"] } 36 | serde = { version = "1.0", features = ["derive"] } 37 | serde_json = "1.0" 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | ADD "https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie" /var/runtime/rie 4 | 5 | RUN chmod +x /var/runtime/rie 6 | RUN apt-get update 7 | RUN apt-get install -y ca-certificates 8 | 9 | ENV AWS_LAMBDA_FUNCTION_NAME="test" 10 | ENV AWS_LAMBDA_FUNCTION_MEMORY_SIZE="3008" 11 | ENV AWS_LAMBDA_FUNCTION_VERSION="1" 12 | ENV AWS_LAMBDA_RUNTIME_API="localhost:9000" 13 | ENV AWS_DEFAULT_REGION="us-west-2" 14 | ENV RUST_BACKTRACE="1" 15 | 16 | COPY "target/release/examples/main" "/var/runtime/bootstrap" 17 | CMD ["/var/runtime/rie", "/var/runtime/bootstrap"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michael Lazear 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axum-aws-lambda 2 | 3 | [![Rust](https://github.com/lazear/axum-aws-lambda/actions/workflows/rust.yml/badge.svg)](https://github.com/lazear/axum-aws-lambda/actions/workflows/rust.yml) 4 | ![crates.io](https://img.shields.io/crates/v/axum-aws-lambda) 5 | 6 | This crate provides a `tower::Layer` that translates `hyper`/`axum` requests to the format used by the `aws-lambda-rust-runtime` crate. This allows users to switch between just running a Hyper server, and running under the Lambda runtime - this dramatically speeds up development! It also means that you can use off-the-shelf components from the Tower ecosystem! 7 | 8 | Check out `examples/main.rs`: running in debug mode runs a hyper server, and building for release mode compiles using the Lambda runtime. 9 | 10 | ### Testing out the Lambda runtime locally 11 | 12 | There is an example Dockerfile for locally spinning up a lambda runtime: 13 | 14 | ```terminal 15 | cargo build --release --example main 16 | docker build . -t lambda-test 17 | docker run -p 9000:8080 lambda-test 18 | ``` 19 | 20 | In `test-lambda-runtime/` there is a python script for testing and a Dockerfile for running it. 21 | 22 | In another shell, from the root of this repository: 23 | 24 | ```terminal 25 | cd test-lambda-runtime 26 | docker build . -t test_lambda_runtime 27 | docker run --network="host" test_lambda_runtime 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/main.rs: -------------------------------------------------------------------------------- 1 | use axum::body::Body; 2 | use axum::extract::State; 3 | use axum::http::header::{ACCEPT, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, ORIGIN}; 4 | use axum::response::IntoResponse; 5 | use axum::{ 6 | routing::{get, post}, 7 | Json, Router, 8 | }; 9 | use hyper::Request; 10 | use hyper::StatusCode; 11 | use serde::{Deserialize, Serialize}; 12 | use std::sync::{Arc, Mutex}; 13 | use tower_http::{compression::CompressionLayer, cors::CorsLayer, trace::TraceLayer}; 14 | 15 | #[derive(Default)] 16 | struct AppState { 17 | data: Vec, 18 | } 19 | 20 | #[derive(Deserialize, Serialize, Debug, Clone)] 21 | struct Data { 22 | name: String, 23 | } 24 | 25 | async fn post_data( 26 | State(state): State>>, 27 | Json(payload): Json, 28 | ) -> impl IntoResponse { 29 | let mut state = state.lock().unwrap(); 30 | state.data.push(payload); 31 | StatusCode::CREATED 32 | } 33 | 34 | async fn get_data(State(state): State>>) -> impl IntoResponse { 35 | let state = state.lock().unwrap(); 36 | (StatusCode::OK, Json(state.data.clone())) 37 | } 38 | 39 | #[tokio::main] 40 | async fn main() { 41 | tracing_subscriber::fmt() 42 | .with_ansi(false) 43 | .without_time() 44 | .with_max_level(tracing::Level::INFO) 45 | .json() 46 | .init(); 47 | 48 | let state = Arc::new(Mutex::new(AppState::default())); 49 | 50 | // Trace every request 51 | let trace_layer = 52 | TraceLayer::new_for_http().on_request(|_: &Request, _: &tracing::Span| { 53 | tracing::info!(message = "begin request!") 54 | }); 55 | 56 | // Set up CORS 57 | let cors_layer = CorsLayer::new() 58 | .allow_headers([ACCEPT, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, ORIGIN]) 59 | .allow_methods(tower_http::cors::Any) 60 | .allow_origin(tower_http::cors::Any); 61 | 62 | // Wrap an `axum::Router` with our state, CORS, Tracing, & Compression layers 63 | let app = Router::new() 64 | .route("/", post(post_data)) 65 | .route("/", get(get_data)) 66 | .layer(cors_layer) 67 | .layer(trace_layer) 68 | .layer(CompressionLayer::new().gzip(true).deflate(true)) 69 | .with_state(state); 70 | 71 | #[cfg(debug_assertions)] 72 | { 73 | let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000)); 74 | let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 75 | axum::serve(listener, app).await.unwrap(); 76 | } 77 | 78 | // If we compile in release mode, use the Lambda Runtime 79 | #[cfg(not(debug_assertions))] 80 | { 81 | // To run with AWS Lambda runtime, wrap in our `LambdaLayer` 82 | let app = tower::ServiceBuilder::new() 83 | .layer(axum_aws_lambda::LambdaLayer::default()) 84 | .service(app); 85 | 86 | lambda_http::run(app).await.unwrap(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use axum::response::IntoResponse; 2 | use http_body_util::BodyExt; 3 | use lambda_http::RequestExt; 4 | use std::{future::Future, pin::Pin}; 5 | use tower::Layer; 6 | use tower_service::Service; 7 | 8 | #[derive(Default, Clone, Copy)] 9 | pub struct LambdaLayer { 10 | trim_stage: bool, 11 | } 12 | 13 | impl LambdaLayer { 14 | pub fn trim_stage(mut self) -> Self { 15 | self.trim_stage = true; 16 | self 17 | } 18 | } 19 | 20 | impl Layer for LambdaLayer { 21 | type Service = LambdaService; 22 | 23 | fn layer(&self, inner: S) -> Self::Service { 24 | LambdaService { 25 | inner, 26 | layer: *self, 27 | } 28 | } 29 | } 30 | 31 | pub struct LambdaService { 32 | inner: S, 33 | layer: LambdaLayer, 34 | } 35 | 36 | impl Service for LambdaService 37 | where 38 | S: Service>, 39 | S::Response: axum::response::IntoResponse + Send + 'static, 40 | S::Error: std::error::Error + Send + Sync + 'static, 41 | S::Future: Send + 'static, 42 | { 43 | type Response = lambda_http::Response; 44 | type Error = lambda_http::Error; 45 | type Future = 46 | Pin> + Send + 'static>>; 47 | 48 | fn poll_ready( 49 | &mut self, 50 | cx: &mut std::task::Context<'_>, 51 | ) -> std::task::Poll> { 52 | self.inner.poll_ready(cx).map_err(Into::into) 53 | } 54 | 55 | fn call(&mut self, req: lambda_http::Request) -> Self::Future { 56 | let uri = req.uri().clone(); 57 | let rawpath = req.raw_http_path().to_owned(); 58 | let (mut parts, body) = req.into_parts(); 59 | let body = match body { 60 | lambda_http::Body::Empty => axum::body::Body::default(), 61 | lambda_http::Body::Text(t) => t.into(), 62 | lambda_http::Body::Binary(v) => v.into(), 63 | }; 64 | 65 | if self.layer.trim_stage { 66 | let mut url = match uri.host() { 67 | None => rawpath, 68 | Some(host) => format!( 69 | "{}://{}{}", 70 | uri.scheme_str().unwrap_or("https"), 71 | host, 72 | rawpath 73 | ), 74 | }; 75 | 76 | if let Some(query) = uri.query() { 77 | url.push('?'); 78 | url.push_str(query); 79 | } 80 | parts.uri = url.parse::().unwrap(); 81 | } 82 | 83 | let request = axum::http::Request::from_parts(parts, body); 84 | 85 | let fut = self.inner.call(request); 86 | let fut = async move { 87 | let resp = fut.await?; 88 | let (parts, body) = resp.into_response().into_parts(); 89 | let bytes = body.into_data_stream().collect().await?.to_bytes(); 90 | let bytes: &[u8] = &bytes; 91 | let resp: hyper::Response = match std::str::from_utf8(bytes) { 92 | Ok(s) => hyper::Response::from_parts(parts, s.into()), 93 | Err(_) => hyper::Response::from_parts(parts, bytes.into()), 94 | }; 95 | Ok(resp) 96 | }; 97 | 98 | Box::pin(fut) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test-lambda-runtime/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim as builder 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends gcc 7 | 8 | COPY requirements.txt . 9 | RUN python -m venv /opt/venv 10 | ENV PATH="/opt/venv/bin:$PATH" 11 | RUN pip install --upgrade pip && \ 12 | pip install -r requirements.txt 13 | 14 | COPY . . 15 | 16 | FROM python:3.9-slim 17 | 18 | WORKDIR /app 19 | 20 | COPY --from=builder /opt/venv /opt/venv 21 | COPY --from=builder /app/test_lambda_runtime.py . 22 | 23 | ENV PATH="/opt/venv/bin:$PATH" 24 | 25 | EXPOSE 5000 26 | 27 | CMD ["python", "test_lambda_runtime.py"] 28 | -------------------------------------------------------------------------------- /test-lambda-runtime/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /test-lambda-runtime/test_lambda_runtime.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | 5 | def mk_request(method="GET", body=None): 6 | return { 7 | "headers": { 8 | "Accept": "*/*", 9 | "Content-Type": "application/json", 10 | "Host": "localhost:5000", 11 | "User-Agent": "insomnia/2022.2.1" 12 | }, 13 | "queryStringParameters": {}, 14 | "requestContext": { 15 | "http": { 16 | "method": method, 17 | "path": "/", 18 | } 19 | }, 20 | "body": json.dumps(body) if body else "", 21 | "isBase64Encoded": False 22 | } 23 | 24 | 25 | requests.post( 26 | "http://localhost:9000/2015-03-31/functions/function/invocations", 27 | json=mk_request(method="POST", body={"name": "John Smith"}), 28 | ) 29 | response = requests.post( 30 | "http://localhost:9000/2015-03-31/functions/function/invocations", json=mk_request() 31 | ).json() 32 | 33 | print(json.dumps(json.loads(response["body"]), indent=2)) 34 | --------------------------------------------------------------------------------