# Interview Questions: Terraform & Infrastructure as Code

Companion question bank for the 9-part tutorial series in this folder:
`01-fundamentals-and-workflow.md`, `02-state-management-and-remote-backends.md`,
`03-modules-and-reusable-design.md`, `04-workspaces-and-environments.md`,
`05-providers-data-sources-and-provisioners.md`, `06-drift-detection-import-and-refactoring.md`,
`07-testing-terraform.md`, `08-cicd-for-terraform.md`, `09-governance-cost-and-multi-cloud-at-scale.md`.

Answers are short and plain — expand out loud using the diagrams and worked examples in the tutorials.

---

# Part 1 Questions: Fundamentals, HCL & the Plan/Apply Workflow

## Conceptual

### 1. What's the practical difference between Terraform and OpenTofu in 2026?
Terraform is HashiCorp/IBM's BSL-licensed tool with HCP Terraform's managed pipeline and Sentinel; OpenTofu is the Linux Foundation-governed, MPL 2.0 fork with its own newer features (state encryption, provider `for_each`). The HCL syntax and provider ecosystem are shared, so switching later is low-friction.

### 2. Why does Terraform track resources by address, not by resource name on the target platform?
Because the address is the only key state and the dependency graph use — renaming a resource's local name creates a brand-new address with no relationship to the old one, which is why an unplanned rename triggers a destroy-and-recreate.

### 3. What's the difference between a resource and a data source?
A resource is something Terraform creates, updates, and destroys. A data source only reads an existing object through a provider — no create/update/delete ever happens through it.

### 4. Explain the plan/apply workflow's refresh step.
Before computing a diff, `terraform plan` queries the real, current state of every tracked resource through its provider, not just trusting the recorded state file — this is the actual mechanism behind drift detection.

### 5. When should you use `count` vs. `for_each`?
`for_each` for anything with a natural key (a name, a region), since removing a middle item only affects that one instance. `count` is best reserved for disposable, order-independent, all-identical resources, since removing a middle index shifts every later index and can trigger unnecessary destroy/recreate churn.

### 6. What does `terraform plan -out=tfplan` followed by `terraform apply tfplan` guarantee that a bare `terraform apply` doesn't?
It guarantees the applied plan is exactly the one that was reviewed — a bare `apply` re-plans internally immediately before applying, which can differ from what a human reviewed if anything changed in between.

### 7. What's the difference between `~>` and `>=` in a version constraint?
`~>` ("pessimistic constraint") allows only the rightmost version segment to increment — `~> 5.60` allows up to but not including `6.0.0`. `>=` is an open-ended lower bound with no automatic upper limit.

## Applied / Scenario

### 8. A PR renames `aws_instance.web` to `aws_instance.app` with no other changes. What does `terraform plan` show, and why?
`1 to add, 1 to destroy` — Terraform sees two unrelated addresses, not a rename, because it has no concept of "rename" as a plan action on its own.

### 9. A reviewer sees a plan for a "harmless refactor" showing `0 to add, 1 to change, 2 to destroy`. What should they check before approving?
Whether the destroy-then-add sequence creates any window where a critical resource (a security group's rules, a load balancer's target group) is briefly absent or misconfigured — a config diff that looks harmless can still produce a genuinely destructive plan.

### 10. Why is `sensitive = true` on a variable not sufficient to protect a real secret long-term?
It only redacts the value from CLI/plan output — the state file itself still stores the raw value in plain JSON. State-level encryption and access control are the actual controls.

---

# Part 2 Questions: State Management & Remote Backends

## Conceptual

### 11. What are the `serial` and `lineage` fields in a state file for?
`serial` is a monotonic counter incremented on every write, used to detect concurrent/stale writes. `lineage` is a UUID identifying one state's history, generated once — a mismatch signals two unrelated state histories being treated as compatible.

### 12. Why does S3-native locking (`use_lockfile`) replace the old DynamoDB-based approach?
Terraform 1.10+ uses S3's own conditional-write support (`If-None-Match`) to create a lock object directly in the bucket, removing the need for a second service (DynamoDB) purely to hold a lock record.

