What Is a Terraform Backend?
The backend answers two questions: where does the state file live, and where does Terraform run its operations? By default, state lives on your laptop in terraform.tfstate. The moment a second engineer joins your team, that stops working — you need a remote backend that everyone can access, with locking to prevent concurrent applies.
Think of the backend as the shared filing cabinet for your team's infrastructure records. The local backend is your personal desk drawer — fine when you work alone, unusable for a team.
Local Backend (Default)
No backend block = local backendState stored at ./terraform.tfstateFine for learning, bad for teamsS3 Remote Backend (Standard for AWS Teams)
# versions.tfterraform { backend "s3" { bucket = "razorpay-terraform-state" # S3 bucket for state key = "prod/api-service/terraform.tfstate" # path within bucket region = "ap-south-1" encrypt = true # SSE encryption for state dynamodb_table = "terraform-state-lock" # DynamoDB table for locking }}Setting Up the S3 Backend Prerequisites
# Create S3 bucket with versioningaws s3api create-bucket \ --bucket razorpay-terraform-state \ --region ap-south-1 \ --create-bucket-configuration LocationConstraint=ap-south-1 # Enable versioning so you can recover old stateaws s3api put-bucket-versioning \ --bucket razorpay-terraform-state \ --versioning-configuration Status=Enabled # Create DynamoDB table for state lockingaws dynamodb create-table \ --table-name terraform-state-lock \ --attribute-definitions AttributeName=LockID,AttributeType=S \ --key-schema AttributeName=LockID,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --region ap-south-1Initialising a Backend
# After adding or changing the backend block:terraform init # Terraform will ask: "Do you want to copy existing state to the new backend?"# Answer yes to migrate local state to S3 # If you change backend config without changing the backend type:terraform init -reconfigure # Migrate state from one backend to another:terraform init -migrate-statePartial Backend Configuration
Hardcoding bucket names in the backend block makes it hard to reuse across environments. Use partial config:
# versions.tf — only the backend type, no valuesterraform { backend "s3" {}}# Pass values at init time — different per environmentterraform init \ -backend-config="bucket=razorpay-terraform-state" \ -backend-config="key=prod/payments/terraform.tfstate" \ -backend-config="region=ap-south-1" \ -backend-config="dynamodb_table=terraform-state-lock"Terraform Cloud Backend
terraform { cloud { organization = "razorpay" workspaces { name = "payments-prod" } }}Backend Comparison
| Backend | State Storage | Locking | Best For |
|---|---|---|---|
| local | Local disk | None | Solo learning only |
| s3 | AWS S3 | DynamoDB | AWS teams |
| gcs | GCP Cloud Storage | Built-in | GCP teams |
| azurerm | Azure Blob | Built-in | Azure teams |
| Terraform Cloud | Managed | Built-in | Any team, managed |
COMMON MISTAKE / WARNING**Security:** Restrict who has access to the S3 state bucket. The state file contains plaintext sensitive values. Use a bucket policy that allows only the CI/CD role and senior engineers to read the bucket directly.
COMMON MISTAKE / WARNING**Common Mistake:** Putting the backend S3 bucket and DynamoDB table inside the same Terraform configuration they are used for. You cannot use Terraform to create the bucket that stores its own state. Create these resources manually or with a separate bootstrap configuration.