Terraform & IaC
The plan/apply lifecycle, remote state and locking, drift and import, modules, count vs for_each, workspaces, taint/replace, and safe state refactors.
13 questions
JuniorTheoryVery commonWhat is Terraform, and what do plan and apply do in its lifecycle?
What is Terraform, and what do plan and apply do in its lifecycle?
Terraform is a declarative Infrastructure-as-Code tool — you write desired state in HCL and it reconciles infrastructure to match. plan previews the config-vs-state diff without changing anything; apply executes that diff and records it in state.
Common mistakes
- ✗Thinking
applyruns without first computing a plan-style diff - ✗Believing Terraform mutates infrastructure step-by-step like a shell script
- ✗Assuming
planchanges real resources rather than only previewing
Follow-up questions
- →Why is applying a saved plan file safer than a bare
applyin CI? - →What does Terraform do when the config matches state exactly?
JuniorTheoryVery commonWhat is the Terraform state file, and why does Terraform need it?
What is the Terraform state file, and why does Terraform need it?
State maps each resource in your config to the real object Terraform created, plus its attributes. It is the source of truth plan diffs against — so never lose it or hand-edit it. It stores those values in plaintext, secrets included, so it must be protected.
Common mistakes
- ✗Committing local
terraform.tfstateto Git, exposing plaintext secrets - ✗Hand-editing state to fix a resource instead of using Terraform commands
- ✗Thinking state is a disposable cache Terraform can rebuild live
Follow-up questions
- →Why does a remote encrypted backend beat a local state file?
- →What breaks if two engineers hold divergent copies of the same state?
JuniorTheoryCommonWhat is drift in Terraform, and how does terraform plan detect it?
What is drift in Terraform, and how does terraform plan detect it?
Drift is real infrastructure diverging from what state records — usually a change made outside Terraform, like a console edit. plan refreshes by reading each resource's live attributes, compares them to the config, and shows the difference it would reconcile.
Common mistakes
- ✗Thinking drift means the HCL config changed, not the real resource
- ✗Believing
plancompares only stored snapshots and never reads live state - ✗Assuming Terraform blocks all out-of-band console changes
Follow-up questions
- →When would you accept drift into state with
apply -refresh-only? - →How does the
lifecycleblock'signore_changesinteract with expected drift?
MiddleTheoryCommoncount versus for_each — which recreates resources when a list changes, and why?
count versus for_each — which recreates resources when a list changes, and why?
count indexes resources by position, so removing or inserting a middle element shifts every later index — Terraform sees new addresses and destroys and recreates them. for_each keys resources by a stable set or map key, so one entry's change touches only that key and reordering does nothing.
Common mistakes
- ✗Using
countover a list whose middle elements can be removed - ✗Thinking reordering a
countlist is a harmless no-op - ✗Believing
countandfor_eachare interchangeable in behaviour
Follow-up questions
- →How do you migrate a
countresource tofor_eachwithout destroying it? - →What key would you choose for
for_eachover a list of objects?
MiddleTheoryCommonHow do you bring an existing resource under Terraform — import block versus CLI?
How do you bring an existing resource under Terraform — import block versus CLI?
import adopts an existing resource — it writes the real object into state under a config address, creating nothing. The CLI form mutates state at once; the newer import block is declarative and reviewable in code review. Either way you still write the matching resource block yourself.
Common mistakes
- ✗Expecting
importto generate the resource's HCL config for you - ✗Thinking
importcreates or rebuilds the real resource - ✗Forgetting to write the resource block before importing
Follow-up questions
- →Why can a mismatched config after import show a spurious replace on the next plan?
- →How do
importblocks help review a large adoption in a pull request?
MiddleTheoryCommonWhat is a Terraform module, and what makes one a good reusable building block?
What is a Terraform module, and what makes one a good reusable building block?
A module is a reusable folder of resources with a defined interface — inputs and outputs. A good one is narrow and composable: it exposes only meaningful inputs, returns useful outputs, hardcodes nothing environment-specific, and is versioned so callers can pin a known release.
Common mistakes
- ✗Hardcoding environment-specific values instead of taking them as inputs
- ✗Consuming a module at
latestinstead of pinning a version - ✗Exposing every internal resource rather than a small output surface
Follow-up questions
- →Why pin a module version instead of tracking the default branch?
- →How do you compose small modules without creating a deep dependency chain?
MiddleTheoryCommonWhat is remote state, and why must Terraform lock it during an apply?
What is remote state, and why must Terraform lock it during an apply?
Remote state keeps the state file in a shared, encrypted backend instead of on local disk, so a team shares one source of truth. During apply Terraform locks it; without a lock two concurrent applies interleave writes and corrupt state, then track resources wrongly.
Common mistakes
- ✗Keeping state on local disk while several engineers apply the same config
- ✗Thinking concurrent applies are safe because writes are atomic
- ✗Assuming the lock is for performance rather than correctness
Follow-up questions
- →How does an S3 backend implement locking with a DynamoDB lock table?
- →What is
terraform force-unlock, and when is it dangerous to use?
MiddleTheoryCommonWhat are Terraform workspaces, and what are their limits for multi-environment setups?
What are Terraform workspaces, and what are their limits for multi-environment setups?
A workspace is a named separate state sharing one config and backend, so dev and prod stay separate. Isolation is weak, though — they share one backend and credentials, so a wrong-workspace apply still reaches prod. For strong separation, teams use separate configs per environment.
Common mistakes
- ✗Treating workspaces as strong prod isolation despite the shared backend
- ✗Thinking each workspace can have its own backend or credentials
- ✗Confusing CLI workspaces with Terraform Cloud workspaces
Follow-up questions
- →When are workspaces a reasonable fit — ephemeral or per-developer stacks?
- →Why do separate backends give stronger blast-radius isolation than workspaces?
MiddleDebuggingOccasionalSomeone changed a resource in the cloud console and plan wants to revert it — explain and resolve
Someone changed a resource in the cloud console and plan wants to revert it — explain and resolve
Terraform detected drift — the console set t3.large while the config still says t3.small, so plan reverts reality to code. To keep the console value, update the config to t3.large, or add ignore_changes for that attribute. To discard it, just apply and let Terraform revert. Never hand-edit state.
Common mistakes
- ✗Hand-editing
terraform.tfstateto match the console value - ✗Tainting the resource, forcing a needless destroy and recreate
- ✗Assuming live infrastructure always wins over the config
Follow-up questions
- →When is
apply -refresh-onlythe right way to accept a console change? - →How does
ignore_changesdiffer from removing the attribute from config?
MiddleDesignOccasionalYour org runs dev, staging and prod for a dozen services across two teams, and you must design the Terraform repository and state layout. Requirements: shared infrastructure modules reused across all environments; each environment isolated so a mistaken apply in dev can never touch prod state; separate remote backends per environment, each with locking and least-privilege credentials; a clear promotion path from dev to prod; and safe collaboration for two teams without lock contention on one giant state. Describe the repo structure, how you split state, how modules are shared and versioned, and how you keep the blast radius small. Do not write code.
Your org runs dev, staging and prod for a dozen services across two teams, and you must design the Terraform repository and state layout. Requirements: shared infrastructure modules reused across all environments; each environment isolated so a mistaken apply in dev can never touch prod state; separate remote backends per environment, each with locking and least-privilege credentials; a clear promotion path from dev to prod; and safe collaboration for two teams without lock contention on one giant state. Describe the repo structure, how you split state, how modules are shared and versioned, and how you keep the blast radius small. Do not write code.
Factor shared infra into versioned modules pinned per environment. Give each environment its own root config and remote backend with locking and least-privilege credentials, so blast radius stays one env. Split state per service to cut lock contention, and promote by bumping pinned versions from dev to prod.
Common mistakes
- ✗Keeping all environments in one shared state and backend
- ✗Isolating environments with workspaces alone instead of separate backends
- ✗Sharing one broad credential across dev, staging and prod
Follow-up questions
- →Where would you draw state boundaries — per service, per layer, or per team?
- →How does version-pinned promotion beat editing prod config directly?
MiddleDebuggingOccasionalterraform apply fails with Error acquiring the state lock — diagnose and safely resolve it
terraform apply fails with Error acquiring the state lock — diagnose and safely resolve it
The backend lock protects state from concurrent writes. The cancelled run died before releasing its lease, so the lock is stale — nothing is applying. Verify no apply is live from the Who/Created fields, then terraform force-unlock with the shown ID. Never force it blindly.
Common mistakes
- ✗Force-unlocking without first checking whether an apply is live
- ✗Deleting the state file or lock table to clear the error
- ✗Assuming the lock expires on its own after a timeout
Follow-up questions
- →Why is a blind
force-unlockdangerous during a real concurrent apply? - →How does a DynamoDB lock table serialize applies against an S3 backend?
SeniorCodeOccasionalRename a Terraform resource without a destroy/recreate — complete the moved block
Rename a Terraform resource without a destroy/recreate — complete the moved block
Add a moved block mapping the old address aws_instance.web to the new aws_instance.app. On the next plan Terraform rewrites the state address in place, so nothing is destroyed or created. It is declarative and reviewable, unlike a one-off terraform state mv.
Common mistakes
- ✗Assuming a plain rename is matched by attributes, not by address
- ✗Reaching for
terraform state mvinstead of a reviewablemovedblock - ✗Thinking
prevent_destroyperforms the rename
Follow-up questions
- →When is a one-off
terraform state mvstill the right tool overmoved? - →How do
movedblocks help when refactoring resources into a module?
SeniorDebuggingOccasionalAfter a provider upgrade, plan wants to replace a live database — diagnose and avoid data loss
After a provider upgrade, plan wants to replace a live database — diagnose and avoid data loss
A provider major bump can change which attribute forces replacement — here engine_version now forces a replace. Read the upgrade guide first. Avoid the destroy with ignore_changes or prevent_destroy; if cosmetic, reconcile via -refresh-only. Never apply a live-DB replace blindly.
Common mistakes
- ✗Applying a
-/+replace on a stateful resource without investigating - ✗Assuming a provider upgrade can never force a replacement
- ✗Thinking
ignore_changescannot stop a forced replacement
Follow-up questions
- →How does
create_before_destroyreduce downtime when a replace is unavoidable? - →Why read the provider's upgrade guide before bumping a major version?