Skip to main content

Remote State

Terraform remote state stores the terraform.tfstate file on a shared backend — like AWS S3 or Terraform Cloud — instead of on a local disk. Remote state allows a whole team to share infrastructure state safely and enables one Terraform configuration to read outputs from another.

What Is Terraform Remote State?

The state file is Terraform's memory. It records every resource Terraform has ever created — the EC2 instance ID, the S3 bucket name, the RDS endpoint. Without it, Terraform has no idea what exists in the real world.

By default, that memory lives in a file called terraform.tfstate on the laptop of whoever ran terraform apply last. That works fine when you are the only engineer. The moment a second engineer joins, you have a problem — they have a different state file, or no state file at all. Two engineers working against different state files is how infrastructure gets duplicated, orphaned, or broken.

Remote state solves this by moving the state file off individual laptops and into a shared, durable location that every engineer and every CI/CD pipeline reads from and writes to.

At Zerodha, the infrastructure team keeps state in an S3 bucket in ap-south-1. Every engineer runs terraform plan against the same state. Every CI/CD pipeline applies against the same state. Nobody can accidentally work against a stale local copy.

Bash
+------------------------------------------+
| WITHOUT remote state (dangerous) |
| |
| Engineer A laptop: terraform.tfstate |
| Engineer B laptop: terraform.tfstate |
| CI/CD pipeline: no state file |
| Result: three different views of |
| infrastructure, conflicts guaranteed |
+------------------------------------------+
|
v
+------------------------------------------+
| WITH remote state (safe) |
| |
| Engineer A ---+ |
| Engineer B ---+---> S3 terraform.tfstate |
| CI/CD ---+ |
| Result: one shared source of truth |
+------------------------------------------+

Setting Up Remote State on AWS S3

You need two AWS resources before configuring remote state: an S3 bucket to store the state file, and a DynamoDB table to handle locking (covered in the State Lock glossary term).

HCL
# bootstrap/main.tf
# Create these resources ONCE manually or with a separate bootstrap config
# Never store the state of your state bucket in itself
resource "aws_s3_bucket" "terraform_state" {
bucket = "razorpay-terraform-state-ap-south-1" # must be globally unique
# Prevent accidental deletion of this critical bucket
lifecycle {
prevent_destroy = true
}
}
# Enable versioning — recover any previous state file version if corruption occurs
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
# Encrypt state at rest — state files contain sensitive values in plaintext
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms" # KMS for state — stronger than AES256
}
}
}
# Block all public access — state files must never be public
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST" # no capacity planning needed
hash_key = "LockID" # exact key name Terraform expects
attribute {
name = "LockID"
type = "S" # string
}
}

Configuring the Backend

Once the S3 bucket and DynamoDB table exist, add the backend block to your Terraform configuration:

HCL
# versions.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "razorpay-terraform-state-ap-south-1"
key = "prod/payments-api/terraform.tfstate" # path inside the bucket
region = "ap-south-1"
dynamodb_table = "terraform-state-lock" # enables locking
encrypt = true # encrypt the state file at rest
}
}

The key argument is the path inside the bucket where Terraform stores the state file. Use a hierarchy that reflects your organisation:

Bash
s3://razorpay-terraform-state-ap-south-1/
prod/
payments-api/terraform.tfstate
notifications/terraform.tfstate
shared-networking/terraform.tfstate
staging/
payments-api/terraform.tfstate
dev/
payments-api/terraform.tfstate

Running terraform init After Adding a Backend

After adding the backend block for the first time, run terraform init. Terraform detects the new backend and offers to migrate any existing local state to S3:

Bash
terraform init
# Initializing the backend...
# Do you want to copy existing state to the new backend?
# Pre-existing state was found while migrating the previous backend.
# Would you like to copy this state to the new backend?
# Enter a value: yes
#
# Successfully configured the backend "s3"!
# Terraform will automatically use this backend unless the backend
# configuration changes.

Reading Remote State From Another Configuration

The most powerful feature of remote state is cross-configuration references. The networking team's Terraform configuration outputs the VPC ID. The compute team's configuration reads that output without needing to hardcode or duplicate the value.

HCL
# In the networking configuration — outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
HCL
# In the compute configuration — reads networking outputs via remote state
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "razorpay-terraform-state-ap-south-1"
key = "prod/shared-networking/terraform.tfstate"
region = "ap-south-1"
}
}
resource "aws_instance" "app" {
# Reference the VPC output from the networking team's state
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
# No hardcoded subnet ID — always reads the current value from state
}

Troubleshooting Remote State

Error Root Cause Fix
NoSuchBucket: The specified bucket does not exist S3 bucket not created yet Create the bucket before running terraform init
AccessDenied on S3 IAM permissions missing Add s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket
ResourceNotFoundException on DynamoDB Lock table not created Create the DynamoDB table with LockID as partition key
Error: Backend configuration changed Backend block was modified Run terraform init -reconfigure
Error: Failed to read state State file corrupted Restore from S3 versioned backup
Remember

The S3 bucket for state must exist before you run terraform init with the backend configured. You cannot use Terraform to create the bucket that stores its own state — bootstrap it manually or with a separate one-time script.

Common Mistake

Putting the backend configuration in a terraform.tfvars file or using variables inside the backend block. Backend blocks cannot use variables — all values must be hardcoded literals or passed via -backend-config flags.

Frequently Asked Questions

Why is local state considered dangerous for anything beyond solo experimentation?

Local state lives as a plain JSON file on one person's disk with no locking, so two people running `terraform apply` at the same time can corrupt the state or silently overwrite each other's changes. Remote state backends like S3 (paired with DynamoDB for locking) or Terraform Cloud solve this by centralizing the state file and enforcing a lock during applies, so only one operation can modify infrastructure at a time — and the state survives even if someone's laptop dies.

What's the risk of storing Terraform state without encryption or access controls?

The state file often contains sensitive values in plaintext — database passwords, API keys, certificate private keys — anything passed as a resource argument or output, even ones marked `sensitive` in the config (that flag only hides it from CLI output, not from the file itself). Always enable encryption at rest on the backend (e.g., S3 bucket encryption) and restrict read access tightly, since read access to state is effectively read access to your infrastructure's secrets.