### 13. What's the difference between `terraform state mv` and a `moved` block?
`state mv` is a one-time, local, unreviewed command run directly against the backend. A `moved` block lives in version control, is reviewable in a PR, and applies automatically and consistently across every environment that runs the configuration.

### 14. Why is one giant, monolithic state file an anti-pattern?
Every plan evaluates the entire file (slower), and the blast radius of any single mistake spans everything the file tracks — a typo in one service's config can produce a plan that touches unrelated shared infrastructure.

### 15. What does `terraform_remote_state` let you do that a bare data source lookup by tag doesn't?
It creates an explicit, declared dependency on another Terraform configuration's own outputs, rather than an invisible, undeclared coupling based on a tag that could be renamed or reassigned with no warning.

## Applied / Scenario

### 16. Two engineers run `terraform apply` against the same S3-backed state within seconds, with `use_lockfile = true`. What happens?
The second apply fails immediately with a lock-held error — the conditional-write lock means only one client can hold it at a time.

### 17. A `terraform destroy` is run against what looks like a disposable load-test directory, but the plan shows `34 to destroy` against clearly-named production resources. What's the likely cause?
A stale or copy-pasted `backend` block still pointing at production's actual state `key`, silently overriding what the directory name suggests — always read the plan's actual resource names, never trust the folder name alone.

### 18. A team wants to split a 140-resource monolithic state into four smaller states. What command family enables this without destroying anything?
`terraform state mv` (with `-state-out` to move resources into a new state), after a `terraform state pull` backup — this moves resources between state files without touching the real infrastructure.

---

# Part 3 Questions: Modules & Reusable Infrastructure Design

## Conceptual

### 19. Why can't a child module contain its own `provider` block and still support `for_each`?
A module's own `provider` block makes it incompatible with `for_each`, `count`, and `depends_on` — Terraform enforces this because providers aren't graph-ordered nodes the way resources are. Provider configuration belongs only in the root module.

### 20. What's the difference between implicit and explicit provider inheritance?
The default (non-aliased) provider is inherited automatically by a child module. Any aliased provider must be passed explicitly via the `providers = {}` map — it's never inherited automatically.

### 21. Why should a module's outputs be treated as append-only?
Removing or renaming an output breaks any caller reading it — silently, and only at that caller's own next `plan`, not at the module's own point of change. Adding a new output is always safe.

### 22. What problem does `optional()` solve in an `object` type constraint?
It lets a caller omit an attribute and get a specified default, instead of being forced to specify every single attribute for every object in a collection — key to keeping a flexible module's interface ergonomic.

### 23. Name two signals that a module is too coarse ("god module") and two that it's too granular.
Too coarse: requires 15+ variables to configure, or two different callers only partially want it. Too granular: exactly one resource with no real logic, or pushes all composition burden onto every caller.

## Applied / Scenario

### 24. A module with an aliased `provider "aws" { alias = "east" }` block is called with `for_each = toset(["a","b"])`. What happens?
`terraform plan` fails with a "module does not support for_each" error — remove the provider block from the module and configure/pass the aliased provider from the root module instead.

### 25. A public registry module bump from `~> 5.1` to `~> 5.4` shows an unexpected in-place change on every managed resource. What's the likely cause, and what should have caught it earlier?
A changed default value for a previously-unset argument, introduced somewhere in the 5.1–5.4 range. Reading the module's changelog before merging the version bump — not just the version-number diff — is what should have caught it.

### 26. A module's `max_size` variable has a `validation` block referencing `var.min_size`. What Terraform version is required, and what did teams do before it existed?
Terraform 1.9+ — before that, cross-variable validation required a workaround using a `precondition` block inside a resource's `lifecycle`, since `validation` blocks couldn't reference other variables.

---

# Part 4 Questions: Workspaces, Environments & Real-World Repository Structure

## Conceptual

### 27. What's the difference between a CLI workspace and an HCP Terraform workspace?
A CLI workspace is a named, isolated state file within the same backend/configuration — a free, built-in feature. An HCP Terraform workspace is an entire managed run environment in HashiCorp's SaaS product. They share a name but are otherwise unrelated concepts.

