Designing Production-Ready AWS Infrastructure with Terraform
Network boundaries, state isolation, reviewed changes, and recovery planning for a hypothetical AWS workload.
Isolate blast radius with accounts, identities, state, and promotion of the same artifact—not a shared admin role with three VPC names.
Calling three VPCs “dev,” “staging,” and “prod” does not make them environments if they share an account, a Terraform state file, and a human administrator role. An environment is a blast-radius decision: what a leaked credential, a bad apply, or a compromised pipeline can reach.
This guide uses a hypothetical AWS organization (the same isolation ideas apply to Azure management groups and subscriptions). It is not a client landing-zone design.
Prefer account or subscription separation for at least production versus everything else. Stronger layouts add a shared-services account, a security/logging account, and per-team development accounts. The cost is more identity and networking work. The benefit is that a development apply cannot delete the production database.
# Educational remote-state layout. One backend per environment.
terraform {
backend "s3" {
bucket = "example-terraform-state-prod"
key = "app/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "example-terraform-locks-prod"
encrypt = true
}
}Do not put every environment in one state key. A single terraform destroy typo should not be able to walk the company.
State backends, KMS keys, and lock tables are production privileges. Restrict who can read state: it often contains secret material even when you tried to avoid that.
Private subnets do not help if a CI role can create IAM users or read every secret. Map:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:example/api:environment:production"
}
}
}
]
}This is an educational trust-policy sketch. Account ids and repo names are placeholders. The important part is the sub restriction: a pull request from a fork should not assume the production role.
Azure equivalent: federated credentials on a workload identity, scoped to the pipeline and environment—not a client secret in a variable group that every branch can use.
Development, staging, and production should run the same image digest and a reviewed infrastructure plan aimed at that environment. Rebuilding for production invents a new artifact. Copy-paste of terraform.tfvars between environments is how a development CIDR lands in production.
Configuration differences (URLs, replica counts, log levels) belong in environment-scoped variables or parameter stores. Secrets belong in a manager, injected with the environment’s identity. They do not belong in Git, AMI images, or a shared “prod-and-dev” secret.
Public load balancers, private application subnets, private data stores. Security groups from the load balancer to the app, not 0.0.0.0/0 on the data tier “because the app is private.”
Production data should not be copied to development laptops or to a shared staging database without masking and a recorded purpose. Restore tests belong in an isolated account.
Production-ready AWS with Terraform covers modules, plans, and recovery. Supply-chain security covers the pipeline identity that crosses these boundaries. See Cloud Infrastructure, DevSecOps, and DevOps consulting. The architecture lab is a single hypothetical VPC—use it as vocabulary, not as a landing zone.
Network boundaries, state isolation, reviewed changes, and recovery planning for a hypothetical AWS workload.
Attribute spend, remove idle waste, and treat unused production capacity as a named reliability choice—not an automatic saving.
How HPA, resource requests, and cluster capacity interact—and why a target utilization is not a capacity plan.