Terraform
The dominant declarative provisioning tool: HCL config, a provider per platform, a state file mapping config to real resources, and the plan/apply loop. Expect Terraform in almost any infra interview, and expect most of the questions to end up at state.
on this pageshowhide
guide
overview
~2 minTerraform is HashiCorp's declarative provisioning tool. You describe infrastructure in HCL, providers turn that description into API calls against a cloud or service, and a state file remembers which real objects belong to which blocks of configuration. It turns up in nearly every infrastructure interview because most cloud teams run it, and because its failures are specific enough to probe: a plan that replaces a database nobody meant to touch, two applies racing each other, a password sitting readable in a bucket. A strong Terraform answer says what Terraform knows at that moment, where it learned it, and what it will do next. The hub has six sections. [HCL & Resources](/topics/cloud-terraform-hcl-resources) is the language: resources, data sources, expressions and the meta-arguments that decide how many instances exist and how they are replaced. [Variables & Outputs](/topics/cloud-terraform-variables-outputs) covers how values enter and leave a configuration. [Modules](/topics/cloud-terraform-modules) is packaging and reuse. [Providers & Registry](/topics/cloud-terraform-providers) is the plugin layer: authentication, pinning, and aliases for more than one region or account. [State & Backends](/topics/cloud-terraform-state) holds the hardest material — remote storage, locking, import, refactoring and secrets. [Plan/Apply Workflow](/topics/cloud-terraform-workflow) wraps it all in commands, workspaces, CI and drift handling. Junior rounds check the model: what `plan` and `apply` each do, how a data source differs from a resource, where credentials should come from. Middle rounds move to identity and ordering — `count` against `for_each`, locking, the lock file, variable precedence. Senior and principal rounds are design and incident conversations: where to split an estate into separate states, how to ship a breaking module version, what to do when state is lost or a lock is stuck, and how production is kept out of reach of a mistake made elsewhere. Learn resources and references first, then state, because most later questions end up there. Variables, modules and providers follow; the workflow section makes sense once you can read a plan.
primer
### Declare the end state, not the steps A configuration says what should exist. Terraform compares that declaration with what it believes exists and proposes creates, in-place updates, replacements and deletes to close the gap. Block order and file layout mean nothing to it; order comes from the dependency graph. So the interview question is rarely which command to run, and usually what the plan will propose and why. ### State is the bridge to the real world A cloud account does not tell Terraform which objects it created. State does: it maps each resource address to a real object and caches what Terraform last saw of it. Nearly every hard topic lives here — shared storage and locking for teams, drift when someone changes things by hand, import and refactoring when the mapping must change without touching the object, and secrets stored in plain text. Treat state like a production database, not a disposable cache. ### An address is an identity Every resource instance has an address: module path, type, name, and an index or key when `count` or `for_each` creates several. Terraform matches configuration to state by address, so renaming a block, moving it into a module or reordering a list changes identity. Unless you record the move, the plan reads it as destroy one object and create another. Many "why does the plan want to recreate this?" questions end here. ### References build the graph Using one resource's attribute inside another creates a dependency edge, and Terraform walks the resulting graph in parallel. Values that exist only after something is created stay unknown during plan and spread to everything built from them. An explicit `depends_on` is the exception, for ordering the configuration cannot express. ### Providers do the real work Terraform core knows no cloud. Each provider is a separately versioned plugin with its own schema, authentication and quirks, and its schema decides whether a change is an in-place update or a forced replacement. Version constraints plus the lock file keep runs reproducible; aliases let one configuration reach several regions or accounts. ### The plan is the review artefact On a team, the plan is what gets reviewed: produced on a pull request, read for destroys and replacements rather than additions, and applied as reviewed. Workspaces, CI, drift detection and targeting are all about keeping that plan honest. ### A module is an interface A module exposes inputs and outputs and hides everything else. Designing one is API design — what the caller must decide, what gets a sensible default, what counts as a breaking change — and versioned sources turn upgrades into a deliberate act.
- HCL
- HashiCorp Configuration Language, the declarative syntax Terraform configurations are written in: blocks, arguments and expressions, with types and functions but no general-purpose control flow.
- Resource
- A block declaring one kind of infrastructure object that Terraform creates, updates and destroys over its whole lifecycle, identified by a type and a local name.
- Data source
- A block that reads information about existing infrastructure for use in the configuration. Terraform never changes what it reads.
- Provider
- A plugin that knows one platform's API and resource schemas. Terraform core calls it for every read, create, update and delete.
- Root module
- The directory Terraform runs in. It owns the backend, provider configurations and variable values for the run, and calls any child modules.
- Child module
- A reusable directory of configuration called from a module block. Callers see only its declared input variables and outputs.
- Resource address
- The identity of a resource instance, such as module.net.aws_subnet.private["a"]. Terraform matches configuration to state by it, so changing it reads as replacement.
- State
- Terraform's record of the objects it manages: each address mapped to a real object ID, with last-known attributes and dependencies. Stored in plain text.
- Backend
- Where state is stored and, for most remote options, how it is locked: the local disk by default, or shared storage for teams.
- State lock
- A lock taken on state before operations that could write it, so two runs cannot overwrite each other's results.
- Plan
- The proposed set of changes that would make real infrastructure match the configuration, computed without changing anything; it can be saved and applied exactly.
- Drift
- A difference between real infrastructure and what state records, usually from changes made outside Terraform. The refresh step detects it.
- Meta-argument
- An argument Terraform itself interprets on any resource or module, such as count, for_each, depends_on, provider or lifecycle.
- count and for_each
- Meta-arguments that create several instances from one block: count indexes them by number, for_each by map key or set value.
- Lifecycle block
- A nested block that changes how a resource is replaced or updated, for example creating the replacement first, refusing destruction, or ignoring chosen attributes.
- Dependency lock file
- The .terraform.lock.hcl file recording which provider versions and package checksums were selected, so every machine installs the same plugins.
- Workspace
- A named, separate state for the same configuration and backend, selected from the CLI. Code and credentials stay the same.
- Known after apply
- The plan's marker for a value that cannot be determined until a resource is created or changed, and for everything computed from it.
Follow one run from the command line. `terraform init` reads the `terraform` block, installs the providers that `required_providers` and the lock file allow, configures the backend and downloads module sources. `terraform plan` then reads all of the root module's `.tf` files, resolves input variables, evaluates locals and module calls, and builds one dependency graph. It takes the state lock, reads state, refreshes each recorded object through its provider, reads data sources whose arguments are already known, and prints the diff. `terraform apply` walks the same graph, asking providers to create, update or delete objects in dependency order, several at a time, writing state as results come back and publishing outputs at the end. Each section of the hub owns one part of that run: - **HCL & Resources** decides what nodes exist in the graph, how many instances each has, and how they are replaced. - **Variables & Outputs** are the edges into and out of a module; root-module values come from outside the run. - **Modules** are subgraphs with an interface, and their path becomes part of every address inside them. - **Providers & Registry** executes each node; providers are configured in the root and passed down. - **State & Backends** is the memory between runs and the lock around each one. - **Plan/Apply Workflow** wraps the loop in review, automation and drift checks. A few lines show several of those decisions meeting in one place: ```hcl terraform { required_providers { aws = { source = "hashicorp/aws", version = "~> 5.0" } } backend "s3" { bucket = "example-tf-state" key = "network/prod.tfstate" region = "eu-west-1" } } resource "aws_subnet" "private" { for_each = var.private_subnets # AZ => CIDR; each key becomes part of an address vpc_id = aws_vpc.main.id # a reference, so an edge in the graph cidr_block = each.value } ``` The provider pin and the backend are root-level decisions that `init` acts on before any resource is evaluated. The `for_each` keys end up inside the addresses stored in state, which is why switching the same block to `count` renames every instance. The reference to the VPC is the whole dependency declaration: no ordering hint is needed. Change the backend `key` and Terraform is looking at a different state entirely, with everything in it looking new.
- HCL & Resources →
The language and the resource model every other section assumes: blocks, references, instances and how replacement is decided.
- State & Backends →
What Terraform remembers between runs, and where the hardest questions sit: backends, locking, import, refactoring and secrets.
- Variables & Outputs →
How values enter and leave a configuration, including the precedence that explains why a value you set was not used.
- Providers & Registry →
Credentials, version pinning, the lock file and aliases: what keeps runs reproducible across laptops, CI and several regions or accounts.
- Modules →
Packaging infrastructure for reuse, which makes sense once inputs, outputs and providers are clear.
- Plan/Apply Workflow →
The operational loop: commands, saved plans, workspaces, CI review, drift and targeting, built on everything above.
Using
countover a list whose items can be removed or reordered: the index is the identity, so one deletion shifts later instances — see count vs for_each.Treating
sensitive = trueas protection: it hides values from CLI output, while state and saved plan files still hold them in plain text.Renaming a resource or moving it into a module and applying the destroy-and-create that follows, instead of recording the move so state follows the object.
Running
terraform force-unlockbecause a lock looks old, without first proving that the run holding it has really ended.Offering CLI workspaces as production isolation: they share the backend, credentials and code, so a mistake in one can still reach the others.
Reaching for
depends_onor-targetto fix an ordering problem, instead of expressing the dependency as a reference and following with a full plan.Leaving the lock file out of version control, or pointing a Git module at a branch, then wondering why two runners behave differently.
Calling a plan safe after reading the summary line: the lines that matter are replacements and destroys, often forced by one argument the provider cannot update in place.
Saying
terraform validateproves a configuration will apply; it checks the configuration against provider schemas and never talks to the real platform.
This guide assumes a current Terraform 1.x release, at least 1.5 wherever it mentions import blocks. Older syntax still shows up in inherited code and in questions, so a few milestones are worth knowing: - **0.12** introduced the second generation of HCL: expressions became first-class, with `for` expressions, dynamic blocks and rich type constraints. Wrapping a whole value in `"${...}"` is a leftover from before it. - **0.13** added provider source addresses in `required_providers`, and allowed `count`, `for_each` and `depends_on` on module blocks. - **0.14** added the dependency lock file and sensitive input variables. - **1.0** started the 1.x compatibility promises. - **1.1** added `moved` blocks, so refactoring can be recorded in configuration rather than done by hand in state. - **1.5** added `import` blocks, which bring existing objects under management through plan and apply, and `check` blocks. - **1.6** added the `terraform test` framework, and was the first release under the Business Source License instead of the Mozilla Public License. - **1.7** added `removed` blocks, for dropping a resource from state without destroying it. When an answer depends on one of these — how import works, whether a move needs state commands, which license applies — name the version you mean.
Interviewers expect you to place Terraform, not only write it. Its closest relative is **OpenTofu**, a community fork created after the license change and kept close to Terraform's language and workflow; for most questions the two answer the same way. **Pulumi** covers the same ground with general-purpose programming languages instead of HCL. Cloud-native tools such as AWS CloudFormation and Azure Bicep are tied to one vendor and keep state on the vendor's side, which removes state management but also removes multi-cloud reach. Terraform provisions infrastructure; it is not a configuration-management tool. Ansible, Chef or Puppet configure what runs inside machines, and Packer builds images, which is why provisioners are a last resort rather than a bridge between the two. Around the core sit tools you may be asked to choose among: **HCP Terraform** (formerly Terraform Cloud) and Atlantis for remote runs and pull-request workflows, Terragrunt for keeping many root modules consistent, and static checkers such as TFLint and Checkov in CI. Whatever wraps it, the questions about state, identity and plan review stay the same.
explore
- HCL & Resources38 questions
- Expressions and Functions6 questions
- count vs for_each5 questions
- Dynamic Blocks5 questions
- Lifecycle Meta-Arguments6 questions
- Dependencies and the Resource Graph5 questions
- Data Sources5 questions
- Provisioners6 questions
- Providers & Registry16 questions
- Provider Configuration and Authentication5 questions
- Aliases and Multi-Region/Account5 questions
- Version Constraints and Lock File6 questions
- State & Backends30 questions
- What State Stores6 questions
- Remote Backends5 questions
- State Locking4 questions
- Import, Moved, and State Surgery6 questions
- Sensitive Data in State5 questions
- Splitting State and Remote State4 questions
- Variables & Outputs24 questions
- Input Variables and Type Constraints5 questions
- Variable Precedence5 questions
- Validation and Custom Conditions5 questions
- Locals4 questions
- Outputs and Sensitivity5 questions
- Modules16 questions
- Module Structure and Interface6 questions
- Sources, Registry, and Versioning5 questions
- Composition Patterns5 questions
- Plan/Apply Workflow31 questions
- Core Commands and Saved Plans6 questions
- Workspaces5 questions
- Refresh and Drift Handling5 questions
- Targeting, Replace, and Parallelism4 questions
- CI/CD Integration5 questions
- Testing and Static Checks6 questions
questions
155 · 6 sectionsIn Terraform, what does a `data` block do, and how is it different from a `resource` block?
basics
~20 sA data block reads information about infrastructure Terraform does not manage and exposes it to the configuration. Terraform never creates, updates or destroys what a data block reads; a resource block declares an object Terraform owns for its whole lifecycle.
In Terraform, what does a dynamic block do, and how would you use one to generate a security group's ingress blocks from a variable?
basics
~20 sA dynamic block produces repeated nested blocks from a collection: for_each supplies one element per block and the content block holds the block body. For a security group you iterate a list of rule objects to emit one ingress block each.
In Terraform HCL, when is the ${ ... } interpolation syntax actually required, and why does writing "${var.name}" as an entire argument value produce a deprecation warning?
basics
~20 sSince Terraform 0.12, expressions are first-class, so you write var.name directly. The ${ } syntax is only for embedding an expression inside a larger string, such as "app-${var.env}". Quoting a whole expression adds nothing and Terraform warns that it is deprecated.
In Terraform, what is the practical difference between the count and for_each meta-arguments, and why can removing one element from the middle of a list used with count destroy resources you never meant to touch?
basics
~20 scount addresses instances by numeric position, so deleting a middle list element shifts every later index and Terraform destroys and recreates those resources. for_each addresses instances by map key or set value, which stays stable, so unrelated instances are untouched.
In Terraform, when is a data source actually read, and why does a plan sometimes show a data source and everything downstream of it as `(known after apply)`?
basics
~20 sTerraform reads a data source during plan whenever all of its arguments are already known. If any argument depends on a value that does not exist yet, the read is deferred to apply, its attributes become unknown, and every value derived from them renders as (known after apply).
In Terraform, how do you create resources in two AWS regions from a single configuration, and how does an individual resource choose the second region?
basics
~20 sDeclare a second aws provider block with an alias argument and its own region, then set the meta-argument provider = aws.<alias> on each resource that belongs in that region. Resources with no provider argument use the unaliased default block.
In a Terraform aws provider block, why should you not set access_key and secret_key, and where does the provider get credentials instead?
basics
~20 sKeys written into HCL get committed, cloned and copied, and cannot be rotated per run. Omit access_key and secret_key; the AWS provider then resolves credentials itself from environment variables, the shared AWS config files, or an attached instance or CI role.
In a Terraform configuration, what does the `required_providers` block do, and why does each entry need a `source` as well as a `version`?
basics
~20 srequired_providers declares every provider plugin a Terraform module depends on. The source argument is the registry address that tells Terraform exactly which plugin to download; version constrains which releases are acceptable. Without an explicit source, Terraform assumes the hashicorp namespace.
A child Terraform module has to create resources in a second AWS region. How does the module get that provider configuration, and what must the child module declare to accept it?
basics
~20 sThe calling module passes it with the module block's providers map, for example providers = { aws = aws.us, aws.replica = aws.eu }. The child declares the extra slot with configuration_aliases inside its required_providers entry, then selects it per resource.
What does Terraform's `.terraform.lock.hcl` record, and should it be committed to version control?
basics
~20 sThe dependency lock file records, per provider, the exact version Terraform selected, the constraints it was selected under, and checksums of the provider packages. It belongs in Git: it is what makes every laptop and CI runner install byte-identical plugins.
Why does Terraform lock the state before a run, and what actually goes wrong if two applies run against the same state at the same time?
basics
~20 sTerraform locks state so only one write-capable run touches it at a time. Two concurrent applies read the same starting snapshot and then overwrite each other's write, so real resources end up with no record in state and later runs recreate or destroy them.
Terraform writes terraform.tfstate to the local working directory by default. What breaks once a second engineer starts running applies on the same project, and what does moving to a remote backend actually fix?
basics
~20 sA remote backend keeps Terraform state in shared, durable storage such as an S3 bucket instead of one laptop's disk. Every engineer and CI job then reads and writes the same state, so runs cannot silently diverge, duplicate resources, or destroy each other's work.
Which files produced by a Terraform run must never be committed to a Git repository, and why is a saved plan file created with terraform plan -out=tfplan as sensitive as the state file itself?
basics
~20 sNever commit terraform.tfstate, terraform.tfstate.backup, the .terraform directory, saved plan files, crash logs, or tfvars files holding secrets. A saved plan embeds the prior state plus planned attribute values, so it carries the same plain-text credentials state does.
In Terraform, what does importing a resource actually do to state and configuration, and how does the `import` block differ from the `terraform import` command?
basics
~20 sImporting binds an already-existing object to a resource address in Terraform state. It creates no infrastructure and writes no configuration for you. The terraform import command needs the resource block written first; a Terraform 1.5+ import block runs inside plan and apply.
A Terraform root module's state file is gone and there is no backup, but everything it created is still running in the cloud. What happens on the next terraform plan and apply, and how do you recover?
basics
~20 sTerraform sees every declared resource as new and plans to create a second copy of everything, while the existing objects become orphans it can no longer read, change or destroy. Recovery means restoring a snapshot or re-adopting each object into state.
In Terraform, what does the `type` argument in a `variable` block do, and what categories of type constraint can you declare?
basics
~20 sTerraform's type argument constrains what a caller may pass, rejecting mismatches during plan before anything is created. Type constraints come in three families: primitives (string, number, bool), collections (list, set, map), and structural types (object, tuple).
In Terraform, what is a locals block, and how does it differ from an input variable and from an output?
basics
~20 sA Terraform locals block names a computed value for reuse inside one module. Unlike an input variable it cannot be set from outside — no tfvars, CLI flag or environment override — and unlike an output it exposes nothing to the module's caller.
In Terraform, what does an `output` block do, and how does a calling configuration read an output declared in a child module?
basics
~20 sAn output block publishes a value out of the module that declares it. Root module outputs are printed after apply and readable with terraform output; a child module's outputs are read by its caller as module.<name>.<output_name>.
In Terraform, what does a `validation` block inside a `variable` block do, and which two arguments does it take?
basics
~20 sA validation block makes Terraform reject an unacceptable input value at plan time. It takes condition, a boolean expression over the variable, and error_message, the text printed when the condition is false — so nothing gets created.
A Terraform variable can be typed `list(string)`, `set(string)` or `map(string)`. How do the three differ, and what does the module receive if a `set(string)` variable is given the value `["b", "a", "a"]`?
basics
~20 sA list is ordered and allows duplicates, a set is unordered and de-duplicated, a map is keyed by strings. Given ["b", "a", "a"], a set(string) variable yields two elements — "a" and "b" — with the caller's ordering and the duplicate discarded.
In a Terraform repository, why is composing several small modules in one flat root configuration usually preferred over nesting modules three or four levels deep?
basics
~20 sFlat composition keeps the dependency wiring and the resource addresses visible in one place. Deep nesting threads every input and output through pass-through variables at each level, hides what is actually created, and makes review, targeting and refactoring much harder.
In a Terraform module block whose source is a Git repository, how do you pin the module to a specific version, and why can't you use the version argument?
basics
~20 sTerraform's version argument works only for registry module sources. A Git-sourced module is pinned inside the source string itself, with a ?ref= query parameter naming an immutable tag or commit SHA. A branch name in ?ref= is not a pin.
A caller of your Terraform module needs the ID of a subnet the module creates, but the module declares no output for it. Why can't the caller reference the resource directly, and what is the correct fix?
basics
~20 sResources inside a child module are not addressable from outside it: module.<name> resolves only declared outputs, so module.network.aws_subnet.private is an error. The fix is to add an output to the module — its outputs are its public API, and adding one is a backward-compatible change.
In Terraform, what is the difference between the root module and a child module, and what can only the root module do?
basics
~20 sThe root module is the directory Terraform is run in; a child module is a directory called from a module block. Only the root takes backend configuration, tfvars and CLI variable values, and the provider configuration for the run.
What kinds of values can the source argument of a Terraform module block take, and how does Terraform decide how to fetch each one?
basics
~20 sTerraform accepts local paths beginning with ./ or ../, module registry addresses of the form namespace/name/provider, Git and Mercurial repositories, plain HTTP or S3/GCS archives. Terraform inspects the string's shape and prefix to pick the fetcher.
In a team's Terraform pipeline, why does terraform plan run on the pull request while terraform apply runs only after the change merges to the main branch?
basics
~20 sA plan on the pull request turns the proposed change into a reviewable diff before anything is touched. Apply runs from main so only merged, reviewed code ever changes real infrastructure, giving one source of truth and one audit trail.
In Terraform, what do `terraform plan` and `terraform apply` each do, and what does the `-auto-approve` flag change about `terraform apply`?
basics
~10 sterraform plan computes and prints the changes needed to make real infrastructure match the configuration, without changing anything. terraform apply makes those changes, pausing for an interactive confirmation first; -auto-approve skips that confirmation prompt.
In a CI job, what does `terraform fmt -check` do differently from plain `terraform fmt`, and what does it miss by default?
basics
~20 sterraform fmt -check reports formatting problems without rewriting anything and exits non-zero if any file would change, which is what makes it usable as a build gate. By default it looks only at the current directory, so nested modules need -recursive.
In Terraform, what does a CLI workspace give you, and what actually changes when you run `terraform workspace select dev`?
basics
~10 sA Terraform CLI workspace is a separate state file for the same configuration in the same backend. Selecting one changes only which state Terraform reads and writes; the code, backend and credentials stay identical.
Why would a team run `terraform plan -out=tfplan` and later `terraform apply tfplan` instead of just running `terraform apply`?
basics
~20 sSo the changes that get applied are exactly the changes that were reviewed. A bare terraform apply computes a brand-new plan at apply time; applying a saved plan file replays the recorded decisions instead, with no re-planning and no approval prompt.