Serverless CI/CD Pipelines: From Push to Production in Minutes
A hands-on guide to building zero-maintenance CI/CD pipelines for serverless applications using AWS CDK, CodePipeline, and GitHub Actions.
The Problem with Traditional CI/CD
Managing Jenkins servers, patching build agents, scaling runners — CI/CD infrastructure often becomes its own project. For serverless applications, this irony is hard to ignore: your app scales to zero, but your build system runs 24/7.
Two Approaches
I've used two patterns successfully:
- GitHub Actions + Serverless Framework — great for small teams, fast to set up
- AWS CodePipeline + CDK Pipelines — better for organizations that want everything in AWS
Let's look at both.
Approach 1: GitHub Actions
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test
- run: npm run lint
- name: Deploy to AWS
run: npx serverless deploy --stage production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ap-southeast-1
Simple, readable, and runs in under 3 minutes for most Lambda projects. The downside: your AWS credentials live in GitHub Secrets.
Better: Use OIDC federation to avoid long-lived credentials:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: ap-southeast-1
Approach 2: CDK Pipelines
For teams that want the pipeline defined as infrastructure-as-code alongside the application:
import { CodePipeline, ShellStep, CodePipelineSource } from "aws-cdk-lib/pipelines";
const pipeline = new CodePipeline(this, "Pipeline", {
synth: new ShellStep("Synth", {
input: CodePipelineSource.gitHub("datj9/my-service", "main"),
commands: [
"npm ci",
"npm test",
"npx cdk synth",
],
}),
});
pipeline.addStage(new StagingStage(this, "Staging"));
pipeline.addStage(new ProductionStage(this, "Production"), {
pre: [new ManualApprovalStep("PromoteToProduction")],
});
This creates a self-mutating pipeline: if you change the pipeline definition, it updates itself on the next run.
Testing Serverless Locally
Before pushing, test locally with:
# Unit tests with Jest
npm test
# Integration tests with LocalStack
docker-compose up -d localstack
npm run test:integration
# Invoke a function locally
serverless invoke local -f processOrder --path events/order.json
Environment Promotion Strategy
feature branch → PR → main → Staging (auto) → Production (manual approval)
- Feature branches get ephemeral stacks:
serverless deploy --stage feat-xyz - Staging is deployed automatically on merge to main
- Production requires manual approval (a Slack notification with an approve link)
Secrets Management
Never hardcode secrets. Use SSM Parameter Store or Secrets Manager:
const dbPassword = ssm.StringParameter.fromSecureStringParameterAttributes(
this, "DbPassword", {
parameterName: "/myapp/production/db-password",
}
);
Conclusion
Serverless CI/CD should be as hands-off as the applications it deploys. GitHub Actions gets you running in 15 minutes. CDK Pipelines gives you a fully managed, self-mutating pipeline defined alongside your infrastructure. Pick the one that matches your team's complexity.