### 28. Why is directory-per-environment generally safer than CLI workspaces for permanent environments like production?
Switching CLI workspaces is a silent, local CLI state change — easy to forget which one is selected. A directory is explicit and visible in every terminal prompt and CI job's working directory, removing that class of mistake structurally.

### 29. What problem does Terragrunt's `include "root"` / `find_in_parent_folders()` mechanism solve?
It lets shared configuration (backend generation, provider generation, common variables) be defined once at a parent level and inherited automatically by every leaf deployment, removing the duplication plain directory-per-environment accumulates as environments multiply.

### 30. What real, structural boundary does account-per-environment provide that directory isolation alone can't?
A credential/permissions mistake in one account has no path to another account at all — it's not a permissions boundary inside one shared account that a misconfiguration could erode, but a genuine, separate identity and API surface.

### 31. What's the actual mechanism behind the hub-and-spoke `assume_role` cross-account pattern?
A central "hub" identity (a CI role or human SSO identity) calls AWS STS to assume a "spoke" role scoped to one target account, receiving short-lived, temporary credentials — no long-lived credential for the target account needs to exist outside that account.

## Applied / Scenario

### 32. A team has three near-identical, disposable load-test environments created and destroyed weekly, and one permanent, structurally distinct production environment. What structure fits each?
CLI workspaces for the load-test environments (ephemeral, identical). A dedicated directory — and ideally its own AWS account — for production (permanent, structurally distinct, high stakes).

### 33. A `terraform plan` for the `staging` directory shows unrecognized ARNs, even though `backend.tf` correctly points at the staging state bucket. What's the likely cause?
The backend (state location) is correct, but the ambient credentials actually in use (a stale `AWS_PROFILE` or session) point at a different account than the directory implies — the fix is an explicit account-ID assertion via a `precondition`, not just correct backend config.

### 34. Why does bootstrapping a new account's own state backend require a deliberate exception to "always use remote state"?
The state bucket that would normally hold remote state is itself infrastructure that needs to be created first — there's nowhere "further down" to delegate that one bootstrap configuration's own state to, so it legitimately uses local state as a permanent, narrow exception.

---

# Part 5 Questions: Providers, Data Sources & Provisioners Deep Dive

## Conceptual

### 35. Why does configuring the `kubernetes` provider from an EKS cluster created in the same apply cause intermittent failures?
Provider configurations aren't nodes in Terraform's dependency graph the way resources are — Terraform can't guarantee the cluster's API is actually reachable at the moment it configures the provider, producing a timing-dependent race. The fix is splitting cluster creation and in-cluster resources into two separate states.

### 36. What's the difference between a data source returning zero results and returning multiple results?
Zero results is usually a typo or a resource that hasn't been created yet in this environment. Multiple results, without a tie-breaker like `most_recent = true`, fails the plan outright with an ambiguous-match error — arguably the safer of the two failure modes, since it surfaces immediately rather than silently.

### 37. Name HashiCorp's recommended order of alternatives before reaching for a provisioner.
A provider-native resource first, then `user_data`/cloud-init, then a Packer-built image, then a configuration-management tool — a provisioner is the last resort after all of these are genuinely exhausted.

### 38. What does `terraform_data` replace, and why is it needed?
It replaces the older `null_resource` pattern as the provider-agnostic, built-in resource for holding a provisioner or a `triggers_replace` value when there's no real cloud resource to attach it to.

### 39. Why can't you reference a resource's own attribute directly inside its own provisioner/connection block?
It would create a dependency cycle — the resource would depend on its own completed creation to compute its own configuration. `self.<attribute>` refers to the current resource's own attributes without that cycle.

## Applied / Scenario

### 40. A `remote-exec` provisioner fails intermittently in production but never in dev. What's the likely cause?
A timing race between the security group rule's API "success" response and its actual enforcement — production's tighter security group takes slightly longer to fully propagate than dev's more permissive one, and `remote-exec`'s SSH attempt can land in that gap.

