Links#
https://developer.hashicorp.com/terraform/language/backend/s3
https://developer.hashicorp.com/terraform/cli/commands/init
https://registry.terraform.io/providers/hashicorp/aws/latest/docs
https://github.com/hashicorp/terraform-provider-aws/blob/main/website/docs/index.html.markdown
https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html1. Important Points#
Terraform AWS S3 backend:
S3 bucket:
stores terraform.tfstate
versioning should be enabled for recovery from accidental state overwrite/delete
state key:
path inside the bucket, for example order/prod/default/ap-east-1/network/terraform.tfstate
use_lockfile:
enables S3-native state locking
creates a .tflock object while Terraform is running
deletes the .tflock object when the run finishes
correct option is use_lockfile, not user_lockfilescope:
this page covers AWS provider guardrails and S3 backend config
includes manual S3 backend bucket bootstrap
AWS resource tag design is covered in Terraform AWS Tags
AWS resource naming pattern is covered in Terraform AWS Naming
S3 bucket naming and bucket policy are covered in Terraform AWS S3 Bucket
IAM role design and AssumeRole usage are covered in Terraform AWS IAM Role
IAM user manual Terraform migration is covered in Terraform AWS IAM User Manual Terraform
GCP and Azure should have their own backend pagesAWS resource tags:
AWS resource naming:
IAM role design and creation usage:
S3 state bucket naming and policy:
2. AWS Provider Account And Region Guardrails#
AWS provider 的账号防呆用 allowed_account_ids。Terraform 初始化 provider 时会检查当前 credentials 对应的 AWS account ID;如果不在允许列表里,provider 会失败,避免在错误账号里 plan/apply。
Region 防呆通常不靠 provider 内置 allow list,而是靠:
region guardrails:
provider region must be explicit
aws_region variable must have validation
provider alias must bind to a specific region/account
backend key must include workload region
CI should run aws sts get-caller-identity and aws configure get region / AWS_REGION checkprovider "aws" {
region = var.aws_region
allowed_account_ids = [
var.aws_account_id
]
default_tags {
tags = local.common_tags
}
}variable "aws_region" {
type = string
description = "AWS region for this root module"
validation {
condition = contains([
"ap-east-1",
"ap-southeast-1"
], var.aws_region)
error_message = "aws_region must be one of: ap-east-1, ap-southeast-1."
}
}
variable "aws_account_id" {
type = string
description = "Expected AWS account ID for this environment"
}aws_region = "ap-east-1"
aws_account_id = "123456789012"why use allowed_account_ids:
prevents using wrong AWS_PROFILE / credentials
catches accidental dev credentials while applying prod
catches CI role misconfiguration early
makes account boundary explicit in code review
why validate region:
prevents typo or unapproved region such as ap-east-2
prevents accidental deploy to a region not approved for this system
makes multi-region expansion explicit in code review
what it is not:
not IAM authorization
not SCP / permission boundary
not a replacement for backend bucket/key isolation
not a replacement for reviewing terraform planwith assume role#
Use allowed_account_ids together with assume_role in multi-account setups. The allowed account should be the final target account Terraform manages. Keep region explicit in the same provider block.
provider "aws" {
region = "ap-east-1"
allowed_account_ids = [
"123456789012"
]
assume_role {
role_arn = "arn:aws:iam::123456789012:role/TerraformProvisionRole"
session_name = "terraform-prod-default"
}
default_tags {
tags = {
Environment = "prod"
ManagedBy = "terraform"
}
}
}provider aliases#
Use provider aliases when one root module legitimately needs more than one AWS provider instance.
good cases:
same environment, same account, multiple workload regions
same environment, cross-account dependency such as shared DNS / shared network
bootstrap module that intentionally wires two tightly related accounts
avoid:
dev and prod in the same root module
uat and prod in the same root module
many unrelated accounts in one root module
using aliases to avoid proper state/workspace/root-module separationGood case: one prod root module manages active resources in two regions in the same account.
provider "aws" {
region = "ap-east-1"
allowed_account_ids = ["123456789012"]
}
provider "aws" {
alias = "singapore"
region = "ap-southeast-1"
allowed_account_ids = ["123456789012"]
}module "prod_hong_kong_ecs" {
source = "./modules/ecs-service"
providers = {
aws = aws
}
}
module "prod_singapore_ecs" {
source = "./modules/ecs-service"
providers = {
aws = aws.singapore
}
}Good case: one prod workload module creates records in a shared DNS account.
provider "aws" {
region = "ap-east-1"
allowed_account_ids = ["123456789012"]
}
provider "aws" {
alias = "shared_dns"
region = "ap-east-1"
allowed_account_ids = ["999999999999"]
assume_role {
role_arn = "arn:aws:iam::999999999999:role/terraform-shared-dns"
session_name = "terraform-prod-order"
}
}module "service_dns" {
source = "./modules/route53-record"
providers = {
aws = aws.shared_dns
}
}rule:
provider aliases should still be part of one coherent deployment boundary
if a plan can change dev and prod together, the root module boundary is wrong
split different environments into different root modules and different backend keysverify account before plan#
aws sts get-caller-identity
aws configure get region
terraform init
terraform plan -var-file=envs/prod.tfvarsfailure pattern:
terraform uses credentials for account 111111111111
provider allowed_account_ids only contains 123456789012
plan/apply fails before managing resources
terraform uses aws_region = eu-west-1
variable validation only allows ap-east-1 / ap-southeast-1
validate/plan fails before provider manages resources
related option:
forbidden_account_ids is the inverse guardrail
allowed_account_ids conflicts with forbidden_account_ids
prefer allowed_account_ids for production root modules
there is no matching allowed_regions option in the AWS providerproduction rule:
every AWS root module should set allowed_account_ids
every AWS root module should validate aws_region
each env tfvars should include expected aws_account_id
each env tfvars should include expected aws_region
CI should pass the same env tfvars used for the reviewed plan
do not set skip_requesting_account_id in normal AWS accounts
do not set skip_region_validation in normal AWS regions3. State Bucket Strategy#
Terraform state bucket 分配先按 AWS account 边界设计,再按管理复杂度决定是否按 region 拆 bucket。不要一开始就把所有账号、所有环境、所有 region 都塞进一个 bucket,除非平台团队明确需要 central state bucket。
recommended default#
recommended:
one state bucket per AWS account
bucket lives in one fixed home region
state key carries env / env_variant / workload region / component
example buckets:
acme-tfstate-111111111111-ap-east-1 # dev account
acme-tfstate-222222222222-ap-east-1 # uat account
acme-tfstate-333333333333-ap-east-1 # prod accountwhy:
account boundary is clear
IAM and bucket policy are simpler
blast radius is smaller than one central bucket
bucket count stays manageable
workload resources can still be multi-region because region is in the keycommon bucket models#
| Model | When To Use | Pros | Cons |
|---|---|---|---|
| Per account, fixed home region | default for most teams | clear account boundary, simple IAM, manageable bucket count | state for all workload regions is stored in home region |
| Per account, per region | strict region isolation or regulatory requirement | strong regional separation | more buckets, more backend configs, harder bootstrap |
| Central state bucket across accounts | mature platform team owns all Terraform state centrally | centralized audit and backup | complex cross-account policy, larger blast radius |
default decision:
use per account + fixed home region
put account ID and bucket region in bucket name
put environment, environment variant, workload region, and component in state keybucket naming#
Detailed bucket naming rules are covered in Terraform AWS S3 Bucket.
S3 bucket name must be globally unique in the shared global namespace. Add organization, AWS account ID, and state bucket region to avoid collision.
recommended pattern:
<company>-aws-terraform-<account_alias>-<account_id>-tfstate-<state_bucket_region>
example:
acme-aws-terraform-prod-123456789012-tfstate-ap-east-1bucket rules:
use lowercase letters, numbers, and hyphen
include AWS account ID
include state bucket region
do not include secret/project codename
do not include environment if one bucket stores many env/variant keys
do not include workload region if state bucket region is fixed4. State Key Strategy#
State key is the object path inside the S3 bucket.
principle:
bucket boundary first
key pattern second
do not repeat account/env/region in key if bucket already provides that boundary
include workload_region in key when one bucket stores states for multiple workload regionskey fields#
Use only the fields needed by the bucket model.
account:
account alias or account ID
env_group:
dev / uat / prod
env_variant:
default / market / hotfix / perf
workload_region:
ap-east-1 / ap-southeast-1 / global
system:
order / payment / platform
component:
network / ecs / rds / iam / route53model 1: one shared bucket for many accounts/environments#
Use this only when a platform team centrally owns state storage and bucket policy.
bucket:
acme-tfstate-central-999999999999-ap-east-1
key pattern:
<account_alias>/<env_group>/<env_variant>/<workload_region>/<system>/<component>/terraform.tfstate
examples:
dev-app/dev/default/ap-east-1/order/network/terraform.tfstate
uat-app/uat/market/ap-east-1/order/ecs/terraform.tfstate
prod-app/prod/default/ap-southeast-1/order/ecs/terraform.tfstatekey must include:
account
env_group
env_variant
workload_region
because bucket does not provide those boundariesmodel 2: each account has its own state bucket#
This is the normal default for many teams.
bucket:
acme-tfstate-111111111111-ap-east-1 # dev account
acme-tfstate-222222222222-ap-east-1 # uat account
acme-tfstate-333333333333-ap-east-1 # prod account
key pattern:
<system>/<env_group>/<env_variant>/<workload_region>/<component>/terraform.tfstate
examples:
order/dev/default/ap-east-1/network/terraform.tfstate
order/uat/default/ap-east-1/network/terraform.tfstate
order/uat/market/ap-east-1/network/terraform.tfstate
order/uat/hotfix/ap-east-1/network/terraform.tfstate
order/prod/default/ap-east-1/ecs/terraform.tfstatekey does not need account:
account boundary is already in bucket name and AWS account policy
keep env_group:
useful when one account contains multiple env groups or variants
can be omitted only if the account is strictly one environmentmodel 3: one account, one state bucket, many workload regions#
Use this when one account deploys resources to multiple AWS regions, but the state bucket is centralized in one home region.
bucket:
acme-tfstate-333333333333-ap-east-1
bucket region:
ap-east-1
workload regions:
ap-east-1
ap-southeast-1
global
key pattern:
<system>/<env_group>/<env_variant>/<workload_region>/<component>/terraform.tfstate
examples:
order/prod/default/ap-east-1/network/terraform.tfstate
order/prod/default/ap-southeast-1/ecs/terraform.tfstate
platform/prod/default/global/iam/terraform.tfstateimportant:
backend region = S3 bucket region
workload_region in key = region where resources are deployed
these two can be differentmodel 4: each account and each workload region has its own state bucket#
Use only when there is a strict regional isolation or compliance requirement.
bucket:
acme-tfstate-333333333333-ap-east-1
acme-tfstate-333333333333-ap-southeast-1
key pattern:
<system>/<env_group>/<env_variant>/<component>/terraform.tfstate
examples:
order/prod/default/network/terraform.tfstate
order/prod/default/ecs/terraform.tfstatekey can omit workload_region:
bucket already represents account + state bucket region
use only if workload resources in that state are in the same region as the bucketkey rules#
rules:
one key = one Terraform root module state
split by ownership boundary and blast radius
keep workload_region in key when bucket stores multiple workload regions
use global as workload_region for global resources
keep env_variant if variants such as uat-market / uat-hotfix exist
include account only when one bucket stores multiple accounts
do not put every AWS resource into one giant state file| Scenario | State Bucket | State Key |
|---|---|---|
| Shared central bucket | acme-tfstate-central-999999999999-ap-east-1 |
prod-app/prod/default/ap-east-1/order/network/terraform.tfstate |
| Per account bucket | acme-tfstate-333333333333-ap-east-1 |
order/prod/default/ap-east-1/network/terraform.tfstate |
| Per account bucket, multi-region workload | acme-tfstate-333333333333-ap-east-1 |
order/prod/default/ap-southeast-1/ecs/terraform.tfstate |
| Per account per region bucket | acme-tfstate-333333333333-ap-southeast-1 |
order/prod/default/ecs/terraform.tfstate |
| Global resource | acme-tfstate-333333333333-ap-east-1 |
platform/prod/default/global/iam/terraform.tfstate |
5. backend.tf#
Use this when the backend config is fixed in the root module.
terraform {
backend "s3" {
bucket = "acme-tfstate-123456789012-ap-east-1"
key = "order/prod/default/ap-east-1/network/terraform.tfstate"
region = "ap-east-1"
encrypt = true
use_lockfile = true
}
}fields:
bucket:
S3 bucket that stores state
enable versioning on this bucket
key:
object path for this root module state
region:
S3 bucket region
encrypt:
asks S3 backend to use server-side encryption
use_lockfile:
enables S3-native locking with a .tflock objectState bucket baseline:
required:
versioning enabled
server-side encryption enabled
public access blocked
access limited to Terraform roles/admins
lifecycle policy reviewed, but do not expire current state
use_lockfile = true for S3-native state locking
avoid:
unversioned state bucket6. Manual State Bucket Bootstrap#
Use this when the Terraform state bucket is created manually or by a one-time bootstrap script before the normal Terraform backend is available.
manual bootstrap order:
create bucket
enable versioning
enable default encryption
block public access
enforce bucket owner object ownership
attach bucket policy
configure backend.hcl
run terraform init -backend-config=backend.hclcreate bucket#
export AWS_REGION=ap-east-1
export STATE_BUCKET=acme-tfstate-123456789012-ap-east-1
aws s3api create-bucket \
--bucket "$STATE_BUCKET" \
--region "$AWS_REGION" \
--create-bucket-configuration LocationConstraint="$AWS_REGION"note:
us-east-1 is special and does not use LocationConstraint
for ap-east-1 / ap-southeast-1 / other regions, include LocationConstraintenable versioning#
aws s3api put-bucket-versioning \
--bucket "$STATE_BUCKET" \
--versioning-configuration Status=EnabledVersioning is important because Terraform state overwrite/delete mistakes need an object version to recover from.
enable encryption#
SSE-S3 baseline:
aws s3api put-bucket-encryption \
--bucket "$STATE_BUCKET" \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
}'SSE-KMS:
use customer managed KMS key when central audit/key control is required
Terraform roles then also need kms:Decrypt and kms:GenerateDataKeyblock public access#
aws s3api put-public-access-block \
--bucket "$STATE_BUCKET" \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=trueobject ownership#
aws s3api put-bucket-ownership-controls \
--bucket "$STATE_BUCKET" \
--ownership-controls '{
"Rules": [
{
"ObjectOwnership": "BucketOwnerEnforced"
}
]
}'why:
disables ACL-based ownership complexity
bucket owner owns state and lockfile objects
simpler for centralized state bucket operationsbucket policy#
Bucket policy examples are covered in Terraform AWS S3 Bucket.
verify#
aws s3api get-bucket-versioning \
--bucket "$STATE_BUCKET"
aws s3api get-bucket-encryption \
--bucket "$STATE_BUCKET"
aws s3api get-public-access-block \
--bucket "$STATE_BUCKET"
aws s3api get-bucket-policy \
--bucket "$STATE_BUCKET"Then initialize Terraform:
terraform init -backend-config=backend.hcl7. backend.hcl#
Use backend.hcl when bucket/key/region differ by environment or repository.
bucket = "acme-tfstate-123456789012-ap-east-1"
key = "order/prod/default/ap-east-1/network/terraform.tfstate"
region = "ap-east-1"
encrypt = true
use_lockfile = trueInitialize with:
terraform init -backend-config=backend.hclwhy backend.hcl:
backend block does not support normal Terraform variables
backend.hcl keeps env-specific backend values outside reusable module code
do not put AWS credentials in backend.hcl8. Backend Examples By Bucket Model#
per account bucket: dev#
bucket = "acme-tfstate-111111111111-ap-east-1"
key = "order/dev/default/ap-east-1/network/terraform.tfstate"
region = "ap-east-1"
encrypt = true
use_lockfile = trueper account bucket: uat market#
bucket = "acme-tfstate-222222222222-ap-east-1"
key = "order/uat/market/ap-east-1/ecs/terraform.tfstate"
region = "ap-east-1"
encrypt = true
use_lockfile = trueper account bucket: prod ap-east-1 workload#
bucket = "acme-tfstate-333333333333-ap-east-1"
key = "order/prod/default/ap-east-1/ecs/terraform.tfstate"
region = "ap-east-1"
encrypt = true
use_lockfile = trueper account bucket: prod ap-southeast-1 workload#
bucket = "acme-tfstate-333333333333-ap-east-1"
key = "order/prod/default/ap-southeast-1/ecs/terraform.tfstate"
region = "ap-east-1"
encrypt = true
use_lockfile = truenote:
region above is the S3 state bucket region
ap-southeast-1 in key is the workload regionshared central bucket#
bucket = "acme-tfstate-central-999999999999-ap-east-1"
key = "prod-app/prod/default/ap-east-1/order/network/terraform.tfstate"
region = "ap-east-1"
encrypt = true
use_lockfile = trueper account per region bucket#
bucket = "acme-tfstate-333333333333-ap-southeast-1"
key = "order/prod/default/ecs/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
use_lockfile = true9. use_lockfile#
terraform plan/apply starts:
create lock object:
order/prod/default/ap-east-1/network/terraform.tfstate.tflock
another terraform process starts:
sees the lock object
fails/waits instead of writing state at the same time
terraform plan/apply finishes:
deletes the .tflock objectnotes:
use_lockfile is the Terraform backend option
user_lockfile is not a Terraform backend option
lockfile is stored in the same S3 bucket as state
S3 Object Lock is not required for use_lockfile
do not apply Object Lock retention/legal hold to .tflock objects
lockfile name is derived from the state key:
<key>.tflock
example:
key:
order/prod/default/ap-east-1/network/terraform.tfstate
lockfile:
order/prod/default/ap-east-1/network/terraform.tfstate.tflock
this is Terraform S3 backend behavior, not a project-specific convention10. Migration From Local State#
If a project already has local terraform.tfstate, migrate it after the S3 backend exists.
terraform init -backend-config=backend.hcl -migrate-stateVerify:
terraform state list
aws s3api list-object-versions \
--bucket acme-tfstate-123456789012-ap-east-1 \
--prefix order/prod/default/ap-east-1/network/rollback:
keep a local backup of terraform.tfstate before migration
do not delete the local backup until remote state is verified
if migration fails, fix backend config and rerun terraform init -migrate-state