Preview environments have become an essential part of modern development workflows. They allow teams to review changes in a live environment before merging code, significantly improving collaboration and reducing bugs.
In this article, I'll walk you through setting up preview environments on AWS that are triggered by PR labels. This approach gives you complete control over your infrastructure while keeping costs low and workflows simple.
Why Preview Environments?
Preview environments (also called review apps) are temporary deployments of your application from pull requests. They let teammates, product managers, and QA teams visually review changes before they hit production .
The benefits are clear:
Faster feedback loops – reviewers see exactly what changed
Reduced risk – issues are caught before merging
Better collaboration – non-technical stakeholders can participate in reviews
Confidence in changes – you can run E2E tests against real infrastructure
The Label-Based Approach
Using PR labels to trigger preview deployments is a pattern that's gained significant traction. Here's why it works so well:
Intent-based activation – you choose when to spin up an environment
Automatic cleanup – removing the label or closing the PR tears everything down
Team-friendly – no complex commands required, just add a label
As one popular implementation describes it: "You can manage preview environments by adding or removing the pullpreview label on your Pull Requests" .
Architecture Overview
The architecture depends on your specific needs, but a typical setup includes:
Option 1: Simple VM-based (using PullPreview)
This approach provisions AWS Lightsail instances for each preview environment . Each instance runs Docker Compose to spin up your application and its dependencies. It's simple, cheap, and works with any app that can be containerized.
Option 2: Infrastructure as Code (AWS CDK)
For more complex applications, you can use AWS CDK to define your infrastructure in TypeScript. This gives you more control and enables sophisticated setups with:
CloudFront CDN in front of S3 buckets
RDS databases with proper lifecycle management
VPC configurations and security groups
Option 3: Kubernetes with GitOps
Larger teams might use Kubernetes with ArgoCD's ApplicationSet and PullRequest generator. This approach creates isolated namespaces per PR, with full service mesh capabilities .
Implementation: PullPreview Approach
Let me show you the simplest way to get started, using the PullPreview GitHub Action.
- Set Up AWS Credentials First, create an IAM user with Lightsail permissions. AWS doesn't provide a default Lightsail policy, so you'll need to create one :
json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "lightsail:",
"Resource": ""
}
]
}
Then store the access keys as repository secrets:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_REGION (e.g., us-east-1)
- Create the Workflow File Add .github/workflows/pullpreview.yml to your repository:
yaml
name: PullPreview
on:
pull_request:
types: [labeled, unlabeled, synchronize, closed, reopened]
jobs:
deploy:
permissions:
contents: read
deployments: write
pull-requests: write
statuses: write
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: pullpreview/action@v5
with:
admins: your-github-username
compose_files: docker-compose.pullpreview.yml
ports: 80,443
default_port: 443
env:
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
AWS_REGION: "us-east-1"
- Create the Docker Compose File Your docker-compose.pullpreview.yml defines what runs in the preview environment:
yaml
services:
proxy:
image: caddy:2
command: "caddy reverse-proxy --from '${PULLPREVIEW_URL}' --to web:4567"
ports:
- "80:80"
- "443:443"
web:
build: .
environment:
DATABASE_URL: "postgres://user:pass@db/postgres"
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: p4ssw0rd
- Create the Label In your GitHub repository, create a label called pullpreview. When you add this label to a PR, the preview environment will spin up automatically.
Implementation: CDK Approach
For teams needing more infrastructure control, the AWS CDK approach offers greater flexibility .
Define Your Stack
typescript
// lib/awesome-stack.ts
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as s3 from "@aws-cdk/aws-s3";
import * as cdk from "@aws-cdk/core";
export default class AwesomeStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, "Bucket", {
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Clean up when PR closes
});
const distribution = new cloudfront.Distribution(this, "Distribution", {
defaultBehavior: {
origin: new cloudfrontOrigins.S3Origin(bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
});
// Export the URL for GitHub Deployments API
new cdk.CfnOutput(this, "DeploymentUrl", {
value: "https://" + distribution.distributionDomainName
});
}
}
Trigger Based on PR Number
Use the PR number and branch name to create unique stack names:
yaml
name: set STAGE variable
run: echo "STAGE=pr-${{ github.event.number }}-${{ env.GITHUB_HEAD_REF_SLUG }}" >> $GITHUB_ENVname: deploy the stack
run: yarn deploy
This ensures each PR gets its own isolated infrastructure .
Cleanup: Don't Leave Resources Running
Preview environments should be ephemeral. Here's how to ensure cleanup:
With PullPreview: Removing the pullpreview label or closing the PR automatically destroys the Lightsail instance.
With CDK: Create a cleanup workflow that triggers on PR closure:
yaml
name: "Pull Request clean-up"
on:
pull_request:
types: [unlabeled, closed]
jobs:
clean-up:
if: |
(github.event.action == 'unlabeled' && github.event.label.name == '🚀 deploy') ||
(github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, '🚀 deploy'))
runs-on: ubuntu-latest
steps:
- name: destroy the stack
run: cdk destroy "AwesomeStack-${{ env.STAGE }}" --force
Cost Considerations
One of the advantages of this approach is cost control:
Lightsail instances start at ~$10/month, prorated to actual usage
CDK resources can be set to auto-delete with RemovalPolicy.DESTROY
Spot instances in Kubernetes can further reduce costs for non-critical workloads
Advanced Features to Consider
Multi-service dependencies: When Service A depends on Service B, developers should be able to point to either staging or another preview environment .
Persistent state: Keep database volumes across deploys so reviewers don't lose data when new commits arrive .
SSH access: Grant trusted team members SSH access to preview instances for troubleshooting .
Conclusion
Preview environments triggered by PR labels provide a powerful way to review changes in a production-like setting. Whether you choose the simplicity of PullPreview with Lightsail, the control of AWS CDK, or the scalability of Kubernetes, the core pattern remains the same: code → label → preview → review → cleanup.
The key benefits are clear:
Low cost – pay only for what you use
Simple activation – just add a label
Automatic cleanup – no manual resource management
Privacy – your code stays in your AWS account
Start small, get the workflow working for a simple app, then expand to your full microservices architecture. Your team's confidence in shipping changes will increase dramatically when everyone can see and test the real thing before it merges.
Top comments (0)