Lambda Multi-Arch Builds: arm64 CI Matrices That Actually Ship

Lambda Multi-Arch Builds: arm64 CI Matrices That Actually Ship

Flipping x86_64 Lambdas to “save money on Graviton” fails when node-gyp artifacts from Ubuntu-x64 CI get uploaded to arm64 functions. The unfair advantage is a CI matrix that rebuilds native modules for each target, verifies with file/readelf, and refuses to publish a mismatched zip.

⚡ TL;DR: Build per architecture (x86_64, arm64) with matching Lambda container images or npm rebuild --arch. Set function Architectures explicitly in IaC. Fail the pipeline if a .node binary ELF machine type does not match. Prefer pure-JS deps when possible. Align runtime choices with Lambda Warm Pools and keep secrets out of builds via Secret-Aware Context Filters.

Matrix that rebuilds natives correctly

# .github/workflows/lambda-multiarch.yml
strategy:
  matrix:
    arch: [x86_64, arm64]
    include:
      - arch: x86_64
        platform: linux/amd64
        lambda_arch: x86_64
      - arch: arm64
        platform: linux/arm64
        lambda_arch: arm64
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3
      - name: Build inside Lambda Node 20 image
        run: |
          docker run --rm --platform=${{ matrix.platform }} \
            -v "$PWD":/var/task -w /var/task \
            public.ecr.aws/lambda/nodejs:20 \
            bash -lc "npm ci && npm run build && npm prune --omit=dev"
      - name: Assert ELF arch
        run: ./scripts/assert-native-arch.sh ${{ matrix.lambda_arch }}
      - run: (cd dist && zip -r ../fn-${{ matrix.lambda_arch }}.zip .)
# scripts/assert-native-arch.sh
#!/usr/bin/env bash
set -euo pipefail
want="$1" # x86_64 | arm64
while IFS= read -r -d "" f; do
  info=$(file "$f")
  case "$want" in
    arm64) echo "$info" | grep -q "ARM aarch64" ;;
    x86_64) echo "$info" | grep -q "x86-64" ;;
  esac || { echo "arch mismatch: $f ($info)"; exit 1; }
done < <(find node_modules dist -name "*.node" -print0 2>/dev/null)

✅ Build inside the Lambda base image for that arch.
npm ci on GitHub’s x64 runner then deploy to arm64.

IaC must pin Architectures

new lambda.Function(this, "Api", {
  runtime: lambda.Runtime.NODEJS_20_X,
  architecture: lambda.Architecture.ARM_64,
  code: lambda.Code.fromAsset(`artifacts/fn-arm64.zip`),
  handler: "index.handler",
});
// BAD: default arch in one stack, zip built for another
new lambda.Function(this, "Api", {
  runtime: lambda.Runtime.NODEJS_20_X,
  code: lambda.Code.fromAsset("fn.zip"), // mystery arch
  handler: "index.handler",
});

Dual-arch publish (two functions or two versions) only if you need gradual migration — do not ship a fat zip with both and hope.

Native module escape hatches

Dep type Strategy
Pure JS Prefer; simplest
Prebuilt *-linux-arm64 wheels/binaries Pin optionalDependencies carefully
Custom C++ addons Rebuild in Lambda container per arch
Sharp / bcrypt / better-sqlite3 Always matrix-test both arches

Closing checklist

  • [ ] CI matrix builds x86_64 and/or arm64 in Lambda base images
  • [ ] assert-native-arch gate on *.node ELF types
  • [ ] Function Architectures set in IaC to match artifact
  • [ ] Smoke invoke on the target arch before prod promote
  • [ ] Document which native deps force matrix complexity
  • [ ] Cost note: Graviton savings vs CI minutes tracked

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply