Terraform interview questions covering IaC, providers, resources, state, modules, variables, workspaces, plans, drift, and cloud provisioning.
Terraform is an Infrastructure as Code tool used to define, provision, and manage infrastructure with declarative configuration files. It supports many providers such as AWS, Azure, Google Cloud, Kubernetes, Cloudflare, Datadog, and GitHub. Terraform compares desired configuration with current state and creates an execution plan before applying changes.
Infrastructure as Code, or IaC, means infrastructure is described in version-controlled files instead of manually configured through consoles. IaC improves repeatability, reviewability, rollback planning, and environment consistency. Terraform is declarative IaC: you describe the desired result, and Terraform decides the operations needed.
A provider is a plugin that lets Terraform manage resources in an external platform. Providers expose resource types and data sources. For example, the AWS provider manages VPCs, EC2 instances, IAM roles, S3 buckets, and many other AWS resources.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
A resource is an infrastructure object managed by Terraform, such as a network, database, bucket, firewall rule, IAM role, or Kubernetes namespace. Terraform tracks resources in state and creates, updates, or destroys them to match configuration.
resource "aws_s3_bucket" "logs" {
bucket = "example-app-logs"
}
A data source reads existing information from a provider without managing it as a Terraform-owned resource. It is useful for looking up existing VPCs, AMIs, DNS zones, secrets, subnets, or account details that other configurations need.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
Terraform state maps configuration addresses to real infrastructure objects. It stores resource IDs, attributes, dependencies, and metadata needed to calculate future plans. State is critical; losing or corrupting it can make Terraform unable to safely understand what it manages.
Remote state stores Terraform state in a shared backend such as S3, Azure Storage, Google Cloud Storage, Terraform Cloud, or Consul. It enables team collaboration, locking, backups, and CI/CD workflows. Local state is risky for shared infrastructure because two people can apply conflicting changes.
State locking prevents multiple Terraform operations from changing the same state at the same time. Without locking, concurrent applies can corrupt state or create conflicting infrastructure changes. Backends such as S3 with DynamoDB locking or Terraform Cloud provide locking behavior.
An S3 backend stores state in an S3 bucket. A DynamoDB table is commonly used for locking in older S3 backend patterns. The bucket should have versioning, encryption, restricted access, and backups because it contains sensitive infrastructure data.
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
terraform init initializes a working directory. It downloads providers, configures the backend, installs modules, and prepares the directory for plan or apply. Run it after changing backend configuration, provider versions, or module sources.
terraform init
terraform plan compares configuration and state with real infrastructure and shows what Terraform intends to create, update, replace, or destroy. A plan should be reviewed before apply, especially in production, because it is the main safety checkpoint for infrastructure changes.
terraform plan -out=tfplan
terraform show tfplan
terraform apply executes the changes from a plan or creates and applies a plan interactively. In CI/CD, teams usually generate a saved plan, review it, then apply that exact plan to avoid applying unreviewed changes.
terraform apply tfplan
terraform destroy removes infrastructure managed by the current state and configuration. It should be tightly controlled because it can delete production resources. Use workspace/environment checks, approvals, restricted credentials, and clear plan review before destructive operations.
Variables parameterize Terraform configuration so modules and environments can reuse the same code with different values. Variables can include type constraints, defaults, descriptions, validation rules, and sensitivity flags.
variable "instance_count" {
type = number
description = "Number of application instances"
default = 2
validation {
condition = var.instance_count > 0
error_message = "instance_count must be greater than zero."
}
}
Outputs expose useful values after apply, such as load balancer DNS names, subnet IDs, security group IDs, or module results. Outputs can be consumed by humans, CI systems, or remote-state data sources. Mark sensitive outputs to reduce accidental display.
output "load_balancer_dns" {
value = aws_lb.app.dns_name
}
Locals define reusable computed values inside a module. They reduce repetition and make expressions easier to read. Use locals for naming conventions, common tags, derived maps, and reusable conditional logic.
locals {
common_tags = {
Project = "billing"
Owner = "platform"
}
}
tfvars files provide variable values for an environment or deployment. They keep code generic while environment-specific values live separately. Do not commit secret values in tfvars files unless the repository and encryption model are designed for that.
terraform plan -var-file=prod.tfvars
A module is a reusable package of Terraform configuration. Every Terraform directory is a module, and root modules can call child modules. Modules help standardize infrastructure patterns such as VPCs, services, IAM roles, databases, and monitoring.
module "network" {
source = "./modules/network"
name = "prod"
cidr = "10.0.0.0/16"
}
Shared modules should be versioned with tags or releases so environments do not unexpectedly change when module code changes. Pin module versions, read changelogs, and test upgrades in lower environments before production.
module "vpc" {
source = "git::https://github.com/example/terraform-vpc.git?ref=v1.4.0"
name = "prod"
cidr = "10.0.0.0/16"
}
Workspaces let one configuration directory use multiple state instances. They are useful for simple environment separation, but they can hide differences between environments and make production mistakes easier if workflows are weak. Many teams prefer separate directories or repositories for major environments.
terraform workspace new dev
terraform workspace select dev
Common approaches include separate directories, separate workspaces, separate state keys, separate cloud accounts/projects, or separate repositories. For production-grade setups, separate state and credentials per environment reduces blast radius and makes approvals clearer.
Provider aliases allow one configuration to use multiple provider configurations, such as different AWS regions or accounts. Resources and modules can explicitly reference the provider alias they should use.
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
resource "aws_s3_bucket" "west_logs" {
provider = aws.west
bucket = "example-west-logs"
}
count creates multiple instances of a resource or module using numeric indexes. It is simple for identical resources but can be risky if list ordering changes, because indexes are part of resource addresses and may cause unwanted replacements.
resource "aws_instance" "web" {
count = 3
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
for_each creates one resource instance per key in a map or set. It is often safer than count for named resources because stable keys preserve resource addresses even if items are reordered.
resource "aws_iam_user" "users" {
for_each = toset(["alice", "bob"])
name = each.key
}
Dynamic blocks generate nested configuration blocks from collections. They are useful for repeated nested settings, but overusing them can make Terraform code hard to read. Prefer clear explicit blocks when the number of items is small and stable.
depends_on creates an explicit dependency when Terraform cannot infer the dependency from references. Use it sparingly. Most dependencies should be expressed naturally by referencing resource attributes.
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
depends_on = [aws_iam_role_policy.app]
}
Terraform analyzes references between resources, data sources, modules, and outputs to build a dependency graph. The graph determines the order of reads, creates, updates, and destroys. Explicit depends_on is only needed when the relationship exists outside normal attribute references.
terraform graph
The lifecycle block customizes how Terraform handles resource changes. Common settings include prevent_destroy, create_before_destroy, ignore_changes, and replace_triggered_by. These are powerful but can hide drift or make replacements more complex.
resource "aws_lb" "app" {
name = "app-lb"
lifecycle {
prevent_destroy = true
}
}
ignore_changes tells Terraform not to plan updates for selected attributes. It is useful when another system legitimately manages a field, but it can also hide configuration drift. Document every ignore_changes use and review it periodically.
Drift occurs when real infrastructure differs from Terraform configuration or state, often because someone changed resources manually or another automation modified them. Detect drift with plan, refresh-only plans, monitoring, and policy that discourages console changes.
terraform plan -refresh-only
Import links an existing real resource to a Terraform address in state. After import, you must write matching configuration and run plan to resolve differences. Import does not automatically generate perfect production-ready code for every resource.
terraform import aws_s3_bucket.logs example-app-logs
moved blocks tell Terraform that a resource address changed, such as during refactoring or module extraction. They avoid unnecessary destroy/create operations when the real resource is the same but its Terraform address moved.
moved {
from = aws_s3_bucket.logs
to = module.logging.aws_s3_bucket.logs
}
Older Terraform workflows used taint to force replacement. Modern Terraform often uses terraform apply -replace=address to explicitly replace a resource. Replacement should be reviewed carefully because it can cause downtime or data loss for stateful resources.
terraform apply -replace=aws_instance.web[0]
Marking a variable sensitive hides it from normal CLI output, but it does not guarantee the value is absent from state. Secrets can still end up in state depending on resources. Use secret managers, secure backends, encryption, RBAC, and avoid storing long-lived secrets in Terraform where possible.
variable "db_password" {
type = string
sensitive = true
}
State stores resource attributes needed for future plans, and some providers return sensitive values. Even if CLI output hides a value, state may still contain it. This is why state backends need encryption, access controls, audit logs, and careful sharing.
Review create/update/delete counts, replacement markers, affected resource addresses, IAM/security changes, network exposure, database/storage changes, and unexpected drift. In production, require peer approval and apply the exact saved plan that was reviewed.
Testing can include terraform fmt, validate, plan in CI, policy checks, static analysis, module unit tests, integration tests in temporary environments, and post-apply smoke tests. The deeper the infrastructure risk, the more important realistic test environments become.
terraform fmt -check
terraform validate
terraform plan
terraform fmt rewrites Terraform configuration to the standard style. It keeps code consistent across contributors and should usually run in CI with -check.
terraform fmt -recursive
terraform validate checks whether configuration is syntactically valid and internally consistent. It does not contact cloud APIs for every real-world condition and does not prove the plan is safe, but it catches many configuration errors early.
Policy as code checks Terraform plans against rules before apply. Examples include blocking public S3 buckets, requiring tags, preventing open security groups, enforcing approved regions, or limiting expensive instance types. Tools include Sentinel, OPA/Conftest, Checkov, tfsec, and cloud-native policy systems.
Provisioners run scripts or commands during resource creation or destruction. They are usually discouraged because they make infrastructure less declarative, harder to retry safely, and more dependent on timing or network access. Prefer cloud-init, baked images, configuration management, or provider-native resources when possible.
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
provisioner "local-exec" {
command = "echo created"
}
}
A common workflow runs fmt and validate on every pull request, generates a plan for review, applies only after approval, uses remote state locking, uses short-lived credentials, stores plan artifacts securely, and separates permissions for plan and apply.
terraform init
terraform fmt -check
terraform validate
terraform plan -out=tfplan
terraform apply tfplan
Common mistakes include using local state for team infrastructure, no state locking, weak backend permissions, no versioning or backups, storing production and dev in the same state, and allowing broad read access to state that may contain secrets.
Terraform is mainly declarative infrastructure provisioning and tracks state. Ansible is commonly used for configuration management, orchestration, and procedural tasks over existing hosts. They can complement each other: Terraform creates infrastructure, while Ansible configures software on it.
CloudFormation is AWS-native and manages AWS resources through AWS. Terraform is multi-provider and uses its own state and provider plugins. CloudFormation can be preferred for AWS-only native integration, while Terraform is useful for multi-cloud or cross-platform infrastructure.
Pin provider versions, read changelogs and upgrade guides, run init -upgrade in a branch, review plans in lower environments, and watch for changed defaults or deprecated arguments. Provider upgrades can change behavior even when your HCL looks the same.
terraform init -upgrade
Common mistakes include committing secrets, exposing state broadly, using overly privileged cloud credentials, ignoring destructive plans, allowing public network access by default, using unpinned modules/providers, and skipping policy checks for IAM or firewall changes.
Common anti-patterns include one huge state for everything, excessive dynamic blocks, no module versioning, manual console changes, using workspaces for very different environments without guardrails, ignoring drift, overusing ignore_changes, and running apply from a developer laptop for production.
First stop all applies. Restore from backend version history or backups if possible. If needed, use state commands carefully to remove, move, or import resources. Never edit state casually; take backups, work in a branch, and verify with plan before applying.
terraform state list
terraform state show aws_s3_bucket.logs
terraform state mv old.address new.address
A strong demo shows a provider, one or two resources, variables, outputs, fmt, validate, plan, and apply. Keep the resource low-risk and cheap, such as an S3 bucket or local provider resource, and explain how remote state and review would change the production workflow.
provider "aws" {
region = var.region
}
variable "region" {
type = string
default = "us-east-1"
}
resource "aws_s3_bucket" "demo" {
bucket = "example-terraform-demo-bucket"
}
output "bucket_name" {
value = aws_s3_bucket.demo.bucket
}
Explore 500+ free tutorials across 20+ languages and frameworks.