TutorialintermediatePart 17 of 18

Using a Makefile for Builds

Automating your build process for speed and consistency.

April 28, 20263 min read

What you'll learn

  • Why standard sam build can be limiting
  • How to use the 'makefile' build method in SAM
  • Creating a powerful Makefile for Rust Lambdas
Prerequisites:Previous post completed (Error Handling the Rust Way)

As your project grows from one function to ten, sam build starts to feel slow and a bit too automated. Sometimes you want more control-like running tests before a build or stripping symbols from the binary to keep it lean.

This is where a Makefile comes in. AWS SAM actually has a built-in feature that lets you offload the entire build process to a Makefile.

Updating template.yaml

First, we need to tell SAM to stop using its default Rust builder and use our Makefile instead.

template.yaml
yaml
  GreetingManager:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: makefile
    Properties:
      CodeUri: .
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures: ["arm64"]

By setting BuildMethod: makefile, SAM will look for a file named Makefile in the CodeUri directory and try to run a target named build-GreetingManager (it uses the resource name).

Creating the Makefile

Create a file named Makefile in your root directory. Here is a simplified version of what we use in production:

Makefile
makefile
RUST_TARGET=aarch64-unknown-linux-gnu
CARGO_OPTS=--release --target $(RUST_TARGET)
 
# This target is what SAM calls
build-GreetingManager:
	@echo "Building GreetingManager..."
	cargo lambda build $(CARGO_OPTS) --bin GreetingManager
	mkdir -p $(ARTIFACTS_DIR)
	cp target/lambda/GreetingManager/bootstrap $(ARTIFACTS_DIR)/

What is $(ARTIFACTS_DIR)?

This is a special environment variable passed by SAM. It points to where SAM expects the final binary to be. Our job in the Makefile is to build the code and copy the bootstrap file into that directory.

Auto-discovering binaries

If you have many functions, you don't want to write a new target for every one. You can use Makefile magic to auto-discover your binaries:

Makefile
makefile
BINARIES := $(patsubst src/bin/%.rs,%,$(wildcard src/bin/*.rs))
 
# General build target
build:
	@$(foreach bin,$(BINARIES), cargo lambda build --release --target aarch64-unknown-linux-gnu --bin $(bin);)
 
# SAM-specific targets
build-%:
	cargo lambda build --release --target aarch64-unknown-linux-gnu --bin $*
	mkdir -p $(ARTIFACTS_DIR)
	cp target/lambda/$*/bootstrap $(ARTIFACTS_DIR)/

With this setup, if you add a new resource in template.yaml named UserManager, SAM will call build-UserManager, which matches our build-% pattern, and it just works!

Why use this approach?

  1. Speed: cargo-lambda is extremely fast and handles caching better than some containerized builds.
  2. Control: You can add cargo test or cargo fmt as prerequisites to your build.
  3. Consistency: You use the exact same build command locally that SAM uses during deployment.
  4. Custom Flags: You can easily add flags like --features or specific optimization levels.

Warning

When using BuildMethod: makefile, you are responsible for ensuring your local machine has the right cross-compilation tools (like zig or a cross-linker) installed. cargo-lambda handles most of this for you!

We've mastered the tools and the code. Now, for the final post in the series, we're going to step back and look at the big picture: when should you use one Lambda per endpoint versus one Lambda for everything?