State, Modules & Drift
The file Terraform uses to know what exists, the modules that keep it reusable, and what happens when reality disagrees.
The File Everything Depends On
A plan is state compared against configuration and against the provider's view of reality. That three-way comparison is why state must be accurate, shared, and never edited casually.
Remote State & Locking
Local state works until a second person runs apply. Then two files disagree, and the one applied second happily destroys what the first created. Remote state in a shared backend — S3 with DynamoDB, Terraform Cloud, GCS, an Azure storage account — makes one copy authoritative.
| Feature | Prevents |
|---|---|
| Shared backend | Two people holding divergent truths |
| State locking | Concurrent applies corrupting the file |
| Versioning on the bucket | An unrecoverable bad write |
| Encryption at rest | Secrets in the state sitting in plaintext |
Split state by blast radius, not by neatness. One enormous state file means every change plans against everything, applies slowly, and puts production networking in the same lock as a developer sandbox. Separate states per environment, and per component where the lifecycle genuinely differs.
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "prod/network/terraform.tfstate" # one key per component
region = "eu-west-2"
dynamodb_table = "tf-locks" # locking
encrypt = true
}
}
Modules
A module is a directory of Terraform with inputs and outputs — the unit of reuse. The useful ones are narrow and opinionated: a module that takes forty variables to cover every possible case is harder to read than the resources it wraps.
| Practice | Why |
|---|---|
| Pin module versions | An unpinned module changes under you between applies |
| Keep inputs minimal | Every variable is an interface you maintain forever |
| Output what callers need | Reaching into a module's internals couples them together |
| No provider blocks inside | Providers are configured by the root, or reuse breaks |
| Version the module itself | Consumers upgrade deliberately |
Drift & Imports
Drift is reality diverging from state: someone changed a security group in the console, an autoscaler resized a group, another tool edited a tag. The next plan wants to undo it, which is either exactly right or an outage, depending on why it changed.
| Cause | Response |
|---|---|
| A manual console change | Fix the code, or remove console access — that is the real fix |
| Another tool managing the same resource | Decide one owner; two systems will fight forever |
| A provider default changing | Pin the provider version |
| Legitimate runtime change (autoscaling) | Ignore that attribute explicitly |
Run plan on a schedule against production and alert on unexpected diffs. Drift found on a Tuesday morning is a ticket; drift found during an incident, while someone is trying to apply an urgent change, is a second incident.
Importing brings existing infrastructure under management: write the configuration to match, import the resource into state, then plan until the diff is empty. An empty plan is the proof — anything else means the code and reality still disagree.
Interview Questions
Why does Terraform need state?
It maps configuration to real resource ids. Without it Terraform cannot tell what already exists, so it cannot distinguish creating a resource from updating one.
Why must state be remote and locked?
Local state diverges the moment a second person applies, and concurrent applies corrupt the file. A shared backend with locking makes one copy authoritative.
Is state sensitive?
Yes. It stores resource attributes including generated passwords and connection strings, so it is encrypted at rest, access-controlled, and never committed.
How should state be split?
By blast radius. One giant state plans against everything, applies slowly and puts unrelated environments under the same lock — separate per environment and per component with a different lifecycle.
What is drift and how do you handle it?
Reality diverging from state, usually via console changes. Detect it with scheduled plans, then fix the cause: correct the code, remove console access, or explicitly ignore genuinely runtime-managed attributes.
How do you bring existing infrastructure under Terraform?
Write configuration matching it, import the resource into state, then plan repeatedly until the diff is empty — an empty plan is the proof that code and reality agree.