Terraform interview questions
Terraform and IaC interview questions from fresher to senior — state and workflow, the language, lifecycle and operations, and scaling and security, each answer tagged by experience level.
Fresher 0–1y · Junior 1–3y · Mid 3–6y · Senior 6+y — every answer is tagged so you can prep for your level.
What is Terraform state?Fresher
A JSON file mapping your configuration to real resource IDs and caching their attributes. It is how Terraform knows what exists, computes diffs, and must be stored remotely with locking for teams.
terraform state list # every tracked resource terraform state show aws_instance.web # state = JSON mapping addresses -> real IDs + attributes
What is the core Terraform workflow?Fresher
init downloads providers and configures the backend; plan shows the diff to reach desired state; apply makes it real; destroy tears it down. write -> plan -> apply is the everyday loop.
terraform init terraform plan -out tf.plan terraform apply tf.plan
plan vs apply?Fresher
plan computes and shows the changes needed to reach desired state; apply executes them. Saving the plan with -out and applying that file guarantees you run exactly what you reviewed.
Saving the plan to a file and applying that exact file is what makes review meaningful — apply then runs precisely what was approved, even if the world changed between plan and apply.
terraform plan -out tf.plan # capture the diff terraform show tf.plan # human review terraform apply tf.plan # run exactly that
Why remote state with locking?Junior
It shares one source of truth across the team and prevents two applies from corrupting state by running at once. Backends like S3+DynamoDB or Terraform Cloud provide the lock.
terraform {
backend "s3" {
bucket = "tf-state"
key = "prod/network.tfstate"
dynamodb_table = "tf-lock" # the lock
encrypt = true
}
}How do you import existing infrastructure?Mid
Write the resource block, then `terraform import <addr> <id>` (or an import block) to bind it to state; a follow-up plan should show no changes once the config matches reality.
Write the resource block first, then bind the real object into state. A follow-up plan must be a no-op — if it wants to change things, your config does not yet match reality.
import {
to = aws_s3_bucket.logs
id = "my-logs-bucket"
}
# then: terraform plan (should be no-op)How do you refactor state safely?Mid
Use terraform state mv to rename or move a resource without destroy/recreate, and moved blocks to make that refactor reviewable in code. state rm forgets a resource without deleting the real thing.
terraform state mv aws_instance.a aws_instance.web terraform state rm aws_instance.old # forget, do not destroy
What is a module?Junior
A reusable, parameterized group of resources with input variables and outputs. The root module calls child modules to keep configuration DRY and composable.
module "vpc" {
source = "./modules/vpc"
cidr = "10.0.0.0/16"
}
# use outputs elsewhere: module.vpc.subnet_idsvariables, locals, and outputs — how do they differ?Junior
variables are inputs a caller sets; locals are named expressions computed once for reuse inside a module; outputs expose values to the caller or to remote state. Keep secrets in sensitive variables, never in committed locals.
variable "env" { type = string }
locals { name = "app-prod" }
output "url" { value = aws_lb.web.dns_name }count vs for_each?Mid
count creates N indexed copies (good for identical resources); for_each creates instances keyed by a map/set, so adding or removing one does not reshuffle the others’ addresses.
count keys by numeric index, so removing the middle item renumbers everything after it and forces recreation. for_each keys by a stable map/set key, so add/remove touches only that instance — prefer it for anything non-identical.
resource "aws_iam_user" "u" {
for_each = toset(["alice", "bob"])
name = each.key
}Data sources vs resources?Junior
A resource creates and manages infrastructure; a data source only reads existing infrastructure or provider data to feed other resources — no create/destroy, just a lookup at plan time.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
}
# use: data.aws_ami.ubuntu.idHow do dynamic blocks and for expressions help?Mid
A for expression transforms or filters a collection; a dynamic block generates repeated nested blocks (like ingress rules) from a collection instead of copy-paste. They keep modules DRY when inputs vary in length.
dynamic "ingress" {
for_each = var.ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
}
}Why avoid provisioners?Mid
They run imperative scripts Terraform cannot track in state or plan, so they break idempotency and drift detection. Prefer cloud-init, images built by Packer, or a config-management tool for in-instance setup.
Provisioners run imperative scripts Terraform cannot see in the plan or track in state, so they break idempotency and only fire at create/destroy. Bake images with Packer, use cloud-init, or hand configuration to Ansible instead.
What does lifecycle { create_before_destroy } do?Mid
It provisions the replacement before destroying the old resource, avoiding downtime for things like instances behind a load balancer. prevent_destroy and ignore_changes are the other lifecycle levers.
By default Terraform destroys then recreates, which causes downtime for a resource behind a load balancer; create_before_destroy flips the order. prevent_destroy blocks accidental deletion, and ignore_changes stops fighting an externally-managed attribute.
resource "aws_instance" "web" {
# ...
lifecycle {
create_before_destroy = true
ignore_changes = [tags]
}
}What is drift and how do you handle it?Mid
Drift is when real infrastructure diverges from state (someone changed it in the console). `terraform plan` reveals it; you then reconcile by reverting the manual change or updating config, and add policy/permissions to prevent out-of-band edits.
A refresh-only plan shows how the real world diverged from state without proposing config changes. You then either codify the manual change or let apply revert it — and tighten IAM so out-of-band edits stop happening.
terraform plan -refresh-only terraform apply -refresh-only # accept real state into tfstate
How do you force-replace a single resource?Mid
terraform apply -replace=ADDRESS recreates one resource (the modern replacement for terraform taint) — useful when a resource is wedged but its configuration has not changed.
terraform apply -replace=aws_instance.web # older Terraform: terraform taint aws_instance.web
When would you use -target, and why is it a smell?Senior
To surgically apply one resource during an incident. It is discouraged because it applies a partial graph and can leave state inconsistent — routine use usually signals modules that are too coupled.
It applies a partial dependency graph, so outputs and dependent resources can end up inconsistent with state. Fine as an incident escape hatch; routine use usually means modules are too coupled or the plan is too big to reason about.
What problem do moved blocks solve?Senior
They record that a resource was renamed or moved into a module so Terraform updates state addresses instead of destroying and recreating — refactoring without downtime or manual `state mv`.
moved {
from = aws_instance.a
to = aws_instance.web
}
# plan updates the address instead of destroy + createHow does terraform plan compute the diff?Senior
It builds a dependency graph from references, refreshes current state, and walks the graph to produce a create/update/replace/destroy action per resource. Ordering and parallelism come from that graph, not file order.
How do you structure state for many environments and teams?Senior
Split state by blast radius — per environment and per component — with separate backends/workspaces, shared modules in a registry, and remote state data sources (or explicit outputs) for cross-stack references. One giant state is a liability.
Split state by blast radius: per environment and per component (network, data, app), each with its own backend key, so a bad apply cannot touch everything. Share logic via versioned modules and read across stacks with remote state or explicit outputs.
# envs/prod/network/backend.tf key = "prod/network.tfstate" # envs/prod/app/backend.tf key = "prod/app.tfstate"
Secrets end up in state — how do you protect them?Senior
Treat state as sensitive: encrypt the backend at rest, lock down access, mark variables sensitive, and source secrets from a manager at apply time. Never commit tfvars with secrets; rotate anything that leaks.
Even sensitive-marked values are stored in plaintext inside state, so the real control is protecting the backend: encryption at rest, strict access, and never state in git. Source secrets from Vault/SSM at apply time and rotate anything that leaks.
variable "db_password" {
type = string
sensitive = true
}
# backend: encrypt=true + IAM-restricted bucket, no public accessHow do you enforce policy on Terraform changes?Mid
Policy-as-code in the pipeline — OPA/Conftest or Sentinel on the plan JSON, plus tfsec/checkov for misconfig — gating merges so public buckets or open security groups fail review, not production.
terraform show -json tf.plan > plan.json conftest test plan.json # OPA/Rego rules tfsec . # or: checkov -d .
How do you manage provider versions and the lockfile?Mid
Pin providers with version constraints in required_providers and commit .terraform.lock.hcl so every run and CI uses identical provider versions and checksums. Bump deliberately with terraform init -upgrade.
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
# commit .terraform.lock.hclHow do you run Terraform safely in CI/CD?Senior
Plan on PR (posted for review), apply only on merge to main from a protected pipeline with least-privilege cloud credentials via OIDC, remote state with locking, and a stored plan so apply runs exactly what was approved.
Run plan on every PR and post it for review; apply only on merge to main from a protected pipeline. Use OIDC for short-lived cloud credentials (no static keys), remote state with locking, and apply the stored plan so it runs exactly what was approved.