All resources
Terraform · Interview prep

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.

ExperienceAll levelsFresherJuniorMidSenior

Fresher 0–1y · Junior 1–3y · Mid 3–6y · Senior 6+y — every answer is tagged so you can prep for your level.

State & workflow
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.

State maps config to real infra
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.

write .tfinitplanapply
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.

Review then 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.

S3 + DynamoDB backend
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 block (1.5+)
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.

Move without recreating
terraform state mv aws_instance.a aws_instance.web
terraform state rm aws_instance.old   # forget, do not destroy
Language
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.

Call a module
module "vpc" {
  source = "./modules/vpc"
  cidr   = "10.0.0.0/16"
}
# use outputs elsewhere: module.vpc.subnet_ids
variables, 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.

The three
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.

Stable keys with for_each
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.

Read existing infra
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
}
# use: data.aws_ami.ubuntu.id
How 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.

Generate ingress rules
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.

Lifecycle & operations
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.

Zero-downtime replace
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.

manual console changeplan -refresh-onlyreconcile: revert or codifylock down IAM
Detect drift
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.

Recreate one resource
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`.

Rename in code, not by hand
moved {
  from = aws_instance.a
  to   = aws_instance.web
}
# plan updates the address instead of destroy + create
How 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.

parse configbuild dependency graphrefresh real statediff -> planned actions
Scale & security
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.

per env (dev/stg/prod)per component (net/data/app)versioned modulesremote state refs
Backend key per component
# 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.

Sensitive var + locked backend
variable "db_password" {
  type      = string
  sensitive = true
}
# backend: encrypt=true + IAM-restricted bucket, no public access
How 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.

Policy on the plan JSON
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.

Pin + lock
terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}
# commit .terraform.lock.hcl
How 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.

PR: plan + policyhuman reviewmerge to mainpipeline apply (OIDC)
Go deeper
Full, hands-on DevSecOps courses
Browse courses