diff --git a/.env b/.env new file mode 100644 index 0000000..6048991 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://root:password@localhost:5001/test?sslmode=disable" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 337c350..1862542 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,3 +1,9 @@ +## test with act +# act push \ +# -W .github/workflows/ci.yaml \ +# --secret-file .env \ +# -P ubuntu-22.04=catthehacker/ubuntu:act-22.04 + name: CI/CD for sam-rust on: @@ -11,97 +17,26 @@ env: CARGO_TERM_COLOR: always jobs: - build-and-test: + deploy: runs-on: ubuntu-22.04 - services: - postgres: - image: postgres:14 - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: password - POSTGRES_DB: test - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U root" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: x86_64-unknown-linux-musl - profile: minimal - components: rustfmt, clippy + uses: actions/checkout@v3 - - name: πŸ“¦ Cache Cargo registry and target - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Install system dependencies + - name: Set up Docker-in-Docker permissions run: | - sudo apt-get update - sudo apt-get install -y libssl-dev pkg-config zip make curl unzip python3-pip - - - name: 🧹 Format and Lint Code (make pretty) - run: make pretty - - - name: πŸ”¬ Run Integration Tests - run: make test - env: - DATABASE_URL: postgres://root:password@localhost:5432/test - - - name: Install AWS SAM CLI - run: pip3 install aws-sam-cli --upgrade + sudo chown -R $USER:$USER /var/run/docker.sock || true - - name: Validate SAM Template - run: sam validate --lint + - name: Make sure Docker is working + run: docker info - - name: Cache Cargo Binaries - id: cache-cargo-bin - uses: actions/cache@v3 - with: - path: ~/.cargo/bin - key: ${{ runner.os }}-cargo-bin-cargo-lambda + - name: Build Rust Lambda with SAM (Docker-based) + run: make aws-build-sam - - name: Install cargo-lambda - if: steps.cache-cargo-bin.outputs.cache-hit != 'true' - run: cargo install cargo-lambda --locked - - - name: Cache Zig - id: cache-zig - uses: actions/cache@v3 - with: - path: ~/.zig - key: ${{ runner.os }}-zig-0.10.1 - - - name: Install Zig - if: steps.cache-zig.outputs.cache-hit != 'true' - run: | - ZIG_VERSION=0.10.1 - mkdir -p ~/.zig/bin - wget https://ziglang.org/download/$ZIG_VERSION/zig-linux-x86_64-$ZIG_VERSION.tar.xz - tar -xf zig-linux-x86_64-$ZIG_VERSION.tar.xz - mv zig-linux-x86_64-$ZIG_VERSION/* ~/.zig/bin - echo "$HOME/.zig/bin" >> $GITHUB_PATH - - - name: Add Zig to PATH - run: echo "$HOME/.zig/bin" >> $GITHUB_PATH - - - name: πŸ› οΈ Build Lambda with SAM - shell: bash - run: | - export PATH="$HOME/.cargo/bin:$HOME/.zig/bin:$PATH" - make sam-build + # - name: Deploy SAM application to AWS + # env: + # AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + # AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + # AWS_REGION: us-east-1 + # run: make aws-deploy-sam diff --git a/.gitignore b/.gitignore index 19a981c..5cdc86c 100644 --- a/.gitignore +++ b/.gitignore @@ -221,6 +221,14 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/rust,osx,linux,windows,pycharm,visualstudiocode +aws/.aws-sam/* .aws-sam/* libpq_layer.zip -postgresql-*. \ No newline at end of file +postgresql-*. + +aws/postgresql-10.23.tar.gz + + +/libpq_layer.zip +/postgresql-10.23/* +/postgresql-10.23.tar.gz \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..60863fe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +# Cargo.toml + +[workspace] +resolver = "2" +members = [ + "backend", + ] + +[workspace.dependencies] + +tokio = { version = "1", features = ["macros"] } +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } + +diesel = { version = "2.2.7", features = ["postgres", "r2d2"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +warp = "0.3" +warp_lambda = "0.1.4" + +openssl = { version = "0.10", features = ["vendored"] } +r2d2 = "0.8" +once_cell = "1.21.3" +lambda_http = { version = "0.13.0" } +lambda_runtime = { version = "0.13.0" } \ No newline at end of file diff --git a/Makefile b/Makefile index 7a2b302..485cfe8 100644 --- a/Makefile +++ b/Makefile @@ -1,98 +1,50 @@ -# Makefile for building and deploying the Rust-based AWS SAM application -# Includes support for building a custom libpq layer for Diesel/PostgreSQL +# Makefile -BACKEND_STACK_NAME ?= warp-lambda-starter-stack - -# Build the LibpqLayer using the SAM `makefile` build method. -# This is invoked by SAM when building the LibpqLayer defined in template.yaml. -# It copies prebuilt libpq shared objects and headers from our local `libpq_layer/` folder -# into the correct layer output directory (defined by $ARTIFACTS_DIR), -# where they will be placed in `/opt` at runtime in the Lambda environment. -build-LibpqLayer: - mkdir -p "$(ARTIFACTS_DIR)/lib" - mkdir -p "$(ARTIFACTS_DIR)/include/libpq" - cp -r libpq_layer/lib/* "$(ARTIFACTS_DIR)/lib/" - cp -r libpq_layer/include/libpq/* "$(ARTIFACTS_DIR)/include/libpq/" - -# Build the full SAM application, including all Lambda functions and layers. -# This sets the required environment variables so `pq-sys` can link against the local libpq shared library. -# The paths are resolved to absolute paths to ensure the build works regardless of working directory. -sam-build: - @echo "πŸ”§ Building with libpq from project-local layer..." - - @test -f libpq_layer/lib/libpq.so || { echo "❌ libpq.so not found. Run 'make sh-libpq'"; exit 1; } - - PQ_LIB_DIR=$(realpath libpq_layer/lib) \ - PQ_INCLUDE_DIR=$(realpath libpq_layer/include/libpq) \ - RUSTFLAGS="-C link-args=-Wl,-rpath=/opt/lib" \ - RUST_LOG=debug \ - sam build --beta-features - -# Build the SAM project and run it locally via the SAM CLI. -# This allows you to test the Lambda functions using Docker on your machine. -sam-run: - @echo "Building SAM application..." - make sam-build - sam local start-api --docker-network sam-local --debug --env-vars env.json - -# Build the libpq shared libraries and headers inside an Amazon Linux 2 container. -# This mimics the Lambda environment and ensures binary compatibility. -# Output is placed in `libpq_layer/lib` and `libpq_layer/include/libpq`. -sh-libpq: - sh ./build_libpq_layer_docker.sh - -# Validate, build, and deploy the full SAM stack to AWS. -# This will upload the Lambda functions and layers, create/update resources, -# and deploy the API Gateway with no manual confirmations. -deploy-sam: - @echo "Validating SAM template..." - sam validate +# ======================================= +# Root Makefile: Orchestrator +# ======================================= - @echo "Building SAM project..." - make sam-build || { echo "Build failed"; exit 1; } +### Include submodule Makefiles +include backend/Makefile +include aws/Makefile - @echo "Deploying SAM stack: $(BACKEND_STACK_NAME)..." - sam deploy --stack-name $(BACKEND_STACK_NAME) \ - --force-upload \ - --no-confirm-changeset --no-fail-on-empty-changeset \ - --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ - --resolve-s3 \ - --debug || { echo "Deployment failed."; exit 1; } +# ======================================= +# Directory-specific Commands +# ======================================= - @echo "Deployment completed successfully for stack: $(BACKEND_STACK_NAME)." +AWS_MAKE = $(MAKE) -C aws +BACKEND_MAKE = $(MAKE) -C backend -# Delete the deployed SAM stack from AWS without prompting. -delete-sam: - sam delete --no-prompts --stack-name $(BACKEND_STACK_NAME) +# AWS commands delegation +aws-%: + @echo "Delegating to aws/$*..." + $(AWS_MAKE) $* +# # Backend commands delegation +be-%: + @echo "Delegating to backend/$*..." + $(BACKEND_MAKE) $* # ======================================= # Utility Commands # ======================================= -# Format all code +# Format all code (including fixing simple style issues) format: - @echo "Formatting all code..." - (cd rust_app && cargo fmt --all) + @echo "Formatting all code with cargo fmt..." + cargo fmt --all -# Lint all code +# Lint all code with clippy and suggest fixes where possible lint: - @echo "Linting all code..." - (cd rust_app && cargo clippy --tests --all-features -- -D warnings) + @echo "Linting all code with cargo clippy..." + cargo clippy --workspace --all-targets --all-features --fix --allow-dirty --allow-staged -- -D warnings -# Format and lint code +# Format and lint pretty: format lint +# ======================================= +# Project Orchestration +# ======================================= -test: - (cd rust_app && DATABASE_URL=$${DATABASE_URL:-postgres://root:password@localhost:5001/test} cargo test --all --all-features -- --nocapture) - - -help: - @echo "Available commands:" - @echo " make format - Run rustfmt on all code" - @echo " make lint - Run clippy and fail on warnings" - @echo " make test - Run unit and integration tests" - @echo " make sam-build - Build Lambda function using SAM" - @echo " make sam-run - Run API locally via SAM" - @echo " make deploy-sam - Deploy to AWS" +run-backend: + cargo watch -p backend -w backend/src -s 'make be-run' diff --git a/README.md b/README.md index 2fb36ff..364a576 100644 --- a/README.md +++ b/README.md @@ -199,3 +199,8 @@ Special thanks to the maintainers of: - [`aws-lambda-rust-runtime`](https://github.com/awslabs/aws-lambda-rust-runtime) --- + + + +cargo install cross --git https://github.com/cross-rs/cross --branch main --force +export CROSS_CONTAINER_ENGINE=docker \ No newline at end of file diff --git a/aws/Makefile b/aws/Makefile new file mode 100644 index 0000000..1e1debb --- /dev/null +++ b/aws/Makefile @@ -0,0 +1,84 @@ +# aws/Makefile +# ------------------------------------------------------------------------------ +# Makefile for building and deploying the Rust-based AWS SAM application. +# This file provides targets for: +# - Building the Lambda binary using Docker with static linking +# - Compiling and packaging the libpq Lambda layer for Diesel +# - Running the Lambda API locally via SAM CLI +# - Deploying and deleting the AWS SAM stack +# ------------------------------------------------------------------------------ + +# Include Makefile from the docker/ subdirectory relative to this file +include $(dir $(lastword $(MAKEFILE_LIST)))docker/Makefile +DOCKER_MAKE = $(MAKE) -C docker + +# Forward docker-* targets to the docker/Makefile +docker-%: + @echo "Delegating to docker/$*..." + $(DOCKER_MAKE) $* + +# ======================================= +# AWS SAM Build and Run Targets +# ======================================= + +# Build the Lambda binary using a Docker image +build-sam: + @echo "Building SAM project..." + rm -rf .aws-sam + mkdir -p .aws-sam + make docker-build-sam + +# Run the Lambda locally using SAM CLI and the provided env.json config +run-sam: + @echo "Starting Local SAM application from aws/..." + sam local start-api \ + --template .aws-sam/template.yaml \ + --env-vars env.json \ + --docker-network sam-local \ + --debug \ + --port 4040 + +# Validate the SAM template syntax and structure +validate-sam: + @echo "Validating SAM template..." + sam validate + +# ======================================= +# Deploy & Cleanup Targets +# ======================================= + +# Name of the AWS CloudFormation stack to create/update +BACKEND_STACK_NAME ?= warp-lambda-starter-stack + +# Validate, build, and deploy the Lambda stack to AWS +deploy-sam: validate-sam build-sam + @echo "Deploying SAM stack: $(BACKEND_STACK_NAME)..." + sam deploy --stack-name $(BACKEND_STACK_NAME) \ + --force-upload \ + --no-confirm-changeset --no-fail-on-empty-changeset \ + --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ + --resolve-s3 \ + --debug || { echo "Deployment failed."; exit 1; } + + @echo "Deployment completed successfully for stack: $(BACKEND_STACK_NAME)." + +# Delete the deployed AWS stack without any confirmation prompts +delete-sam: + sam delete --no-prompts --stack-name $(BACKEND_STACK_NAME) + +# ======================================= +# Layer & Build Method Targets +# ======================================= + +# Copy the compiled backend binary into the Lambda-compatible bootstrap file +# Used by SAM's makefile-based build method +build-BackendFunction: + cp /app/target/x86_64-unknown-linux-musl/release/backend ${ARTIFACTS_DIR}/bootstrap + +# Copy libpq static libraries and headers into the appropriate layer directories +# Used by SAM to package and attach the PostgreSQL C client libraries as a Lambda Layer +build-LibpqLayer: + mkdir -p "$(ARTIFACTS_DIR)/lib" + mkdir -p "$(ARTIFACTS_DIR)/include/libpq" + cp libpq_layer/lib/libpq.a "$(ARTIFACTS_DIR)/lib/" + cp -r libpq_layer/include/libpq/* "$(ARTIFACTS_DIR)/include/libpq/" diff --git a/aws/build.toml b/aws/build.toml new file mode 100644 index 0000000..eed925f --- /dev/null +++ b/aws/build.toml @@ -0,0 +1,25 @@ +# This file is auto generated by SAM CLI build command + +[function_build_definitions.25657852-45cc-4054-8566-a45364d3c35b] +codeuri = "/app/aws" +runtime = "provided.al2" +architecture = "x86_64" +handler = "bootstrap" +source_hash = "53ffe2b9e1dcb55cd2065de726a801d89d648ef21ed2d775de51bb2c8a8bc5ed" +manifest_hash = "" +packagetype = "Zip" +functions = ["BackendFunction"] + +[function_build_definitions.25657852-45cc-4054-8566-a45364d3c35b.metadata] +BuildMethod = "makefile" +BuildArchitecture = "x86_64" + +[layer_build_definitions.53b48ff7-8185-4881-9062-8e6ebd6ded41] +layer_name = "LibpqLayer" +codeuri = "/app/aws" +build_method = "makefile" +compatible_runtimes = ["provided.al2"] +architecture = "x86_64" +source_hash = "53ffe2b9e1dcb55cd2065de726a801d89d648ef21ed2d775de51bb2c8a8bc5ed" +manifest_hash = "" +layer = "LibpqLayer" diff --git a/aws/docker/Dockerfile.build-sam b/aws/docker/Dockerfile.build-sam new file mode 100644 index 0000000..67130e3 --- /dev/null +++ b/aws/docker/Dockerfile.build-sam @@ -0,0 +1,34 @@ +FROM alpine:3.19 + +RUN apk add --no-cache \ + musl-dev \ + gcc \ + build-base \ + openssl-dev \ + zlib-dev \ + libgcc \ + libstdc++ \ + curl \ + python3 \ + py3-pip \ + py3-virtualenv \ + bash \ + perl + +# Set up virtualenv for AWS SAM CLI +RUN python3 -m venv /venv && \ + . /venv/bin/activate && \ + pip install --upgrade pip && \ + pip install aws-sam-cli + +ENV PATH="/venv/bin:$PATH" + +# Install Rust and musl target +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable +ENV PATH="/root/.cargo/bin:${PATH}" +RUN rustup target add x86_64-unknown-linux-musl + +WORKDIR /app +COPY . . + +CMD ["bash"] diff --git a/aws/docker/Makefile b/aws/docker/Makefile new file mode 100644 index 0000000..e89021b --- /dev/null +++ b/aws/docker/Makefile @@ -0,0 +1,42 @@ +# Get the absolute path to the project root (two levels up from aws/docker/) +REPO_ROOT := $(realpath $(CURDIR)/../..) + +# ================================================================ +# πŸ”§ build-sam: Builds the Rust backend binary and SAM artifacts +# ================================================================ +build-sam: + # 🐳 Build the Docker image used for compiling the Rust Lambda + docker build --platform=linux/amd64 -t rust-sam-build -f Dockerfile.build-sam . + + # 🐳 Run the Docker container to compile and build the project + # Mount entire project into /app in the container + # Mount prebuilt libpq layer for static linking + # Mount the build output dir for AWS SAM + # Set working directory to /app/aws (where template.yaml lives) + docker run --platform=linux/amd64 --rm \ + -v "$(REPO_ROOT)":/app \ + -v "$(REPO_ROOT)/aws/libpq_layer":/aws/libpq_layer \ + -v "$(REPO_ROOT)/aws/.aws-sam":/app/aws/.aws-sam \ + -w /app/aws \ + rust-sam-build \ + sh -c "\ + cd /app && \ + RUSTFLAGS='-L /aws/libpq_layer/lib \ + -C link-arg=-lpq \ + -C link-arg=-lssl \ + -C link-arg=-lcrypto \ + -C link-arg=-lz \ + -C link-arg=-static' \ + OPENSSL_NO_VENDOR=1 cargo build --release --target x86_64-unknown-linux-musl --bin backend && \ + sam build --template aws/template.yaml \ + --build-dir aws/.aws-sam \ + --cache-dir aws/.aws-sam/cache" + +# ================================================================ +# 🧱 sh-libpq: Builds a static libpq + OpenSSL layer inside Alpine +# ================================================================ +# This builds the PostgreSQL C client library (libpq.a), along with +# static OpenSSL and zlib, for linking in musl-based Rust Lambda builds. +# Output is saved in: aws/libpq_layer/{lib/, include/} +sh-libpq: + sh ./build_libpq_layer_docker.sh diff --git a/aws/docker/build_libpq_layer_docker.sh b/aws/docker/build_libpq_layer_docker.sh new file mode 100755 index 0000000..2688104 --- /dev/null +++ b/aws/docker/build_libpq_layer_docker.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -euo pipefail + +# ───────────────────────────────────────────────────────────────────────────── +# Script: build_libpq_layer_docker.sh +# Builds static libpq.a and dependencies inside Alpine container +# ───────────────────────────────────────────────────────────────────────────── + +# Determine the project root dynamically (two levels up from this script) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(realpath "$SCRIPT_DIR/../..")" + +AWS_DIR="$REPO_ROOT/aws" +LAYER_DIR="$AWS_DIR/libpq_layer" +LIB_DIR="${LAYER_DIR}/lib" +INCLUDE_DIR="${LAYER_DIR}/include/libpq" +PG_VERSION=10.23 +PG_TARBALL=postgresql-${PG_VERSION}.tar.gz +PG_SRC_DIR=postgresql-${PG_VERSION} + +# Clean up previous builds +rm -rf "${LAYER_DIR}" "${PG_TARBALL}" "${PG_SRC_DIR}" "$REPO_ROOT/aws/libpq_layer.zip" +mkdir -p "${LIB_DIR}" "${INCLUDE_DIR}" + +echo "πŸ“¦ Starting static build inside Alpine Linux (musl)..." + +docker run --rm \ + -v "$REPO_ROOT/aws":/layerbuild \ + -w /layerbuild \ + alpine:latest sh -c " + set -eux + + apk add --no-cache build-base musl-dev openssl-dev zlib-static wget tar + + echo '⬇️ Downloading PostgreSQL source...' + wget https://ftp.postgresql.org/pub/source/v${PG_VERSION}/${PG_TARBALL} + tar -xzf ${PG_TARBALL} + cd ${PG_SRC_DIR} + + echo 'βš™οΈ Configuring with static build options...' + CFLAGS='-fPIC -O2' ./configure \ + --prefix=/tmp/pg \ + --disable-shared \ + --without-readline \ + --without-zlib \ + --without-gssapi + + echo 'πŸ”¨ Building libpq...' + cd src/interfaces/libpq + make -j\$(nproc) + make install + + echo 'πŸ“ Copying outputs...' + cp /tmp/pg/lib/libpq.a /layerbuild/libpq_layer/lib/ + cp -r /tmp/pg/include/* /layerbuild/libpq_layer/include/libpq/ + + echo 'πŸ“ Copying static OpenSSL and Zlib dependencies...' + cp /usr/lib/libssl.a /layerbuild/libpq_layer/lib/ || echo '⚠️ libssl.a not found' + cp /usr/lib/libcrypto.a /layerbuild/libpq_layer/lib/ || echo '⚠️ libcrypto.a not found' + cp /usr/lib/libz.a /layerbuild/libpq_layer/lib/ || echo '⚠️ libz.a not found' + " + +echo 'πŸ“¦ Zipping the layer for inspection or manual use...' +(cd "$LAYER_DIR" && zip -r "$REPO_ROOT/aws/libpq_layer.zip" .) + +# Clean up source tarballs and directories +rm -rf "${AWS_DIR}/${PG_TARBALL}" "${AWS_DIR}/${PG_SRC_DIR}" + + +echo "βœ… Static libpq layer build complete." +echo "β†’ Library: ${LIB_DIR}/libpq.a" +echo "β†’ Dependencies: ${LIB_DIR}/libssl.a, libcrypto.a, libz.a" +echo "β†’ Headers: ${INCLUDE_DIR}/" diff --git a/env.json b/aws/env.json similarity index 100% rename from env.json rename to aws/env.json diff --git a/events/event.json b/aws/events/event.json similarity index 100% rename from events/event.json rename to aws/events/event.json diff --git a/libpq_layer/include/libpq/libpq-events.h b/aws/libpq_layer/include/libpq/libpq-events.h similarity index 100% rename from libpq_layer/include/libpq/libpq-events.h rename to aws/libpq_layer/include/libpq/libpq-events.h diff --git a/libpq_layer/include/libpq/libpq-fe.h b/aws/libpq_layer/include/libpq/libpq-fe.h similarity index 100% rename from libpq_layer/include/libpq/libpq-fe.h rename to aws/libpq_layer/include/libpq/libpq-fe.h diff --git a/libpq_layer/include/libpq/postgresql/internal/libpq-int.h b/aws/libpq_layer/include/libpq/postgresql/internal/libpq-int.h similarity index 100% rename from libpq_layer/include/libpq/postgresql/internal/libpq-int.h rename to aws/libpq_layer/include/libpq/postgresql/internal/libpq-int.h diff --git a/libpq_layer/include/libpq/postgresql/internal/pqexpbuffer.h b/aws/libpq_layer/include/libpq/postgresql/internal/pqexpbuffer.h similarity index 100% rename from libpq_layer/include/libpq/postgresql/internal/pqexpbuffer.h rename to aws/libpq_layer/include/libpq/postgresql/internal/pqexpbuffer.h diff --git a/aws/libpq_layer/lib/libpq.a b/aws/libpq_layer/lib/libpq.a new file mode 100644 index 0000000..4d3efe0 Binary files /dev/null and b/aws/libpq_layer/lib/libpq.a differ diff --git a/aws/libpq_layer/lib/libz.a b/aws/libpq_layer/lib/libz.a new file mode 100644 index 0000000..e165037 Binary files /dev/null and b/aws/libpq_layer/lib/libz.a differ diff --git a/samconfig.toml b/aws/samconfig.toml similarity index 100% rename from samconfig.toml rename to aws/samconfig.toml diff --git a/aws/template.yaml b/aws/template.yaml new file mode 100644 index 0000000..24c910a --- /dev/null +++ b/aws/template.yaml @@ -0,0 +1,69 @@ +# template.yaml + +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + sam-rust + + Sample AWS SAM template to deploy a Rust-based Lambda function + using Diesel with PostgreSQL. Includes a custom Lambda Layer + for the `libpq` C library, required for Diesel's `postgres` backend. + +Globals: + Function: + Timeout: 30 + MemorySize: 512 + +Resources: + + LibpqLayer: + Type: AWS::Serverless::LayerVersion + Metadata: + BuildMethod: makefile + BuildArchitecture: x86_64 + Properties: + ContentUri: . + Description: PG deps for diesel + CompatibleRuntimes: + - provided.al2 + RetentionPolicy: Delete + + BackendFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: makefile + BuildArchitecture: x86_64 + Properties: + CodeUri: ./ + Handler: bootstrap + Runtime: provided.al2 + Layers: + - !Ref LibpqLayer + Architectures: + - x86_64 + Environment: + Variables: + RUST_LOG: debug + PGSSLMODE: disabled + RUST_BACKTRACE: "1" + DATABASE_URL: postgres://root:password@test-db:5432/test?sslmode=disable + Events: + HelloWorld: + Type: Api + Properties: + Path: /{proxy+} + Method: ANY + +Outputs: + + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + + BackendFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt BackendFunction.Arn + + BackendFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt BackendFunctionRole.Arn diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..6048991 --- /dev/null +++ b/backend/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://root:password@localhost:5001/test?sslmode=disable" diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..0e396ee --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,28 @@ +# rust_app/Cargo.toml + +[package] +name = "backend" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +[features] +default = ["lambda"] +lambda = ["lambda_http", "lambda_runtime"] + +[dependencies] + +tokio = { workspace = true} +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +diesel = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +warp = { workspace = true } +warp_lambda = { workspace = true } +r2d2 = { workspace = true } +once_cell = { workspace = true } +openssl = { workspace = true } + +lambda_http = { workspace = true, optional = true } +lambda_runtime = { workspace = true, optional = true } \ No newline at end of file diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..4ff84cd --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,9 @@ +# Backend-specific Makefile + +build: + @echo "Building Rust backend (server + job_runner)..." + cargo build --release --manifest-path ./Cargo.toml + +run: + @echo "Running Warp server..." + cd ../ && export $$(cat .env | xargs) && RUST_LOG=debug cargo run --manifest-path backend/Cargo.toml --no-default-features diff --git a/backend/build.rs b/backend/build.rs new file mode 100644 index 0000000..4cc0c8f --- /dev/null +++ b/backend/build.rs @@ -0,0 +1,3 @@ +fn main() { + //TODO: Set env var for DATABASE_URL here on build? +} diff --git a/rust_app/src/db.rs b/backend/src/db.rs similarity index 95% rename from rust_app/src/db.rs rename to backend/src/db.rs index 391540f..4fc9aae 100644 --- a/rust_app/src/db.rs +++ b/backend/src/db.rs @@ -10,6 +10,7 @@ pub type DbPool = Pool>; pub static DB_POOL: OnceCell> = OnceCell::new(); pub fn init_diesel_pool() -> Arc { + println!("πŸ” DATABASE_URL: {:?}", std::env::var("DATABASE_URL")); let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { eprintln!("❌ DATABASE_URL is not set"); std::process::exit(1); diff --git a/rust_app/src/lib.rs b/backend/src/lib.rs similarity index 100% rename from rust_app/src/lib.rs rename to backend/src/lib.rs diff --git a/rust_app/src/main.rs b/backend/src/main.rs similarity index 53% rename from rust_app/src/main.rs rename to backend/src/main.rs index 9ce2cb3..d639142 100644 --- a/rust_app/src/main.rs +++ b/backend/src/main.rs @@ -1,7 +1,7 @@ // rust_app/src/main.rs -use sam_rust::{db_healthcheck_handler, init_diesel_pool}; -use tracing::{debug, error, info}; +use backend::{db_healthcheck_handler, init_diesel_pool}; +use tracing::{error, info}; use warp::Filter; #[tokio::main] @@ -21,25 +21,23 @@ async fn main() { info!("πŸš€ Starting warp_lambda runtime..."); - for var in &[ - "LD_LIBRARY_PATH", - "PQ_LIB_DIR", - "PQ_INCLUDE_DIR", - "PGSSLMODE", - ] { - match std::env::var(var) { - Ok(value) => info!("πŸ”§ {} = {}", var, value), - Err(_) => debug!("⚠️ {} is not set", var), - } - } - init_diesel_pool(); - let routes = warp::path!("Prod" / "hello") + let all_routes = warp::path!("Prod" / "hello") .and(warp::get()) .and_then(db_healthcheck_handler); - warp_lambda::run(warp::service(routes)) - .await - .expect("Failed to start warp_lambda runtime"); + #[cfg(feature = "lambda")] + { + let warp_service = warp::service(all_routes); + warp_lambda::run(warp_service) + .await + .expect("An error occurred"); + } + + #[cfg(not(feature = "lambda"))] + { + info!("Running as a local Warp server..."); + warp::serve(all_routes).run(([0, 0, 0, 0], 3000)).await; + } } diff --git a/rust_app/tests/healthcheck.rs b/backend/tests/healthcheck.rs similarity index 93% rename from rust_app/tests/healthcheck.rs rename to backend/tests/healthcheck.rs index cd922a0..62c5ef1 100644 --- a/rust_app/tests/healthcheck.rs +++ b/backend/tests/healthcheck.rs @@ -1,7 +1,7 @@ use warp::http::StatusCode; use warp::Filter; -use sam_rust::{db_healthcheck_handler, init_diesel_pool}; +use backend::{db_healthcheck_handler, init_diesel_pool}; #[tokio::test] async fn test_hello_db_healthcheck() { diff --git a/build_libpq_layer_docker.sh b/build_libpq_layer_docker.sh deleted file mode 100755 index 5324091..0000000 --- a/build_libpq_layer_docker.sh +++ /dev/null @@ -1,65 +0,0 @@ -# build_libpq_layer_docker.sh - -#!/bin/bash -set -e - -# ───────────────────────────────────────────────────────────────────────────── -# Script: build_libpq_layer_docker.sh -# -# Builds the PostgreSQL `libpq.so` shared library and headers inside an -# Amazon Linux 2 container (Lambda-compatible). Outputs them to a `libpq_layer` -# directory structure compatible with AWS Lambda Layers. -# -# The resulting files will be used in: -# - Lambda Layer at runtime (/opt/lib/libpq.so) -# - Diesel's `pq-sys` crate at build time via PQ_LIB_DIR/PQ_INCLUDE_DIR -# ───────────────────────────────────────────────────────────────────────────── - -LAYER_DIR=libpq_layer -LIB_DIR=${LAYER_DIR}/lib -INCLUDE_DIR=${LAYER_DIR}/include/libpq -PG_VERSION=10.23 -PG_TARBALL=postgresql-${PG_VERSION}.tar.gz -PG_SRC_DIR=postgresql-${PG_VERSION} - -# Clean up previous builds -rm -rf ${LAYER_DIR} ${PG_TARBALL} ${PG_SRC_DIR} libpq_layer.zip -mkdir -p ${LIB_DIR} ${INCLUDE_DIR} - -docker run --rm \ - -v "$PWD":/layerbuild \ - -w /layerbuild \ - amazonlinux:2 bash -c " - # Install build dependencies - yum install -y gcc make tar gzip wget readline-devel zlib-devel openssl-devel zip && - - # Download and extract PostgreSQL source - wget https://ftp.postgresql.org/pub/source/v${PG_VERSION}/${PG_TARBALL} && - tar -xzf ${PG_TARBALL} && - - # Configure and build only the libpq client library - cd ${PG_SRC_DIR} && - ./configure --prefix=/tmp/pg --without-readline --without-zlib && - cd src/interfaces/libpq && - make && - make install && - - # Copy output artifacts to host-mounted build directory - cp /tmp/pg/lib/libpq.so* /layerbuild/${LIB_DIR}/ && - cp -r /tmp/pg/include/* /layerbuild/${INCLUDE_DIR}/ -" - -# Create symlink: libpq.so β†’ libpq.so.5 -echo 'πŸ”— Creating .so symlink...' -cd ${LIB_DIR} -ln -sf libpq.so.5 libpq.so -cd - > /dev/null - -# Optional: zip the layer for manual upload (not used by SAM builds) -echo 'πŸ“¦ Zipping the layer...' -(cd ${LAYER_DIR} && zip -r ../libpq_layer.zip .) - -# Clean up downloaded and extracted PostgreSQL files -rm -rf ${PG_TARBALL} ${PG_SRC_DIR} - -echo 'βœ… Layer build complete: `libpq_layer` directory is ready' diff --git a/docs/index.md b/docs/index.md index 597065d..849f82d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,83 +1,56 @@ --- + title: "Build a Serverless Rust API with Warp, Diesel, and AWS Lambda" -description: "How to deploy and run production-grade Rust APIs on AWS Lambda with PostgreSQL using Warp, Diesel, and SAM. Complete guide with CI/CD, Docker, and local testing." ---- +description: "How to deploy and run production-grade Rust APIs on AWS Lambda with PostgreSQL using Warp, Diesel, and SAM. Now featuring custom Docker builds, static linking, and full local dev workflows." +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ # Build a Serverless Rust API with Warp, Diesel, and AWS Lambda [![GitHub stars](https://img.shields.io/github/stars/apjames93/warp-lambda-starter?style=social)](https://github.com/apjames93/warp-lambda-starter) - -> Run production-grade Rust APIs on AWS Lambda with PostgreSQL using Warp, Diesel, and AWS SAMβ€”with full CI/CD and local testing support. - - -# Serverless Rust on AWS Lambda with Warp + Diesel - -Learn how to build a serverless Rust API with Warp, Diesel, and AWS Lambda β€” complete with PostgreSQL support, CI/CD, Docker, and AWS SAM. This guide walks you through deploying a production-ready, async Rust backend that runs serverlessly on Lambda while connecting to a Postgres database using Diesel. Until recently, writing performant, native serverless APIs in Rust required heavy lifting. With the right tools, you can now deploy production-ready, async Rust backends to AWS Lambdaβ€”with PostgreSQL access and blazing-fast HTTP routing. - -In this guide, we'll walk through a complete implementation of a serverless Rust backend using: - -- **[Warp](https://github.com/seanmonstar/warp)**: a high-performance async HTTP server -- **[Diesel](https://diesel.rs)**: a safe, reliable ORM for PostgreSQL -- **[warp_lambda](https://crates.io/crates/warp_lambda)**: to adapt Warp to AWS Lambda -- **[AWS SAM](https://docs.aws.amazon.com/serverless-application-model/)**: to define and deploy your serverless stack -- **A custom `libpq` Lambda Layer**: to link Diesel with PostgreSQL in the Lambda environment - -By the end, you’ll have a fully operational Rust API running on AWS Lambda, backed by PostgreSQL, with everything tested locally and deployable to the cloud. +> A production-grade setup to deploy Rust APIs to AWS Lambda using Warp, Diesel, Docker, and AWS SAM. --- -## πŸ’‘ Why This Stack? +## πŸš€ Overview -Rust’s safety and performance make it ideal for backend APIs. But Diesel (like many native crates) depends on C libraries like `libpq`, which aren't available by default in Lambda. This setup bridges that gap: +This guide covers: -- Warp gives you a clean and async API layer -- Diesel handles safe and performant SQL access -- AWS SAM + Lambda offers serverless scale without server ops -- A Docker-built `libpq` layer satisfies native runtime/linking needs - -Let’s build it. +* **Warp**: Fast async web server +* **Diesel**: Battle-tested ORM for PostgreSQL +* **warp\_lambda**: Runs Warp on AWS Lambda +* **AWS SAM**: Infrastructure-as-code for Lambda + API Gateway +* **Custom Docker builds**: For cross-compilation and static linking +* **Lambda Layer**: Ships runtime C deps like `libpq` --- -## πŸ“† Getting Started +## πŸ› οΈ Prerequisites -### Prerequisites -Make sure the following tools are installed: +Install these tools: ```bash brew install rustup awscli docker aws-sam-cli -cargo install cargo-lambda +cargo install cargo-watch # for hot reloads +brew install act # optional: test CI locally ``` -Optional but recommended: -```bash -brew install act -``` - -> `act` lets you test GitHub Actions locally - --- -## πŸ› οΈ Clone the Starter Project - -Everything in this guide is available in the open-source GitHub repo: +## πŸ“¦ Clone the Starter ```bash git clone https://github.com/apjames93/warp-lambda-starter.git cd warp-lambda-starter ``` -This starter includes: +You'll get: -- A working Warp + Diesel Rust app -- Custom `libpq` Lambda Layer for PostgreSQL support -- `Makefile` for local builds, testing, and deployment -- GitHub Actions CI pipeline -- AWS SAM config for infrastructure as code -- Local Postgres with Docker - -πŸ‘‰ [View the repo on GitHub](https://github.com/apjames93/warp-lambda-starter) +* Warp + Diesel backend +* Docker-based builds +* AWS SAM template + Lambda Layer +* CI/CD via GitHub Actions +* Local dev with hot reloads --- @@ -85,308 +58,238 @@ This starter includes: ```text . -β”œβ”€β”€ rust_app/ # Rust source code (Cargo.toml, main.rs, modules) -β”œβ”€β”€ libpq_layer/ # Compiled libpq binaries and headers -β”œβ”€β”€ build_libpq_layer_docker.sh # Dockerized script to compile libpq -β”œβ”€β”€ docker-compose.yaml # Local Postgres (with pgvector) -β”œβ”€β”€ Makefile # Commands: build, lint, test, deploy -β”œβ”€β”€ template.yaml # AWS SAM function & layer definition -β”œβ”€β”€ .github/workflows/ci.yml # Continuous integration tests & format checks +β”œβ”€β”€ backend/ # Rust source +β”œβ”€β”€ aws/ # SAM template, Docker builds +β”œβ”€β”€ aws/libpq_layer/ # Prebuilt libpq + OpenSSL +β”œβ”€β”€ docker-compose.yaml # Local Postgres +β”œβ”€β”€ Makefile # Build/run shortcuts +└── .github/workflows/ # CI pipeline ``` --- -## πŸ“ Step-by-Step Implementation - -Before diving in, it's helpful to understand how the included `Makefile` abstracts away some of the complexity of SAM buildsβ€”particularly with native dependencies like `libpq`. - -### 1. Build the PostgreSQL Lambda Layer +## 🐘 Local Postgres with Docker Compose -Diesel requires `libpq` (the C Postgres client library). We'll compile it into a Lambda-compatible format: +Run PostgreSQL (pgvector-enabled) locally: ```bash -./build_libpq_layer_docker.sh +docker-compose up ``` -> This uses an Amazon Linux 2 Docker image to match the Lambda runtime and produces `.so` and header files in `libpq_layer/`. +Access it via: -#### πŸ” What’s Going on with `LibpqLayer` - -To support Diesel's `postgres` backend, we must compile the native `libpq` library into a Lambda-compatible shared object and make it available to the Lambda function. This is done through a custom Lambda Layer defined in `template.yaml`: - -```yaml -LibpqLayer: - Type: AWS::Serverless::LayerVersion - Metadata: - BuildMethod: makefile - BuildArchitecture: x86_64 - Properties: - ContentUri: . - Description: PG deps for diesel - CompatibleRuntimes: - - provided.al2 - RetentionPolicy: Delete ``` -This layer is built using the Makefile’s `build-LibpqLayer` target, which copies the compiled `.so` and header files into `.aws-sam/build/LibpqLayer/opt/lib` and `opt/include/libpq`, making them available to your Lambda function at runtime. - -We mount this layer in our main function using: - -```yaml -Layers: - - !Ref LibpqLayer +postgres://root:password@localhost:5001/test ``` -And configure the required environment variables so that both `pq-sys` (at compile time) and Lambda (at runtime) can locate and link `libpq.so`: +Inside SAM (Docker network `sam-local`): -```yaml -Environment: - Variables: - PQ_LIB_DIR: /opt/lib - PQ_INCLUDE_DIR: /opt/include/libpq - LD_LIBRARY_PATH: /opt/lib +```json +"DATABASE_URL": "postgres://root:password@test-db:5432/test?sslmode=disable" ``` -At build time, `pq-sys` uses `PQ_LIB_DIR` and `PQ_INCLUDE_DIR`. At runtime, `LD_LIBRARY_PATH` ensures `libpq.so` can be dynamically loaded. +--- -Also note: our `Cargo.toml` includes the following to map the SAM config: +## πŸ§ͺ Build the libpq Lambda Layer -```toml -[package] -default-run = "bootstrap" +> **Quick start:** The `aws/libpq_layer/` directory is pre-committed so you can start building and deploying immediately. +> +> **Advanced:** If you want to regenerate the layer (e.g. for a different PostgreSQL version, security updates, or reduced size), run: -[[bin]] -name = "bootstrap" -path = "src/main.rs" +```bash +make aws-docker-sh-libpq ``` -This aligns with `cargo.toml`: - -```yaml -artifact_executable_name: bootstrap -Handler: bootstrap -``` +This command: -#### πŸ”§ What's in `build_libpq_layer_docker.sh` +* Builds `libpq.a`, `libssl.a`, and headers using Alpine + musl +* Outputs everything to `aws/libpq_layer/{lib, include}` +* Packages the layer as `libpq_layer.zip` for AWS Lambda use -The `build_libpq_layer_docker.sh` script automates the creation of this layer. Here's what it does: +You’re free to customize or rebuild the layer anytimeβ€”just modify `aws/docker/build_libpq_layer_docker.sh`. -1. Launches an `amazonlinux:2` Docker container -2. Installs build dependencies (`gcc`, `make`, `openssl-devel`, etc.) -3. Downloads PostgreSQL source (v10.23) -4. Builds just the `libpq` client library -5. Copies the resulting `.so` and headers into `libpq_layer/lib` and `libpq_layer/include/libpq` -6. Creates a symlink for `libpq.so` -7. Optionally zips the result for manual upload. AWS SAM will handle this when we deploy +Rebuild if: -This ensures that everything inside the `libpq_layer` folder is Lambda-compatible and can be reused across builds. +* You need a newer PostgreSQL version +* You want smaller artifacts +* You hit Lambda runtime linking errors --- -## 2. Start a Local Postgres DB (with pgvector) +## 🧰 Build the Statically Linked Rust Binary -This runs a local Postgres instance, ideal for dev + testing: +Compile for Lambda using a dedicated Dockerfile: ```bash -docker-compose up -d +make aws-build-sam ``` -Accessible at: +* Uses `musl` toolchain +* Statically links `libpq`, `libssl`, `zlib` +* Outputs `bootstrap` binary +* Runs `sam build` to package it -``` -postgres://root:password@test-db:5001/test +Set in Docker: + +```bash +RUSTFLAGS="-L /aws/libpq_layer/lib \ + -C link-arg=-lpq -C link-arg=-lssl -C link-arg=-lcrypto \ + -C link-arg=-lz -C link-arg=-static" ``` --- -## 3. Build the Rust Lambda Binary +## πŸ”§ Local Dev vs Lambda Mode -```bash -make sam-build +Your `main.rs` supports both: + +```rust +#[cfg(feature = "lambda")] +warp_lambda::run(service).await?; + +#[cfg(not(feature = "lambda"))] +warp::serve(routes).run(([0, 0, 0, 0], 3000)).await; ``` -What it does: +In `Cargo.toml`: -- **Sets environment variables** like `PQ_LIB_DIR` and `PQ_INCLUDE_DIR` so that Diesel’s `pq-sys` crate knows where to find the native PostgreSQL client libraries and headers -- **Adds a custom `RUSTFLAGS` setting** to embed a runtime linker path (`-rpath=/opt/lib`) that ensures Lambda can locate `libpq.so` during execution -- **Runs `sam build --beta-features`**, which compiles the Rust Lambda using `cargo lambda` and integrates the `libpq` layer into the build output structure -- **Fails early** if `libpq_layer/lib/libpq.so` is missing, prompting you to run `make sh-libpq` to generate the layer +```toml +[features] +default = ["lambda"] +lambda = ["lambda_http", "lambda_runtime"] +``` + +So you can: -This enables a seamless local and remote build experience, whether running locally via Docker or deploying to AWS. +* Run locally with `cargo run` +* Deploy with `make aws-build-sam` --- -## 4. Run Locally via SAM -```bash -make sam-run -``` +If you want to clean up your stack after testing or deployment: -Test your endpoint: +`make aws-delete-sam` -``` -http://localhost:3000/Prod/hello -``` +This deletes the deployed stack using sam delete --no-prompts -You'll see: +--- -```json -{ "message": "Hello World with DB!" } +## πŸ”— Why the Lambda Layer Is Still Needed + +Even with static linking, Lambda may expect `.so` files at runtime: + +* Diesel sometimes loads symbols dynamically +* Musl quirks can cause fallback dynamic linking + +To ensure compatibility, we attach the same static artifacts as a Lambda Layer: + +```yaml +Layers: + - !Ref LibpqLayer +Environment: + Variables: + LD_LIBRARY_PATH: /opt/lib + PQ_LIB_DIR: /opt/lib + PQ_INCLUDE_DIR: /opt/include/libpq ``` +🧠 **Lightbulb moment**: Build-time and runtime use the exact same files. No duplication. No surprises. + --- -## πŸ” Inside the Rust App +## πŸ”„ Local Dev with Hot Reloads -### `main.rs` -Initializes the logger, logs environment setup, runs `init_diesel_pool()`, and starts the Warp server with Lambda support: +Run the API locally with: -```rust -#[tokio::main] -async fn main() { - // Setup tracing - // Set panic hook - // Log env vars like PQ_LIB_DIR - init_diesel_pool(); - - let routes = warp::path!("Prod" / "hello") - .and(warp::get()) - .and_then(db_healthcheck_handler); - - warp_lambda::run(warp::service(routes)).await.expect("Failed to start"); -} +```bash +make run-backend ``` -### `lib.rs` -Exports core logic: +This: -```rust -pub mod db; +* Starts Warp on `localhost:3000` +* Watches files via `cargo-watch` -pub use db::{get_db_conn, init_diesel_pool, run_diesel_query}; -``` +Great for development without SAM overhead. -Also contains `db_healthcheck_handler()`: +--- -```rust -pub async fn db_healthcheck_handler() -> Result { - let result = timeout(Duration::from_secs(10), async { - run_diesel_query(|conn| sql_query("SELECT 1").execute(conn).map(|_| ())).await - }) - .await; - // ... more code -} -``` +## 🌐 Test Locally with SAM -### `db.rs` -Configures a global `PgPool` with `once_cell` and `r2d2`: +Test your Lambda function locally via Docker: -```rust -pub fn init_diesel_pool() { - let manager = ConnectionManager::::new(db_url); - let pool = r2d2::Pool::builder().build(manager).unwrap(); - POOL.set(pool).unwrap(); -} +```bash +make aws-run-sam ``` -### `healthcheck.rs` -Contains `test_hello_db_healthcheck`: +Visit: -```rust -#[tokio::test] -async fn test_hello_db_healthcheck() { - let _ = std::panic::catch_unwind(|| init_diesel_pool()); - - let api = warp::path!("Prod" / "hello") - .and(warp::get()) - .and_then(db_healthcheck_handler); - - let res = warp::test::request() - .method("GET") - .path("/Prod/hello") - .reply(&api) - .await; - - assert_eq!(res.status(), StatusCode::OK); -} +``` +http://localhost:4040/Prod/hello ``` --- -## πŸ§ͺ CI: Format, Lint, Test +## ☁️ Deploy to AWS -Your `Makefile` supports: +Deploy your app with: ```bash -make format # rustfmt check -make lint # clippy -make test # run unit + integration tests +make aws-deploy-sam ``` -### GitHub Actions -Our `.github/workflows/ci.yml` GitHub Actions workflow runs automatically on push and pull requests to `main`. It: - -- Spins up a Postgres 14 service -- Installs system dependencies (e.g., OpenSSL headers) -- Runs format and lint checks via `make pretty` -- Runs tests against a live database -- Installs and validates the AWS SAM CLI -- Caches dependencies and toolchains (Cargo, Zig) -- Builds the Lambda function using `make sam-build` +This runs `sam deploy` and provisions: -This provides confidence the project works before deploying, with consistent feedback during development. +* Lambda function (with `Handler: bootstrap`) +* API Gateway endpoint +* Attached Lambda Layer --- -## πŸ“¦ Dependencies Overview +## πŸ”„ CI/CD Pipeline -```toml -[dependencies] -tokio = { version = "1", features = ["macros"] } -diesel = { version = "2.2.7", features = ["postgres", "r2d2"] } -warp = "0.3" -warp_lambda = "0.1.4" -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -openssl = { version = "0.10", features = ["vendored"] } -r2d2 = "0.8" -once_cell = "1" -``` +GitHub Actions CI runs on push to `main`: ---- +* Builds with `make aws-build-sam` +* Runs `make aws-deploy-sam` +* Validates SAM templates -## πŸš€ Deploying to AWS +Test locally with: ```bash -sam deploy --guided -``` - -Configure your stack, region, and IAM roles. Once complete, your endpoint will look like: - -``` -https://.amazonaws.com/Prod/hello +act push \ + -W .github/workflows/ci.yaml \ + --secret-file .env \ + -P ubuntu-22.04=catthehacker/ubuntu:act-22.04 ``` --- -## βœ… Final Thoughts +## βœ… Summary -This pattern unlocks scalable, cost-effective Rust APIs with: +* ⌨️ Local dev with hot reloads using `cargo-watch` +* 🐳 Cross-compiled builds with musl in Docker +* πŸ“¦ Precompiled `libpq` layer for Diesel and OpenSSL +* πŸ” Shared artifacts for build and runtime environments +* πŸš€ Seamless deployment to AWS Lambda via SAM -- No infrastructure to manage -- Native PostgreSQL support via Diesel -- High performance from Rust and Warp -- Full local dev/test + CI workflow +**Why this setup works:** -If this helped, consider sharing it or contributing. Happy shipping πŸ¦€πŸ’¨ +* **πŸ” Reuse is power** – the same `.a` and `.h` files power both phases +* **πŸ¦€ Native performance** – statically linked with `musl`, optimized for cold starts +* **🌐 Local-first DX** – develop like a normal Warp app, deploy serverlessly --- ---- +## 🧭 Next Steps (Coming Soon) -## πŸ™Œ Like this project? +* Set up full AWS infrastructure via CloudFormation (VPC, RDS, subnets, security groups) +* Add RDS Proxy for pooled DB connections from Lambda +* Support custom domains via API Gateway and Certbot in SAM -If you found this guide helpful, please consider [⭐️ starring the repo on GitHub](https://github.com/apjames93/warp-lambda-starter) or [sharing it with your team](https://github.com/apjames93/warp-lambda-starter). Every star helps! +--- -[![GitHub stars](https://img.shields.io/github/stars/apjames93/warp-lambda-starter?style=social)](https://github.com/apjames93/warp-lambda-starter) +πŸ¦€ A solid foundation for production-ready, serverless Rust APIs. + +**Happy building!** diff --git a/libpq_layer.zip b/libpq_layer.zip deleted file mode 100644 index cc3c8bd..0000000 Binary files a/libpq_layer.zip and /dev/null differ diff --git a/libpq_layer/lib/libpq.so b/libpq_layer/lib/libpq.so deleted file mode 120000 index 740e522..0000000 --- a/libpq_layer/lib/libpq.so +++ /dev/null @@ -1 +0,0 @@ -libpq.so.5 \ No newline at end of file diff --git a/libpq_layer/lib/libpq.so.5 b/libpq_layer/lib/libpq.so.5 deleted file mode 100755 index 2d4c579..0000000 Binary files a/libpq_layer/lib/libpq.so.5 and /dev/null differ diff --git a/libpq_layer/lib/libpq.so.5.10 b/libpq_layer/lib/libpq.so.5.10 deleted file mode 100755 index 2d4c579..0000000 Binary files a/libpq_layer/lib/libpq.so.5.10 and /dev/null differ diff --git a/rust_app/Cargo.toml b/rust_app/Cargo.toml deleted file mode 100644 index 4aa1fac..0000000 --- a/rust_app/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -# rust_app/Cargo.toml - -[package] -name = "sam_rust" -version = "0.1.0" -edition = "2021" -default-run = "bootstrap" # maps to the template.yaml artifact_executable_name: bootstrap - -[[bin]] -name = "bootstrap" # maps to the template.yaml artifact_executable_name: bootstrap -path = "src/main.rs" - - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] - -tokio = { version = "1", features = ["macros"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } - -diesel = { version = "2.2.7", features = ["postgres", "r2d2"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -warp = "0.3" -warp_lambda = "0.1.4" - -openssl = { version = "0.10", features = ["vendored"] } -r2d2 = "0.8" -once_cell = "1.21.3" - -[dev-dependencies] -warp = "0.3" -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/template.yaml b/template.yaml deleted file mode 100644 index b954090..0000000 --- a/template.yaml +++ /dev/null @@ -1,110 +0,0 @@ -# template.yaml - -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Description: > - sam-rust - - Sample AWS SAM template to deploy a Rust-based Lambda function - using Diesel with PostgreSQL. Includes a custom Lambda Layer - for the `libpq` C library, required for Diesel's `postgres` backend. - -Globals: - Function: - Timeout: 30 - MemorySize: 512 - -Resources: - - # ───────────────────────────────────────────────────────────────────────────── - # LibpqLayer - # - # This defines a custom Lambda Layer that packages the `libpq.so` shared - # library and header files needed by Diesel's `postgres` backend. The layer - # is built using a `makefile` build method, and AWS SAM automatically - # runs the `build-LibpqLayer` target from the Makefile to generate the - # correct file structure inside the `.aws-sam/build/LibpqLayer/opt/` directory. - # - # These files become accessible to Lambda functions at `/opt/lib` and - # `/opt/include/libpq`, and the function must set corresponding environment - # variables to find them at runtime and during build (for `pq-sys`). - # ───────────────────────────────────────────────────────────────────────────── - LibpqLayer: - Type: AWS::Serverless::LayerVersion - Metadata: - BuildMethod: makefile # Custom Makefile defines how to build the layer - BuildArchitecture: x86_64 # Build architecture for compatibility - Properties: - ContentUri: . # Root of the repo is passed to `make build-LibpqLayer` - Description: PG deps for diesel # Human-friendly layer description - CompatibleRuntimes: - - provided.al2 # Lambda runtime for Rust with Cargo Lambda - RetentionPolicy: Delete # Clean up layer when deleted - - # ───────────────────────────────────────────────────────────────────────────── - # HelloWorldFunction - # - # This is the main Lambda function built using `cargo lambda build`. - # It depends on Diesel with the `postgres` backend, and uses the `LibpqLayer` - # to provide the `libpq` dynamic library at runtime. - # - # Environment variables are used during both build and runtime to configure - # linking and dynamic library loading (`LD_LIBRARY_PATH`, `PQ_LIB_DIR`, etc). - # The function is triggered by API Gateway with a catch-all `{proxy+}` route. - # ───────────────────────────────────────────────────────────────────────────── - HelloWorldFunction: - Type: AWS::Serverless::Function - Metadata: - BuildMethod: rust-cargolambda # Uses Cargo Lambda to compile Rust code - BuildProperties: - artifact_executable_name: bootstrap # This aligns with `cargo.toml` [[bin]] name = "bootstrap" and [package] default-run = "bootstrap": - binary: bootstrap # This aligns with `cargo.toml` [[bin]] name = "bootstrap" and [package] default-run = "bootstrap": - - Properties: - CodeUri: ./rust_app # Points to the Cargo.toml directory - Handler: bootstrap # Executable generated by Cargo Lambda - Runtime: provided.al2 # Rust custom runtime for Lambda - Layers: - - !Ref LibpqLayer # Mounts our custom libpq layer under /opt - Architectures: - - x86_64 - Environment: - Variables: - # These paths match where the Lambda layer will be mounted. - # They are used by `pq-sys` (build-time) and by the binary at runtime. - PQ_LIB_DIR: /opt/lib # libpq.so search path (used by `pq-sys`) - PQ_INCLUDE_DIR: /opt/include/libpq # Header path (used by `pq-sys`) - LD_LIBRARY_PATH: /opt/lib # Required for dynamic linking at runtime - - # Other useful runtime config - RUST_LOG: debug - PGSSLMODE: disabled - RUST_BACKTRACE: "1" - - # Local or Docker test DB connection string - DATABASE_URL: postgres://root:password@test-db:5432/test?sslmode=disable - - Events: - HelloWorld: - Type: Api # API Gateway trigger - Properties: - Path: /{proxy+} # Catch-all route for any method/path - Method: ANY - -Outputs: - - # ───────────────────────────────────────────────────────────────────────────── - # Outputs for convenience when deploying - # ───────────────────────────────────────────────────────────────────────────── - - HelloWorldApi: - Description: "API Gateway endpoint URL for Prod stage for Hello World function" - Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" - - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldFunctionIamRole: - Description: "Implicit IAM Role created for Hello World function" - Value: !GetAtt HelloWorldFunctionRole.Arn