### 41. A data source filtered only on `tag:Team = "platform"` silently resolves to the wrong security group after an org reorg temporarily shares that tag across two services. What's the fix?
Filter on the most specific identifying attributes available — a fully-qualified, environment-and-service-prefixed name, or a combination of tag plus VPC scope — rather than a single loosely-shared tag that can match more than one real object.

### 42. What does `templatefile()` provide that a `remote-exec` inline script doesn't?
Its rendered result is a plain, diffable string argument, fully visible in `terraform plan` output — a provisioner's effect is completely invisible to plan and untracked in state.

---

# Part 6 Questions: Drift Detection, Import & Refactoring Existing Infrastructure

## Conceptual

### 43. What does `terraform plan -refresh-only` do, and what does it deliberately NOT do?
It updates Terraform's understanding of current reality and shows exactly what changed, without touching real infrastructure and without proposing any remediation — pure detection, fully separated from any fix decision.

### 44. Name the three legitimate responses to detected drift.
Update `.tf` files to match reality (if the change was legitimate), `apply -refresh-only` then a normal apply to revert it (if the change was wrong), or `ignore_changes`/`removed` (if the resource shouldn't be Terraform's concern at all).

### 45. What's the difference between a `moved` block and a `removed` block?
`moved` tells Terraform two addresses are the same real object, avoiding a destroy-and-recreate on a rename/refactor. `removed` tells Terraform to stop managing a resource entirely, with an explicit choice (`destroy = true/false`) about whether the real infrastructure should be deleted too.

### 46. What does `-generate-config-out` do, and what's its biggest limitation?
Paired with an `import` block, it inspects a real resource and writes matching HCL automatically, instead of requiring hand-written configuration. It's still experimental, doesn't work for `count`/`for_each`-targeted resources that don't already exist in config, and doesn't reach into module-internal resources.

### 47. Why is `terraform apply -replace=<addr>` generally preferred over the older `terraform taint` command?
`-replace` is scoped to one specific apply invocation and leaves no persistent marker. `taint` persists in state until the next apply, which can surprise a completely unrelated later apply that nobody remembers the taint was there for.

## Applied / Scenario

### 48. An engineer renames `aws_instance.web` to `aws_instance.frontend`, reasoning "the plan will just show a rename." What actually happens?
The plan shows `1 to add, 1 to destroy` — Terraform has no "rename" concept, only address identity. A `moved` block is what supplies the "these are the same object" information Terraform can't infer on its own.

### 49. A `terraform import` of an S3 bucket succeeds, but the next `plan` still shows several changes instead of "no changes." What's wrong, and is re-running the import the fix?
The hand-written (or generated) `.tf` configuration doesn't yet exactly match the real bucket's attributes — the import itself already succeeded; re-running it doesn't help. The fix is adjusting the configuration to match what the plan diff shows, iterating until it's clean.

### 50. A security group is manually widened to `0.0.0.0/0` during an incident and never reverted. A scheduled drift check catches it the next night. What's the correct two-step fix?
`terraform apply -refresh-only` first (to accept the current drifted state), then a normal `terraform apply` (to revert it back to the originally-declared, narrower rule) — never skip the refresh-only step first, or the revert plan can be confusing to read.

---

# Part 7 Questions: Testing Terraform

## Conceptual

### 51. What does `tflint` catch that Checkov doesn't, and vice versa?
`tflint` catches provider-specific schema mistakes (invalid instance types, deprecated arguments). Checkov catches security/compliance misconfigurations (unencrypted storage, open ingress). Neither is a superset of the other — most teams run both.

### 52. What's the difference between a plan-mode and an apply-mode `terraform test` run block?
Plan mode (`command = plan`) answers "would Terraform compute the right values" without touching real infrastructure — fast, often free. Apply mode (`command = apply`) answers "does the value match reality after real creation" — slower, needs real provider access unless mocked.

### 53. What does `mock_provider` let you do that a real provider connection doesn't?
It fakes provider responses entirely, so even `command = apply` tests run fast and fully offline with no real credentials or infrastructure — useful for testing logic that only exists post-apply without paying real provisioning time/cost.

