Multi-region is not about standing up duplicate infrastructure — it is about being able to switch over intentionally with consistent operations. Terraform gives you reproducible configuration, but meeting the RTO/RPO requirements of a real DR plan requires careful provider design, deliberate state boundaries, and controlled concurrent execution.
Based on the documented behavior, this article walks through the Active-Active vs Active-Passive trade-offs, provider aliases, backend availability and locking, the proper role of workspaces, and the pitfalls most often tested on the exam.
DR requirements are defined by RTO (Recovery Time Objective) and RPO (Recovery Point Objective). Terraform provides reproducible infrastructure definitions, but actually hitting your RTO/RPO depends on how you design state management, dependencies, and apply order. Active-Active prioritizes traffic distribution and data consistency, while Active-Passive prioritizes a clear cutover procedure and lower cost.
Terraform's job is to apply the same definition to multiple regions without contradictions, to keep concurrent applies from clashing, and to never corrupt state. DNS or traffic cutover is handled by cloud-side services (such as global DNS or a traffic manager) and woven into your overall operational plan.
Conceptual multi-region apply flow
Iterating across regions is cleanest and safest when you invoke one module per region. Providers cannot be used with for_each, so you define one aliased provider per region and pass it into each module. Inside the module, you simply accept the default aws (or target-cloud) provider, and the same code runs against each region.
Make dependencies explicit through module inputs and outputs instead of relying on implicit data source references. Locking prevents conflicts during concurrent applies, and you should avoid mutual references — enforce a one-way wiring discipline.
Example of aliased providers and module invocations (using AWS to illustrate the pattern)
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = ">= 5.0" }
}
}
variable "regions" {
# primary/secondaryを固定名で扱う。実務では環境毎の変数ファイルで値を切替
type = object({
primary = object({ region = string })
secondary = object({ region = string })
})
}
provider "aws" {
alias = "primary"
region = var.regions.primary.region
}
provider "aws" {
alias = "secondary"
region = var.regions.secondary.region
}
# 1リージョン=1モジュールの原則。呼び出し側で2回呼ぶ
module "app_primary" {
source = "./modules/app"
providers = { aws = aws.primary }
region = var.regions.primary.region
}
module "app_secondary" {
source = "./modules/app"
providers = { aws = aws.secondary }
region = var.regions.secondary.region
}
# modules/app/main.tf(抜粋)
# module側はデフォルトawsプロバイダを受けるだけ
variable "region" { type = string }
resource "aws_s3_bucket" "logs" {
bucket = "nicheelab-logs-${var.region}"
tags = { managed_by = "terraform", region = var.region }
}
Draw state boundaries based on blast radius and the independence of parallel work streams. For multi-region setups, weigh the trade-offs between managing both regions in a single state versus splitting state per region. Generally, prioritize fault isolation during a regional outage and easy rollback — splitting state per region is the safer default.
Make locking mandatory on every remote backend. With the S3 backend, pair it with a DynamoDB lock table, and enable versioning plus encryption on the bucket. Cross-Region Replication is useful as a backup, but remember that the DynamoDB lock table itself is region-scoped. With the Terraform Cloud or Enterprise remote backend, locking, queuing, and high availability are provided by the service.
| State management unit | Pros | Risks / caveats | Typical use cases |
|---|---|---|---|
| Single state managing both regions | The whole picture fits in one plan/apply; dependencies are highly visible | A failed plan/apply takes down everything; parallel work easily collides | Small or learning environments; configurations with tight mutual dependencies that must update together |
| Per-region state (recommended) | Isolates blast radius; rollback is easy; handles parallel development well | Cross-region dependencies must be explicitly wired through remote state | Staged updates in Active-Passive or Active-Active topologies |
| Further split by service × region | Tiny change units; clear separation of responsibilities | More state files means more operational complexity and dependency overhead | Large organizations with clear team boundaries, in either monorepo or multi-repo setups |
Partial S3 backend configuration with per-region apply
# backendは変数参照不可。部分定義して起動時に注入
terraform {
backend "s3" {}
}
# backend-us-east-1.hcl
bucket = "tfstate-prod"
key = "app/us-east-1/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-locks-prod"
encrypt = true
# backend-ap-northeast-1.hcl
bucket = "tfstate-prod"
key = "app/ap-northeast-1/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "tf-locks-prod"
encrypt = true
# 初期化と適用(リージョン単位で分けて実行)
terraform init -backend-config=backend-us-east-1.hcl
terraform apply -auto-approve
# 別ディレクトリ/別ワークスペースでap-northeast-1を同様に実行
terraform init -backend-config=backend-ap-northeast-1.hcl
terraform apply -auto-approve
Locking is non-negotiable. Use DynamoDB locking with the S3 backend, or workspace locking and queuing with Terraform Cloud/Enterprise, to prevent simultaneous runs. Extend the wait time with -lock-timeout when needed.
Connect dependencies primarily through module inputs and outputs, and avoid overusing data sources. Keep the remote state data source strictly one-way — never create cycles. For plan stability, an explicit depends_on in a module is a useful tool.
Detect drift with terraform plan or refresh-only applies. When destructive changes are required, use lifecycle's create_before_destroy or replace_triggered_by to make the swap effectively atomic.
Example of one-way dependency wiring via remote state
# us-east-1側のネットワーク出力を、ap-northeast-1側で参照(片方向)
# ap側のコード(抜粋)
data "terraform_remote_state" "primary_net" {
backend = "s3"
config = {
bucket = "tfstate-prod"
key = "network/us-east-1/terraform.tfstate"
region = "us-east-1"
}
}
module "app_secondary" {
source = "./modules/app"
providers = { aws = aws.secondary }
region = var.regions.secondary.region
# 片方向に必要最小限の情報だけを受け取る
vpc_id = data.terraform_remote_state.primary_net.outputs.vpc_id
}
Build your pipeline around one stage per region and stop downstream stages on failure. For Active-Passive, the safe sequence is: update the standby first, verify health, switch traffic, then update the primary.
Run plan and apply against the same remote state and the same code revision. Rather than carrying a local plan around, standardize on a fresh plan in CI followed immediately by apply. With Terraform Cloud/Enterprise, per-workspace queuing controls concurrent execution for you.
GitHub Actions skeleton for sequential per-region apply (conceptual)
jobs:
apply-us:
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -backend-config=backend-us-east-1.hcl
- run: terraform plan -out=plan.out -lock-timeout=5m
- run: terraform apply -auto-approve plan.out
apply-ap:
needs: apply-us
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -backend-config=backend-ap-northeast-1.hcl
- run: terraform plan -out=plan.out -lock-timeout=5m
- run: terraform apply -auto-approve plan.out
Backend configuration cannot be parameterized with variables. Inject values via -backend-config or use separate environment files. Workspaces are intended for logical separation such as dev/stg/prod environments, not for splitting by region.
When renaming resources or moving them between regions, declare the state migration with a moved block to avoid unnecessary recreation. To bring existing resources under management, use either the import block or terraform import.
Targeted apply (-target) is a temporary workaround. Relying on it in steady-state operations will break your dependencies. Solve dependencies through module design instead.
Example of a moved block for region migration or renames
terraform {
required_version = ">= 1.5.0"
}
# 以前: aws_s3_bucket.primary
# 以後: aws_s3_bucket.this
moved {
from = aws_s3_bucket.primary
to = aws_s3_bucket.this
}
resource "aws_s3_bucket" "this" {
bucket = "nicheelab-logs-us-east-1"
}
Pro
問題 1
You run a production workload with us-east-1 as primary and ap-northeast-1 as standby. Which Terraform design best balances DR readiness with consistency?
正解: A
Per-region state isolates blast radius and makes rollback controllable, while S3 + DynamoDB locking enforces consistency. Invoking the same module twice maximizes reuse, and one-way remote state references make cross-region dependencies explicit. Running plan/apply sequentially per region in CI limits the impact of any failure. The other options bake in anti-patterns: large-scale failures on a single state, disabled locking, workspace misuse, and -target dependence.
If I enable Cross-Region Replication (CRR) on the S3 backend, does that also make the DynamoDB lock highly available?
No. CRR replicates S3 objects (state files), but the DynamoDB lock table is a separate, region-scoped service. Locks are taken against a table in a specific region, so you need to decide which table holds the lock and document the failover procedure separately from your replication strategy.
Is it acceptable to use Terraform workspaces to switch between regions?
Not recommended. Workspaces are best used for logical separation such as environments (dev/stg/prod). Region differences are typically handled with split state files or by invoking the same module twice. Using workspaces for regions blurs dependency and permission boundaries and raises the risk of misapplied changes.
Can I safely run apply against both regions in parallel?
Sequential execution is the safe default. If you must parallelize, give each region its own state, enable locking on every state, and verify there is no mutual dependency via remote state. Cross-references between states can cause cycles or partial failures.
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...