Terraform interview questions and answers
25 Terraform questions from real interviews, sorted by seniority: state and locking, modules, count against for_each, drift, and the production mistakes that get talked about out loud.
All 25 questions
1
What problem does Terraform solve that a shell script does not?
JuniorTerraform is declarative and stateful. You describe the infrastructure you want, and it works out the difference between that and what exists, then makes only the changes needed. A script is imperative: it describes steps, and running it twice does something different from running it once.
That gives you three things a script does not:
- A plan you can review before anything changes.
- Idempotence, so converging on the desired state is safe to repeat.
- A dependency graph built from references between resources, so it knows a subnet needs its VPC first without you ordering the calls by hand.
It also spans providers, so one configuration can cover AWS, Cloudflare DNS and a GitHub repository in the same apply.
2
Walk me through terraform init, plan and apply.
Junior- 1`terraform init` prepares the working directory. It downloads the provider plugins, installs modules, and configures the backend where state will live. It is the only command that touches
.terraform, and it is safe to re-run. - 2`terraform plan` refreshes state, compares it to the configuration, and prints the actions needed. It changes nothing. The output is the review artefact, which is why teams run it in CI on pull requests.
- 3`terraform apply` executes that plan. Without a saved plan file it re-plans and asks for confirmation.
The detail worth adding: in CI you should save the plan and apply exactly that file, so what was reviewed is what runs.
terraform plan -out=tfplan terraform apply tfplan- 1`terraform init` prepares the working directory. It downloads the provider plugins, installs modules, and configures the backend where state will live. It is the only command that touches
3
What is the Terraform state file and why does it matter?
JuniorState is Terraform's record of which real resources correspond to which configuration addresses, plus the attribute values it last saw. Without it, Terraform cannot tell an instance it created from one somebody made by hand, so it cannot know whether to create, update or destroy.
Three consequences interviewers look for:
- It is the source of truth for the mapping, so losing it means Terraform will try to create everything again.
- It contains secrets in plain text, because any attribute a provider returns is stored, including generated passwords.
- It must be shared and locked for a team, which is why a local
terraform.tfstatedoes not survive contact with a second engineer.
4
What is the difference between a Terraform variable, a local and an output?
JuniorDirection Set from outside Used for `variable` Input Yes, by tfvars, -var, env or a module callParameterising a configuration `local` Internal No Avoiding a repeated expression `output` Output No Exposing a value to the operator, a parent module or another state The distinction that gets probed: a local is computed, not configurable, so reaching for one where the caller needs control is a design mistake rather than a style choice.
variable "env" { type = string } locals { name_prefix = "acme-${var.env}" } output "vpc_id" { value = aws_vpc.main.id }5
How do you manage Terraform state for a team, and what does locking do?
MidPut state in a remote backend that supports locking, so the file is shared and two applies cannot run at once. On AWS that is S3, with native locking since Terraform 1.10 and a DynamoDB table before that.
terraform { backend "s3" { bucket = "acme-tfstate" key = "production/terraform.tfstate" region = "eu-west-1" encrypt = true use_lockfile = true } }Locking prevents the failure it is there for: two concurrent applies both reading the same state, each writing back its own view, and the second silently discarding the first one's resources from the record.
Enable bucket versioning as well. It is the only undo you get if state is corrupted, and it costs nothing until you need it.
6
When would you use
Midterraform import, and what does it not do?When a resource exists but Terraform does not know about it: something created by hand, inherited from another team, or made by a different tool.
import { to = aws_s3_bucket.assets id = "acme-assets-prod" }The thing it does not do is write your configuration. Import only populates state. You still have to author HCL that matches the real resource, and if it does not match, the next plan will propose changes you did not intend. The workflow is import, then plan repeatedly, adjusting the configuration until the plan is empty.
Since Terraform 1.5 the
importblock makes this reviewable in a plan rather than a side-effecting CLI call, which is a meaningful improvement for teams.7
What makes a good Terraform module?
MidAny directory of
.tffiles is a module, so the question is really about interface design.A good module does one thing at a coherent level of abstraction, such as "a VPC with public and private subnets", not "all of our infrastructure". It exposes a small number of meaningful inputs with types and descriptions, and sensible defaults where a default is genuinely sensible. It outputs the identifiers a caller will need, because a caller that has to reach into a module's internals means the interface is wrong.
It also avoids two things. It does not configure providers internally, since that stops the caller controlling regions and aliases. And it does not hardcode environment names or account IDs, which is what makes it reusable at all.
Version your modules and pin the version at the call site, so upgrading is a deliberate change that shows up in a plan.
8
When would you use
Midfor_eachrather thancountin Terraform?Almost always, and the reason is how the resources are addressed.
Addressed by Removing an item Right for `count` Index, web[0]Every later index shifts down, so unchanged resources are destroyed and recreated A conditional resource `for_each` Key, web["api"]Affects only that entry Everything else The index shift is the classic painful
countbug, and it is what the question is really asking about.resource "aws_instance" "web" { for_each = var.instances instance_type = each.value.size tags = { Name = each.key } }countis still right for one thing: a conditional resource,count = var.enabled ? 1 : 0.9
What is Terraform drift, and how do you handle it?
MidDrift is the real infrastructure no longer matching state, because something changed outside Terraform: a console edit, another tool, or an autoscaling action.
Detect it with a plan, which refreshes state before comparing. In CI, a scheduled plan that should be empty is an effective drift alarm:
terraform plan -detailed-exitcodeExit code
0means no changes,2means changes pending,1means an error, which makes it easy to wire into a pipeline.Then decide per case. Usually you apply, so Terraform reasserts the declared state. If the manual change was correct, bring it into configuration instead. And where a value is legitimately managed elsewhere, such as a desired count under autoscaling, use
lifecycle { ignore_changes = [desired_capacity] }so it stops being reported as drift.10
Why lock provider versions, and what does the lock file do?
MidBecause providers are separate software with their own releases, and a new major version can change defaults, rename attributes or alter behaviour. Without a constraint, a fresh
initon a new machine can pull a different provider and produce a different plan from identical configuration.terraform { required_version = ">= 1.9" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.60" } } }.terraform.lock.hclrecords the exact versions and their checksums, and belongs in version control. It is what makes CI and a laptop resolve the same providers, and the checksums also protect against a tampered plugin.11
Terraform says the state is locked. What do you do?
MidAssume it is legitimate first. The message names who holds the lock and when they took it, and most of the time that is a CI run or a colleague mid-apply.
terraform apply -lock-timeout=10mThat waits rather than failing, and usually the other apply finishes inside the window.
Only when you are certain nothing is running do you break it:
terraform force-unlock 8f3c1e2a-4b7d-11ef-9c2aSay out loud that this is the dangerous step. Breaking a lock during a real apply can leave state not matching reality, or corrupt it outright. The senior version of this answer mentions bucket versioning as the recovery path, and that a lock left behind by a cancelled CI job is best fixed by making the pipeline clean up rather than by unlocking by hand each time.
12
What is the precedence order for setting Terraform variables?
MidLater wins. In order:
- 1Defaults in the
variableblock - 2
TF_VAR_environment variables - 3
terraform.tfvars - 4
terraform.tfvars.json - 5Any
*.auto.tfvars, in lexical order - 6
-varand-var-fileon the command line
The practical point is that the command line beats everything, which is what makes CI overrides work, and that a file named
terraform.tfvarsis picked up automatically while any other name needs-var-file.Worth adding: a variable with no default and no value supplied makes Terraform prompt interactively, which hangs a pipeline.
terraform plan -input=falseturns that into an error instead, which is what you want in CI.- 1Defaults in the
13
How do you structure Terraform state across environments, and what are the trade-offs?
SeniorThe main choice is separate state per environment, and the main mistake is one state for everything.
Separate root modules per environment, each with its own backend key, is the usual answer:
production/terraform.tfstate,staging/terraform.tfstate. Blast radius is contained, permissions can differ per environment, and a mistake in staging cannot destroy production. The cost is duplicated root configuration, which shared modules mitigate.Workspaces keep one configuration with multiple state files. They are convenient but weak for environments, because every workspace shares the same backend and therefore the same credentials and the same blast radius, and it is easy to apply to the wrong one. They suit short-lived parallel copies, such as per-branch review environments.
Also split by lifecycle, not just by environment. Networking, data stores and applications change at very different rates, and separating them keeps a routine deploy from planning against the VPC. The cost is cross-state references through
terraform_remote_stateor data sources, which is real coupling to manage.14
Someone deleted the Terraform state file. Walk me through recovery.
SeniorEstablish what still exists before touching anything, because the resources are almost certainly fine. Only Terraform's record of them is gone.
If the backend bucket has versioning, this is a restore, not a rebuild:
aws s3api list-object-versions --bucket acme-tfstate \ --prefix production/terraform.tfstate \ --query 'Versions[:5].[VersionId,LastModified]' --output tableFetch the last good version and push it back with
terraform state push. Check a plan afterwards: it should be empty or close to it.Without versioning there is no undo, and you are rebuilding state by importing every resource. That is slow and error-prone, which is exactly why versioning and a lifecycle policy on the state bucket are part of setting up a backend, not an optimisation.
The follow-up worth pre-empting: prevent it rather than recover it. Versioning, deny delete in the bucket policy for everyone but a break-glass role, and never let a local state file be the only copy.
15
What does the Terraform
Seniorlifecycleblock give you, and when is each part the right tool?Four behaviours, and each solves a specific real problem.
create_before_destroybuilds the replacement before removing the original, which avoids downtime for something like a launch template or an instance behind a load balancer. It requires that both can exist at once, so it fails on anything with a unique name.prevent_destroymakes Terraform error rather than delete. Correct for a production database or a state bucket. Note it blocks the whole apply, and does not stop somebody removing the block first.ignore_changestells Terraform to stop managing named attributes after creation. Right for values legitimately changed elsewhere, such as a desired count under autoscaling or a tag applied by another system.replace_triggered_byforces a replacement when a referenced resource changes, which is how you rebuild an instance when its launch template version moves.The senior caveat: all four hide a modelling problem when overused. Reaching for
ignore_changeson a long list of attributes usually means something else owns the resource and Terraform should not.16
How do you run Terraform in CI safely?
SeniorFour things, roughly in order of how much damage their absence causes.
Separate plan from apply. Plan runs on the pull request with read-only credentials and posts its output for review. Apply runs after merge with write credentials, from the saved plan file, so the reviewed change is the applied change.
Short-lived credentials. OIDC from the CI provider into a role, never a long-lived access key in a variable. This is the single biggest real-world risk on the list.
Locking and serialisation. A remote backend with locking, plus concurrency control in the pipeline so two merges do not race. And handle cancellation, since a killed job leaves the lock behind.
Guardrails.
-input=falseand-no-color, policy checks with OPA, Sentinel or Conftest, and a required human approval for destructive plans. Scanning withtfsecorcheckovcatches the obvious misconfigurations before they exist.Worth saying that state and plan files are sensitive. Plan output contains attribute values and should not be posted to a public pull request or kept as a build artefact indefinitely.
17
Terraform state holds secrets in plain text. What do you actually do about it?
SeniorAccept that it does, and reduce the exposure rather than pretend to encrypt it away.
Treat state as a secret store. Encrypt at rest, restrict the bucket to the roles that genuinely need it, enable access logging, and keep it out of version control. Most real leaks are an over-permissive bucket or a state file committed by accident, not a cryptographic failure.
Do not generate secrets in Terraform where you can avoid it. A
random_passwordwritten into an RDS instance is now in state forever. Prefer having the service generate it, or store it in a secret manager and have the application read it at runtime, so Terraform only manages the reference.Mark outputs sensitive so they are redacted from CLI output and plans. Be clear in the interview that this affects display only, not storage.
Rotate. If state was exposed, the credentials in it are compromised and rotating them is the only real remedy.
18
How do you upgrade a widely used module without breaking its consumers?
SeniorTreat it as an API, because that is what it is.
Version it and let callers pin. Follow semantic versioning honestly: renaming an input or changing a default that alters a resource is a major bump, whatever it looks like in the diff.
For a breaking change, add the new path alongside the old one and deprecate rather than replace. A new optional variable with a default that preserves current behaviour lets consumers migrate on their own schedule. Where a resource address has to change, ship a
movedblock so consumers get a no-op plan instead of a destroy and recreate:moved { from = aws_instance.web to = aws_instance.app }That block is the detail that separates people who have done this from people who have read about it. Without it, a refactor inside a module looks like data loss to everyone using it.
19
A terraform plan shows a resource being destroyed and recreated that you did not change. Why?
SeniorSomething upstream of it changed in a way that forces replacement, and the plan says which attribute with a
# forces replacementmarker. Read that first, because it names the cause.The usual candidates: an attribute the provider cannot change in place, such as an EC2 subnet or an RDS engine version on some paths; a
countindex shift after removing an earlier element; a module refactor that changed the resource address, so Terraform sees a removal and an addition rather than a rename; or a provider upgrade that changed a default.terraform plan | grep -B5 "forces replacement"For the address-change case the fix is a
movedblock. For an index shift it is migrating fromcounttofor_each. And if the replacement is genuinely required but the resource cannot take downtime,create_before_destroyis the mitigation.The habit worth stating: never approve a plan with an unexplained destroy. That is how databases disappear.
20
What is the difference between a Terraform data source and a resource?
MidA resource is something Terraform creates, updates and destroys, and therefore owns. A data source is something Terraform only reads, so it can reference values it did not create.
data "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] } }Two practical points. Data sources are read during plan, so a data source that depends on a resource created in the same apply can fail on the first run, which is why you sometimes need
depends_onor a two-stage apply. And a data source like the one above resolves to whatever is newest at plan time, so it is a source of surprise replacements unless you pin the AMI.21
How do you deploy the same Terraform configuration to two regions or accounts?
MidProvider aliases, passed explicitly to the resources or modules that need them.
provider "aws" { region = "eu-west-1" } provider "aws" { alias = "us" region = "us-east-1" } resource "aws_s3_bucket" "logs_us" { provider = aws.us bucket = "acme-logs-us" }For modules you pass a mapping with
providers = { aws = aws.us }. This is exactly why a reusable module should not declare its own provider block: doing so takes this control away from the caller.A CloudFront certificate is the canonical example, since ACM certificates for CloudFront must live in
us-east-1regardless of where everything else is.22
What does it mean that Terraform builds a dependency graph?
JuniorTerraform reads the references between resources and derives the order of operations from them. Writing
subnet_id = aws_subnet.private.idtells it the subnet must exist first; you never specify the order yourself.It uses that graph to parallelise as well, applying independent resources at the same time, which is why an apply is much faster than the same calls in sequence.
Where it needs help is a dependency that is real but not expressed as a reference, such as an IAM policy that must attach before a service can start.
depends_onstates that explicitly. Needing it often is usually a sign the configuration is passing values around that it should be referencing directly.23
How would you introduce Terraform to infrastructure that was all built by hand?
SeniorIncrementally, and starting with the parts where a mistake is cheap.
Set up the foundations first: a state backend with versioning and locking, a module layout, CI running plan on pull requests, and OIDC credentials. That is all additive and breaks nothing.
Then bring existing resources under management one bounded area at a time, lowest risk first. Networking and IAM are usually a bad place to start despite being foundational, because a wrong plan there is an outage. Something like a staging environment, or DNS records, gives you the workflow without the risk.
For each area: write HCL, import, then iterate until
terraform planis empty. An empty plan is the acceptance test, and reaching it is most of the work because hand-built resources have accumulated attributes nobody documented.Two organisational points that matter as much as the technical ones. Close the console door progressively, or drift undoes the work as fast as you do it. And do not try to model everything: some things are genuinely better left out, and pretending otherwise produces a configuration nobody trusts.
24
How do you debug a provider error that is not obvious from the plan?
MidTurn on logging, which is the part people forget exists.
TF_LOG=DEBUG TF_LOG_PATH=./tf.log terraform applyTF_LOG=DEBUGincludes the API requests and responses the provider makes, which is usually where the real error is. A permissions problem, for instance, shows as a specific denied API call rather than the generic message Terraform surfaces.Then narrow it.
terraform plan -target=aws_instance.webisolates one resource, andterraform consolelets you evaluate expressions against real state to check whether a value is what you think:terraform console > local.name_prefix > aws_vpc.main.cidr_blockSay that
-targetis a debugging tool, not a workflow. Routinely applying with-targetproduces state that no plan has ever fully validated.25
Which Terraform files should and should not be in version control?
JuniorCommit the configuration and the lock file:
*.tf,*.tf.json, and.terraform.lock.hcl. The lock file belongs in Git specifically so CI and every laptop resolve identical provider versions.Do not commit
.terraform/, which is a local cache of plugins and modules and can be hundreds of megabytes. Do not committerraform.tfstateor its backups, both because it belongs in a remote backend and because it contains secrets in plain text. Do not commit*.tfvarsfiles holding real values, since that is a common way credentials end up in a repository, and do not commit saved plan files..terraform/ *.tfstate *.tfstate.* *.tfvars !example.tfvars tfplan crash.logKeeping a checked-in
example.tfvarswith placeholder values is a good habit, since it documents the inputs without carrying any.
Live exercise: the plan wants to destroy something you did not touch
Asked in almost every senior Terraform interview, usually with a plan on screen. The habit being tested is that you never approve an unexplained destroy.
Does the plan name an attribute with
# forces replacement?terraform plan | grep -B5 'forces replacement'- yes
- An immutable attribute changed. The provider cannot alter it in place, so it must replace the resource. Read which attribute, because that is the actual cause.
- no
- Nothing forced a replacement, so Terraform is seeing a removal and a separate addition. That means the resource address changed, not the resource.
Did you rename a resource or move it between modules?
- yes
- Terraform has no way to know that is a rename. Add a
movedblock and the plan becomes a no-op. - no
- Check whether a provider upgrade changed a default, which shows up as a change nobody made. The lock file tells you whether the provider moved.
Is the resource created with
count?terraform state list | grep '\['- yes
- Removing an earlier element shifts every later index down one, so resources that did not change are planned for replacement. Migrate to
for_eachso addressing is by key. - no
- If it is genuinely a required replacement, the question becomes whether it can be done without downtime.
Can the resource tolerate being replaced?
- yes
- Apply, and prefer
create_before_destroyso the replacement exists before the original goes. - no
- Stop. For a database or anything holding state, add
prevent_destroy, and change the approach so the attribute does not need to move.
What each level is testing
- 1
Junior6 questions
Whether you understand it is declarative and stateful. The
init,plan,applycycle, what state is for, and the difference between a variable, a local and an output. Saying that state maps configuration to real resources is most of the answer. - 2
Mid11 questions
Whether you have worked in a team. Remote backends and locking,
countagainstfor_eachand why the index shift matters, module interfaces, drift, and provider version pinning. - 3
Senior8 questions
Whether you have cleaned up after an incident. State layout across environments, recovering a deleted state file,
movedblocks when refactoring a shared module, running Terraform in CI without long-lived credentials, and the fact that state holds secrets in plain text.
What the round is like
Terraform rounds concentrate on state, because that is where the expensive mistakes live. Expect definitions early, then a stretch on modules and iteration, and then a scenario: a plan proposing something destructive, a lock that will not clear, or state that has been lost. Interviewers are listening for whether you have operated Terraform with other people, not whether you have read the documentation.
How to prepare with this
- 1Learn the state answers cold. State is the single most asked area, and the locking and recovery questions are where candidates either sound experienced or do not.
- 2Be able to say why
for_eachbeatscountin one sentence about stable addressing. This comes up constantly and a vague answer is very visible. - 3Practise the destructive-plan scenario out loud. Reading
forces replacementfrom a plan and explaining what caused it is a common live exercise. - 4Know the
movedblock exists. It is the clearest single signal that you have refactored a module other people depended on. - 5Have an opinion on workspaces against separate root modules, and be able to defend it. Interviewers ask this to see whether you reason about blast radius.