### 54. Why is `defer terraform.Destroy(...)` placed immediately after building `terraformOptions` in a Terratest suite, before `InitAndApply` runs?
Go's `defer` guarantees the destroy call runs when the function returns, including on failure — but only if the `defer` statement was actually reached. Placing it first ensures cleanup happens even if `InitAndApply` itself panics.

### 55. What's the honest limitation of a complete Part 7 test suite, even with 100% of these tools passing?
It verifies each module's own internal correctness, not its effect on other configurations consuming its outputs, and it can't catch a genuinely novel misconfiguration outside any policy library's coverage — a green CI run narrows what a human reviewer needs to check, it doesn't eliminate the need for one.

## Applied / Scenario

### 56. A module's `terraform test` suite has ten `run` blocks, all `command = apply`, taking 12 minutes. What's the likely design problem?
Most checks probably don't actually need real, post-apply values — defaulting to the most expensive test mode rather than the cheapest one that answers the question. Converting logic-only checks to `command = plan` (or mocking) would likely cut most of that time.

### 57. Why should Terratest never run against a shared dev/staging account?
It genuinely provisions real infrastructure and incurs real cost, and a crashed test run (never reaching its `defer terraform.Destroy`) can leave real, orphaned resources — a dedicated, isolated test account contains both the cost and the risk.

---

# Part 8 Questions: CI/CD for Terraform

## Conceptual

### 58. What is the single most important structural rule for a Terraform CI/CD pipeline?
The apply stage must never re-run `terraform plan` — it must consume the exact plan artifact a human already reviewed, or the entire point of reviewing the plan is defeated.

### 59. How does OIDC eliminate the need for a long-lived cloud credential in CI?
CI requests a signed JWT from GitHub's OIDC provider, then exchanges it for short-lived STS credentials scoped by an IAM trust policy's conditions (repo, branch) — nothing long-lived is stored anywhere, and credentials expire automatically.

### 60. Why isn't a workflow YAML's `if: github.event_name == 'push'` condition itself a security boundary?
It only controls whether a specific job, as currently written, runs under normal circumstances — it provides no actual guarantee, since a different workflow run or a modified workflow file could bypass it. The IAM trust policy's own conditions are the real boundary.

### 61. What does Atlantis's `apply_requirements: [approved, mergeable]` do?
It's Atlantis's built-in equivalent of a GitHub Environment approval gate — `atlantis apply` is refused unless the PR already has required approvals and is in a mergeable state.

### 62. What does "rollback" actually mean for a Terraform pipeline?
A `git revert` of the offending commit, run through the exact same plan/apply pipeline as any other change — it produces a new, real plan to review, not an instant restoration of prior infrastructure.

## Applied / Scenario

### 63. Two PRs targeting the same project are merged a minute apart. What prevents the second apply from corrupting state, and what pipeline improvement avoids the resulting failed run?
Terraform's own state-serial check refuses to apply a plan whose expected starting state no longer matches reality. A `concurrency` group on the apply job (or Atlantis's native per-project locking) queues the second PR's apply instead of racing it.

### 64. An OIDC trust policy's `sub` condition matches any ref in a repository, not just `main`. What's the concrete risk?
A PR from a feature branch (potentially untrusted or compromised) could trigger a workflow run capable of assuming a production-capable role, entirely bypassing the "only merges to main apply" logic that exists only in workflow YAML, not in the actual IAM trust boundary.

### 65. A custom Slack notification built on `terraform show -json` leaks a real database password in plain text. What went wrong?
The custom tooling read raw attribute values without checking each field's own `sensitive` marker in the JSON — `sensitive = true` only auto-redacts through Terraform's own built-in formatters; hand-rolled JSON-consuming tools must check and respect that marker themselves.

---

# Part 9 Questions: Terraform at Team Scale — Governance, Cost & Multi-Cloud

## Conceptual

### 66. What's the practical difference between Sentinel and OPA/Conftest for policy as code?
Sentinel is HCP Terraform/Enterprise-native, deeply integrated with the managed run pipeline, but doesn't work with OpenTofu at all. OPA/Conftest is vendor-neutral, evaluating plan JSON as an ordinary CI step, working identically with Terraform or OpenTofu.

