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#
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.
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.
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.
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.
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.
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.
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#
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.
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.
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#
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.
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.
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.
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.
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#
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.
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.
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#
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.
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.
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.
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.
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#
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.
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.
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#
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.
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.
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.
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.
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#
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).
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.
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#
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.
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.
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.
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.
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#
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.
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.
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#
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.
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).
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.
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.
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#
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.
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.
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#
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.
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.
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.
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.
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#
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.
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#
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.
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.
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.
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.
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#
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.
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.
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#
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.
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.
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.
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.
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#
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.
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.
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 |