Terraform

Terraform Multi-Environment Design: Safely Separating dev/stg/prod

2026-04-19
NicheeLab Editorial Team

The single most important thing in multi-environment design is separating state and permissions. Production has an outsized blast radius, so any design weakness translates directly into risk.

This article walks through how to safely run dev/stg/prod using directory splits and shared modules as the foundation, combined with the right backend, variable design, and Terraform Cloud/Enterprise patterns. It also flags the pitfalls that show up most often on the exam.

Multi-Environment Principles and Isolation Levels

Think about dev/stg/prod isolation across five layers: code, state files, credentials, cloud account (or subscription/project), and network. At minimum, isolate state files and credentials per environment, and split the cloud account itself whenever you can.

Terraform workspaces are convenient but they are not a security boundary. They are just a logical state partition inside the same backend, so do not lean on workspaces alone to protect prod when strong isolation is required. The official guidance is to use a remote backend with reliable locking, isolated state, and clear access control.

  • Top priority: fully separate state files (per-environment key, ideally a separate bucket, storage account, or workspace)
  • Separate credentials per environment as well (different roles or service principals)
  • Prefer cloud account separation (keep IAM for prod distinct from non-prod)
  • Use a remote backend (S3 + DynamoDB lock, GCS, AzureRM, or Terraform Cloud/Enterprise)
  • Avoid the default workspace; name environments explicitly (dev/stg/prod)

Common-mistake checklist (exam prep)

BAD: Protecting prod with workspaces alone
BAD: Referencing variables in the backend block to switch environments (backends do not interpolate variables)
GOOD: Injecting per-environment backend config via -backend-config
GOOD: Using a different auth principal per environment (e.g. IAM Role, SPN)

Recommended Architecture: Directory Split + Remote Backend + Shared Modules

The pattern that holds up best both operationally and on the exam is to centralize shared modules in modules/ and absorb per-environment differences with envs/dev|stg|prod overlays. Split state per environment in the remote backend (separate key or storage) and enable locking.

In CI/CD, set up one job per environment directory, and clearly separate plan from apply. Require review on prod and restrict who can run apply.

  • Combine code reuse (modules/) with safe per-environment isolation (envs/)
  • Use S3/GCS/AzureRM or Terraform Cloud as the backend (locking and RBAC included)
  • Trigger CI per environment directory; require manual approval only for prod

Reference architecture (directory split + remote state + TFC integration)

Git Repo
  modules/
    network/
    app/
  envs/
    dev/
      main.tf      -> module invocation (dev variables)
      backend.hcl  -> state: s3://tfstate-nonprod/dev/...
    stg/
      main.tf
      backend.hcl  -> state: s3://tfstate-nonprod/stg/...
    prod/
      main.tf
      backend.hcl  -> state: s3://tfstate-prod/prod/...  (separate account/bucket)

CI/CD
  Job: plan-dev   -> envs/dev   -> Remote Backend (locking enabled)
  Job: plan-stg   -> envs/stg   -> Remote Backend (locking enabled)
  Job: plan-prod  -> envs/prod  -> Remote Backend (locking enabled, approval required)

Cloud Accounts/Projects
  nonprod-account  (dev, stg)
  prod-account     (prod)

Representative file excerpts

# envs/prod/backend.hcl (S3 example. Backends do not interpolate variables; pass via -backend-config)
bucket         = "tfstate-prod"
key            = "proj/prod/terraform.tfstate"
region         = "ap-northeast-1"
dynamodb_table = "tf-locks-prod"
encrypt        = true

# envs/prod/main.tf (invoke the shared module)
terraform {
  required_version = ">= 1.4.0"
  backend "s3" {}
}

provider "aws" {
  region = var.region
  # Use the prod-specific role/profile
}

module "network" {
  source = "../../modules/network"
  cidr_block = var.cidr
  tags = {
    env = "prod"
  }
}

# Example execution (CI etc.)
# terraform -chdir=envs/prod init -backend-config=backend.hcl -upgrade
# terraform -chdir=envs/prod plan -var-file=prod.tfvars

Comparing Design Patterns: Workspace-Driven vs Directory-Driven vs Repo Split

Workspaces are lightweight and convenient, but they are insufficient for strong isolation or permission separation. For the exam, lock in the principle that workspaces are not a security boundary. In practice, directory split + shared modules strikes the best balance. When organizational boundaries are clear, splitting by repository is also a viable option.