### 67. Why should most new governance policies default to soft-mandatory rather than hard-mandatory?
A hard-mandatory policy with no override path can block a genuine, time-sensitive emergency change entirely. Soft-mandatory with an audited override preserves normal-operations enforcement while adding the escape valve a real incident needs.

### 68. What changed about HCP Terraform's pricing in 2026, and why does it matter for architecture decisions?
The legacy unlimited-seat Free plan ended March 31, 2026; the new tiers bill per managed resource (Essentials/Standard/Premium). This is a real budget line item that makes self-hosted backends and third-party TACOS platforms genuine, not just academic, alternatives at scale.

### 69. Why is a "golden path" module considered the primary governance enforcement layer, with policy as code as the backstop?
A well-designed module bakes compliance in by default, making the compliant choice the easy choice structurally — policy as code only ever fires after someone has already written non-compliant configuration, catching what slipped past the module.

### 70. Why is a shared module abstracting AWS RDS and GCP Cloud SQL behind one interface considered a mistake?
It's a false abstraction — the two services are structurally different enough that hiding them behind one interface accumulates ever-growing per-cloud conditionals, the same god-module anti-pattern from module design applied one level up.

## Applied / Scenario

### 71. An organization commits to OpenTofu for its license, then a consultant recommends Sentinel for policy enforcement. What's wrong?
Sentinel only runs inside HCP Terraform/Enterprise and has no OpenTofu integration — this is a direct mismatch with the OpenTofu decision. OPA/Conftest is the vendor-neutral, correct recommendation instead.

### 72. A team's Checkov override rate for one specific check climbs steadily in `dev` but stays near zero in `staging`/`prod`. What should this prompt, and what shouldn't it prompt?
It should prompt investigating whether the check is miscalibrated specifically for `dev`'s lower-stakes risk profile, likely followed by scoping its enforcement level per environment. It shouldn't automatically prompt tightening enforcement further, which would add friction without fixing a genuine environment-specific mismatch.

### 73. An auditor asks a platform team to prove every production change last quarter was reviewed before being applied, and the team has no dedicated change-management system. What should they point to?
The pipeline's own existing artifacts — PR history with each plan, GitHub Environment required-reviewer approval records, and CI logs tied to short-lived OIDC identities — primary evidence generated automatically at the time each change happened, stronger than a separately-maintained log.

---

## Quick-Fire / Rapid Recall

| Q | A |
|---|---|
| Terraform vs. OpenTofu license? | BSL 1.1 (HashiCorp/IBM) vs. MPL 2.0 (Linux Foundation) |
| What does `moved` fix? | A resource address change, without destroy-and-recreate |
| What does `removed` fix? | Stopping management of a resource, with an explicit destroy choice |
| `terraform_remote_state` vs. a tag-filtered data source? | Explicit, declared cross-config dependency vs. an invisible, undeclared coupling |
| Why no `provider` block in a child module? | Blocks `for_each`/`count`/`depends_on` on any call to that module |
| CLI workspace vs. HCP Terraform workspace? | A named state file vs. a whole managed run environment — same name, unrelated concepts |
| Why account-per-environment over directory-only isolation? | A structural credential boundary, not just a conventional one |
| Provisioner alternatives, in order? | Native resource → user_data/cloud-init → Packer image → config management → provisioner |
| Why split cluster creation and in-cluster resources into two states? | Provider configs aren't graph-ordered — avoids the same-apply timing race |
| `-refresh-only` does what, exactly? | Detects drift, touches nothing, proposes no fix |
| `tflint` vs. Checkov? | Provider-schema correctness vs. security/compliance — run both |
| The one non-negotiable CI/CD rule? | Apply must consume the exact reviewed plan artifact, never re-plan |
| What does OIDC remove from CI? | Long-lived cloud credentials entirely |
| What's a Terraform "rollback," really? | A new, reviewed plan from a `git revert` — never instant |
| Sentinel vs. OPA/Conftest? | HCP-native, no OpenTofu vs. vendor-neutral, works with both |
| Soft- vs. hard-mandatory policy? | Overridable-with-audit-trail vs. no override at all |
| Primary vs. backstop governance layer? | A golden-path module vs. policy as code |
