Skip to content

Learn · State

The state file is the truth, and it is often wrong

The configuration says what you want and the cloud says what exists. The state file says what the tool believes — and every interesting failure is one of the three disagreeing.

Three things, not two

A plan is not a comparison between your configuration and the cloud. It is a comparison between your configuration and the state file, after the state file has been refreshed against the cloud.

That third thing is where the surprises live. A resource deleted in a console is still in state until a refresh notices; a resource created by hand is not in state at all, and the tool will happily create a second one beside it; a state file restored from the wrong backup describes infrastructure that no longer exists.

serial goes up on every write and is what stops two concurrent applies overwriting each other. lineage identifies the file — two state files with different lineages are not interchangeable even if they describe the same resources.

The secrets are in there, in the clear

A plan prints (sensitive value) where a secret would go. The state file does not. Every password, every generated token and every private key the provider returned is in it as plain text, because the tool has to be able to diff it next time.

That is not a bug and there is no setting that fixes it. It is why the state file is treated as a secret in its own right, and why terraform.tfstate in a git repository is a genuine incident rather than a tidiness problem.

Before you run it

A database's password is marked sensitive by the provider. Where does the value end up?

moved and import: changing the file without changing the cloud

Renaming a resource in your configuration destroys the old one and creates a new one, because the address is the identity as far as state is concerned. A moved block says this used to be called that, and the plan comes out empty — the record is renamed and nothing is touched.

import goes the other way: an object exists in the cloud and not in state, and an import block brings it under management. The plan shows the resource with importing rather than create, and if your configuration disagrees with what is actually there, it shows the difference too.

Both are the same idea — the file and the cloud are separate things, and either can be edited without the other.

A moved block whose plan is empty is a moved block that worked. It is the one construct in the language whose success looks like nothing happening.

The lock, and the two-people-applying disaster

An apply takes a lock on the state before it starts and releases it at the end. Two people applying at once is what it exists to prevent, and what happens without it is not a merge conflict — it is two applies both reading serial 41, both writing serial 42, and one of them describing resources the other has just destroyed.

The lock is advisory and there is a flag to skip it. The flag exists for the case where a previous apply died and left the lock held, which is a real situation with a real fix — and it is reached for far more often than that.

State here is local, so the lock is local too. What it teaches is the same: the second apply is told who holds it and since when, and taking the flag is a decision rather than an accident.

The plan

The same service every claim above was made about. Nothing here is graded — load whatever you like, or change nothing and read what the configuration already produces.

No change loaded — the configuration as it stands.
Plan: 0 to add, 0 to change, 0 to destroy.

Nothing in this plan is destroyed.

The state file

serial
15
lineage
world-orders
resources
15

1 value is in this file in plain text, masked here and not in the file itself.

cloud_bucket.uploads

arn
"arn:cloud:s3:eu-west-1:000000000000:orders-uploads-eu-west-1"
bucket
"orders-uploads-eu-west-1"
force_destroy
false
id
"orders-uploads-eu-west-1"
object_lock_enabled
false
tags
{environment = "prod", managed_by = "opentofu", team = "orders"}

cloud_db_instance.primary

allocated_storage
100
arn
"arn:cloud:rds:eu-west-1:000000000000:db:orders-primary"
availability_zone
"eu-west-1a"
backup_retention_period
7
db_name
"orders"
db_subnet_group_name
"orders-private"
endpoint
"orders-primary.83d691363f86.eu-west-1.cloud-rds.internal:5432"
engine
"postgres"
engine_version
"16.4"
id
"orders-primary"
identifier
"orders-primary"
instance_class
"db.r6g.large"
multi_az
false
password
••••••••
skip_final_snapshot
false
storage_encrypted
true
tags
{environment = "prod", managed_by = "opentofu", team = "orders"}
username
"app"

cloud_iam_policy.bucket_access

arn
"arn:cloud:iam:eu-west-1:000000000000:policy/orders-bucket-access"
description
"Read and write the orders bucket"
id
"arn:cloud:iam:eu-west-1:000000000000:policy/orders-bucket-access"
name
"orders-bucket-access"
path
"/"
policy
"{\"Version\":\"2012-10-17\",\"Statement\":[]}"
tags
{environment = "prod", managed_by = "opentofu", team = "orders"}

cloud_iam_role.application

arn
"arn:cloud:iam:eu-west-1:000000000000:role/orders-application"
assume_role_policy
"{\"Version\":\"2012-10-17\",\"Statement\":[]}"
description
"The orders service"
id
"orders-application"
max_session_duration
3600
name
"orders-application"
path
"/"
tags
{environment = "prod", managed_by = "opentofu", team = "orders"}

cloud_vpc.main

arn
"arn:cloud:ec2:eu-west-1:000000000000:vpc/vpc-5ad440a3"
cidr_block
"10.0.0.0/16"
enable_dns_hostnames
true
enable_dns_support
true
id
"vpc-f2bc8340"
instance_tenancy
"default"
tags
{environment = "prod", managed_by = "opentofu", team = "orders", name = "orders-main"}

cloud_security_group.database

