When you first learn Terraform, state usually lives in a local terraform.tfstate file. That is fine for practice, but it breaks down quickly in real teams.
Imagine a shared production environment where:
Local state is not designed for that. A remote backend solves these problems by storing state in a shared external system.
In Terraform, a backend determines where state is stored and how Terraform interacts with it.
Remote backends matter for three big reasons:
Everyone works from the same source of truth. The state is not trapped on one developer laptop.
Pipelines can run Terraform without depending on a local file that only one person has.
State can be stored in protected infrastructure such as S3, Terraform Cloud, or GCS, with access controls, encryption, backup, and audit features.
A backend is configured in the terraform block.
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "network/prod/terraform.tfstate"
region = "us-east-1"
}
}The backend is not a provider resource. It is part of Terraform's own configuration. It tells Terraform where to store and read state.
Backends may also provide extra capabilities such as:
Let us compare local and remote state in practical terms.
| Concern | Local State | Remote Backend |
|---|---|---|
| Shared access | Poor | Strong |
| CI/CD usage | Awkward | Natural |
| Durability | Depends on one machine | Stored centrally |
| Locking | Usually weak or absent | Often supported |
| Access control | File-system based | Cloud IAM / platform ACLs |
| Backups | Manual | Easier with backend features |
For production, remote state is the standard pattern.
On AWS, the most common remote backend uses:
This combination is popular because it is simple, durable, and integrates well with existing AWS IAM policies.
terraform {
backend "s3" {
bucket = "devopslesson-terraform-state"
key = "network/prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}bucket: the S3 bucket that stores the state filekey: the path-like object name inside the bucketregion: AWS region where the bucket existsdynamodb_table: the table Terraform uses for lockingencrypt: enables server-side encryption for the stored objectYou usually create the backend infrastructure once before using it.
resource "aws_s3_bucket" "terraform_state" {
bucket = "devopslesson-terraform-state"
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
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 = "AES256"
}
}
}
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
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}S3 versioning is highly recommended. If state is overwritten accidentally or corrupted, versioning gives you recovery options.
Locking prevents two Terraform operations from writing to the same state at the same time.
Here is the problem without locking:
terraform applyterraform apply before A finishesThis can produce broken or inconsistent state.
With DynamoDB locking, Terraform first tries to create a lock entry. If the lock already exists, the second operation waits or fails.
terraform apply
-> request lock from DynamoDB
-> if lock acquired, read state from S3
-> run plan and apply changes
-> write updated state back to S3
-> release lock in DynamoDBThis pattern is a major reason S3 + DynamoDB is considered production-friendly.
terraform initAfter you add or change a backend block, Terraform needs to initialize it.
terraform initIf local state already exists, Terraform typically asks whether it should migrate that state into the configured remote backend.
This is one of the most convenient parts of Terraform: it can move your state for you during initialization.
Teams often avoid hardcoding every backend detail in source code, especially values that vary per environment.
A common pattern is partial backend configuration.
terraform {
backend "s3" {}
}bucket = "devopslesson-terraform-state"
key = "network/prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = trueThen initialize with:
terraform init -backend-config="backend-prod.hcl"This pattern is helpful when:
You can also pass multiple -backend-config flags if needed.
In CI/CD, a pipeline may choose a backend config based on branch, workspace, or deployment environment. That keeps the Terraform code generic while the pipeline controls where state actually lives.
Terraform Cloud is HashiCorp's managed platform for Terraform workflows.
A simple backend configuration looks like this:
terraform {
cloud {
organization = "devopslesson"
workspaces {
name = "production"
}
}
}Terraform Cloud can provide:
This can be a great option for teams that want less backend infrastructure to manage themselves.
On Google Cloud, a common backend choice is Google Cloud Storage.
Example:
terraform {
backend "gcs" {
bucket = "devopslesson-terraform-state"
prefix = "network/prod"
}
}The idea is similar to S3:
The exact details differ by platform, but the mental model stays the same.
A very common workflow is:
Terraform makes this easier than many beginners expect.
terraform initTerraform then stores future state in the remote backend.
You do not normally need to manually copy JSON files around. Terraform handles the migration logic safely through initialization.
Terraform workspaces let you maintain multiple state snapshots for the same configuration.
For example, you might have:
defaultdevstagingprodEach workspace has separate state.
This sounds convenient, but beginners should use workspaces carefully. They are helpful in some situations, but many teams prefer separate directories, separate backends, or separate root modules for strongly isolated environments.
A simple way to think about workspaces is:
Remote state is better than local state for teams, but you still need to secure it properly.
The backend is configured in the terraform block, not created with a normal resource block inside the same initialized workflow.
Be careful with credentials and backend config patterns. Backend config files may still reveal structure, even if they do not directly contain secrets.
terraform initWhenever backend configuration changes, terraform init is required.
Remote backends improve security, but access still needs to be locked down properly.
terraform init after backend changes.Remote backends are what turn Terraform from a personal tool into a team-ready platform. They centralize state, make CI/CD practical, improve durability, and reduce the risk of conflicting updates.
For AWS teams, S3 with DynamoDB locking is the most common starting point because it is simple and reliable. For teams that want a managed experience, Terraform Cloud is an excellent alternative. On Google Cloud, GCS fills a similar role.
The key idea is straightforward:
terraform init handles migration when you switch backendsOnce you understand remote backends, you are ready to manage Terraform state much more safely in real environments.
A practical habit for teams is to decide backend strategy early: one backend per environment, clear naming for state keys, locked-down access, and documented initialization steps. That small investment prevents confusion later when projects, teams, and automation pipelines all start touching the same infrastructure.
<MCQExercise exercise={{ id: 'tf-remote-backends-q1', title: 'Question 1: Why Remote Backends', description: 'What is one of the biggest reasons teams move from local state to a remote backend?', options: [ { id: 'a', text: 'Remote backends make providers unnecessary' }, { id: 'b', text: 'Remote backends allow shared state storage and safer team collaboration' }, { id: 'c', text: 'Remote backends remove the need for terraform init' }, { id: 'd', text: 'Remote backends automatically write your modules for you' }, ], correctId: 'b', explanation: 'Remote backends centralize state so teams and CI/CD pipelines can work from the same source of truth, and many of them support locking to prevent concurrent updates.', wrongExplanations: { a: 'Providers are still required to interact with cloud services. Backends only manage state storage.', c: 'terraform init is still required, especially when configuring or changing a backend.', d: 'Backends store state. They do not generate module code.', }, }} />
<MCQExercise exercise={{ id: 'tf-remote-backends-q2', title: 'Question 2: S3 and DynamoDB', description: 'In the common AWS remote backend pattern, what are S3 and DynamoDB each used for?', options: [ { id: 'a', text: 'S3 stores modules, and DynamoDB installs providers' }, { id: 'b', text: 'S3 stores the Terraform state file, and DynamoDB is used for state locking' }, { id: 'c', text: 'S3 stores provider credentials, and DynamoDB stores variables.tf' }, { id: 'd', text: 'S3 performs apply operations, and DynamoDB runs plan operations' }, ], correctId: 'b', explanation: 'In the standard AWS setup, S3 holds the remote state object and DynamoDB provides a lock so only one Terraform operation can modify that state at a time.', wrongExplanations: { a: 'Modules and providers are unrelated to the main purpose of the S3 + DynamoDB backend pattern.', c: 'State backends do not store variables.tf, and provider credentials are managed separately.', d: 'S3 and DynamoDB are storage and coordination services. They do not execute Terraform plan or apply themselves.', }, }} />
<MCQExercise exercise={{ id: 'tf-remote-backends-q3', title: 'Question 3: Migration', description: 'How do teams usually migrate Terraform state from local storage to a new remote backend?', options: [ { id: 'a', text: 'By deleting terraform.tfstate and letting Terraform recreate everything' }, { id: 'b', text: 'By adding a backend block and running terraform init so Terraform can migrate the state' }, { id: 'c', text: 'By renaming variables.tf to backend.tf' }, { id: 'd', text: 'By running terraform destroy before each migration' }, ], correctId: 'b', explanation: 'The normal workflow is to configure the backend, run terraform init, and let Terraform copy or migrate the existing state into the new backend.', wrongExplanations: { a: 'Deleting state is dangerous and can make Terraform lose track of infrastructure.', c: 'Renaming variables.tf has nothing to do with state migration.', d: 'Destroying infrastructure is not required to migrate state to a backend.', }, }} />
Create an S3 bucket with versioning and encryption enabled, then point your backend "s3" block at it with a key naming the state path. Modern Terraform supports native S3 locking via use_lockfile, which removes the need for a separate DynamoDB table; older versions require a DynamoDB table with a LockID partition key referenced by dynamodb_table. Either way, the bucket and table must exist before terraform init. They cannot be created by the same configuration that stores its state in them.
The chicken-and-egg problem is real: the bucket must exist before Terraform can use it for state. Two workable approaches. Create the bucket and lock table in a small separate configuration with local state, committed alongside everything else. It changes rarely, so local state is tolerable. Or create the resources with a CLI script or CloudFormation once, then reference them. Some teams create the backend resources in Terraform with local state, then migrate that configuration's own state into the bucket it just made.
Terraform refuses to proceed and reports the lock ID, who holds it and when it was acquired. This usually follows a process killed mid-apply. Confirm nobody is genuinely running an apply, then clear it with terraform force-unlock <LOCK_ID>. Do this carefully, force-unlocking while an apply is actually running is how state gets corrupted, which is exactly what locking exists to prevent.
Yes. Update the backend block and run terraform init, and Terraform detects the change and offers to migrate the existing state to the new location. It copies rather than moves, so the old state remains as a fallback. Take a manual backup first regardless, and run a plan immediately afterwards expecting no changes. A non-empty plan after a backend migration means something did not transfer correctly.
Use a separate state file per environment, typically by giving each its own key in the same bucket, production/terraform.tfstate, staging/terraform.tfstate. This means a mistake in staging cannot touch production state, blast radius is contained, and the two can be applied independently. Backend configuration cannot use variables, so supply the differing values with -backend-config at init time, or use separate directories per environment.
Use the Terraform state commands with confidence: state list, show, mv and rm, plus terraform import and refresh, and when each is safe to run.
Learn what Terraform modules are, when to write one, how to design a clean interface, version and source them, and move resources in without recreating them.
Learn how to call Terraform modules with a module block from local paths or the Registry, pass inputs, pin versions, run init and use module outputs.