Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Manage Multiple Environments in IaC

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.

One IaC Repo, Three EnvironmentsIaC ModulesTerraform / OpenTofuDevSmall, open accessStagingProd-like scaleProductionLocked, monitoredShared: VPC module, RDS module, ALB moduleDifferent: instance size, replica count, CIDR blocks
Manage multiple environments in IaC by reusing modules while varying only environment-specific inputs.

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.

PatternHow it worksBest forWatch out for
Directory per environmentenvs/dev, envs/prod each call shared modulesSmall teams, clear ownershipDuplicated backend config if copy-pasted
WorkspacesOne root module, terraform workspace selectIdentical topology, different namesEasy to apply to wrong workspace
Separate state backendsUnique S3 bucket or key per envStrong isolation, audit trailsMore backend boilerplate
Stack-per-env (Pulumi/CDK)Programmatic stacks with config layersTeams already using real languagesRequires 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.

Remote State IsolationDev ApplyStaging ApplyProd ApplyKey: dev/...Separate stateKey: staging/...Separate stateKey: prod/...Separate stateS3 Bucket + DynamoDB Lock TableEncryption on, versioning enabled, no shared keys
Separate state keys per environment prevent accidental cross-environment destroys during terraform apply.

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:

  1. Pull request triggers terraform fmt -check, validate, and plan for dev.
  2. Merge to main runs apply to dev automatically.
  3. Staging apply requires a manual job or release tag.
  4. Production apply needs two approvers and a fresh plan artifact.
  5. 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.

IaC Promotion PipelinePR + PlanDev ApplyStagingProductionSecurity scan + fmt + validate on every commitManual approvalRequired for prodTwo reviewers minSmoke testsHTTP health checkDB connectivity probe
CI promotion gates let you manage multiple environments in IaC without skipping staging or bypassing review on production.

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.

Environment Pattern DecisionSame topology?YesNoWorkspacesOr tfvars onlyDir per envSeparate modulesNeed hard isolation?Separate cloud accountsPulumi stacks?See Pulumi stack docs
Choose workspaces, directory layouts, or separate accounts based on topology sameness and isolation requirements.

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

You define dev, staging, and production once in Terraform, OpenTofu, or Pulumi, reuse one module tree, and promote changes through CI instead of manual console edits.

Dev, staging, and production carry different risk, cost, and access requirements. Dev can tolerate broken builds; staging should mirror production topology at smaller scale; production needs change control, backups, and monitoring. Without IaC discipline, environments drift—someone patches a security group by hand in staging and 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 of managing multiple environments in code rather than by memory.

Four patterns cover most teams. Directory per environment uses folders like envs/dev and envs/prod calling shared modules—best for small teams with clear ownership, but watch for duplicated backend config if copy-pasted. Workspaces use one root module with terraform workspace select—good when topology is identical and only names differ, but easy to apply to the wrong workspace. Separate state backends use unique S3 keys or bucket prefixes per env for strong isolation and audit trails. Stack-per-env with Pulumi or CDK suits teams already using real programming languages but needs discipline on shared libraries.

State isolation is non-negotiable because one corrupted or deleted state file can take down production. Each environment gets its own remote backend with a separate state key or bucket prefix, locking via DynamoDB, encryption, and versioning enabled. Never point dev and prod at the same state file. Some teams use separate AWS accounts per environment—that is the strongest blast-radius control because dev engineers cannot accidentally read production secrets from the same account console. Separate state keys also prevent accidental cross-environment destroys during terraform apply.

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—for example dev might use t3.small instances with db_multi_az false, while production uses m7g.large with multi-AZ enabled. Pull secrets from a vault at apply time using AWS Secrets Manager, HashiCorp Vault, or Sealed Secrets in GitOps. Use locals and validation blocks to derive name prefixes and tags from a single environment variable, catching typos before AWS API calls burn minutes on partial applies.

Manual terraform apply from a laptop works until it does not. A promotion pipeline I trust runs terraform fmt -check, validate, and plan for dev on every pull request. Merge to main applies 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 with tfsec and Checkov to catch public S3 buckets before they ship. For containerised workloads, add Trivy scans on the same pipeline. GitOps-heavy teams can let Argo CD reconcile Kubernetes while Terraform owns network and data plane.

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.

Workspaces separate state within one backend. They do not replace account-level isolation or IAM boundaries when compliance or blast radius demands stronger controls.

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).

Most failures are organisational, not syntactic. Engineers reuse production IAM roles in dev because it is faster, then a bad script runs terraform destroy with prod credentials—separate roles per environment add friction upfront and save weekends later. Hard-coded resource names cause collisions when two environments try to create the same resource in one account; prefix every name with an environment-derived local. Drift from console edits breaks the next plan—deny manual changes in prod accounts or run scheduled drift detection. Skipping staging, bypassing review on production, and applying unchecked AI-generated IaC output are other recurring traps I have seen on real client projects.

Workspaces suit identical infrastructure where dev and prod differ only by resource name prefixes and counts, not by entire subsystems. HashiCorp documents this in the official Terraform workspaces guide. 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. Choose workspaces when topology sameness is high and isolation requirements are modest. Choose directories or separate accounts when sizing, subsystems, or compliance boundaries diverge meaningfully between environments.

Pass domain_name and certificate ARNs as variables per environment through each env folder's terraform.tfvars. Dev might use a subdomain like dev.example.com with a cheap certificate. Production uses the primary domain like app.example.com with auto-renewal through ACM or your provider's equivalent. Keep certificate resources in the same shared module that provisions the load balancer or CDN front door so nobody clicks through cert creation manually after the first apply. This keeps TLS promotion aligned with the same CI gates that govern the rest of your infrastructure changes.

IaC provisions the RDS instance or database server; migrations change the schema inside it. Both need their own promotion path. Coordinate database migrations in team environments so staging never lags production by ten revisions—otherwise your terraform plan looks clean but the application fails on missing columns or indexes. On Laravel 13.x apps running PHP 8.3 or higher, run migrations as part of the same CI promotion that applies infrastructure, with smoke tests confirming health endpoints after both complete. Treat schema and infra as parallel tracks that must arrive together, not independent silos.

Run IaC security scans on every pull request before any apply job executes. Tools covered in IaC security scanning with tfsec and Checkov catch misconfigurations like public S3 buckets, overly permissive security groups, and missing encryption flags before they ship to dev—let alone production. For containerised workloads running alongside your Terraform-managed ECS or Kubernetes layers, add Trivy scans on the same pipeline. Scanning at PR time keeps fixes cheap; scanning only at production apply time means you discover the problem when approvers are waiting and rollback pressure is highest.

Preview environments for pull requests catch integration issues early, before changes reach shared dev or staging. Pair them with Docker Compose for local Laravel development so application assumptions stay aligned with what Terraform provisions. The mental model matches directory-per-env IaC: same recipe, different variables, different credentials. A PR preview validates that new module code works with current app code without risking staging state. On sister sites I maintain with Deployer 7 and GitLab CI, staging and production are separate deploy targets even when the IaC layer is thin—preview environments extend that same isolation mindset upstream in the development cycle.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: