
Real-World Full-Stack CI/CD: Dev, Staging, and Prod with GitHub Actions and Terraform
Modern full-stack teams face relentless pressure to ship features quickly while maintaining reliability. Relying on a single deployment environment leads to outages, flaky releases, and broken integrations. In 2024, production-grade full-stack CI/CD means rigorously managing dev, staging, and production environments—each isolated, reproducible, and automated end-to-end.
What Is Multi-Environment CI/CD? (With Real Config Example)
In cloud-native engineering, "multi-environment CI/CD" refers to automatically building, testing, and deploying changes across several isolated environments (commonly dev, staging, and prod). Each environment mirrors production as closely as possible while supporting safe preview, integration testing, and phased rollouts. Tooling like GitHub Actions, Terraform, and AWS make this practical at scale.
A minimal multi-environment setup with GitHub Actions and Terraform v1.5+ might look like this:
# .github/workflows/deploy.yml
name: Deploy Full-Stack App
on:
push:
branches: [ main, develop, staging ]
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
env: [dev, staging, prod]
steps:
- uses: actions/checkout@v4
- name: Set environment variables
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "ENV=prod" >> $GITHUB_ENV
elif [ "${{ github.ref }}" == "refs/heads/staging" ]; then
echo "ENV=staging" >> $GITHUB_ENV
else
echo "ENV=dev" >> $GITHUB_ENV
fi
- name: Terraform Init
run: terraform init -backend-config=env/${{ env.ENV }}/backend.hcl
- name: Terraform Apply
run: terraform apply -var-file=env/${{ env.ENV }}/terraform.tfvars -auto-approve
Key insight: Each branch maps to a specific environment, enforcing separation and reproducibility.
Step 1: Isolate State and Resources Per Environment
Why Segregation Is Critical
Mixing dev, staging, and prod resources is a recipe for disaster. I've seen teams lose production data or incur massive cloud costs from accidental resource deletion. Each environment must have:
- Separate cloud accounts or logically partitioned resources (e.g., AWS Organizations, Azure Subscriptions)
- Unique state backends (S3 buckets, Terraform state files)
- Distinct secrets, credentials, and endpoints
How to Structure State Backends
For Terraform, use a dedicated S3 bucket (or Azure Blob/Google Cloud Storage) and DynamoDB lock table per environment. Concrete example:
# env/dev/backend.hcl
bucket = "myapp-terraform-state-dev"
dynamodb_table = "terraform-lock-dev"
key = "dev/terraform.tfstate"
region = "us-east-1"
Repeat for staging and prod. Never share state files or lock tables across environments.
Separate Infrastructure and Application Layers
I recommend using different workspaces or even separate repos for infrastructure and app code. This prevents accidental cross-env deployments and speeds up troubleshooting.
Key insight: Isolating state, secrets, and cloud resources per environment is non-negotiable for production resilience.
Step 2: Parameterize Deployments for Each Environment
Why Parameterization Matters
Hardcoding environment-specific values leads to drift and brittle pipelines. Instead, use parameter files and environment variables to drive configuration.
Terraform Variable Files Example
Organize environment-specific variables clearly:
# env/staging/terraform.tfvars
app_image_tag = "staging-20240623"
db_instance_type = "db.t3.medium"
domain_name = "staging.myapp.com"
api_rate_limit = 1000
Your CI/CD pipeline injects the correct parameters using the branch or workflow context.
Handling Secrets Securely
Use a managed secrets store like AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault. In GitHub Actions, use OpenID Connect (OIDC) for short-lived credentials instead of static secrets.
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::<account>:role/myapp-${{ env.ENV }}-deploy
aws-region: us-east-1
Parameterize App Deployments (Frontend + Backend)
For full-stack apps (React/Next.js + Node.js, for example), set environment variables specific to each environment in your deployment manifest or build script.
- name: Build frontend
run: |
REACT_APP_API_URL=${{ secrets.API_URL }} npm run build
Key insight: Parameterization ensures repeatable, environment-specific deployments and prevents hardcoded drift.
Step 3: Automate Promotion and Rollbacks Across Environments
Why Promotion Matters
Manual promotion wastes engineering time and increases the risk of human error. Automated promotion (dev → staging → prod) guarantees only tested, approved builds reach production.
Implementing Promotion Workflows
Use GitHub Actions environments with required approvals for staging and prod:
# .github/workflows/deploy.yml (fragment)
environment:
name: ${{ env.ENV }}
url: https://${{ secrets.DOMAIN_NAME }}
Configure environment protection rules:
- Require pull request review for staging/prod deploys
- Require status checks and integration tests to pass
You can use release branches or GitHub Actions workflow_dispatch triggers to promote a build:
on:
workflow_dispatch:
inputs:
environment:
required: true
type: environment
Rollback Strategies
For infrastructure, terraform destroy is rarely appropriate in prod. Instead, use versioned releases (e.g., tag Docker images with Git SHA), and support blue/green or canary deployments for fast rollback. In AWS ECS, for example:
- name: Deploy to ECS
run: |
aws ecs update-service --cluster myapp-${{ env.ENV }} --service frontend --force-new-deployment
Roll back by re-deploying a previous image tag.
Key insight: Automated promotion and fast rollback are essential to safe, fast, and reliable releases.
Step 4: Validate, Test, and Monitor at Every Stage
Why End-to-End Validation Is Non-Negotiable
Testing only in dev or only in prod is a major anti-pattern. Each environment needs automated validation:
- Linting and static analysis (e.g., ESLint, tsc, Terraform validate)
- Unit and integration tests
- End-to-end (E2E) smoke tests (e.g., Cypress, Playwright)
- Infrastructure compliance (e.g., tfsec, Checkov)
- Deployment health checks
Real Pipeline Fragment
- name: Run Linter
run: npm run lint
- name: Run Unit Tests
run: npm test
- name: Terraform Validate
run: terraform validate
- name: Checkov Security Scan
uses: bridgecrewio/checkov-action@v12.2376.0
Monitoring and Observability Per Environment
Instrument each environment with:
- Structured logging (e.g., AWS CloudWatch, Datadog, ELK)
- Metrics (e.g., Prometheus, CloudWatch Metrics)
- Distributed tracing (e.g., OpenTelemetry)
- Alerting (e.g., PagerDuty, Opsgenie)
Provision separate log groups, dashboards, and alerts for each environment. Tag resources accordingly for cost and incident attribution.
Key insight: Validation and observability at every environment is the only way to catch issues before they impact users.
Comparison Table: CI/CD Toolchains for Multi-Environment Full-Stack Apps
| Tool | Pros | Cons | Best For |
|---|---|---|---|
| GitHub Actions | Native GitHub integration, free for small teams, OIDC support, great ecosystem | Limited concurrency, slower for large monorepos | GitHub-hosted repos, OSS, small/med teams |
| GitLab CI/CD | Built-in environments, review apps, strong Docker support | Complex YAML, self-hosted runners can be costly | GitLab-based orgs, container-heavy apps |
| CircleCI | Fast builds, matrix jobs, Docker layer caching | Usage-based pricing, less flexible for infra-as-code | Fast feedback, mid-size SaaS teams |
| Jenkins (w/ plugins) | Extreme flexibility, custom pipelines, open source | Steep learning curve, manual scaling, plugin sprawl | Enterprises, legacy/hybrid deployments |
| AWS CodePipeline | Native AWS integration, IAM roles, cost-effective | AWS-only, less friendly UI, slower feedback loops | AWS-centric, infrastructure-heavy apps |
| ArgoCD + GitOps | Declarative sync, rollback, K8s native, multi-env support | Needs K8s, learning curve for non-K8s teams | Kubernetes-first, GitOps pipelines |
Key insight: Choose a CI/CD toolchain that natively supports multi-environment orchestration and integrates with both your code and infra workflows.
Frequently Asked Questions
Q: How do I prevent developers from accidentally deploying to production? A: Enforce environment protection rules such as required reviews, status checks, and restricted permissions in your CI/CD platform. In GitHub Actions, use environments with required reviewers for staging and prod, and limit who can trigger production deployments.
Q: Should dev, staging, and prod use different cloud accounts or projects? A: For strict isolation and compliance, use separate accounts/projects (e.g., in AWS Organizations or Google Cloud Projects). At minimum, use resource-level isolation and never share state or secrets across environments.
Q: How can I keep infrastructure and application deployments in sync? A: Use coordinated pipelines where infrastructure changes run before application deploys. Pin app versions to infrastructure outputs (e.g., API endpoints, DB URIs), and reference them in your deployment scripts via variables or secrets management.
Key Takeaways
- Always isolate state, secrets, and resources for each environment to prevent cross-contamination and outages.
- Parameterize all configuration, secrets, and deploy scripts—never hardcode environment values.
- Automate promotion and rollback to minimize human error and speed up delivery.
- Validate and test at every stage, including infra compliance and E2E smoke tests.
- Instrument logging, metrics, and alerting separately for dev, staging, and prod.
- Choose a CI/CD toolchain that natively supports multi-environment orchestration and integrates with your cloud provider and secrets management.