With Terraform Cloud/Enterprise, the standard approach is to wire up VCS integration, create one workspace per environment directory, and apply operational guardrails through variable sets and policy sets.

  • When strong isolation is required, separate prod at the account or project level
  • Workspaces work well for preview or ephemeral environments, but they are insufficient to protect prod
  • Promote module reuse by publishing to a Private Module Registry
PatternIsolation / permission controlOperational costSuited use cases
Workspaces alone for environment splittingWeak (logical partition of the same backend; limited RBAC)LowPreviews, ephemeral environments, learning
Directory split + shared modules + per-environment stateMedium to strong (state and credentials separated, guarded in CI)MediumThe default answer for the majority of projects
Repo split + module registryStrong (VCS, RBAC, and approvals are all easy to separate)Medium to highSplitting domains with strict organizational, budgetary, or audit boundaries
Terraform Cloud: one Workspace per environment + variable/policy setsStrong (RBAC, policies, and State Sharing controls)MediumGovernance-heavy environments and cross-team operations

Use workspaces in a supporting role (reflected in naming and tagging)

locals {
  env = terraform.workspace
  name_prefix = "proj-${terraform.workspace}"
}

resource "aws_s3_bucket" "logs" {
  bucket = "${local.name_prefix}-logs"
  tags = { env = local.env }
}
# Caveat: this alone is not enough to isolate prod. You still need separated state, credentials, and accounts.

Best Practices for Variables and State

Inject per-environment differences via -var-file. terraform.tfvars and *.auto.tfvars are loaded automatically, but if you let multiple environments' files live side by side you risk unintended apply, so it is safer to explicitly pass something like -var-file=envs/prod.tfvars at run time.

Backend configuration cannot reference variables — inject per-environment values with -backend-config=backend.hcl or similar. Never store secrets in VCS; use environment variables (TF_VAR_xxx), Terraform Cloud Sensitive variables, or external secret management such as Vault.

Cross-stack linking goes through data.terraform_remote_state to reference outputs, but keep cross-environment dependencies — and the permissions needed to read them — to the bare minimum.

  • Explicitly specify the var-file. Limit auto.tfvars usage to a single environment
  • Inject the backend via -backend-config (variable interpolation is not supported)
  • Use Sensitive variables, environment variables, or external secret management for secrets
  • Minimize remote_state references and the permissions that back them

Example of per-environment tfvars and a locals map

# envs/stg/variables.tfvars (example)
region = "ap-northeast-1"
cidr   = "10.20.0.0/16"

# modules/app/main.tf (absorb per-environment differences with var and locals)
variable "region" {}
variable "cidr" {}

locals {
  tags_common = { app = "proj" }
}

resource "aws_vpc" "this" {
  cidr_block = var.cidr
  tags = merge(local.tags_common, { env = terraform.workspace })
}

# Run (stg)
# terraform -chdir=envs/stg init -backend-config=backend.hcl
# terraform -chdir=envs/stg plan -var-file=variables.tfvars

Separating dev/stg/prod on Terraform Cloud/Enterprise

On Terraform Cloud/Enterprise, create one Workspace per environment directory and configure the VCS integration to trigger only on the relevant paths. Attach Variable Sets by tag, and put prod behind an approval flow with strict RBAC. Use Policy Sets (such as Sentinel) to enforce tag and region constraints and to protect critical resources.

Cross-Workspace state references go through the remote backend via data.terraform_remote_state. Grant only the minimum required read permissions — it is critical to prevent dev from casually reading prod state.

  • Workspace naming convention: proj-{env}-{stack}
  • Apply Variable Sets by tag, and protect prod with a separate set
  • Apply guardrails through Policy Sets (Sentinel/OPA)
  • Automate security and compliance checks with Run Tasks

Cross-Workspace state reference via the remote backend (TFC)

data "terraform_remote_state" "network_prod" {
  backend = "remote"
  config = {
    organization = "your-org"
    workspaces = { name = "proj-network-prod" }
  }
}

output "vpc_id" {
  value = data.terraform_remote_state.network_prod.outputs.vpc_id
}
# Note: read permission on the target Workspace's state is required

Deploy Order and Dependency Management (Cross-State Wiring)

Within a single environment, the typical apply order is network → security → platform → app. Terraform's depends_on is only effective within a single plan, so cross-Workspace or cross-state ordering must be orchestrated in CI/CD.

