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.
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.
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)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.
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.tfvarsWorkspaces 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.
| Pattern | Isolation / permission control | Operational cost | Suited use cases |
|---|---|---|---|
| Workspaces alone for environment splitting | Weak (logical partition of the same backend; limited RBAC) | Low | Previews, ephemeral environments, learning |
| Directory split + shared modules + per-environment state | Medium to strong (state and credentials separated, guarded in CI) | Medium | The default answer for the majority of projects |
| Repo split + module registry | Strong (VCS, RBAC, and approvals are all easy to separate) | Medium to high | Splitting domains with strict organizational, budgetary, or audit boundaries |
| Terraform Cloud: one Workspace per environment + variable/policy sets | Strong (RBAC, policies, and State Sharing controls) | Medium | Governance-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.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.
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.tfvarsOn 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.
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 requiredWithin 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.
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 stateRelying 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.
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 prodPro
問題 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.
正解: 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.
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.
Practice with certification-focused question sets
無料で問題を解いてみる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.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...