Prepare for Terraform interview questions grouped by experience level.
Terraform Interview Question & Answers
0-2 Years
Terraform is an Infrastructure as Code tool, letting you actually define cloud and on-premises infrastructure through declarative configuration files rather than manually clicking through a provider's own console. It solves the genuine problem of infrastructure changes being hard to track, review, and reliably reproduce when done by hand.
A declarative approach genuinely describes the desired end state, and Terraform figures out what actual changes are genuinely needed to reach it. An imperative approach would instead genuinely require specifying the exact step-by-step commands to actually perform, which Terraform's own declarative model deliberately avoids.
IaC means genuinely defining infrastructure through actual code rather than manual, click-through configuration. This lets infrastructure be genuinely version-controlled, reviewed, and reliably recreated the exact same way every single time it's actually applied, rather than depending on someone remembering the correct manual steps.
HCL (HashiCorp Configuration Language) is the genuine language Terraform configuration files are actually written in, designed to be both genuinely human-readable and machine-parseable, striking a balance between plain text and a genuinely structured format like JSON.
Terraform is genuinely focused specifically on provisioning infrastructure itself, servers, networks, databases, described declaratively. Ansible is genuinely more focused on configuring software and settings on an already-existing server, though the two genuinely, commonly get used together in the exact same overall pipeline.
.tf files genuinely contain the actual HCL configuration itself. .tfvars files genuinely hold variable values, kept separate from the main configuration. The .tfstate file genuinely tracks the actual current state of the infrastructure Terraform manages, updated automatically as changes are actually applied.
A resource block genuinely defines a single piece of infrastructure Terraform should actually manage, like a virtual machine or a storage bucket, specifying its type and a genuine set of configuration arguments describing exactly how it should actually be configured.
A provider is a genuine plugin that lets Terraform actually communicate with a specific platform's API, like AWS, Azure, or GCP, translating Terraform's own declarative configuration into the actual API calls needed to genuinely create, update, or delete resources on that specific platform.
An argument genuinely assigns a value to a specific configuration key, like name = 'my-server'. A block genuinely groups a related set of arguments together under a specific type, like a resource or provider block, providing genuine structure to the overall configuration.
A meta-argument, like count, for_each, or depends_on, is genuinely recognized by Terraform itself across every resource type, controlling how Terraform manages that resource's own creation or lifecycle, rather than configuring an actual property of the underlying infrastructure the way a regular, resource-specific argument does.
aws_instance.web.id genuinely references the id attribute of a resource named web, of type aws_instance, letting one resource's configuration actually depend on a value Terraform will only genuinely know once that referenced resource has actually been created.
A string genuinely holds text, wrapped in quotes. A number genuinely holds a numeric value, with no quotes needed. A boolean genuinely holds either true or false. HCL is genuinely typed, and Terraform validates that a given argument actually receives the correct expected type.
# this is a comment (or // for a single line, and /* */ for genuinely multiple lines) marks text Terraform genuinely ignores entirely, used to actually explain a specific piece of configuration or temporarily disable it without deleting it outright.
terraform init genuinely initializes a working directory, downloading the necessary provider plugins and setting up the genuine backend for storing state. It's genuinely run once when a project is first set up, and again whenever a genuinely new provider or module is actually added.
terraform plan genuinely compares the current configuration against the actual, existing infrastructure (tracked in the state file) and shows exactly what changes would actually be made, without actually applying anything yet, letting you genuinely review a proposed change before committing to it.
terraform apply genuinely executes the actual changes needed to make the real infrastructure match the configuration, essentially genuinely running the plan and, after a confirmation prompt (or automatically, if run non-interactively), actually creating, updating, or destroying resources as needed.
terraform destroy genuinely removes every resource Terraform is currently managing according to the configuration. It requires careful, deliberate use because it can genuinely, permanently delete real infrastructure, including data, and there's genuinely no automatic undo once resources are actually removed.
Edit the configuration, run terraform plan to genuinely review the proposed change, and if it looks correct, run terraform apply to genuinely actually make that change to the real infrastructure. This sequence lets you genuinely catch an unintended change before it's actually applied.
This gives you a genuine chance to actually review exactly what will change, added, modified, or destroyed, before it genuinely happens, catching an unintended or unexpected change, like a resource being accidentally destroyed and recreated, before it actually affects real infrastructure.
Terraform state is a genuine file tracking what infrastructure Terraform has actually created and its current configuration. Terraform genuinely relies on that state to actually determine what changes are needed on a subsequent run, comparing the actual desired configuration against what it genuinely believes already exists.
terraform.tfstate is a genuine JSON file storing Terraform's own record of every resource it's actually managing, including each resource's real, current attribute values as last genuinely known by Terraform.
Manually editing the state file risks genuinely introducing an inconsistency between what Terraform believes exists and what actually, genuinely exists in real infrastructure, which can cause Terraform to make an incorrect decision on its genuinely next run, like trying to recreate a resource that already actually exists.
Terraform genuinely loses track of what infrastructure it's actually managing, and without the state file, it has no genuine way of knowing what already exists, potentially leading it to try to create genuinely duplicate resources on the next apply, which is exactly why storing state somewhere genuinely durable and backed up matters.
Local state stores the state file genuinely directly on the machine running Terraform. Remote state stores it in a genuinely shared, external location, like an S3 bucket, letting multiple team members or a CI/CD pipeline all actually work from the exact same, genuinely consistent, shared state.
terraform show genuinely displays the current state's contents in a genuinely readable format, and terraform state list genuinely lists every resource currently tracked in the state, both letting you actually inspect what Terraform believes exists without risking a manual, direct edit to the state file itself.
A state snapshot is a saved copy of the state file at a specific point in time. Retaining a history of snapshots lets you actually compare how infrastructure has changed over time, and in some backends, roll back to a previous snapshot if a recent apply genuinely introduced a problem, giving a safety net beyond just the single, current state file alone.
An input variable, declared with a variable block, lets you genuinely parameterize a Terraform configuration, supplying a genuinely different value for something like an instance size or a region without needing to actually edit the configuration file itself every single time.
A value can genuinely be supplied through a .tfvars file, a command-line flag (-var), an environment variable, or a genuine default value specified directly within the variable block itself, with Terraform genuinely using a defined order of precedence if a value is provided in more than one place.
An output value, declared with an output block, exposes a genuinely specific piece of information about the created infrastructure, like a server's actual IP address, after terraform apply completes, letting you actually retrieve that value without needing to manually inspect the state file directly.
A type constraint genuinely specifies what kind of value a variable actually accepts, like string or number, and Terraform genuinely validates input against it. A default value genuinely provides a fallback used automatically if no genuinely explicit value is actually supplied for that variable elsewhere.
Setting sensitive = true on a variable block genuinely prevents its actual value from being displayed in Terraform's console output, like during a plan or apply, helping avoid accidentally exposing something like a genuinely sensitive password or API key in a log or a terminal output.
A locals block defines a genuinely named value computed from an expression within the configuration itself, used to actually avoid repeating the exact same computed expression in multiple places. Unlike a variable, a local value genuinely can't be set from outside the configuration, it's always computed internally.
provider 'aws' { region = 'us-east-1' } genuinely configures the AWS provider, specifying the region every resource created through that provider should actually be provisioned in, unless a genuinely specific resource overrides it.
The Terraform Registry is a genuinely public repository of providers and modules that Terraform can automatically download and use, letting you actually find and use a well-maintained provider or module for a specific platform rather than genuinely needing to write that same integration entirely from scratch.
Using the alias argument, provider 'aws' { alias = 'west' region = 'us-west-2' }, creates a genuinely second, distinctly named instance of the provider, and a resource can then explicitly reference that specific aliased provider to actually be created in that genuinely different configuration.
terraform init genuinely downloads the actual provider plugin's code, since a provider is genuinely a separate, external binary that Terraform's own core program communicates with, rather than the provider's own actual logic being genuinely built directly into Terraform's core itself.
required_providers, inside a terraform block, genuinely specifies which provider (and which version constraint) a configuration actually requires, used during terraform init to actually determine what to download. A provider block genuinely configures that provider's actual settings, like credentials or a region, once it's genuinely already available.
3-6 Years
A module is a genuinely reusable, self-contained package of Terraform configuration, letting you actually define a common infrastructure pattern once and reuse it across multiple genuinely different places, rather than duplicating the exact same configuration repeatedly.
module 'web_server' { source = './modules/web-server' instance_type = 't3.micro' } genuinely calls a module located at the given source path, passing it genuine input values, and the module's own internal resources are actually created as part of the overall configuration.
The root module is the genuine top-level configuration directory where terraform apply is actually run. A child module is genuinely called from the root module (or from another module), encapsulating a specific, genuinely reusable piece of infrastructure logic.
The module itself declares an output block, and the calling configuration then genuinely references it as module.web_server.instance_id, letting a value computed genuinely inside the module actually be used elsewhere in the broader configuration.
Module versioning lets you actually pin a specific, known-good version of a module, source = 'terraform-aws-modules/vpc/aws' version = '5.0.0', ensuring a genuine change to that module later doesn't unexpectedly, silently affect your own existing configuration the next time you actually run Terraform.
A backend determines where Terraform's genuine state is actually stored and how operations are actually performed. Remote backends, like an S3 bucket or Terraform Cloud, let genuinely multiple team members or an automated pipeline share the exact same, consistent state rather than each maintaining their own genuinely separate, local copy.
A fully specified backend configuration includes every required setting directly in the .tf file itself. A partial configuration deliberately leaves some values out, supplying them instead at init time through a separate file or command-line flags, which is commonly done specifically to avoid hardcoding an environment-specific value, like a bucket name that genuinely differs between dev and production, directly into shared configuration code.
State locking genuinely prevents two people (or two pipeline runs) from actually running Terraform against the exact same state simultaneously, which could otherwise genuinely corrupt the state or produce a conflicting, incorrect result. A backend like S3, paired with DynamoDB for locking, genuinely provides this protection.
A terraform block with a backend 's3' section specifying the bucket, key, and region genuinely configures Terraform to actually store its state in that specific S3 bucket rather than genuinely locally, and pairing it with a DynamoDB table genuinely adds state locking on top of that.
terraform state mv genuinely moves an existing resource's entry within the state file, used when you actually rename a resource in your configuration or genuinely refactor it into a module, letting Terraform correctly associate the existing, real infrastructure with its genuinely new configuration reference instead of trying to destroy and recreate it.
terraform import brings an already-existing piece of infrastructure, created outside of Terraform, under Terraform's own management, adding it to the state file without actually recreating it. It solves the genuine problem of adopting Terraform for infrastructure that already genuinely exists, rather than needing to destroy and recreate everything from scratch.
An implicit dependency exists when one resource's configuration genuinely references another resource's attribute, like using a VPC's id in a subnet's own configuration. Terraform genuinely detects this reference automatically and ensures the referenced resource is actually created first.
depends_on explicitly genuinely declares a dependency between resources when there's genuinely no actual attribute reference connecting them, but an ordering still actually matters, like a resource needing a specific IAM policy to genuinely already be in effect before it's actually created, even though it doesn't directly reference that policy's own attributes.
Terraform builds a genuine dependency graph based on both implicit references and any explicit depends_on declarations, then actually processes resources in the correct order that graph requires, creating genuinely independent resources in parallel where no dependency actually exists between them.
Terraform genuinely detects the circular dependency and returns an error during terraform plan, since it can't actually determine a valid order to create resources that each genuinely, directly or indirectly depend on the other.
Adding a lifecycle block with create_before_destroy = true changes the genuine ordering of that replacement, creating the new resource before destroying the old one. Alternatively, terraform taint marks a genuinely specific resource to actually be destroyed and recreated on the very next apply.
A data source, declared with a data block, genuinely reads information about an already-existing piece of infrastructure that Terraform isn't actually managing directly, like looking up a genuinely existing VPC's ID by name. A resource block, by contrast, genuinely creates and manages the actual, real infrastructure itself.
Looking up the genuinely latest available Amazon Machine Image (AMI) matching a specific name pattern, using a data 'aws_ami' block, lets your configuration always actually reference the genuinely current AMI without hardcoding a specific, potentially outdated AMI ID directly.
A provisioner runs a genuine script or command on a resource after it's actually created, like a remote-exec provisioner running a setup script on a new server. It's genuinely considered a last resort because it introduces imperative, Terraform-unaware logic into an otherwise declarative configuration, and Terraform's own documentation genuinely recommends using a genuinely dedicated configuration management tool instead wherever reasonably possible.
A local-exec provisioner genuinely runs a command on the machine actually running Terraform itself. A remote-exec provisioner genuinely runs a command on the actual, newly-created resource, typically over SSH, requiring genuine network connectivity to that specific resource.
Terraform provides genuine built-in functions for actually transforming values within an expression, like upper('hello') returning HELLO, or length(list) returning the genuine number of items in a list, none of which genuinely require any custom code to actually use.
condition ? true_value : false_value, similar to a ternary operator in many programming languages, lets you actually choose between two possible values based on a genuine condition, like instance_type = var.is_production ? 't3.large' : 't3.micro'.
for_each genuinely creates a resource once for every genuine item in a given map or set, and unlike count, each created instance is genuinely, individually identified by its actual key rather than a plain numeric index, which makes for_each genuinely more resilient when a genuinely specific item is later removed from the middle of the collection.
count genuinely creates a specified number of instances, identified by a genuine numeric index, which can cause Terraform to genuinely, unexpectedly recreate several resources if an item is removed from the middle of a list, shifting every subsequent index. for_each genuinely avoids that issue by identifying each instance with its own genuinely stable, unique key instead.
6-8 Years
A workspace lets you genuinely maintain multiple, separate state files for the exact same configuration, commonly used to actually manage genuinely similar environments, like dev, staging, and production, using the exact same underlying configuration code but with genuinely separate, isolated state for each.
Workspaces share the exact same underlying configuration, so any genuinely environment-specific difference has to be handled through variables and conditional logic within that one shared configuration, which can become genuinely harder to reason about than having fully genuinely separate, independent configuration directories for each specific environment.
terraform state rm genuinely removes a resource's entry from the state file, without touching the actual, real resource itself, useful when you actually want Terraform to genuinely stop managing something without deleting it, perhaps because it's actually being migrated to be managed by a genuinely different tool or team.
State drift happens when the actual, real infrastructure genuinely diverges from what Terraform's state file believes exists, typically caused by a manual, out-of-band change made directly through a provider's console. Running terraform plan genuinely reveals drift, since it compares the actual current infrastructure against the state and configuration.
Check the specific attribute causing that forced replacement, since certain resource attributes are genuinely marked by the provider as requiring a full replacement rather than an in-place update. If the change is genuinely unintentional, adjusting the configuration to avoid modifying that specific attribute avoids the unwanted replacement.
It genuinely updates Terraform's state to actually reflect the real, current state of existing infrastructure, without genuinely making any actual changes to that infrastructure itself, useful for actually reconciling Terraform's own knowledge with reality after a change was made outside of Terraform's normal workflow.
Extract genuinely common infrastructure patterns, like a standard VPC setup or a standard web application stack, into their own reusable, well-documented modules, and have genuinely individual projects call those shared modules with their own specific input values, rather than each project defining the exact same underlying resources independently.
Module composition means genuinely building a larger module out of several smaller, more focused modules combined together, similar to composing functions in programming. It provides the genuine benefit of keeping each individual module focused and independently testable, while still letting you assemble genuinely complex infrastructure from those well-defined, smaller building blocks.
Organize configuration by genuine environment and by logical component, separate directories for networking, compute, and data infrastructure, for instance, rather than one single, enormous configuration file managing genuinely everything, and use shared modules for anything genuinely repeated across multiple of those components.
DRY means genuinely avoiding duplicated configuration by extracting shared logic into a module. Applied too aggressively, it can lead to an overly generic, genuinely complex module trying to handle every possible use case at once, which can actually become harder to understand and maintain than simply accepting some genuinely reasonable duplication between two similar but not identical use cases.
I'd weigh how genuinely often that exact same pattern actually repeats across projects, and how likely it is to genuinely need to change consistently everywhere at once, against the real, added indirection a module introduces for something genuinely simple enough that duplicating it once or twice isn't actually a real problem.
8-10 Years
Use genuinely separate state files (and typically separate configuration directories or workspaces) per environment, with genuinely shared modules defining the common infrastructure pattern, and environment-specific variable files supplying the actual specific values, like instance size or scaling limits, appropriate for each genuinely individual environment.
Terraform Cloud provides genuinely remote state storage and locking, remote execution of plan and apply, and features like policy enforcement and a genuine collaborative workflow for reviewing infrastructure changes, none of which open-source Terraform run purely locally genuinely provides on its own.
Policy as code lets an organization genuinely define and automatically enforce rules about what infrastructure is actually allowed to be created, like requiring encryption on every storage resource, checked automatically before a genuine apply is allowed to actually proceed, rather than relying purely on manual review to actually catch a policy violation.
I'd favor a genuinely modular structure from the start, even if a project is currently genuinely small, since retrofitting modularity onto a genuinely large, monolithic configuration later is meaningfully harder than starting with reasonable structure upfront, without over-engineering for a genuinely hypothetical scale that may never actually materialize.
Pull secrets from a genuinely dedicated secrets manager, like AWS Secrets Manager or HashiCorp Vault, using a data source at actual runtime, rather than hardcoding a genuinely sensitive value directly into a .tf or .tfvars file, which would otherwise genuinely expose it in version control.
I'd weigh the genuine time saved by Terraform Cloud's own built-in collaboration and policy features against the real, ongoing subscription cost and any genuinely specific customization a self-managed setup might allow that Terraform Cloud genuinely doesn't.
Use terraform state mv extensively to actually move existing resources into their genuinely new module locations within the state file, verifying with terraform plan after each specific move that it shows genuinely no actual changes, confirming Terraform correctly recognizes the existing infrastructure under its genuinely new configuration structure.
The pipeline genuinely runs terraform plan automatically on every pull request, posting the proposed changes for genuine human review, and runs terraform apply automatically once that change is actually merged and approved, ensuring genuinely every infrastructure change goes through the exact same reviewed, consistent process.
A plan's output shows exactly what will genuinely change, and reviewing it catches a genuinely unintended consequence, like an unexpected resource replacement, before it actually happens, which is exactly why most organizations genuinely require an explicit human approval step specifically for a production apply, even within an otherwise fully automated pipeline.
A remote backend with genuine state locking, like S3 with DynamoDB, ensures that if genuinely two pipeline runs try to apply against the exact same state simultaneously, the genuinely second one waits for the lock to actually be released, rather than corrupting the state through a genuinely conflicting, concurrent write.
Require an genuinely explicit, separate approval step or a genuinely distinct pipeline specifically for a destroy operation, rather than allowing it through the exact same automated path as a genuinely routine apply, and restrict which specific users or pipeline roles are actually authorized to trigger that genuinely destructive action at all.
Store credentials in the CI/CD platform's own genuinely encrypted secrets storage, injected as environment variables at actual pipeline runtime, rather than hardcoding them directly into a configuration file or a genuinely plaintext pipeline definition checked into version control.
terraform validate genuinely checks configuration syntax and internal consistency. A tool like Terratest can actually spin up real infrastructure in a genuinely isolated test environment and verify it behaves as expected, then tear it down again, providing genuinely real, functional verification beyond what a plain syntax check alone can offer.
10+ Years
I'd weigh Terraform's genuine cross-cloud portability and its genuinely large, active community and module ecosystem against a native tool's genuinely tighter, deeper integration with that specific provider's own latest features, which sometimes become available in a native tool before Terraform's own corresponding provider genuinely catches up.
I'd migrate incrementally, starting with a genuinely willing pilot team to work out real issues before a broader rollout, and invest early in genuinely clear documentation and shared modules so each subsequent team's own migration goes noticeably faster than the very first one did.
I check whether it's genuinely solving the real problem rather than over-engineering for a genuinely hypothetical future need, whether state management and remote backend configuration are genuinely handled correctly, and whether it's genuinely consistent with patterns already established elsewhere in the organization's own infrastructure.
Enforce genuinely hard requirements through policy as code, automatically failing a plan that violates a genuine required standard, rather than relying on manual, ad hoc review. For conventions that genuinely resist full automation, I'd document the handful of decisions that actually matter most, along with the real reasoning behind each one.
I'd weigh the genuine time saved by built-in collaboration, policy enforcement, and remote state management against the real, ongoing subscription cost, and whether the organization's own current pain, inconsistent manual state management, a lack of policy enforcement, genuinely justifies that specific, real investment.
I'd check for a genuine state locking conflict from a concurrent pipeline run, or a genuine difference in credentials or environment variables between the CI environment and a local machine, both genuinely common causes of exactly this kind of environment-specific, inconsistent failure.
A scheduled, automated terraform plan run, checked for whether it genuinely reports any actual proposed change, can alert a team if real infrastructure has genuinely drifted from what Terraform's configuration expects, catching a manual, out-of-band change before it actually causes a genuine, real problem.
Treat the module's actual input variables and outputs as a genuine contract with every consuming team. Adding a genuinely new optional variable is generally safe. Changing or removing an existing one needs a documented deprecation period and direct communication before actual removal, along with a genuine version bump signaling that breaking change.
I'd run terraform plan immediately to genuinely understand the exact, current actual state versus the intended configuration, and carefully re-run terraform apply to actually complete the remaining changes, rather than genuinely panicking and running terraform destroy, which could make the genuinely already-inconsistent situation meaningfully worse.
I'd break a genuinely monolithic configuration into smaller, independently-applicable pieces as it grows, since a single, enormous configuration's plan and apply time can genuinely become impractically slow, and smaller, focused configurations also genuinely reduce the blast radius of any single specific change.
This is a judgment question interviewers use to see how you reason under genuine uncertainty, not to test a specific textbook fact. A strong answer names the actual constraint that forced the decision, the realistic options that were genuinely on the table, why you picked one knowing it wasn't guaranteed to be right, and what you'd do differently with what you know now.
I'd walk through an actual, real drift scenario together, showing concretely how a manual change made directly through a provider's console causes Terraform's genuinely next plan to actually propose reverting it unexpectedly, rather than explaining the risk purely in the abstract. Seeing that specific, real consequence tends to build that discipline far more effectively.
I wouldn't lead with Infrastructure as Code as an abstract best practice. I'd point to a specific, real, already-experienced incident caused by an undocumented, manual infrastructure change, and show concretely how Terraform's genuine version-controlled, reviewable configuration would have actually prevented that exact same specific problem.
I'd bring the actual, concrete question of how often that same pattern genuinely repeats, and how likely it is to genuinely need a consistent update everywhere at once, into the discussion, rather than a general, abstract preference for modularity. Grounding the discussion in the specific, real usage pattern resolves it faster than an abstract debate.
I'd translate the investment into terms leadership already tracks: the hours lost to a specific past incident caused by inconsistent manual infrastructure changes, and the ongoing risk of an undetected policy violation, like an unencrypted storage resource, going unnoticed until it actually causes a real problem. Framed as risk reduction with a concrete, already-incurred cost behind it, it competes far better for prioritization.