When state migration is required, avoid casual direct edits — work through it deliberately with import and state rm, or with state mv plus the appropriate options. With a remote backend, follow the tool's constraints and best practices to migrate safely.

  • Define job dependencies in CI to control apply ordering
  • Run terraform apply separately per environment; watch for lock contention if you parallelize
  • Treat the -target option as an emergency-only workaround, not a steady-state tool

Sample CI execution order with locking in mind

stages: [plan, approve, apply]

job plan-dev   : terraform -chdir=envs/dev  plan   -var-file=dev.tfvars
job apply-dev  : terraform -chdir=envs/dev  apply  -var-file=dev.tfvars (manual)
job plan-stg   : terraform -chdir=envs/stg  plan   -var-file=stg.tfvars (needs: apply-dev)
job approve-stg: manual approval
job apply-stg  : terraform -chdir=envs/stg  apply  -var-file=stg.tfvars
job plan-prod  : terraform -chdir=envs/prod plan   -var-file=prod.tfvars (needs: apply-stg)
job approve-prd: manual approval (RBAC: restricted)
job apply-prod : terraform -chdir=envs/prod apply  -var-file=prod.tfvars
# Remote backend locking prevents concurrent apply against the same state

Common Pitfalls and Exam-Ready Checklist

Relying on workspaces alone for prod isolation, trying to interpolate variables in the backend, and letting multiple environments' *.auto.tfvars files coexist all count against you in production and on the exam. Absorb changes through modules and var-files, and stay disciplined about separating state and permissions.

  • Workspaces are not a security boundary (frequently tested)
  • The backend block cannot reference variables — use -backend-config instead
  • Multiple environments' *.auto.tfvars files are dangerous — use explicit -var-file
  • Protect prod with a separate account and auth principal. Do not use the default workspace
  • Minimize remote_state references; do not leak unnecessary prod information into dev
  • Separate plan from apply, and require review on prod

Command cheat sheet

# Initialize (per-environment backend)
terraform -chdir=envs/prod init -backend-config=backend.hcl -upgrade

# Plan / apply (per-environment var-file)
terraform -chdir=envs/stg plan  -var-file=stg.tfvars
terraform -chdir=envs/stg apply -var-file=stg.tfvars

# Workspaces (use in a supporting role)
terraform workspace list
terraform workspace new dev
terraform workspace select prod

Check Your Understanding

Pro

問題 1

Which multi-environment design best satisfies all of these requirements? 1) prod is strongly isolated from dev/stg, 2) code duplication is minimized, 3) plan and apply reviews are separated, and 4) RBAC can be applied for future audits.

  1. Place shared modules in modules/ and invoke them from envs/dev|stg|prod directories. Separate remote backend state per environment and use a different account and auth principal for prod. Separate plan from apply per directory in CI, and require approval on prod.
  2. Manage everything in a single directory with a single backend, switch dev/stg/prod via workspaces only, and leave RBAC to Git reviews.
  3. Prepare three *.auto.tfvars files — one per environment — and add or remove them as needed. Share state to prevent drift.
  4. Share a single backend bucket between prod and dev/stg, separating only by key. Use terraform.workspace conditionals at apply time to skip critical resources.

正解: A

Strong isolation and auditability require separated state and credentials plus CI guards. A avoids duplication with shared modules and combines backend, account, RBAC, and review separation to satisfy every requirement. B isolates only via workspaces and is insufficient. C misuses *.auto.tfvars and also violates the isolation requirement by sharing state. D may be acceptable depending on operations, but it is a weak protection model for prod, and skipping critical resources through workspace conditionals is not a recommended design.

Frequently Asked Questions

Can I separate dev/stg/prod using workspaces alone?

Not recommended. Workspaces are a logical partition inside the same backend, not a security boundary. At minimum, isolate state and credentials for prod, and split the cloud account itself when possible.

Can I switch environments by using variables inside the backend block?

No. Backends are initialized before the plan phase, so they do not participate in variable interpolation. Pass per-environment files with -backend-config=... or use separate Terraform Cloud/Enterprise workspaces.

What is the safest way to manage per-environment differences?

Build a shared module and inject the differences from each environment directory via -var-file. Absorb naming and tags through terraform.workspace or a locals map, and never commit secrets to VCS.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.