Terraform: Language & Workflow: interview questions
The question you are most likely to get for each topic, a model answer, and what else comes up. From chapter 12 of the course.
Walk me through what terraform init, plan and apply each do. Junior
terraform initprepares the working directory: configures the backend (where state lives), downloads providers (and modules) into.terraform/, and writes or checks.terraform.lock.hcl. It touches no infrastructure. Run it after cloning and after changing providers, modules or the backend.terraform planrefreshes state (asks the providers what really exists), diffs it against the configuration, and shows the actions:+create,~update in place,-/+replace,-destroy. It changes nothing.-out=tfplansaves exactly that plan.terraform applymakes the changes - a fresh plan and ayesprompt, or exactly the saved plan withterraform apply tfplan. A failed apply is not rolled back: what was created stays in state, you fix the cause and apply again.
Terraform is declarative: you describe the end state, and because it is idempotent, applying the same code twice changes nothing the second time.
Also asked: What is the difference between declarative and imperative infrastructure tools? · What is Terraform state, and why does it exist? · What is the difference between a resource and a data source?
What is Infrastructure as Code, and why is Terraform called declarative? Junior
Infrastructure as Code means the infrastructure - networks, machines, databases, firewall rules - is described in text files kept in git. So it is reproducible (the same code builds the same environment), reviewable (a change is a diff someone reads before it happens) and versioned (history, blame, revert).
Declarative: a resource block does not say "create"; it says "this must exist":
resource "azurerm_resource_group" "orders" {
name = "rg-orders-dev"
location = "westeurope"
}
Terraform compares that with what exists and does whatever is needed - nothing, if it is already there. That makes it idempotent, safe to run on every commit. An imperative tool, like a bash script, is a list of steps: run it twice and you get two of everything or an error.
Terraform provisions (creates the infrastructure); configuration management like Ansible sets up what runs inside a machine. They complement each other.
Also asked: What is the difference between Terraform and Ansible? · What is a Terraform provider? · What does "multi-cloud" mean with Terraform, and what does it not mean?
Learn it: 12.1 What Terraform is for, and the blocks it is made of
What is .terraform.lock.hcl, and should it be committed? Junior
It is the dependency lock file, like package-lock.json: for every provider it records the exact version that terraform init chose, the constraints the code allowed, and hashes (checksums) of the download, so a tampered or different binary is refused.
Commit it. Without it, two engineers resolving ~> 4.14 a week apart can get different provider versions and see different plans from identical code.
Things to know:
- It locks providers only, not modules.
- Only
initchanges it;terraform init -upgradeis the deliberate move to the newest allowed version, and the lock-file diff goes into the same PR. - Hashes are per platform: a lock made on a Mac may fail on Linux CI until you run
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64.
The version constraint itself lives in required_providers: ~> 4.14 allows 4.x from 4.14 up, but not 5.0.
Also asked: What does the ~> version constraint mean? · How do you use two configurations of the same provider, for two regions? · Where should provider credentials come from?
How do you pass different values to the same Terraform code for dev and prod? Junior
Declare input variables and give them values from outside:
variable "env" {
type = string
validation {
condition = contains(["dev", "test", "prod"], var.env)
error_message = "env must be dev, test or prod."
}
}
Then one .tfvars file per environment: terraform plan -var-file=prod.tfvars. Other ways: -var 'env=prod', TF_VAR_env, terraform.tfvars and *.auto.tfvars (read automatically), and the default.
Precedence, lowest to highest: default, TF_VAR_ environment variables, terraform.tfvars, *.auto.tfvars, then -var / -var-file in command-line order. When a value is not what you expect, walk that list from the top.
Inside the code use var.env; build derived values once in locals; expose results with output. A secret variable gets sensitive = true - which hides it in output but not in state.
Also asked: A variable is marked sensitive. Is its value safe? · What is the difference between a variable and a local? · What happens when a required variable has no value in a CI job?
Learn it: 12.5 Variables, locals and outputs
How do you make a Terraform configuration reject bad input before it creates anything? Mid
Three layers, earliest first:
- Type constraints on variables:
number,list(string),map(object({ cidr = string, nsg = optional(bool, true) })). Terraform converts what it safely can ("3"to 3) and rejects the rest at the door, naming the variable. Beware: extra object attributes are silently dropped. validationblocks for which values are acceptable:condition = can(cidrhost(var.cidr, 0)), with anerror_messagethat tells the person what is allowed. Since 1.9 a condition may refer to other variables.- Custom conditions for rules about how things fit together: a
preconditionin a resource'slifecyclechecks an assumption before the change (and blocks the plan); apostconditionchecks the result throughself; an output precondition guards what a module promises.
A check block is different: it warns but never fails the run - for monitoring things that can drift, like a certificate's expiry.
Also asked: What is the difference between a list, a set and a map in Terraform? · What does nullable = false do on a variable? · When would you use a check block instead of a precondition?
Explain for expressions and splat expressions, and when you would use each. Junior
A for expression transforms one collection into another, like JavaScript's array.map(); square brackets make a list, braces make a map, and an if filters:
[for s in var.subnets : s.name if s.public]
{ for name, cidr in var.subnets : name => cidrhost(cidr, 4) }
A splat [*] is shorthand for "this attribute of every element": azurerm_subnet.app[*].id is the same as [for s in azurerm_subnet.app : s.id].
Use a splat for the simple case on a list (a count resource). Use a for expression when you filter, reshape, build a map, or work on a for_each resource (which is a map: values(azurerm_subnet.app)[*].id or a for).
Try either in terraform console before writing it down. And for JavaScript habits: + is arithmetic only - build strings with interpolation ("rg-${var.env}") - and == never converts types.
Also asked: How do you build strings in Terraform? · What does a dynamic block do? · What is the difference between templatefile and interpolation?
Learn it: 12.11 Expressions: references, operators, for, splat and templates
Which Terraform functions do you use most, and for what? Junior
The everyday ones, all testable in terraform console:
merge(a, b)- combine maps, later arguments win: layering tags (merge(local.common_tags, var.extra_tags)).lookup(map, key, default)- a safe map index: per-environment sizes.cidrsubnet(prefix, newbits, netnum)/cidrsubnets- carve subnets without overlaps:cidrsubnet("10.20.0.0/16", 8, 3)is10.20.3.0/24.cidrhostpicks an address in a subnet.toset- turn a list into the set of stringsfor_eachaccepts.flatten- turn nested lists into one, for nestedfor_each.try,coalesce,one- missing values:tryreturns the first expression that does not error,coalescethe first not null or empty,one(x[*].id)reads a conditionalcountresource.format,lower,replace,substr- names that follow the cloud's naming rules.jsonencode,templatefile,file- generated settings and start-up scripts.
There are no user-defined functions; locals and modules are how you reuse logic.
Also asked: What is the difference between try and coalesce? · How would you split a /16 into subnets of different sizes in Terraform? · How do you safely reference a resource created with count = var.enabled ? 1 : 0?
How does Terraform decide the order in which it creates resources? Junior
From references, not from file order. When azurerm_virtual_network.main uses azurerm_resource_group.main.name, it implicitly depends on the group. Terraform builds a dependency graph from every reference, then walks it - independent resources in parallel (10 at a time by default), dependents after what they need. Destroy walks the same graph backwards. terraform graph prints it.
The classic bug: a resource names its resource group with a literal string instead of a reference, so it has no edge in the graph and fails on a fresh environment with ResourceGroupNotFound. The fix is the reference, not depends_on.
depends_on is only for hidden dependencies that no attribute shows - for example a secret that can only be written once a role assignment exists. Overusing it slows plans and, on a data source, defers the read to apply.
A data source is read-only: it looks up something that exists, Terraform never creates or deletes it.
Also asked: What is the difference between a data source and terraform import? · When would you use depends_on? · What are meta-arguments in Terraform?
Learn it: 12.18 Data sources, references and the dependency graph
What is the difference between count and for_each, and why does it matter? Junior
Both create several instances of a resource; the difference is how each instance is addressed, and the address is the identity Terraform matches to state:
count = 3- addressed by index:azurerm_subnet.s[0],[1],[2].for_each = { web = ..., app = ... }(a map or a set of strings) - addressed by key:azurerm_subnet.s["web"].
Remove the second item of a list under count and every element after it shifts down an index; anything whose name cannot change in place is replaced. One removed subnet becomes four destroyed. With for_each, removing "app" is exactly one destroy.
Rule: for_each for anything with an identity, keyed by something unique, stable and known at plan time (a name, not an ID that exists only after apply). count for identical anonymous copies and the on/off switch (count = var.enabled ? 1 : 0, read with one(...[*])).
Already on count? Rename the state entries with moved blocks, and check the plan shows no replacements.
Also asked: Why does for_each not accept a list? · What does "known at plan time" mean for for_each keys? · How do you migrate a resource from count to for_each without recreating it?
Learn it: 12.20 count vs for_each - the one that causes outages
How do you read a Terraform plan before approving it? Junior
- Read the summary line last, not first. "1 to add, 1 to change, 1 to destroy" can hide a replacement - that is the add and the destroy together.
- Search for
must be replacedand# forces replacement.-/+means destroy, then create; the marked attribute is why. A replaced storage account or database loses its data. - Check every
-destroy is intended. (known after apply)is normal on creates, suspicious on something that should be stable.- "Objects have changed outside of Terraform" above the plan means someone changed things by hand, and this plan may undo it.
Then apply exactly what was reviewed: terraform plan -out=tfplan, review, terraform apply tfplan. A plain apply re-plans and can differ. In automation, terraform show -json tfplan | jq can list every delete and stop the job.
Also asked: What does terraform validate check, and what does it not? · Why should you not use -target routinely? · What does terraform plan -detailed-exitcode return, and what is it used for?
Learn it: 12.24 The workflow, read like a reviewer
Practise these answers with flashcards and labs Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.