
September 11, 2026
10 min read
By Kokil Thapa | Last reviewed: September 2026
Production Laravel apps rarely run in a single static environment. When you manage multiple environments in IaC, you define dev, staging, and production once in Terraform, OpenTofu, or Pulumi, then promote changes through CI instead of manual clicking. On real client projects, I've seen teams start with one .tfvars file and outgrow it within a sprint. This guide covers patterns that survive audits, cost controls, and an urgent Friday deploy.
Why do you need separate environments in Infrastructure as Code?
Separate environments exist because risk, cost, and access differ. Dev can tolerate broken builds. Staging should mirror production topology at smaller scale. Production demands change control, backups, and monitoring.
Without IaC discipline, each environment drifts. Someone patches a security group by hand in staging. Production never gets the fix. Six months later, a deploy fails because the two worlds no longer match.
IaC makes divergence visible. A terraform plan against staging shows what production will receive after promotion. That visibility is the whole point. For Laravel 13.x apps on PHP 8.3+, the same pattern applies whether you run on AWS, DigitalOcean, or a single Ubuntu 24 server managed through Linux system administration.
What are the main patterns to manage multiple environments in IaC?
Four patterns cover most teams. Pick based on team size, cloud account layout, and how strictly you must isolate blast radius.
| Pattern | How it works | Best for | Watch out for |
|---|---|---|---|
| Directory per environment | envs/dev, envs/prod each call shared modules | Small teams, clear ownership | Duplicated backend config if copy-pasted |
| Workspaces | One root module, terraform workspace select | Identical topology, different names | Easy to apply to wrong workspace |
| Separate state backends | Unique S3 bucket or key per env | Strong isolation, audit trails | More backend boilerplate |
| Stack-per-env (Pulumi/CDK) | Programmatic stacks with config layers | Teams already using real languages | Requires discipline on shared libs |
On sister sites I maintain with Deployer 7 and GitLab CI, we treat staging and production as separate targets even when the IaC layer is thin. The mental model matches Terraform: same recipe, different variables, different credentials. See staging that mirrors production for application-level parity.
Directory layout that scales
A layout I use repeatedly:
infra/
modules/
vpc/
rds/
ecs-service/
envs/
dev/
main.tf
backend.tf
terraform.tfvars
staging/
main.tf
backend.tf
terraform.tfvars
production/
main.tf
backend.tf
terraform.tfvars
Each envs/* folder is a thin wrapper. It passes environment = "dev" and instance sizes into shared modules. Module logic lives once. Promotion means merging a PR that changes module code, then applying env folders in order.
When workspaces beat directories
Workspaces suit identical infrastructure with different resource name prefixes. HashiCorp documents workspace behaviour in the official Terraform workspaces guide. Use them when dev and prod differ only by labels and counts, not by entire subsystems.
I prefer directories when staging runs a smaller RDS instance but production adds read replicas. That difference is clearer in separate tfvars than in workspace-conditional spaghetti.
How do you isolate Terraform state across environments?
State isolation is non-negotiable. One corrupted or deleted state file can take down production. Follow the guidance in managing Terraform state safely: remote backend, locking, encryption, and versioning.
Each environment gets its own state key or bucket prefix. Never point dev and prod at the same state file.
# envs/production/backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/network/terraform.tfstate"
region = "ap-south-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
# envs/dev/backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "dev/network/terraform.tfstate"
region = "ap-south-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Some teams use separate AWS accounts per environment. That is the strongest blast-radius control. Dev engineers cannot accidentally read production secrets from the same account console.
How should environment-specific variables and secrets be handled?
Variables carry sizing, feature flags, and domain names. Secrets carry database passwords, API keys, and TLS private keys. Never commit secrets to Git, even in private repos.
Store non-secret defaults in terraform.tfvars per environment:
# envs/dev/terraform.tfvars
environment = "dev"
instance_type = "t3.small"
db_multi_az = false
min_capacity = 1
max_capacity = 2
domain_name = "dev.example.com"
# envs/production/terraform.tfvars
environment = "production"
instance_type = "m7g.large"
db_multi_az = true
min_capacity = 2
max_capacity = 8
domain_name = "app.example.com"
Pull secrets from a vault at apply time. AWS Secrets Manager, HashiCorp Vault, or Sealed Secrets in GitOps each work. I document the AWS path in managing secrets with AWS Secrets Manager.
For local sanity checks, validate JSON payloads with the JSON formatter before piping them into CI variables. Small typos in tfvars cause expensive partial applies.
Locals and validation blocks
Use locals to derive tags and names from a single environment variable:
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "environment must be dev, staging, or production."
}
}
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
Validation catches typos before AWS API calls burn minutes. On Symfony 8.1 apps with PHP 8.4.1, a parallel idea lives in Symfony environment config—centralise env naming early.
How do you promote IaC changes safely through CI/CD?
Manual terraform apply from a laptop works until it does not. CI gives logs, approvals, and repeatable runners. A promotion pipeline I trust looks like this:
- Pull request triggers
terraform fmt -check,validate, andplanfor dev. - Merge to main runs apply to dev automatically.
- Staging apply requires a manual job or release tag.
- Production apply needs two approvers and a fresh plan artifact.
- Post-apply smoke tests hit health endpoints before the pipeline marks green.
Scan IaC on every PR. Tools covered in IaC security scanning with tfsec and Checkov catch public S3 buckets before they ship. For containerised workloads, add Trivy scans on the same pipeline.
GitLab CI example for a plan-only job on pull requests:
plan:dev:
stage: plan
image: hashicorp/terraform:1.9
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- cd infra/envs/dev
- terraform init -input=false
- terraform plan -input=false -var-file=terraform.tfvars -out=plan.cache
artifacts:
paths:
- infra/envs/dev/plan.cache
For GitOps-heavy teams, Argo CD can reconcile Kubernetes layers while Terraform owns the network and data plane. Read GitOps across multiple clouds with Argo CD for the split-responsibility model.
What common mistakes break multi-environment IaC setups?
Most failures are organisational, not syntactic. Engineers reuse production IAM roles in dev because it is faster. A bad script later runs terraform destroy with prod credentials. Separate roles per environment add friction upfront and save weekends later.
Another classic mistake: hard-coded resource names. Two environments cannot both create app-db in one account. Prefix every name with ${local.name_prefix}.
Drift from console edits breaks the next plan. Enable AWS prescriptive guidance for Terraform practices: deny manual changes in prod accounts, or run scheduled drift detection.
Preview environments for pull requests help catch integration issues early. See preview environments for every PR. Pair that with Docker Compose for local Laravel dev so app and infra assumptions stay aligned.
Teams evaluating programmatic IaC should read Pulumi in real programming languages. The stack model maps cleanly to environment promotion if you treat stack config like tfvars.
Database schema changes still need their own promotion path. IaC provisions the RDS instance; migrations change the schema inside it. Coordinate both through database migrations in team environments so staging never lags production by ten revisions.
On legal-tech portals like Court Marriage In Nepal and sister sites on shared EC2, environment separation at the app layer often precedes full cloud IaC. The same rules apply: isolated config, isolated credentials, automated deploy paths. For larger builds, enterprise application development and ongoing support and maintenance keep infra and app releases in sync.
Multi-cloud state adds another layer. If you span providers, read managing multi-cloud state with Terraform before copying backends by hand. Validate generated plans with AI-generated IaC guardrails—never apply unchecked output.
For Kubernetes clusters at scale, Rancher for multiple clusters complements Terraform-managed networking. Each cluster becomes another environment surface with its own kubeconfig and RBAC.
Key Takeaways
- Reuse one module tree; isolate state with separate backend keys or accounts per environment.
- Keep secrets out of Git—inject them from a vault at apply time with distinct IAM roles per env.
- Promote changes dev → staging → production through CI with plan artifacts and manual prod approval.
- Prefix every resource name with an environment variable to avoid cross-env naming collisions.
- Run IaC security scans on every pull request before any apply job executes.
- Pair infra promotion with app migrations and smoke tests so staging actually predicts production behaviour.
People Also Ask
Should dev and production share the same Terraform code?
Yes—share modules and root patterns. Do not share state files, secrets, or unparameterised resource names. Differences belong in tfvars, workspace config, or stack settings, not forked copies of module logic.
Are Terraform workspaces enough for production isolation?
Workspaces separate state within one backend. They do not replace account-level isolation or IAM boundaries. Use workspaces for naming and small deltas; use separate accounts when compliance or blast radius demands it.
How do you handle environment-specific DNS and TLS in IaC?
Pass domain_name and certificate ARNs as variables per environment. Dev might use a subdomain with a cheap cert. Production uses the primary domain with auto-renewal. Keep cert resources in the same module to avoid manual ACM clicks.
What is the cheapest way to start multi-environment IaC on a small budget?
Start with directory-per-env on one cloud account, remote state on object storage, and GitLab CI free-tier runners. Dev uses minimal instance sizes—often Rs 3,000–5,000/month (~USD 22–37)—while production scales up. Add account separation when revenue or compliance requires it.
Ship environments you can trust
You manage multiple environments in IaC by treating infrastructure like application code: reviewed changes, isolated state, and promotion gates that respect production. Start with a clear directory layout, lock down state, wire CI next, then tighten secrets and scanning. If you want help designing Terraform or GitLab CI pipelines for a Laravel or WordPress stack, contact us to plan a rollout that fits your team size and budget.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