arn
"arn:cloud:ec2:eu-west-1:000000000000:security-group/sg-2ff112a6"
description
"Managed by Terraform"
egress
[]
id
"sg-9905d588"
ingress
[]
name
"orders-database"
tags
{environment = "prod", managed_by = "opentofu", team = "orders"}
vpc_id
"vpc-f2bc8340"

cloud_security_group.web

arn
"arn:cloud:ec2:eu-west-1:000000000000:security-group/sg-4eb879ab"
description
"Managed by Terraform"
egress
[]
id
"sg-dc3c10ff"
ingress
[]
name
"orders-web"
tags
{environment = "prod", managed_by = "opentofu", team = "orders"}
vpc_id
"vpc-f2bc8340"

cloud_subnet.private["a"]

arn
"arn:cloud:ec2:eu-west-1:000000000000:subnet/subnet-464caea8"
availability_zone
"eu-west-1a"
cidr_block
"10.0.1.0/24"
id
"subnet-c8249f67"
map_public_ip_on_launch
false
tags
{environment = "prod", managed_by = "opentofu", team = "orders", name = "orders-private-a"}
vpc_id
"vpc-f2bc8340"

cloud_subnet.private["b"]

arn
"arn:cloud:ec2:eu-west-1:000000000000:subnet/subnet-ef93cca1"
availability_zone
"eu-west-1b"
cidr_block
"10.0.2.0/24"
id
"subnet-c923d137"
map_public_ip_on_launch
false
tags
{environment = "prod", managed_by = "opentofu", team = "orders", name = "orders-private-b"}
vpc_id
"vpc-f2bc8340"

cloud_subnet.private["c"]

arn
"arn:cloud:ec2:eu-west-1:000000000000:subnet/subnet-c206fc26"
availability_zone
"eu-west-1c"
cidr_block
"10.0.3.0/24"
id
"subnet-9486b397"
map_public_ip_on_launch
false
tags
{environment = "prod", managed_by = "opentofu", team = "orders", name = "orders-private-c"}
vpc_id
"vpc-f2bc8340"

cloud_instance.worker[0]

ami
"ami-0a1b2c3d4e5f60718"
arn
"arn:cloud:ec2:eu-west-1:000000000000:instance/i-edc72b4be6c41f445"
availability_zone
"eu-west-1a"
id
"i-740986cf9622122c3"
instance_type
"t3.medium"
key_name
null
monitoring
false
private_ip
"10.0.1.226"
public_ip
"52.195.252.190"
subnet_id
"subnet-c8249f67"
tags
{environment = "prod", managed_by = "opentofu", team = "orders", name = "orders-worker"}
user_data
null
vpc_security_group_ids
["sg-dc3c10ff"]

cloud_instance.worker[1]

ami
"ami-0a1b2c3d4e5f60718"
arn
"arn:cloud:ec2:eu-west-1:000000000000:instance/i-767b78828f7099885"
availability_zone
"eu-west-1a"
id
"i-6f731e0b1836039e6"
instance_type
"t3.medium"
key_name
null
monitoring
false
private_ip
"10.0.1.16"
public_ip
"52.189.23.140"
subnet_id
"subnet-c8249f67"
tags
{environment = "prod", managed_by = "opentofu", team = "orders", name = "orders-worker"}
user_data
null
vpc_security_group_ids
["sg-dc3c10ff"]

cloud_instance.worker[2]

ami
"ami-0a1b2c3d4e5f60718"
arn
"arn:cloud:ec2:eu-west-1:000000000000:instance/i-24fd4c9fa06d5994c"
availability_zone
"eu-west-1a"
id
"i-0ef9be0db515c57c0"
instance_type
"t3.medium"
key_name
null
monitoring
false
private_ip
"10.0.1.41"
public_ip
"52.223.134.146"
subnet_id
"subnet-c8249f67"
tags
{environment = "prod", managed_by = "opentofu", team = "orders", name = "orders-worker"}
user_data
null
vpc_security_group_ids
["sg-dc3c10ff"]

cloud_lb.public

arn
"arn:cloud:elasticloadbalancing:eu-west-1:000000000000:loadbalancer/app/orders-public/dd83d73d5d75ca7d"
dns_name
"orders-public-133879307.eu-west-1.cloud-elb.internal"
enable_deletion_protection
true
id
"orders-public"
internal
false
load_balancer_type
"application"
name
"orders-public"
security_groups
["sg-dc3c10ff"]
subnets
["subnet-c8249f67", "subnet-c923d137", "subnet-9486b397"]
tags
{environment = "prod", managed_by = "opentofu", team = "orders"}
zone_id
"Z421B1B24846BA"

cloud_dns_record.api

fqdn
"api.orders.example.com"
id
"api.orders.example.com_CNAME"
name
"api.orders.example.com"
records
["orders-public-133879307.eu-west-1.cloud-elb.internal"]
ttl
300
type
"CNAME"
zone_id
"Z0SAMPLEZONEID01"
Answer the question above first.Read a plan
Everything you do here stays in this browser.Part of liter8.sh · v0.1.0