Macro Rust Service Deployment Patterns: CI-Driven Microservices with Nix, Crane, and Pulumi
Macro's Rust services deploy through a consistent, Nix-based CI pipeline that uses crane for Dockerized binaries, cargo-zigbuild for Lambda handlers, and Pulumi for infrastructure orchestration.
The macro-inc/macro codebase implements a service-agnostic deployment architecture designed for reproducibility and speed. Every Rust microservice—whether a long-lived binary or an event-driven Lambda—follows the same automated workflow from build to production. This article breaks down the deployment patterns driving Macro's Rust services, with direct references to source implementation files.
Binary Builds with Crane and Nix
Services running as persistent processes—such as document-storage-service and search_service—are compiled to native binaries inside a hermetic Nix environment. The crane library handles the build, producing slim Docker images containing only the compiled binary and its runtime dependencies.
In tooling/xtask/crates/xtask_workflows/src/workflows/reusable_deploy_service.rs, the binary build job chains several steps:
// tooling/xtask/crates/xtask_workflows/src/workflows/reusable_deploy_service.rs
Job::new("build-binaries")
.add_step(build_with_crane())
.add_step(package_docker_image())
.add_output("image-tarball", "result/image.tar.gz")
This approach eliminates "works on my machine" problems by pinning the entire toolchain—Rust compiler, system libraries, and build utilities—in the Nix flake.
Lambda Builds with cargo-zigbuild
Event-driven handlers like upload_extractor_lambda_handler and docx_unzip_handler require a different build path. Macro uses cargo-zigbuild to cross-compile to AWS-compatible LLVM-IR without needing a full target toolchain installation.
The .github/scripts/build-cloud-storage-lambdas-nix.sh script orchestrates this:
#!/usr/bin/env bash
set -euo pipefail
nix build .#lambdaDeployCargoArtifacts
cp -L result/lambda-artifacts.tar.gz lambda-artifacts.tar.gz
The resulting lambda-artifacts.tar.gz contains the compiled artifacts ready for Lambda deployment. This build path is defined in reusable_deploy_service.rs as a discrete job type with its own caching strategy.
Artifact Hand-off via nsc
Both build paths converge on a standardized artifact hand-off mechanism. The CI job computes a SHA checksum of the output, then uses the nsc artifact command to register it for downstream consumption.
This pattern allows the build stage to remain completely decoupled from deployment. The nsc tool manages artifact versioning and availability across job boundaries in GitHub Actions.
Service Matrix Generation from JSON Config
Macro avoids rebuilding the entire workspace on every PR. The .github/services-config.json file declares which services have binaries (deploy_binaries) versus Lambdas (deploy_lambdas):
{
"services": {
"search_service": {
"deploy_binaries": ["search-service"],
"deploy_lambdas": []
},
"upload_extractor_lambda_handler": {
"deploy_binaries": [],
"deploy_lambdas": ["upload-extractor"]
}
}
}
The workflow parses this with jq to generate dynamic job matrices:
# .github/workflows/reusable_deploy_service.yml
- name: Generate service matrix
id: matrix
run: |
cfg=.github/services-config.json
lambdas=$(jq -c '[.services | to_entries[] | select((.value.deploy_lambdas // []) | length > 0) | .key]' "$cfg")
echo "lambdas=$lambdas" >> "$GITHUB_OUTPUT"
Only affected services enter the build queue, minimizing CI time and resource usage.
Warm-Lambda Dependency Caching
Because cargo-zigbuild compiles from scratch for the x86_64-unknown-linux-musl target, dependency resolution is expensive. Macro solves this with a dedicated "warm-lambdas" job that pre-builds shared dependencies on a sticky Nix store.
Located in tooling/xtask/crates/xtask_workflows/src/workflows/deploy_all_services.rs, this job runs before any Lambda build:
- Establishes a warm Nix store with common crates cached
- Subsequent Lambda builds mount this store, reusing compiled dependencies
- Reduces typical Lambda build times from minutes to seconds
The cache invalidates on Cargo.lock changes or explicit cache-bust triggers.
Pulumi Preview and Apply Stages
After artifacts are built and handed off, infrastructure changes proceed through two-phase Pulumi execution:
Preview stage (validation):
- uses: ./.github/actions/preview-cloud-storage-pulumi
with:
stack: prod
service: ${{ matrix.service }}
Apply stage (deployment):
- Updates Lambda function versions with new artifact ARNs
- Refreshes ECS task definitions for binary services
- Pushes new Docker images to the container registry
The preview step acts as a safety gate—failed previews block deployment. This logic resides in .github/actions/deploy-cloud-storage-pulumi/action.yml.
GitHub Actions Reusable Workflows
All deployment logic is encapsulated in composable GitHub Actions workflows:
| Workflow | Purpose |
|---|---|
reusable_deploy_service.yml |
Core build-deploy sequence for any service |
deploy_on_push.yml |
Triggered on merge to main; determines affected services |
deploy_all_services.yml |
Full workspace deployment with warm-lambda preparation |
These workflows expose typed inputs (service, environment, dry-run) enabling the same CI definition to service any workspace member.
Local Development with Just
Developers mirror CI behavior locally through just tasks defined in the top-level justfile:
# Build Lambda artifacts locally
just build-lambda-artifacts
# Deploy a specific service
just deploy-service search_service
# Full workspace deployment
just deploy-all
This alignment ensures that failures caught in CI are reproducible on developer machines, using identical Nix environments and build flags.
Summary
Macro's Rust service deployment patterns demonstrate how to scale microservice CI without sacrificing reproducibility or developer velocity:
crane+ Nix builds hermetic, minimal Docker images for long-lived servicescargo-zigbuildcross-compiles Lambda handlers with Zig's toolchainnsc artifactprovides decoupled artifact hand-off between build and deploy stages- JSON-driven matrices scope CI work to changed services only
- Warm-lambda caching amortizes dependency build costs across PRs
- Pulumi two-phase deploy validates infrastructure before applying changes
justtask runner unifies local and CI workflows
Frequently Asked Questions
How does Macro achieve reproducible Rust builds across CI and local development?
Macro pins the entire build environment—including Rust version, system libraries, and build tools—inside a Nix flake. Both CI runners and local developer machines execute builds through Nix, ensuring identical outputs regardless of host system state.
What is cargo-zigbuild and why does Macro use it for Lambda functions?
cargo-zigbuild is a Cargo subcommand that uses Zig as the linker for cross-compilation. Macro uses it because Zig ships with libc implementations for multiple targets, eliminating the need to install musl cross-compilation toolchains. This produces smaller, fully static binaries that run on AWS Lambda's provided runtime.
How does Macro avoid rebuilding every service on every PR?
The .github/services-config.json file declares each service's deployment artifacts. CI parses this file to generate job matrices via jq, selecting only services with changes in the current branch. This matrix generation step runs before any expensive build operations.
Why warm Lambda dependencies separately from the main build?
Lambda builds with cargo-zigbuild compile all dependencies from source for the musl target—a slow operation. The warm-lambdas job runs once per workflow, populating a shared Nix store that subsequent Lambda builds mount. This cache reuse typically reduces Lambda build times by 60-90% according to the workflow implementation in deploy_all_services.rs.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →