Organize the answer around ownership, limits, failure, and recovery. Definitions become interview-ready when they survive a concrete production scenario.
Question set
8 detailed answers
01Why does Terraform state exist at all, and how do you protect it when working in a team?
middle
Short answer: The state file maps resource addresses from your config to real cloud IDs and attributes, plus the dependency graph. Without it Terraform wouldn't know what it manages and would have to scan the whole cloud on every plan. For teams: remote backend + locking + encryption.
In depth:
- Mapping —
aws_instance.web↔i-0abc123with attributes; this is exactly what plan diffs against. - Performance — refresh queries only the resources the state knows about, not the entire account.
- Sensitive data — DB passwords and keys sit in state in plaintext: treat the state file as a secret.
Team setup:
terraform {
backend "s3" {
bucket = "corp-tfstate"
key = "prod/network.tfstate"
dynamodb_table = "tf-locks" # state locking
encrypt = true
}
}
- Remote backend (S3/GCS/Terraform Cloud) — a single source of truth instead of state on someone's laptop;
- Locking (DynamoDB) — concurrent applies can't clobber each other;
- Encryption + restricted access — because of the secrets inside.
⚠️ Common mistake: "state is just a cache, you can delete and rebuild it." Lose the mapping and Terraform "forgets" its resources — the next apply creates duplicates.
02Someone changed infrastructure by hand in the console. How does Terraform behave, and what do you do?
middle
Short answer: That's drift. Terraform notices nothing until the next plan: on refresh it compares the real cloud against state and shows the divergence. Then you decide case by case — revert (apply restores the declared state) or codify (update the config to match reality).
In depth:
- Detection —
terraform plan(refresh) reads current attributes from the API and diffs them against the config. - Decide per case:
| Option | When | Action |
|---|---|---|
| Revert | the manual change was a mistake or a temporary hotfix | terraform apply restores the declared state |
| Codify | the change should stay | update the config until plan shows "no changes" |
- Prevention — read-only console access for humans (changes go through the pipeline only), scheduled drift-detection plans in CI that alert on a non-empty diff.
⚠️ Common mistake: assuming Terraform continuously auto-reverts manual changes. That's what GitOps-style reconcilers do (Argo CD etc.) — Terraform only compares reality to config when you run it.
03How do you bring a hand-built production resource under Terraform management?
middle
Short answer: terraform import (or import blocks in modern Terraform) attaches an existing resource to a state address. Classic import does NOT generate config — you write it yourself and iterate until plan shows "no changes".
In depth:
# 1. Write a config skeleton
resource "aws_s3_bucket" "legacy" { ... }
# 2. Attach the real resource to a state address
terraform import aws_s3_bucket.legacy my-prod-bucket
# 3. Refine the config until plan is empty
terraform plan # → No changes = config matches reality
# TF 1.5+: declarative, with config generation
import {
to = aws_s3_bucket.legacy
id = "my-prod-bucket"
}
terraform plan -generate-config-out=generated.tf
- Import only touches state — the cloud is untouched; the dangerous moment is apply while the plan still isn't clean.
terraform state mv— for refactors (renaming a resource, moving it into a module) without destroy/recreate: you change the state address, not the resource.
⚠️ Common mistake: importing and immediately applying with a half-written config — Terraform "fixes" production to match the incomplete config and strips the settings it doesn't mention.
04terraform plan says it will destroy and recreate the production database. Why might that be, and what do you do?
senior
Short answer: Most often: a ForceNew attribute changed (immutable in the provider API), the resource address was renamed (Terraform sees "delete old, create new"), or an identifier changed. The graded skill is READING the plan and stopping before apply.
In depth:
# aws_db_instance.main must be replaced
-/+ resource "aws_db_instance" "main" {
~ engine_version = "14.7" -> "15.3"
~ identifier = "app-db" -> "app-db-v2"
# forces replacement
}
- ForceNew attributes — the parameter can't be changed in place via the API (AZ, identifier, some disk types) → the provider demands recreation; plan tags the culprit with "forces replacement".
- Address renames — fixed with a
movedblock orterraform state mv, not destroy/recreate. - Protections:
lifecycle { prevent_destroy = true }on critical resources — apply fails with an error; in CI, apply strictly from a saved plan file so exactly what was reviewed is what runs.
⚠️ Common mistake: skimming the plan and hitting yes. The -/+ marker ("must be replaced") is the key thing the interviewer wants to hear about reading plans.
05Two engineers run terraform apply at the same time. What happens with and without state locking?
junior
Short answer: With locking, the second apply blocks or fails with "Error acquiring the state lock" — the first one holds the lock. Without it — a race: both read the same state version and overwrite each other's writes, leaving the state inconsistent.
In depth:
with lock: A: apply ──► lock OK ──► works ──► unlock
B: apply ──► Error acquiring the state lock ✗
without lock: A: read state v1 ──► write v2a ─┐
B: read state v1 ──► write v2b ─┴─► last writer
"wins"
- Consequences of the race — lost records: a resource exists in the cloud but not in state (orphan), or the state is corrupted and gets fixed by hand via
state rm/import. - Mechanics — the backend takes a lock for the duration of the operation (a DynamoDB table for S3);
force-unlockis only for stuck locks. - The full answer — applies run only through the CI pipeline, serially; humans run plan locally.
⚠️ Common mistake: treating locking as an optional nicety "for big teams" — one overlapping apply is enough to spend a day untangling state by hand.
06How do you structure Terraform code for dev/stage/prod environments?
middle
Short answer: Shared versioned modules + thin per-environment root configs (directory per env or workspaces). Split state by blast radius — network / data / app separately, so a single apply physically cannot take everything down at once.
In depth:
modules/ # shared, versioned
vpc/ rds/ app/
envs/
dev/ main.tf ──► own state
stage/ main.tf ──► own state
prod/
network/ ──► state 1 (layers = blast radius)
data/ ──► state 2
app/ ──► state 3
- Directory per env vs workspaces — directories are explicit: different module versions, different backends, env diffs visible in git; workspaces are more compact, but environments differ only by a variable and it's easy to apply to the wrong one.
- Splitting state by layer — the network changes rarely, the app changes often; separate states shrink the blast radius and plan time.
- Cross-stack references — the
terraform_remote_statedata source, or publishing outputs via SSM.
⚠️ Common mistake: one giant state for the whole company — every plan takes forever, and any apply potentially touches everything.
07Terraform vs Ansible: when do you use each?
junior
Short answer: Terraform is declarative provisioning with state and lifecycle: create/change/destroy infrastructure. Ansible is configuration management and orchestration: configure what already exists, agentless over SSH. The typical split: Terraform brings up the VM/cluster, Ansible gets it ready.
In depth:
| Terraform | Ansible | |
|---|---|---|
| Job | infrastructure provisioning | configuration and orchestration |
| Model | declarative + state | playbook top to bottom, idempotent modules |
| State | state file, plan/apply, drift | no state — checks actual host facts |
| Delivery | cloud APIs | agentless push over SSH |
| Strength | lifecycle, dependencies, destroy | packages, configs, services, ad-hoc tasks |
- The boundary shifts — with immutable images (Packer) configuration is baked into the image at build time, shrinking Ansible's runtime role to a minimum.
- No state means no deletion — Ansible won't remove what you simply deleted from the playbook; Terraform will, because it remembers.
⚠️ Common mistake: trying to do "everything with one tool" — Terraform is awkward for fine-grained OS configuration, and stateless Ansible manages cloud resource lifecycles poorly.
08What does idempotency mean in Ansible, and how do shell/command tasks break it?
middle
Short answer: Idempotency means a re-run of the playbook converges to the described state instead of repeating actions: a module checks the current state and reports changed or ok. command/shell always execute and always report changed unless you constrain them.
In depth:
# Bad: runs on every play
- shell: tar xzf /opt/app.tar.gz -C /opt/app
# Acceptable: guard conditions
- shell: tar xzf /opt/app.tar.gz -C /opt/app
args:
creates: /opt/app/bin/run # skip if the file already exists
- command: /usr/local/bin/migrate
register: out
changed_when: "'applied' in out.stdout"
# Best: a proper module — it checks state itself
- unarchive:
src: /opt/app.tar.gz
dest: /opt/app
remote_src: true
- Why it matters — a perpetual
changedbreaks reporting and handlers (notifications fire on every run), and the actions themselves may not be safely repeatable. - The tools:
creates:/removes:,changed_when:/failed_when:— or replace the task with a purpose-built module.
⚠️ Common mistake: a playbook full of raw shell tasks is a bash script in YAML syntax: it has no idempotency, only the appearance of it.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.