Using a Makefile for Builds
Automating your build process for speed and consistency.
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
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.
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:
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:
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?
- Speed:
cargo-lambdais extremely fast and handles caching better than some containerized builds. - Control: You can add
cargo testorcargo fmtas prerequisites to your build. - Consistency: You use the exact same build command locally that SAM uses during deployment.
- Custom Flags: You can easily add flags like
--featuresor 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?