Preview environments—temporary, isolated deployments of your code—have become essential for modern development workflows. They let you test changes in a production-like setting before merging, catch issues early, and give stakeholders a tangible way to review work in progress.
One of the most elegant ways to trigger these environments? PR labels. Slap a label on a pull request, and boom—a full environment spins up on AWS. Remove the label, and it tears down. Simple, powerful, and cost-effective. Let me walk you through how to set this up.
Why PR Labels?
Managing preview environments manually is a nightmare. You don't want to:
Spin up environments for every PR ($$$)
Manually track which PRs need environments
Remember to clean up after merge
PR-based labels solve this beautifully. You're in control—only PRs flagged with your chosen label (like preview or deploy) get environments. Remove the label or close the PR? Environment destroyed.
The Architecture at a Glance
There are several solid approaches depending on your stack and complexity needs. Here are the main patterns:
- The Simple Path: PullPreview + AWS Lightsail If you want the simplest route and your app runs on Docker Compose, PullPreview is hard to beat . It's a GitHub Action that provisions a cheap AWS Lightsail instance (around $10/month, prorated) whenever you label a PR . The workflow stays entirely within GitHub Actions—your code never touches third-party servers .
Here's a minimal workflow file:
yaml
.github/workflows/pullpreview.yml
name: PullPreview
on:
pull_request:
types: [labeled, unlabeled, synchronize, closed, reopened]
push:
branches:
- main
jobs:
deploy:
if: github.event_name == 'push' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview')
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: pullpreview/action@v5
with:
admins: your-github-username
always_on: main # Keeps a staging environment for main branch
compose_files: docker-compose.yml
default_port: 80
env:
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
AWS_REGION: "us-east-1"
What makes this great:
No Kubernetes complexity—each preview is a single VM
State persistence across deploys via Docker volumes
SSH access for troubleshooting (GitHub keys auto-installed)
Privacy-first—code never leaves your infrastructure
- The CDK Approach: Full Infrastructure as Code If you want total control and your team is comfortable with TypeScript, AWS CDK gives you infrastructure-as-code superpowers . You define your entire preview environment in code, and CDK handles the heavy lifting.
The basic flow:
Label a PR → triggers GitHub Action
Action derives a unique stack name (AwesomeStack-pr-42-feat-new-feature)
CDK deploys a full stack (CloudFront + S3, ECS, or whatever you need)
Deployment URL shows up in GitHub's UI via Deployments API
Remove label or close PR → cdk destroy cleans everything up
Key CDK snippets:
typescript
// lib/awesome-stack.ts - Infrastructure defined as code
const bucket = new s3.Bucket(this, "Bucket", {
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Critical for cleanup!
});
const distribution = new cloudfront.Distribution(this, "Distribution", {
defaultBehavior: {
origin: new cloudfrontOrigins.S3Origin(bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
});
// Export the URL for GitHub integration
new cdk.CfnOutput(this, "DeploymentUrl", {
value: "https://" + distribution.distributionDomainName
});
Pro tip: The removalPolicy: cdk.RemovalPolicy.DESTROY is essential—without it, your S3 buckets and databases stick around even after PRs close, costing you money .
- The Kubernetes Route: ArgoCD + EKS For teams already running Kubernetes on AWS (EKS), ArgoCD's ApplicationSet with PullRequest generator offers a GitOps-native approach .
The workflow:
Developer creates PR with preview label
GitHub Actions builds Docker image, pushes to ECR
ArgoCD detects the PR via ApplicationSet, provisions resources in a dedicated namespace
Preview environment gets its own Ingress URL
Remove label or close PR → ArgoCD deletes everything
This approach works especially well for microservices with dependencies. One neat trick from production implementations: use a hybrid approach where databases are provisioned per-PR but dependent services are shared from staging, saving costs while keeping data isolated .
Two Big Considerations
Cost Control
Preview environments can get expensive if you're not careful. Here are strategies to keep costs in check:
Spot instances for non-critical workloads (perfect for previews)
Auto-cleanup—remove the preview label if a PR goes stale for a certain period
Lightsail nano instances (512MB RAM) for lighter workloads
Scheduled cleanup jobs to catch dangling resources
Security
IAM policies should be least-privilege—give actions only what they need
CIDR restrictions to limit access to specific IP ranges
Never hardcode credentials—use GitHub Secrets for AWS access keys
The Bottom Line
PR-based preview environments on AWS transform how you ship code. Whether you go with:
PullPreview + Lightsail for dead-simple setup
AWS CDK for full infrastructure-as-code flexibility
ArgoCD + EKS for Kubernetes-native GitOps
…the core principle is the same: label-driven, ephemeral environments that give your team confidence to ship faster.
My recommendation: Start with PullPreview if you're using Docker Compose and want to be live in minutes. Graduate to CDK if you need custom infrastructure. And if you're already deep in Kubernetes, ArgoCD is your friend.
The key is to make preview environments so easy that your team actually uses them. Because when you combine speed with safety, that's when development gets truly enjoyable.
Happy deploying! 🚀
Top comments (0)