Stop Waiting on Other Developers – Ship Features Faster!
When working in a team, you want a fast, reliable feedback loop without blocking others. I often had to wait my testing of a serverless application because others were still testing their own features on the ‘test’ environment. But there is a solution! The key? Deployment stages (sometimes called ephemeral environments). With deployment stages, each feature can be tested in isolation, preventing conflicts and delays.
Much has been written about deployment stages and ephemeral environments in AWS, but in short, they allow you to:
Deploy feature branches independently.
Test in isolation before merging.
Maintain production stability while iterating quickly.
What is a Deployment Stage?
A stage is an environment where your application runs with a complete copy of the infrastructure. In CloudFormation terms, its own stack. Since with serverless you only pay for what you use, you incur minimal extra cost for this. Each stage mimics production but is independent, making debugging and validation easier before changes reach production.
Implementing Deployment Stages in AWS CDK
How do you implement deployment stages using AWS CDK? Here’s a simple approach:
Define a CDK Stack – This is your base infrastructure.
Instantiate Stacks Per Stage – Deploy separate instances for
feature
,dev
, andprod
environments. Thefeature
stage will differ for each developer working on a different feature.Parameterize Deployments – Use configs to ensure each stage has the right settings.
Automate with CI/CD – Deploy stages using GitHub Actions.
a detailed reference implementation can be found at the bottom.
CDK Implementation
1. Define a CDK Stack (stack.ts
)
Define your AWS resources.
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
export interface MyStackProps extends cdk.StackProps {
stageName: string;
removalPolicy: cdk.RemovalPolicy;
autoDeleteObjects: boolean;
}
export class MyStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: MyStackProps) {
super(scope, `${props.stageName}-stack`, { ...props, stackName: `${props.stageName}-stack` });
// Ensure bucket name is lowercase and truncate stage name to 10 characters
const bucketName = `${props.stageName.substring(0, 10).toLowerCase()}-my-bucket`;
new s3.Bucket(this, 'MyBucket', {
bucketName,
removalPolicy: props.removalPolicy,
autoDeleteObjects: props.autoDeleteObjects,
});
}
}
2. Instantiate Stacks Per Stage (app.ts
)
Deploy separate instances for feature
, dev
, and prod
environments. The feature stage will differ for each developer working on a different feature.
import * as cdk from 'aws-cdk-lib';
import { MyStack } from './stack';
import { config } from './config';
const app = new cdk.App();
// The most important part: each developer gets their own independent infra.
// featureStageName is dynamically set, ensuring that two developers working on different
// features have isolated stacks and resources, avoiding conflicts.
const rawFeatureStageName = app.node.tryGetContext('stageName') || 'feature';
const featureStageName = rawFeatureStageName.replace(/[^a-zA-Z0-9-]/g, '-').substring(0, 10);
new MyStack(app, `FeatureStack`, {
stageName: featureStageName,
env: { account: config.feature.account, region: config.feature.region },
removalPolicy: config.feature.removalPolicy,
autoDeleteObjects: config.feature.autoDeleteObjects,
});
new MyStack(app, `DevStack`, {
stageName: 'dev',
env: { account: config.dev.account, region: config.dev.region },
removalPolicy: config.dev.removalPolicy,
autoDeleteObjects: config.dev.autoDeleteObjects,
});
new MyStack(app, `ProdStack`, {
stageName: 'prod',
env: { account: config.prod.account, region: config.prod.region },
removalPolicy: config.prod.removalPolicy,
autoDeleteObjects: config.prod.autoDeleteObjects,
});
app.synth();
3. Parameterize Deployments (config.ts
)
Ensure each stage has the correct settings.
import * as cdk from 'aws-cdk-lib';
export const config = {
feature: {
account: '111111111111',
region: 'eu-central-1',
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
},
dev: {
account: '222222222222',
region: 'eu-central-1',
removalPolicy: cdk.RemovalPolicy.RETAIN,
autoDeleteObjects: false,
},
prod: {
account: '333333333333',
region: 'eu-central-1',
removalPolicy: cdk.RemovalPolicy.RETAIN,
autoDeleteObjects: false,
},
};
4. Automate with CI/CD (feature-deployment.yml
and feature-destroy.yml
)
GitHub Actions workflows to deploy and clean up feature branches.
Deployment (feature-deployment.yml
)
name: Feature Deployment
on:
push:
branches:
- 'feature/*'
jobs:
deploy:
runs-on: ubuntu-latest
environment: feature
permissions:
id-token: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup node/npm
uses: actions/setup-node@v4
with:
always-auth: true
node-version: 18.x
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@master
with:
role-to-assume: ${{ env.DEPLOYMENT_ROLE_ARN }}
aws-region: eu-central-1
- name: Deploy Feature Stack
run: npm run cdk deploy -- "feat-stack" --require-approval never -c stageName=${{ github.head_ref }}
Cleanup (feature-destroy.yml
)
name: Cleanup - Feature Stack
on:
pull_request:
branches:
- main
types:
- closed
jobs:
cleanup_feat:
if: startsWith(github.head_ref, 'feature/')
runs-on: ubuntu-latest
environment:
name: feature
permissions:
id-token: write
contents: read
steps:
- name: Set env var STAGE_NAME
run: echo "STAGE_NAME=$(echo "${{ github.head_ref }}" | tr '/' '-')" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Setup node/npm
uses: actions/setup-node@v4
with:
always-auth: true
node-version: 18.x
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@master
with:
role-to-assume: ${{ env.DEPLOYMENT_ROLE_ARN }}
aws-region: eu-central-1
- name: CDK destroy
run: npm run cdk destroy -- "feat-stack" --force -c stageName=$STAGE_NAME
Common Mistakes and How to Avoid Them
1. Stack or Resource Name Collisions
Problem: Your stack cannot be deployed twice in the same AWS account without unique names.
Solution: Implement helper functions for stack and resource naming based on stage.
2. Incorrect Automated Resource Naming
Problem: AWS services have different naming conventions (e.g., S3 requires lowercase, SSM prefers path-style).
Solution: Standardize resource names in a helper function per service.
3. Unable to destroy your feature stack
Problem: resources cannot be destroyed, so your feature stage must be manually deleted
Solution: set removal policies to destroy, and deletion protection to ‘off’ for all resources in a feature stage.
4. Unable to set up connections to other systems
Problem: Your serverless app needs connections to other systems.
Solution: This can be challenging, depending on the issue. If possible, set up a dedicated user or reuse resources from the 'dev' stack. For example, if your stack creates a role ARN that should be used for the connection in the other system, reuse the static role from the dev environment instead of creating a new role for your feature stack.
What if I don’t even use feature branches?
Some teams argue for trunk-based development—just push to main
and deploy to production continuously. While this works for small teams, enterprise teams often:
Work on features for days or weeks.
Have multiple features in progress simultaneously.
Need feature isolation to avoid production disruptions.
Deployment stages allow teams to iterate safely without blocking each other while still enabling fast releases.
But stages can benefit those without feature branches, too! Having multiple instances of your infrastructure isolated from each other in the same AWS account also allows for better fault-tolerance in multi-tenant architectures. If one set of infrastructure fails, only a part of the customers is impacted.
Conclusion
By implementing CDK deployment stages, you ensure:
✅ Fast, reliable deployments without breaking production.
✅ Independent testing of features before merging.
✅ A scalable approach for teams working on multiple features simultaneously.
This guide outlined the minimum viable setup for CDK deployment stages, but you can extend it further by:
Adding automated integration tests before deployment.
Using feature flags for dynamic releases.
Helper functions for consistent resource naming, stack name generation etc.