A comprehensive guide to building reliable CI/CD pipelines for serverless applications. Learn the four-step workflow pattern used by the best serverless teams.
Continuous Integration and Continuous Delivery (CI/CD) is the practice of automating the build, test, and deployment process for your applications. For serverless applications, CI/CD is especially important because the deployment unit is typically a single function or service, and teams often manage dozens or hundreds of independently deployable services.
A well-designed CI/CD workflow ensures that every code change is automatically tested, reviewed, staged, and deployed to production with minimal manual intervention. This reduces human error, increases deployment frequency, and gives teams confidence that their changes work correctly before reaching users.
The Serverless Framework includes a built-in CI/CD service through Serverless Framework Pro. It is purpose-built for serverless workflows and handles deployment orchestration, secrets management, and multi-service coordination out of the box. The free tier includes one concurrent build, making it easy to get started. For teams that prefer to use their own CI provider, GitHub Actions is a popular alternative that pairs well with the Serverless Framework CLI. This guide covers both approaches.
Before building your CI/CD pipeline, ensure your workflow meets these fundamental requirements:
All application code and infrastructure definitions (serverless.yml) must be stored in version control (Git).
Unit tests, integration tests, and end-to-end tests should run automatically on every commit and pull request.
Development, staging, and production environments must be isolated to prevent cross-environment contamination.
Secrets, environment variables, and configuration must be managed separately from code and injected at deploy time.
The recommended CI/CD workflow for serverless applications follows four stages, each with a clear purpose and set of actions.
Developers work on feature branches and deploy to personal development stages. Each developer gets their own isolated environment to test changes without affecting others.
# Each developer deploys to their own stage
serverless deploy --stage dev-john
# Test your changes
serverless invoke --function hello --stage dev-john
# View logs in real-time
serverless logs --function hello --stage dev-john --tailWhen a feature is ready, the developer opens a pull request. The CI pipeline automatically deploys a preview environment and runs the test suite. Reviewers can test the changes in an isolated environment before approving.
# .github/workflows/review.yml
name: Review
on:
pull_request:
branches: [main]
jobs:
deploy-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- run: npx serverless deploy --stage preview-${{ github.event.number }}After the pull request is merged to the main branch, the CI pipeline automatically deploys to the staging environment. Staging mirrors production and serves as the final validation step before release.
# .github/workflows/stage.yml
name: Stage
on:
push:
branches: [main]
jobs:
deploy-staging:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- run: npx serverless deploy --stage staging
- run: npm run test:integrationAfter staging validation passes, the same artifact is promoted to production. This can be triggered automatically or require manual approval depending on your team's risk tolerance.
# .github/workflows/release.yml
name: Release
on:
workflow_dispatch:
# Or trigger automatically after staging succeeds
jobs:
deploy-production:
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx serverless deploy --stage prodPreview deployments give every pull request its own isolated serverless environment. When a developer opens a PR, the CI pipeline automatically deploys all services to a stage named after the branch (or PR number). Reviewers can test the changes against real AWS resources without affecting staging or production.
When the branch is deleted or the PR is closed, a cleanup workflow removes the preview stage automatically. This prevents unused stacks from accumulating and keeps your AWS account tidy.
# .github/workflows/preview-deploy.yml
name: Preview Deploy
on:
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- run: npx serverless deploy --stage preview-${{ github.event.number }}
# ---
# .github/workflows/preview-cleanup.yml
name: Preview Cleanup
on:
delete:
branches:
jobs:
remove:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx serverless remove --stage preview-${{ github.event.ref }}With Serverless Framework Pro, preview deployments are handled natively. The Pro CI/CD system detects pull requests, deploys a preview stage using the branch name, and tears it down when the branch is deleted. No workflow files are required.
Throughout this guide, we will reference a workshop-style full-stack application composed of three Serverless Framework services. This is a realistic example that demonstrates multi-service coordination, shared outputs, and stage-specific parameters.
fullstack-databaseProvisions a DynamoDB table using custom variables. Publishes the table name and ARN as outputs so other services can consume them without hardcoding resource identifiers.
fullstack-restapiA Lambda-backed API Gateway service that handles HTTP requests. It consumes the outputs from fullstack-database to read and write data, keeping the two services loosely coupled.
fullstack-tasksA Lambda function that runs on a 5-minute interval schedule. It pulls configuration values using parameters stored in the Serverless Framework Pro dashboard, such as API keys and feature toggles.
For larger applications, organize your serverless services within a monorepo structure. Each service has its own serverless.yml and can be deployed independently.
my-app/
services/
api/
serverless.yml
handler.js
package.json
auth/
serverless.yml
handler.js
package.json
notifications/
serverless.yml
handler.js
package.json
shared/
lib/
utils.js
database.js
tests/
integration/
e2e/
package.json
README.mdUse Serverless Framework stages to manage multiple environments. Each stage creates a completely isolated set of resources with its own CloudFormation stack.
service: my-api
provider:
name: aws
runtime: nodejs20.x
stage: ${opt:stage, 'dev'}
region: us-east-1
environment:
STAGE: ${sls:stage}
TABLE_NAME: ${self:service}-${sls:stage}-table
custom:
# Stage-specific settings
domainName:
prod: api.example.com
staging: staging-api.example.com
dev: ${sls:stage}-api.example.com
functions:
hello:
handler: handler.hello
events:
- httpApi:
path: /hello
method: get
resources:
Resources:
DataTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${sls:stage}-table
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASHUse different AWS credentials and configuration profiles for each environment. This ensures separation of concerns and prevents accidental deployments to the wrong environment.
Individual developer AWS accounts or sandboxed roles with broad permissions for experimentation.
Shared staging account with production-like settings. CI/CD pipeline uses a dedicated IAM role with scoped permissions.
Locked-down production account. Deployments only via CI/CD with manual approval gates and audit logging.
The recommended approach is to use three separate AWS accounts managed through AWS Organizations: one each for development, staging, and production. Account-level isolation is the strongest boundary AWS provides. It prevents configuration mistakes in one environment from affecting another, and it makes cost attribution straightforward.
Serverless Framework Pro supports AWS Access Roles, which generate short-lived credentials that rotate every hour. This eliminates the need to store long-lived AWS access keys in your CI system. You link each deployment profile to an IAM role in the target account, and the framework handles credential exchange automatically.
Store stage-specific configuration values (API keys, feature toggles, resource identifiers) in the Serverless Framework Pro dashboard under each deployment profile. Reference them in your serverless.yml using the ${param:...} syntax. When the service is deployed to a given stage, the framework resolves each parameter from the matching profile automatically.
provider:
environment:
STRIPE_KEY: ${param:stripeKey}
SENDGRID_KEY: ${param:sendgridKey}
SENTRY_DSN: ${param:sentryDsn}Just as you use separate AWS accounts per environment, use separate accounts for third-party services like Stripe, SendGrid, and Sentry. Store each set of credentials in the corresponding deployment profile. This keeps test data out of production systems and prevents accidental charges or notifications from non-production environments.
When multiple services need to share resources (like a DynamoDB table or an SQS queue), use CloudFormation outputs and Fn::ImportValue to share resource references across stacks.
# Service A: Export the table ARN
resources:
Outputs:
DataTableArn:
Value: !GetAtt DataTable.Arn
Export:
Name: ${sls:stage}-data-table-arn
# ---
# Service B: Import the table ARN
provider:
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
Resource:
Fn::ImportValue: ${sls:stage}-data-table-arnServerless Framework Pro provides a simpler alternative for sharing values between services. Use the outputs block in your serverless.yml to publish values, then reference them from other services with the ${output:...} syntax. This avoids the CloudFormation export/import coupling and works across stages and regions.
# fullstack-database/serverless.yml - Publish outputs
outputs:
tableArn: !GetAtt DataTable.Arn
tableName: !Ref DataTable
# ---
# fullstack-restapi/serverless.yml - Consume outputs (same app, stage, region)
provider:
environment:
TABLE_ARN: ${output:fullstack-database.tableArn}
TABLE_NAME: ${output:fullstack-database.tableName}
# Cross-stage reference (full syntax)
# ${output:<app>:<stage>:<region>:<service>.<key>}
# Example: ${output:myapp:prod:us-east-1:fullstack-database.tableArn}A robust CI/CD pipeline needs clear strategies for promoting code between environments and rolling back when issues are detected.
A common pattern is to use Git branches as the source of truth for each environment. The main branch maps to staging and a prod branch maps to production. Promoting to production is as simple as merging main into prod. Your CI pipeline watches the prod branch and deploys automatically on push.
To roll back, revert the merge commit on the prod branch via a pull request. This creates a clean audit trail in Git, triggers the CI pipeline to redeploy the previous known-good state, and lets the team review the revert before it takes effect. Because the revert goes through the same PR workflow, it benefits from the same automated tests and approval gates as any other change.
Feature flags allow you to decouple deployment from release. Deploy code to production with features hidden behind flags, then enable them gradually for specific users or percentages of traffic.
Enable new features for a percentage of users, gradually increasing as confidence grows.
Disable a problematic feature immediately without redeploying code.
Run experiments by exposing different feature variants to different user segments.
Enable features in staging but keep them disabled in production until ready.
The Serverless Framework makes it easy to build robust CI/CD workflows for your serverless applications. Get started today.