Skip to main content

Terraform State Strategy

Decision: Store Terraform state in each workload account, not centralized in the management account.

Why Per-Account State?

Security Isolation

AspectCentralized (Bad)Distributed (Good)
Dev compromiseExposes prod stateOnly dev affected
IAM rolesComplex cross-accountSimple per-account
Audit trailMixed in one bucketClear per account
ComplianceHard to demonstrateEasy separation

Industry Best Practices

AWS Well-Architected Framework:

"Store Terraform state in the same account as the resources being managed. This maintains the security boundary and follows the principle of least privilege."

Gruntwork/Terragrunt:

"Each environment (dev/staging/prod) should have its own state backend to prevent accidental cross-environment modifications and maintain security isolation."

Compliance Requirements

HIPAA:

  • Production PHI-related infrastructure state should NOT be in same bucket as dev/test
  • Separate accounts = separate audit trails
  • Easier to demonstrate to auditors

SOC 2:

  • Logical separation between environments
  • Different access controls per environment
  • Clear audit trails per environment

State Storage Architecture

Dev Account (216482851263)
├── S3: docustack-terraform-state-216482851263
│ ├── vpc/terraform.tfstate
│ ├── ecs/terraform.tfstate
│ └── rds/terraform.tfstate
└── DynamoDB: docustack-terraform-locks

Staging Account (staging-id)
├── S3: docustack-terraform-state-<staging-id>
└── ...

Production Account (prod-id)
├── S3: docustack-terraform-state-<prod-id>
└── ...

Management Account (348665872333)
├── S3: docustack-terraform-state-348665872333
│ └── management/scp-policies/terraform.tfstate # Only mgmt resources
└── DynamoDB: docustack-terraform-locks

Configuration

Account Configuration

# infrastructure-live/dev/account.hcl
locals {
account_name = "dev"
account_id = "216482851263" # Dev account
}

# infrastructure-live/staging/account.hcl
locals {
account_name = "staging"
account_id = "<staging-account-id>"
}

# infrastructure-live/prod/account.hcl
locals {
account_name = "prod"
account_id = "<prod-account-id>"
}

# infrastructure-live/management/account.hcl
locals {
account_name = "management"
account_id = "348665872333"
}

Root Terragrunt Config

# infrastructure-live/root.hcl
remote_state {
backend = "s3"
config = {
# State bucket in SAME account as resources
bucket = "docustack-terraform-state-${local.account_id}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "docustack-terraform-locks"
}
}

Deployment Workflow

Deploying to Dev

# Authenticate to DEV account
export AWS_PROFILE=docustack-sandbox-pwrusr
aws sts get-caller-identity # Verify: 216482851263

# Deploy bootstrap (creates state bucket)
cd infrastructure-live/bootstrap
terragrunt apply
# Creates: docustack-terraform-state-216482851263

# Deploy infrastructure
cd ../dev/us-east-1/vpc
terragrunt apply
# State stored in: s3://docustack-terraform-state-216482851263/dev/us-east-1/vpc/

Deploying to Management

# Switch to management account
export AWS_PROFILE=docustack-management-admin
aws sts get-caller-identity # Verify: 348665872333

# Deploy bootstrap for management
cd infrastructure-live/bootstrap-management
terragrunt apply
# Creates: docustack-terraform-state-348665872333

# Deploy management resources
cd ../management/us-east-1/scp-policies
terragrunt apply
# State stored in: s3://docustack-terraform-state-348665872333/management/us-east-1/scp-policies/

Common Mistakes

Wrong: Centralized State

# DON'T DO THIS
locals {
account_id = "348665872333" # Management account for ALL state
}

Problems:

  • Security risk: Developers need cross-account access to management
  • Blast radius: Compromise of management = all environments exposed
  • IAM complexity: Complex cross-account role assumptions
  • Against AWS best practices: Management should be minimal
  • HIPAA concern: Dev environment state in same bucket as prod

Right: Per-Account State

# DO THIS
locals {
account_id = "216482851263" # Dev account for dev state
}

Benefits:

  • Security isolation: Dev state can't access prod state
  • Compliance ready: Separate state for HIPAA/SOC2
  • Least privilege: Developers only access dev account state
  • Blast radius reduction: Compromise limited to one account

FAQ

Q: Can accounts share one bootstrap bucket?

A: No. Security isolation requires separate buckets.

Q: What if we have 50 accounts?

A: Use automation script. Still only ~5 min per account, $0.50/month each.

Q: Can we skip bootstrap and use Terragrunt auto-init?

A: Yes, but manual bootstrap gives more control and is easier to troubleshoot.

Q: What happens if we delete the bootstrap bucket?

A: You lose all state! Only delete after destroying all resources in that account.

Q: Do all accounts need the same DynamoDB table name?

A: Yes, for consistency. Table is per-account, so no conflicts.