This objective moves beyond writing code and into the operational reality of Terraform. State is the source of truth, the memory of your infrastructure. Mismanage it, and you will experience production outages that no amount of clever
HCLcan fix.
Below is the outline of Objective 6. We will cover each part in depth, with explanations, examples, and hands-on labs. You can jump to any section directly, but I recommend following the order for the best learning experience.
Keep these three things separate:
- Configuration describes the desired end state.
- State is Terraform's memory of what it believes already exists.
- Real infrastructure is the actual cloud resource or local object.
Terraform compares configuration to state, then refreshes state from reality. If state drifts away from reality, Terraform can make the wrong choices even when the configuration is correct. That is why state must be treated as operational data, not disposable scratch space.
The practical rule: prefer declarative refactors (moved, import, refresh-only) over manual state editing.
Source: Initialize Terraform Configuration, Backend Type local, Backend block configuration overview
A backend in Terraform determines two things:
- Where state is stored.
- How Terraform performs operations (locally or remotely).
Every Terraform configuration has exactly one backend. If you do not explicitly configure one, Terraform uses the local backend.
The local backend stores state in a plain JSON file named terraform.tfstate in the root directory of your configuration. This is the simplest setup and is perfectly fine for a single developer working on a personal project.
What Terraform does with the local backend:
- During
terraform plan, it readsterraform.tfstatefrom the current directory. - During
terraform apply, it updatesterraform.tfstatewith the new state. - Before updating, it copies the existing state to
terraform.tfstate.backupas a safety measure.
Key files created by the local backend:
terraform.tfstate— The primary state file. Contains all resource metadata in JSON.terraform.tfstate.backup— The previous state, saved before the lastapply. Used for recovery if state is corrupted during an apply.
Example local backend configuration (explicit, though default):
# This is what Terraform implicitly uses when no backend is specified.
# You rarely need to write this explicitly. We write it here to understand it.
terraform {
# The backend block defines where state is stored
backend "local" {
# 'path' is the only argument. Defaults to "terraform.tfstate"
path = "terraform.tfstate"
}
}Explanation of the local backend configuration:
terraform {— The top-level block for configuring Terraform itself (not infrastructure).backend "local" {— Declares that we want to use thelocalbackend type."local"is the backend type identifier.path = "terraform.tfstate"— Specifies the exact file path where the state JSON will be written. This is relative to the root module directory. If omitted, it defaults toterraform.tfstate.
The local backend has critical limitations for team use:
| Limitation | Consequence |
|---|---|
| No state locking | Terraform has no shared lock service in the local backend, so separate copies of the same state can be modified concurrently and overwrite each other. |
| State stored on disk | The state file sits on a developer's laptop. Disk failures, accidental deletion, or git clean can destroy the only record of your infrastructure. |
| No encryption at rest | The state file contains all resource attributes, including secrets, in plaintext on disk. Anyone with filesystem access can read them. |
| No collaboration | Team members cannot share state. Each person has their own copy, leading to divergence. |
The local backend is suitable only for:
- Learning and experimentation.
- Single-developer personal projects.
- Scenarios where infrastructure is disposable and no collaboration is needed.
Let's examine the structure of terraform.tfstate to understand what we're protecting.
{
"version": 4,
"terraform_version": "1.12.0",
"serial": 3,
"lineage": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"outputs": {
"instance_id": {
"value": "i-1234567890abcdef0",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"ami": "ami-0c55b159cbfafe1f0",
"id": "i-1234567890abcdef0",
"instance_type": "t3.micro",
"private_ip": "10.0.1.42",
"tags": {
"Name": "web-server"
},
"password": "MySecretPassword123"
},
"dependencies": [
"aws_subnet.main",
"aws_security_group.allow_ssh"
]
}
]
}
]
}Key fields explained:
version— The state file format version. Terraform increments this when the state schema changes.terraform_version— The version of Terraform that last wrote this state. If you try to read this state with an older Terraform that doesn't support this format, it will error.serial— A monotonically increasing integer that records the version of the state. Each successful state write increments it. Remote backends use it as part of concurrency safety and optimistic checks, but it is not the same thing as a lock.lineage— A UUID generated when the state is first created. This uniquely identifies the "ancestry" of the state. If you lose state and try to recreate it from scratch, the new state will have a different lineage, and Terraform will refuse to mix them unless you explicitly override.outputs— Contains all root module output values, stored in plaintext.resources— An array of all managed resources. Each entry includes the resource type, local name, provider, and one or more instances (forcount/for_each).instances[].attributes— All the arguments and exported attributes of the resource, including sensitive ones. Note thepasswordfield—it's in plaintext.instances[].dependencies— The list of resource addresses that this resource depends on. This is used to determine the correct order for destruction.
Directory: obj6a-lab
File: main.tf — We'll create a resource that generates a "secret" so we can find it in state.
# Configures the required provider for this configuration.
# Without this block, terraform init won't know what to download.
terraform {
required_providers {
random = {
source = "hashicorp/random" # The registry address of the provider
version = "~> 3.6.0" # Accept any 3.6.x version, but not 3.7
}
}
}
# This resource generates a random string. We give it a local name "password"
# so we can reference it elsewhere (e.g., random_password.password.result).
# The actual generated value will be stored in the state file in plaintext.
resource "random_password" "password" {
length = 16 # Generate 16 characters
special = false # Don't include special characters like !@#$
}
# This output prints the generated password to the CLI after apply.
# It is marked sensitive so Terraform redacts it in the terminal output,
# but the value is STILL stored in plaintext in terraform.tfstate.
output "generated_password" {
value = random_password.password.result
sensitive = true # Redacts from CLI output only, NOT from state file
}Steps:
terraform init— Downloads therandomprovider into.terraform/providers/.terraform apply -auto-approve— Creates the random password. The output is marked sensitive, so you'll see(sensitive value)in the terminal.- Open
terraform.tfstatein VS Code. Search for"result". You will see the actual generated password in plaintext inside the JSON. - Note the
serialvalue (probably 1). Note thelineagevalue. - Run
terraform planorterraform apply -auto-approveagain. If there are no changes, the state serial should remain unchanged because Terraform does not rewrite state when nothing changed. - Note that
terraform.tfstate.backupalso exists. This is the state from BEFORE the last apply. If your state gets corrupted, you can copy this file overterraform.tfstateto revert to the previous known-good state.
Key takeaway: The sensitive = true flag on a variable or output redacts the value from CLI output only. It does not encrypt the value in the state file. The state file is a plaintext JSON document.
- True/False: The local backend supports automatic state locking to prevent concurrent modifications.
- Multiple Choice: What is the purpose of the
lineagefield in a state file? A. It tracks the number of timesapplyhas been run. B. It uniquely identifies the state's history to prevent mixing state from different configurations. C. It stores the Terraform version that generated the state. D. It records the user who last ranterraform apply. - True/False: Marking a variable as
sensitive = trueencrypts its value in theterraform.tfstatefile.
Show Answers
Answers & brief explanations
- False. The local backend does not support state locking. This means that if two people run
terraform applyat the same time from different machines, they can both read the same state and make changes, but the second one to write will overwrite the first, causing lost changes and corrupted state. - B. The
lineagefield is a UUID that uniquely identifies the history of the state. If you lose your state and try to recreate it from scratch, the new state will have a different lineage. Terraform uses this to prevent mixing state from different configurations, which could lead to resource mismanagement. - False. The
sensitive = trueflag on a variable or output only redacts the value from CLI output. It does not encrypt the value in the state file. The state file is a plaintext JSON document, and all values, including sensitive ones, are stored in plaintext.
Source: State Locking State Storage and Locking
Imagine two DevOps engineers, Alice and Bob, both responsible for the same AWS infrastructure. They both have the Terraform configuration on their laptops.
- Alice runs
terraform plan. Terraform reads the state from an S3 bucket. The state says there's one EC2 instance. - Bob also runs
terraform planat the exact same time. Terraform reads the same state from the S3 bucket. The state also shows one EC2 instance. - Alice runs
terraform apply. She adds a new EC2 instance. Terraform writes the new state (with two instances) to S3. Serial changes from 10 to 11. - Bob runs
terraform apply. His local state still says one instance. Terraform creates another new instance. It writes the state to S3, overwriting Alice's state with a version that only has Bob's new instance. Serial 11.
Now:
- The S3 state file shows Bob's version (two instances: original + Bob's).
- In AWS, there are three instances (original + Alice's + Bob's).
- Alice's instance still exists, but Terraform no longer knows about it. It is orphaned. Terraform won't manage it, update it, or destroy it. This is a disaster.
State locking prevents this scenario entirely.
State locking is a mechanism that ensures only one person (or CI pipeline) can modify the state at any given time.
Common control flags:
-lock-timeout=10mtells Terraform how long to keep retrying before giving up on a locked state.-lock=falsedisables locking entirely. This is sometimes used for emergency reads or troubleshooting, but it is dangerous for normal apply/destroy workflows because it can reintroduce the very race conditions locking is meant to prevent.
How it works:
- Before any operation that could modify state (
planwith default refresh,apply,destroy), Terraform acquires a lock on the state. - If the lock is already held by another process, Terraform waits (for a configurable timeout) or immediately errors, depending on configuration.
- Once the lock is acquired, Terraform proceeds with the operation.
- After the operation completes (success or failure), Terraform releases the lock.
If two people try to apply simultaneously:
- Alice runs
terraform apply. Terraform acquires the lock. Lock holder: Alice. - Bob runs
terraform apply. Terraform tries to acquire the lock. It sees the lock is held by Alice. Bob's Terraform immediately errors:Error acquiring the state lock. Bob must wait for Alice to finish. - Alice's apply completes. Terraform releases the lock.
- Bob can now run
terraform applyagain. This time, Terraform acquires the lock and proceeds. Bob's apply reads the state that includes Alice's changes, so his plan is correct.
Not all backends support locking. The local backend has no locking at all (no coordination between different laptops).
| Backend Type | Locking Support | Mechanism |
|---|---|---|
| local | ❌ No | No shared lock coordination |
| s3 | ✅ Yes (when configured) | DynamoDB table. Terraform writes a lock record to DynamoDB before proceeding. The record includes the operation type, timestamp, and user. |
| gcs | ✅ Yes | Object generation and preconditions. GCS uses object metadata to track locks. |
| azurerm | ✅ Yes | Azure Blob Storage leases. Terraform acquires a lease on the blob containing the state file. |
| consul | ✅ Yes | Consul's built-in session and lock mechanism. |
| HCP Terraform | ✅ Yes | Built into the platform. No additional configuration needed. |
This is the most common remote backend for teams not using HCP Terraform.
# This backend block tells Terraform to store state in an S3 bucket
# and use a DynamoDB table for state locking.
terraform {
backend "s3" {
# The S3 bucket where state will be stored
bucket = "my-company-terraform-state"
# The path/key within the bucket. Use a naming convention that reflects
# the project and environment (e.g., "prod/network/terraform.tfstate")
key = "prod/app/terraform.tfstate"
# The AWS region where both the bucket and DynamoDB table exist
region = "us-east-1"
# The DynamoDB table used for state locking. Terraform will create/use
# an item in this table with the same key as the state file.
dynamodb_table = "terraform-locks"
# Encrypt the state file at rest using S3 server-side encryption (AES-256)
encrypt = true
}
}Explanation:
backend "s3" {— Specifies the backend type ass3.bucket = "my-company-terraform-state"— The name of the S3 bucket. This bucket must already exist.key = "prod/app/terraform.tfstate"— The S3 object key (path). This uniquely identifies the state file for this specific configuration within the bucket.region = "us-east-1"— The AWS region where both the bucket and DynamoDB table are located.dynamodb_table = "terraform-locks"— The name of the DynamoDB table. This table must already exist with a primary key namedLockID(of type String). Terraform uses this table to create lock records.encrypt = true— Enables AES-256 server-side encryption on the S3 object. Protects the state file from being read directly from S3 by unauthorized parties.
How the DynamoDB lock works step-by-step:
- Terraform writes a new item to the DynamoDB table with
LockID = "prod/app/terraform.tfstate"and a condition that the item doesn't already exist (or the existing lock has expired). - If the item already exists (someone else holds the lock), DynamoDB returns a
ConditionalCheckFailedException. Terraform interprets this as "lock already held" and waits or errors. - When the operation completes, Terraform deletes the item from DynamoDB, releasing the lock.
We can't easily set up S3 and DynamoDB locally, but we can demonstrate the concept by trying to run two apply operations on the same local state. The local backend uses file advisory locks, which might block on the same machine.
Alternative approach: We'll create two identical configurations in separate directories that share a "remote" state via a local file that simulates a remote backend. This isn't a true remote backend but demonstrates collision.
Since this is a bit contrived, I'll focus on a clear conceptual demonstration instead.
Conceptual lab — Simulating a race condition:
- Create directory
obj6b-lab-awith a Terraform config that creates alocal_file. - Run
terraform applyand note the state serial. - Manually copy the state file to
obj6b-lab-b. - In
obj6b-lab-a, modify the config to create a second file. Runterraform apply. Serial increments. - In
obj6b-lab-b, also modify the config to create a different second file. Runterraform apply. - Observe that
obj6b-lab-b's apply succeeds because it's using its own local state, not the updated state fromobj6b-lab-a. The two directories have diverged.
Key takeaway: Without a shared remote backend with locking, state divergence is inevitable.
- True/False: State locking is automatically enabled when using the
s3backend without any additional configuration. - Multiple Choice: In the S3 + DynamoDB setup, what is the primary key of the DynamoDB table used for locking?
A.
StateKeyB.LockIDC.SerialNumberD.LineageUUID - Multiple Answer: Which of the following backends support state locking? (Select THREE)
A.
localB.s3with DynamoDB C.gcsD.httpE.azurerm
Show Answers
-
False. The
s3backend does not lock by itself. You only get locking when you also configure DynamoDB for the lock record. -
B.
LockIDis the required primary key for the DynamoDB lock table. Terraform uses that key to create and release locks for the state path. -
B, C, E.
s3with DynamoDB,gcs, andazurermsupport shared state locking. The local backend andhttpbackend do not provide that same lock coordination.
Source: Backend block configuration overview, State Storage and Locking
We've seen the S3 backend. Let's look at the general structure.
terraform {
backend "BACKEND_TYPE" {
# Backend-specific arguments go here.
# These arguments CANNOT reference variables, locals, or resource attributes.
# All values must be literals or use partial configuration.
}
}Important Rule — No Interpolation in Backend Blocks:
The following is INVALID and will produce an error:
# ❌ INVALID: Cannot use variables in a backend block!
terraform {
backend "s3" {
bucket = var.state_bucket # Error: Variables not allowed
key = "prod/terraform.tfstate"
region = var.aws_region # Error: Variables not allowed
}
}Why this restriction exists: Terraform must configure the backend very early in the initialization process, before it has evaluated any variables, locals, or resources. It's the first thing terraform init does. If backend configuration depended on variables, Terraform couldn't know where to fetch state, and therefore couldn't evaluate anything.
Since we can't use variables in the backend block, how do we handle dynamic values (like bucket names that differ per environment)? Partial configuration.
We leave the dynamic arguments out of the backend block and provide them at init time via:
- A file specified with
-backend-config - Command-line key/value pairs with
-backend-config=KEY=VALUE
Method 1: Using a file
# main.tf
# Notice the dynamic arguments are omitted—they will be provided at init time.
terraform {
backend "s3" {
# bucket, key, region, etc. NOT specified here
# We'll provide them when running terraform init
}
}Create a separate file, backend.hcl (by convention named *.backendname.tfbackend, e.g., config.s3.tfbackend):
# backend.hcl — This file contains the actual configuration values.
# It looks just like the contents of the backend block, but without the block wrapper.
# Each line is a simple key = value assignment.
bucket = "my-state-bucket-prod"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = trueRunning init:
terraform init -backend-config=backend.hclExplanation of the command:
terraform init— The initialization command.-backend-config=backend.hcl— Tells Terraform to read additional backend configuration from the filebackend.hcl. Terraform merges this with any hardcoded values in thebackendblock (in this case, there are none). This file contains the dynamic environment-specific values.
Method 2: Using command-line key/value pairs
terraform init \
-backend-config="bucket=my-state-bucket-prod" \
-backend-config="key=prod/network/terraform.tfstate" \
-backend-config="region=us-east-1" \
-backend-config="dynamodb_table=terraform-locks" \
-backend-config="encrypt=true"Using partial configuration makes your code environment-agnostic. The same .tf files can be used for dev, staging, and prod by passing different backend.hcl files.
If you already have a local state file and you want to move to a remote backend, Terraform will detect the change when you run terraform init with the new backend configuration.
Terraform will display:
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend
to the newly configured "s3" backend. No existing state was found in the
newly configured "s3" backend. Do you want to copy this state to the new
"s3" backend? Enter "yes" to copy and "no" to start with an empty state.
Key options during backend migration:
- Type
yes: Terraform copies the local state file to the S3 bucket. This is the correct choice when you're moving an existing project to a remote backend. - Type
no: Terraform discards the local state and starts fresh with an empty state in S3. This is appropriate if you're starting from scratch. - Use
-migrate-state: Automatically answers "yes" to the migration prompt. Useful in automation. - Use
-reconfigure: Tells Terraform to skip the migration check entirely and reconfigure the backend without copying any existing state. This is also used when you're changing backend configuration for the same backend type (e.g., moving to a different S3 bucket).
# Migrate state from local to S3, automatically answering yes
terraform init -migrate-state -backend-config=backend.hcl
# Reconfigure backend without migration (e.g., changing buckets)
terraform init -reconfigure -backend-config=new-backend.hclFrom the offical study guide:
You cannot configure a
backendblock when the configuration also contains acloudconfiguration for storing state data.
- The
cloudblock is a simplified way to connect directly to HCP Terraform or Terraform Enterprise. - When you use
cloud, HCP Terraform manages state storage, locking, and remote execution automatically—no need for a separatebackendblock. - If you try to use both,
terraform initwill error.
# Use THIS for HCP Terraform:
terraform {
cloud {
organization = "my-org"
workspaces {
name = "my-workspace"
}
}
}
# OR use THIS for an independent remote backend (S3, GCS, etc.):
terraform {
backend "s3" {
bucket = "my-bucket"
key = "state.tfstate"
region = "us-east-1"
}
}
# NEVER both in the same configuration!We will cover the cloud block in detail in Objective 8.
When you use a remote backend:
- The state file is no longer stored locally (except during in-memory operations).
terraform.tfstatewill not exist in your working directory (or if it does, it's a leftover from before migration and can be deleted).- Terraform reads and writes state directly to the remote backend on every operation.
Exception: If the remote backend write fails (e.g., network error), Terraform will save a local copy of the state as errored.tfstate to prevent data loss. You can use this file to manually push the state with terraform state push errored.tfstate.
Since we can't use S3, we'll configure a local backend with a custom path to simulate the concept of moving state.
Directory: obj6c-lab
Step 1: Create initial config without backend.
# main.tf
resource "local_file" "example" {
filename = "${path.module}/test.txt"
content = "Hello, initial state."
}Run terraform init && terraform apply -auto-approve. State is at terraform.tfstate.
Step 2: Add a backend block to simulate migration.
terraform {
backend "local" {
path = "remote-state/terraform.tfstate" # Simulates a remote location
}
}Run terraform init. Terraform will ask: "Do you want to copy existing state to the new backend?" Type yes. Terraform moves the state from ./terraform.tfstate to ./remote-state/terraform.tfstate.
Step 3: Verify migration.
terraform.tfstateno longer exists (or is empty)../remote-state/terraform.tfstateis the active state.
Step 4: Observe that terraform plan now uses the new state location.
Any changes to the configuration will be compared against the state in remote-state/terraform.tfstate.
This simulates exactly what happens when you migrate from local to S3, just using the local filesystem as the "remote" location.
- True/False: The
backendblock can reference input variables declared in the same module. - Multiple Choice: What is the correct command to migrate an existing local state file to a newly configured S3 backend?
A.
terraform apply -migrate-stateB.terraform init -migrate-stateC.terraform plan -reconfigureD.terraform state push - Multiple Answer: Which of the following are valid ways to provide backend configuration values? (Select TWO)
A. Using variables referenced in the
backendblock. B. Using the-backend-configcommand-line flag with a file path. C. Using-backend-config=KEY=VALUEon the command line. D. Setting environment variables namedTF_BACKEND_*.
Show Answers
- False. The
backendblock cannot reference input variables, locals, or resource attributes. All values in the backend block must be literals or provided via partial configuration at init time. - B. The correct command to migrate an existing local state file to a newly configured S3 backend is
terraform init -migrate-state. This command initializes the new backend and automatically answers "yes" to the prompt about copying existing state. - B and C. Valid ways to provide backend configuration values include using the
-backend-configcommand-line flag with a file path (B) and using-backend-config=KEY=VALUEon the command line (C). Variables cannot be used directly in the backend block, and there are no environment variables namedTF_BACKEND_*for backend configuration.
Source: Manage resource drift, Use refresh-only mode to sync Terraform state, Moved block reference, Removed block reference, Refactor Terraform state, Terraform State
Drift occurs when the actual state of your infrastructure differs from what is recorded in the Terraform state file and defined in your configuration. Drift happens when someone (or something) makes changes outside of Terraform's control.
Common causes of drift:
- A teammate manually changes a security group rule in the AWS Console.
- An auto-scaling event changes the desired capacity of an ASG.
- A resource is deleted accidentally (or maliciously).
- A cloud provider updates a default value for a resource attribute.
The recommended approach to detect and review drift is to use the -refresh-only flag with terraform plan.
terraform plan -refresh-onlyWhat this does:
- Terraform queries the cloud provider APIs for the actual state of all resources.
- It compares the actual state with the state file.
- It generates a plan showing what would change in the state file (not the infrastructure) to match reality.
Example output:
# aws_instance.web has been terminated outside of Terraform.
- resource "aws_instance" "web" {
id = "i-1234567890abcdef0" -> null
}
This plan says: "If I were to update the state file to match reality, I would remove aws_instance.web because it no longer exists."
To accept these state file changes:
terraform apply -refresh-onlyImportant: This only updates the state file. It does not modify your infrastructure. After this, running terraform plan will show that Terraform wants to recreate the missing instance (because the configuration still defines it, but the state no longer has it).
Why -refresh-only is preferred over the deprecated terraform refresh:
terraform refresh(deprecated) automatically updates the state file without showing you a plan first. This can cause surprise state changes.-refresh-onlygives you a preview and requires confirmation, making it much safer.
Once drift is detected, you have two options:
- Keep the external change: Update your Terraform configuration to match the new state of the world. Then run
terraform applyto sync the state. - Revert the external change: Run
terraform apply(without-refresh-only) so Terraform re-applies your configuration, reverting the drifted resource back to the configured state.
Terraform provides commands to inspect and manipulate the state file directly. These are for advanced use only and should be avoided in routine workflows.
terraform state list — Lists all resources in the state file.
terraform state list
# Output:
# random_pet.server
# local_file.config
# module.vpc.aws_vpc.mainterraform state show RESOURCE_ADDRESS — Shows detailed attributes of a specific resource from the state file.
terraform state show local_file.config
# Output:
# resource "local_file" "config" {
# content = "test"
# filename = "./config.txt"
# id = "abc123..."
# }-
terraform state rm RESOURCE_ADDRESS— Removes a resource from the state file without destroying the real infrastructure. Use this when you want Terraform to "forget" about a resource so you can stop managing it without deleting it. -
terraform state mv SOURCE DESTINATION— Moves a resource from one address to another within the state file. Useful when refactoring and renaming resources. -
terraform state pull— Downloads the current state from the configured backend and prints it as JSON to stdout. Useful for scripting and inspection. -
terraform state push STATE_FILE— Uploads a state file to the configured backend. Extremely dangerous. Overwrites the remote state. Terraform protects against accidental push by checking lineage and serial numbers, but-forcebypasses these protections.
Instead of using terraform state mv (which is manual and error-prone), Terraform 1.1+ supports the moved block to declaratively refactor state.
# Suppose we rename our module from 'network' to 'networking'
# We add this moved block so Terraform knows this is a rename, not a destroy/create
moved {
from = module.network # The old address in state
to = module.networking # The new address in the configuration
}When you run terraform plan, Terraform recognizes that the resource at module.network in the state should map to module.networking in the configuration. It will move the state entry without destroying or recreating any infrastructure.
Benefits of moved over state mv:
- It's in the configuration, so it's version-controlled and reviewable.
- It's visible during
plan, so the team can see the refactoring. - Terraform validates that the move makes sense.
To bring existing infrastructure (created outside Terraform) under management, use the import block (Terraform 1.5+) or the legacy terraform import command.
Configuration-driven import (Terraform 1.5+):
# Declare the resource you want to import (with its required arguments)
resource "aws_instance" "legacy" {
instance_type = "t3.micro"
ami = "ami-abc123"
}
# The import block tells Terraform to find an existing resource
# with the given ID and bind it to the resource address in state.
import {
id = "i-1234567890abcdef0" # The cloud provider's identifier for the resource
to = aws_instance.legacy # The Terraform resource address to import into
}Then run terraform plan -generate-config-out=generated.tf to optionally generate the full configuration, or just run terraform apply to import. Terraform will find the resource in AWS, pull its attributes into the state file, and associate it with aws_instance.legacy.
We will cover import in depth in Objective 7.
Directory: obj6d-lab
File: main.tf
# Standard setup
terraform {
required_providers {
local = { source = "hashicorp/local", version = "~> 2.5" }
}
}
# Create a file that we will delete manually to simulate drift
resource "local_file" "config" {
filename = "${path.module}/config.txt"
content = "Important configuration data."
}Steps:
terraform init && terraform apply -auto-approve- Verify
config.txtexists and state is clean. - Simulate drift: Manually delete
config.txt(right-click delete in VS Code orrm config.txt). - Run
terraform plan -refresh-only. Observe Terraform detects the file is missing and proposes to remove it from state. - Run
terraform apply -refresh-only. Terraform updates the state to reflect the missing file. - Run
terraform state list. The file is gone from state. - Run
terraform plan. Terraform now wants to createconfig.txtagain because it's in the configuration but not in state after the refresh-only apply. - Run
terraform apply. The file is recreated.
Additional state exercises:
- Run
terraform state listto see the resource address. - Run
terraform state show local_file.configto see its attributes. - Use a
movedblock to rename the resource. Addmoved { from = local_file.config; to = local_file.app_config }and rename the resource inmain.tf. Runterraform planto see the move.
Use these habits in real projects:
- Store state in a remote backend with locking for any shared environment.
- Restrict access to state files and backend credentials to the smallest necessary team.
- Prefer
moved,import,-refresh-only, andterraform statecommands over manual edits toterraform.tfstate. - If you need to read outputs from another stack, use
terraform_remote_statecarefully and treat it like an API boundary, not a casual shortcut. - Before dangerous refactors, take a backup of state and confirm the target backend has versioning or rollback support.
- Never commit state files or backup state files to version control.
- True/False:
terraform apply -refresh-onlymodifies both the state file and the real infrastructure to reconcile drift. - Multiple Choice: Which command would you use to see all resources currently tracked in the state file?
A.
terraform showB.terraform state listC.terraform planD.terraform output - Multiple Answer: Which approaches can be used to safely refactor state when renaming resources? (Select TWO)
A.
terraform state mvB. Themovedconfiguration block C.terraform state rmfollowed byterraform importD. Manually editingterraform.tfstate
Show Answers
- False.
terraform apply -refresh-onlyonly updates the state file to match the actual infrastructure. It does not modify any real resources. To reconcile drift by modifying infrastructure, you would runterraform applywithout the-refresh-onlyflag after updating the state. - B. The command to see all resources currently tracked in the state file is
terraform state list. This lists the addresses of all resources in the state. - A and B. The
terraform state mvcommand can be used to manually move a resource in state, but themovedconfiguration block is the recommended way to declaratively refactor state when renaming resources. Both methods are valid, but themovedblock provides better visibility and version control. Manually editingterraform.tfstateis dangerous and should be avoided.
You should know the drill by now, no peeping at the answers until you've given it your best shot!
Question 1 (True/False):
The default local backend supports state locking using filesystem advisory locks that are reliable across multiple machines.
Question 2 (Multiple Choice):
You have a Terraform project using the local backend. You want to migrate to an S3 backend with DynamoDB locking. You add the backend "s3" block to your configuration. What command should you run next?
⬜ `terraform apply`
⬜ `terraform init -migrate-state`
⬜ `terraform plan -reconfigure`
⬜ `terraform state push`
Question 3 (Multiple Answer):
Which of the following are stored directly in the Terraform state file? (Select THREE)
⬜ The `serial` number
⬜ The `lineage` UUID
⬜ The provider plugin binaries
⬜ Resource attribute values (including secrets)
⬜ The dependency graph of resources
Question 4 (True/False):
The backend block inside terraform {} can reference var.bucket_name to dynamically set the S3 bucket for state storage.
Question 5 (Multiple Choice):
What is the primary reason state locking is essential for team workflows?
⬜ It encrypts the state file at rest.
⬜ It prevents two users from running `terraform plan` simultaneously.
⬜ It prevents concurrent `apply` operations from corrupting the state file.
⬜ It enables remote execution of Terraform commands.
Question 6 (Multiple Choice):
You have an existing local state file with resources. You configure an S3 backend for the first time. After running terraform init, Terraform prompts you about migrating state. You type no. What happens?
⬜ The local state file is encrypted and kept as backup.
⬜ The local state is deleted and the S3 backend starts with an empty state.
⬜ The local state is copied to S3 without any prompt.
⬜ An error is thrown and `init` fails.
Question 7 (True/False):
terraform apply -refresh-only will update your infrastructure to match the configuration if drift is detected.
Question 8 (Multiple Choice):
Which CLI command is the safest way to inspect what changes would occur to the state file due to external resource modifications?
⬜ `terraform refresh`
⬜ `terraform plan -refresh-only`
⬜ `terraform apply -auto-approve`
⬜ `terraform state pull`
Question 9 (Multiple Answer):
Which backends support state locking? (Select THREE)
⬜ `local`
⬜ `s3` with a DynamoDB table
⬜ `gcs`
⬜ `azurerm`
⬜ `http`
Question 10 (True/False):
When using a remote backend, the terraform.tfstate file is still written to the local disk after every successful apply.
Question 11 (Multiple Choice):
What does the moved block in Terraform configuration do?
⬜ It physically moves cloud resources to a different region.
⬜ It tells Terraform to rename a resource in the state file without destroying the actual resource.
⬜ It backs up the state file to a remote location.
⬜ It locks the state file during long-running operations.
Question 12 (Multiple Choice):
You accidentally delete an EC2 instance manually via the AWS Console. This instance was managed by Terraform. What will terraform plan show the next time you run it?
⬜ No changes; Terraform will update the state automatically.
⬜ Terraform will propose to recreate the instance.
⬜ Terraform will error because the state is corrupted.
⬜ Terraform will ask you to import the instance.
Question 13 (True/False):
The terraform state rm command destroys the real-world resource before removing it from the state file.
Question 14 (Multiple Answer):
Which of the following are valid methods for providing backend configuration values when they cannot be hardcoded? (Select TWO)
⬜ Using `-backend-config=KEY=VALUE` at `init` time.
⬜ Using `-backend-config=PATH` to point to a configuration file.
⬜ Setting environment variables like `TF_BACKEND_BUCKET`.
⬜ Using `locals` in the `backend` block.
Question 15 (Multiple Choice):
What is the effect of running terraform state rm aws_instance.web?
⬜ Terraform destroys the EC2 instance and removes it from state.
⬜ Terraform removes the EC2 instance from state but does NOT destroy it.
⬜ Terraform locks the state to prevent further changes.
⬜ Terraform renames the resource in state.
Question 16 (True/False):
The terraform.tfstate.backup file is a JSON file that can be manually restored to revert state to the previous version.
Question 17 (Multiple Choice):
A configuration has both a cloud block and a backend block defined in the terraform {} block. What happens when you run terraform init?
⬜ The `cloud` block takes precedence; the `backend` block is ignored.
⬜ The `backend` block takes precedence; the `cloud` block is ignored.
⬜ Terraform merges both configurations.
⬜ Terraform raises an error because they are mutually exclusive.
Question 18 (True/False):
State locking guarantees that only one terraform apply can modify state at a time, but multiple terraform plan operations can run concurrently without any issue.
Question 19 (Multiple Choice):
You want to move a Terraform-managed resource from aws_instance.old_name to aws_instance.new_name in the same state file without destroying the instance. Which approach is the most declarative and recommended?
⬜ Use `terraform state mv aws_instance.old_name aws_instance.new_name`
⬜ Use a `moved` block: `moved { from = aws_instance.old_name; to = aws_instance.new_name }`
⬜ Edit `terraform.tfstate` directly to change the name
⬜ Run `terraform destroy` and recreate with the new name
Question 20 (Multiple Answer):
Which of the following statements about the state file are true? (Select TWO)
⬜ It should be committed to version control to enable collaboration.
⬜ It can contain sensitive data in plaintext.
⬜ It maps configuration resource addresses to real-world resource IDs.
⬜ It is automatically encrypted by Terraform when using the `local` backend.
Answer
- False. The local backend uses filesystem advisory locks, which are not reliable across multiple machines. This means that if two users run
terraform applyat the same time from different machines, they can both read the same state and make changes, but the second one to write will overwrite the first, causing lost changes and corrupted state. terraform init -migrate-state. This command initializes the new backend and automatically answers "yes" to the prompt about copying existing state from the local backend to the S3 backend. This is the correct way to migrate state when changing backends.- The
serialnumber, thelineageUUID, and resource attribute values (including secrets) are stored in the state file. The provider plugin binaries are not stored in the state file; they are downloaded and stored separately on disk. The dependency graph is not explicitly stored in the state file; it is derived from the configuration and resource relationships. - False. The
backendblock cannot reference variables, locals, or resource attributes. All values in the backend block must be literals or provided via partial configuration at init time. This is because Terraform needs to configure the backend before it can evaluate any variables. - It prevents concurrent
applyoperations from corrupting the state file. State locking ensures that only oneterraform applycan modify the state at a time. This prevents two users from making conflicting changes that could corrupt the state file and lead to resource mismanagement. - The local state is deleted and the S3 backend starts with an empty state. If you type
noat the migration prompt, Terraform does not copy the existing local state to S3. Instead, it starts with an empty state in the new backend. This means that Terraform will not be aware of any existing resources until you manually import them or recreate them. - False.
terraform apply -refresh-onlyonly updates the state file to match the actual infrastructure. It does not modify any real resources. To reconcile drift by modifying infrastructure, you would runterraform applywithout the-refresh-onlyflag after updating the state. terraform plan -refresh-only. This command allows you to see what changes would occur to the state file due to external resource modifications without actually applying any changes to the infrastructure. The deprecatedterraform refreshcommand would update the state file without showing a plan first, which can lead to surprise changes.s3with a DynamoDB table,gcs, andazurermsupport state locking. Thelocalbackend does not support state locking across multiple machines, and thehttpbackend does not have built-in locking capabilities.- False. When using a remote backend, the
terraform.tfstatefile is not written to the local disk. Instead, Terraform reads and writes state directly to the remote backend on every operation. The localterraform.tfstatefile may exist as a leftover from before migration, but it is not used after the backend is configured. - It tells Terraform to rename a resource in the state file without destroying the actual resource. The
movedblock allows you to declaratively refactor state when renaming resources. Terraform will move the state entry from the old address to the new address without destroying or recreating any infrastructure. - Terraform will propose to recreate the instance. Since the instance was deleted outside of Terraform, the next time you run
terraform plan, Terraform will see that the resource defined in the configuration is missing from the state and will propose to create it again. - False. The
terraform state rmcommand removes a resource from the state file but does NOT destroy the real-world resource. This is useful when you want Terraform to "forget" about a resource so you can stop managing it without deleting it. - Using
-backend-config=KEY=VALUEatinittime and using-backend-config=PATHto point to a configuration file are valid methods for providing backend configuration values. Setting environment variables likeTF_BACKEND_BUCKETis not a standard method for backend configuration, and usinglocalsin thebackendblock is not allowed since the backend block cannot reference variables or locals. - Terraform removes the EC2 instance from state but does NOT destroy it. The
terraform state rmcommand only modifies the state file, allowing you to stop managing the resource with Terraform while leaving it intact in the cloud provider. - True. The
terraform.tfstate.backupfile is a JSON file that contains the previous version of the state before the last successful operation. It can be manually restored by copying it back toterraform.tfstateif you need to revert to the previous state. - Terraform raises an error because they are mutually exclusive. You cannot have both a
cloudblock and abackendblock in the same configuration. Thecloudblock is used for HCP Terraform or Terraform Enterprise, while thebackendblock is used for independent remote backends. Terraform will not allow both to be configured at the same time. - True. State locking ensures that only one
terraform applycan modify the state at a time, preventing corruption. However, multipleterraform planoperations can run concurrently without issue since they do not modify state. - Use a
movedblock:moved { from = aws_instance.old_name; to = aws_instance.new_name }. This is the most declarative and recommended approach for refactoring state when renaming resources. It allows you to version control the change and provides visibility duringplan. Usingterraform state mvis manual and less transparent, while editingterraform.tfstatedirectly is dangerous and should be avoided. - It can contain sensitive data in plaintext and it maps configuration resource addresses to real-world resource IDs. The state file can contain sensitive information such as resource attributes, which may include secrets. It also serves as a mapping between the resource addresses defined in your configuration and the actual resource IDs in the cloud provider. The state file should not be committed to version control, and it is not automatically encrypted when using the
localbackend.
Look at you now, a Terraform state management expert! You've learned about the critical role of the state file, how to manage it with remote backends, detect and resolve drift, and even manipulate state directly when necessary. This knowledge is essential for working with Terraform in real-world scenarios, especially in team environments where collaboration and state integrity are paramount.
Let's proceed to Objective 7: Maintain infrastructure with Terraform, where we'll cover how to import existing resources, manage secrets in state, and handle more complex state management scenarios.