Lambda Deploy
Pattern for deploying Lambda code from CI/CD using direct zip upload with Terraform infrastructure separation.
Pattern for deploying Lambda code from CI/CD using direct zip upload, with clear separation between code deployment (GitHub Actions) and infrastructure management (Terraform).
Problem
Lambda services need automated code deployment from CI/CD. Without a standard:
- Some services use S3 artifacts, others use direct upload, others rely on manual
terraform apply - Terraform and CI/CD fight over Lambda code state
- No clear boundary between code changes (safe, frequent) and infrastructure changes (risky, infrequent)
- New services reinvent the deployment pipeline each time
Context
Use this pattern when:
- Deploying AWS Lambda functions from GitHub Actions
- Lambda zip bundles are under 40 MB
- The service uses Terraform for infrastructure management
- You need clear separation between code and infrastructure deployments
Don't use when:
- Bundle exceeds 40 MB zipped — escalate to S3 artifact mode (see Variations)
- Compliance requires persistent artifact retention with audit trail
- Sub-30-second rollback SLA is required
Solution
Lambda code deploys use direct zip upload. GitHub Actions builds, Terraform configures.
Two workflows handle CI/CD. Terraform manages infrastructure but ignores code changes. The Lambda-DeployRole (OIDC Deploy tier) provides the necessary permissions.
Workflow Structure
PR to master:
ci.yaml -> lint, test, build, terraform plan
Push to master:
deploy.yaml -> build, deploy each Lambda via direct upload
Terraform Separation
Terraform (developer): GitHub Actions (automated):
- Create/delete Lambda - Build zip
- IAM roles, policies - aws lambda update-function-code
- Environment variables - Smoke tests (optional)
- Memory, timeout, runtime
- API Gateway, DynamoDB, etc.
Structure
my-service/
|-- .github/
| +-- workflows/
| |-- ci.yaml # PRs: lint, test, build, tf plan
| +-- deploy.yaml # master: build + deploy code
|-- .infra/
| |-- lambda.tf # Lambda definitions with ignore_changes
| +-- ...
|-- src/
| |-- my-function/
| | +-- index.ts
| +-- other-function/
| +-- index.ts
|-- dist/ # Build output (gitignored)
| |-- my-function.zip
| +-- other-function.zip
+-- Taskfile.yaml # build, test, deploy:lambdas tasks
Implementation
1. Terraform Lambda Configuration
Add lifecycle { ignore_changes } to prevent Terraform from reverting CI/CD code deployments:
resource "aws_lambda_function" "my_function" {
function_name = "${var.project}-${var.environment}-my-function"
role = aws_iam_role.my_function.arn
handler = "index.handler"
runtime = "nodejs20.x"
filename = "${path.module}/bootstrap/placeholder.zip"
source_code_hash = filebase64sha256("${path.module}/bootstrap/placeholder.zip")
lifecycle {
ignore_changes = [filename, source_code_hash]
}
}
Why filename points to a placeholder: AWS requires code when creating a Lambda — you cannot create an empty function. The filename is only used during the initial terraform apply to bootstrap the resource. After that, lifecycle { ignore_changes } ensures Terraform never touches the code again; CI/CD takes over via update-function-code.
The placeholder zip is a minimal handler committed to the repo at .infra/bootstrap/placeholder.zip:
// .infra/bootstrap/handler.js
exports.handler = async () => ({ statusCode: 503, body: "Not deployed yet" });
# Generate once, commit the zip
cd .infra/bootstrap && zip placeholder.zip handler.js
This avoids requiring a full build before terraform apply and makes terraform plan work in CI without build artifacts.
2. Taskfile Tasks
vars:
LAMBDAS: my-function other-function
tasks:
build:
desc: Build all artifacts
cmds:
- task: build:lambdas
# - task: build:frontend (if applicable)
build:lambdas:
desc: Compile and package Lambda zips
cmds:
- npx tsx esbuild.config.ts # or: uv run build
- for lambda in {{.LAMBDAS}}; do
(cd dist/$lambda && zip -qr ../$lambda.zip .) ;
done
deploy:lambdas:
desc: Deploy all Lambda functions (requires AWS credentials)
deps: [build]
cmds:
- for lambda in {{.LAMBDAS}}; do
aws lambda update-function-code
--function-name "{{.PROJECT}}-{{.ENVIRONMENT}}-$lambda"
--zip-file "fileb://dist/$lambda.zip"
--publish ;
done
3. CI Workflow (ci.yaml)
name: CI
on:
pull_request:
branches: [master]
permissions:
id-token: write
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: arduino/setup-task@v2
- name: Configure AWS (CodeArtifact read)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActions-CodeArtifact-ReadRole
aws-region: eu-central-1
- name: Install, lint, test, build
run: task ci # wraps: install, lint:check, typecheck, test, build
plan:
runs-on: ubuntu-latest
needs: validate
steps:
- uses: actions/checkout@v4
- uses: arduino/setup-task@v2
- name: Configure AWS (Terraform plan)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActions-Terraform-PlanRole
aws-region: eu-central-1
- name: Terraform Plan
run: task infra:plan
4. Deploy Workflow (deploy.yaml)
name: Deploy Lambdas
on:
push:
branches: [master]
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: arduino/setup-task@v2
- name: Configure AWS (CodeArtifact read)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActions-CodeArtifact-ReadRole
aws-region: eu-central-1
- name: Build
run: task build
- name: Configure AWS (Lambda deploy)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActions-Lambda-DeployRole
aws-region: eu-central-1
- name: Deploy
run: task deploy:lambdas
Note the two configure-aws-credentials steps: first assumes the read role for npm install (CodeArtifact), then switches to the deploy role for update-function-code.
Variations
S3 Artifact Mode
Instead of uploading zips directly to Lambda, this variation stores them in S3 first:
- Create an S3 bucket following
{project}-{env}-lambda-artifactsnaming convention - Upload zips to
lambdas/{component}/{version}-{sha}.zip - Use
--s3-bucketand--s3-keyinstead of--zip-file - Enable bucket versioning for rollback support
- Add lifecycle policy (90 days expiry, 30 noncurrent versions)
The Lambda-DeployRole already has S3 permissions for buckets matching *-*-lambda-artifacts*.
Bundle Size CI Check
Add to your Taskfile to catch bundle growth proactively:
build:check-size:
desc: Warn if any Lambda zip exceeds 40 MB
cmds:
- |
for zip in dist/*.zip; do
size=$(stat -f%z "$zip" 2>/dev/null || stat -c%s "$zip")
mb=$((size / 1048576))
if [ "$mb" -ge 40 ]; then
echo "WARNING: $zip is ${mb}MB (threshold: 40MB)"
exit 1
fi
done
Choosing Between Models
Use this comparison to decide which model fits your service:
| Dimension | Direct Upload (default) | S3 Artifacts |
|---|---|---|
| Max bundle size | 50 MB (hard AWS limit) | 250 MB |
| Infrastructure | None | S3 bucket + lifecycle + versioning |
| Deploy command | --zip-file fileb:// | aws s3 cp + --s3-bucket/--s3-key |
| Rollback | Git revert + CI rebuild (~2 min) | Repoint to previous S3 key (~5 sec) |
| Artifact retention | None (rebuild from git SHA) | Persistent in S3 with versioning |
| Deployment traceability | Git commit SHA | S3 key with {version}-{sha} + timestamp |
| CI dependency for rollback | Yes — broken CI blocks rollback | No — S3 artifacts are independent |
| Multi-Lambda consistency | Sequential update, brief version skew | Same, unless combined with CodeDeploy |
| Complexity | Minimal | Moderate (bucket, lifecycle, IAM already covered) |
Decision Rule
Use Direct Upload (this pattern) when all of these are true:
- Bundles are under 40 MB
- Rebuild-based rollback (~2 min) meets your SLA
- No compliance requirement for persistent binary artifacts
- CI pipeline is reliable
Escalate to S3 Artifacts when any of these apply:
- Bundle approaching or exceeding 40 MB
- Need instant rollback without rebuild (sub-30 sec SLA)
- Regulatory or contractual requirement for artifact retention
- CI pipeline reliability is a concern during incidents
Related Patterns
- GitHub Actions Workflows — Workflows as thin wrappers around Taskfile
- Infrastructure Layout —
.infra/convention for Terraform - CodeArtifact + GitHub Actions — Private package authentication in CI
- Taskfile as Contract — Operational interface standard
References
- ADR-0011: Lambda Deploy Strategy — Full decision context
- ADR-003: GitHub Actions OIDC Trust Tier Model — OIDC roles and trust tiers
- ADR-0004: Infrastructure Layout —
.infra/convention
Taskfile as Contract
Pattern for using Taskfile as the single operational interface for repository operations, with prescriptive task naming, CI namespace, environment handling, and recipes.
Python Module Naming
Convention for distinguishing public API modules from internal implementation using underscore prefix.