Terraform interviews test more than HCL syntax. Strong answers explain the difference between configuration, plan, state, and remote reality; identify the blast radius of an apply; and protect both infrastructure and the secrets that can appear in plans and state.
This guide contains exactly 32 questions and uses the Terraform 1.16 documentation set current in September 2026. Provider behavior still varies, so verify resource-specific lifecycle, import, ephemeral, and write-only support in the provider version you pin.
Table of Contents
- Terraform Fundamentals Questions
- HCL Language Questions
- State Management Questions
- Module Questions
- Workspace and Environment Questions
- Best Practices Questions
- Troubleshooting and Scenario Questions
Terraform Fundamentals Questions
Understanding Terraform's core concepts is essential for any DevOps interview.
What is Infrastructure as Code and why does it matter?
Infrastructure as Code (IaC) means managing infrastructure through configuration files rather than manual processes. Instead of clicking through cloud consoles or running ad-hoc commands, you define your entire infrastructure in version-controlled files that can be reviewed, tested, and applied consistently.
This approach brings versioning, review, validation, and automation to infrastructure changes. It improves reproducibility, but the same configuration does not guarantee the same result when provider versions, data sources, defaults, credentials, regions, or external APIs differ. Pin dependencies, record inputs, review plans, and design explicit recovery; reverting Git does not automatically roll back an already-applied destructive change.
Key benefits:
- Version control: Track changes, review PRs, and support deliberate recovery
- Repeatability: Pinned configuration and inputs make changes explainable
- Automation: CI/CD for infrastructure
- Documentation: Code is the documentation
How does Terraform compare to other IaC tools?
Each IaC tool has different state, lifecycle, preview, language, and operational trade-offs. Terraform uses provider plugins and a dependency graph across many APIs. CloudFormation is an AWS control-plane service. Ansible commonly runs idempotent tasks against inventories, while Pulumi uses general-purpose languages and AWS CDK synthesizes CloudFormation. None is universally superior, and several can overlap in provisioning and configuration.
The choice often depends on your organization's needs. Multi-cloud or hybrid environments benefit from Terraform's consistency. AWS-only shops might prefer CloudFormation's native integration. Teams with strong programming backgrounds might choose Pulumi or CDK for their familiar language syntax.
| Tool | Type | Language | Best For |
|---|---|---|---|
| Terraform | Desired-state plan/apply | HCL | Provider-managed infrastructure across APIs |
| CloudFormation | Declarative | YAML/JSON | AWS-only shops |
| Pulumi | Desired-state SDK | Python/TS/Go/.NET/Java/YAML | General-purpose language workflows |
| Ansible | Task/playbook automation | YAML | Configuration and orchestration |
| AWS CDK | CloudFormation synthesis | Supported programming languages | AWS-native stacks as code |
When would you choose Terraform over CloudFormation?
This common interview question tests your understanding of tool selection based on requirements. Terraform excels when you need to work across multiple cloud providers or manage non-cloud resources like GitHub repositories, Datadog monitors, or Kubernetes clusters. Its provider ecosystem covers virtually any API-driven service.
Choose Terraform when you need multi-cloud support, consistent tooling across providers, or management of non-AWS resources. Choose CloudFormation when you're AWS-only, need tight AWS integration like StackSets and native drift detection, or when your organization has standardized on it.
What is the core Terraform workflow?
Terraform follows a simple but powerful workflow: initialize, plan, apply. Understanding this workflow and what happens at each stage demonstrates operational competency to interviewers.
The init phase installs selected provider/module dependencies and configures the backend. Plan refreshes managed objects by default, evaluates configuration against state and remote reality, then proposes actions. A plan is not a guarantee: remote state can change before apply. For controlled promotion, save a reviewed plan artifact and apply that exact artifact with the same code, lock file, variables, credentials, and backend rather than generating an unreviewed plan later.
# 1. Initialize - download providers, set up backend
terraform init
# 2. Plan - preview changes without applying
terraform plan
# 3. Apply - create/update infrastructure
terraform apply
# 4. Destroy - tear down infrastructure
terraform destroyWhat happens during init:
- Downloads provider plugins
- Initializes backend (local or remote)
- Downloads modules
- Creates
.terraformdirectory
How do providers and resources work in Terraform?
Providers are plugins that know how to interact with specific APIs—AWS, Azure, Kubernetes, or any service with an API. Resources are the actual infrastructure components you want to manage, defined using the provider's resource types.
Each resource has a type (combining provider and resource kind) and a local name you use to reference it elsewhere in your configuration. The block contents specify the configuration arguments for that resource. Understanding this anatomy helps you read and write Terraform configurations fluently.
# Configure the AWS provider
provider "aws" {
region = "us-east-1"
}
# Create a resource
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}Resource anatomy:
aws_instance- resource type (provider_resource)"web"- local name (for referencing)- Block contents - configuration arguments
Referencing resources:
# Reference another resource's attribute
resource "aws_eip" "web_ip" {
instance = aws_instance.web.id # type.name.attribute
}HCL Language Questions
HCL (HashiCorp Configuration Language) is Terraform's domain-specific language for defining infrastructure.
How do you use variables in Terraform?
Variables make your Terraform configurations reusable and flexible. Input variables act as parameters—you define them in your configuration and provide values at runtime. Output variables export values for use by other configurations or for human consumption. Local variables are computed values for reuse within a module.
Understanding the different variable types and how to set them is fundamental Terraform knowledge. Variables can have defaults, validation rules, and type constraints that catch errors early.
Input variables: Parameters for your configuration
# variables.tf
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "environment" {
description = "Environment name"
type = string
# No default = required variable
}
variable "allowed_ports" {
description = "List of allowed ports"
type = list(number)
default = [80, 443]
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {}
}Setting variables:
# Command line
terraform apply -var="environment=prod"
# Variable file
terraform apply -var-file="prod.tfvars"
# Environment variable
export TF_VAR_environment=prod
# Auto-loaded files: terraform.tfvars, *.auto.tfvarsOutput variables: Export values for other configs or users
# outputs.tf
output "instance_ip" {
description = "Public IP of the instance"
value = aws_instance.web.public_ip
}
output "database_password" {
description = "Database password"
value = random_password.db.result
sensitive = true # Redacted in normal CLI/UI output; may still be in state/plan
}Local variables: Computed values for reuse within a module
locals {
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_instance" "web" {
# ...
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web"
})
}What data types does Terraform support?
Terraform supports primitive types (string, number, bool) and collection types (list, set, map) as well as structural types (object, tuple). Understanding these types helps you write type-safe configurations and catch errors during planning rather than at apply time.
Lists maintain order and allow duplicates. Sets are unordered and unique. Maps store key-value pairs. Objects combine named attributes with different types, while tuples are ordered collections with mixed types.
# Primitives
string = "hello"
number = 42
bool = true
# Collections
list = ["a", "b", "c"] # Ordered, same type
set = toset(["a", "b", "c"]) # Unordered, unique
map = { key = "value" } # Key-value pairs
# Structural
object({
name = string
age = number
})
tuple([string, number, bool])How do you write conditional expressions in Terraform?
Conditional expressions let you make decisions in your configuration based on variable values or other conditions. Terraform uses the ternary syntax common in many programming languages: condition ? true_value : false_value.
You can use conditionals for attribute values or combined with count to conditionally create entire resources. This pattern is essential for writing flexible modules that adapt to different environments or requirements.
# Ternary expression
resource "aws_instance" "web" {
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
}
# Conditional resource creation
resource "aws_eip" "web" {
count = var.create_eip ? 1 : 0
instance = aws_instance.web.id
}What is the difference between count and for_each?
This is one of the most common Terraform interview questions because it reveals understanding of resource addressing and state management. Count creates resources indexed by number, while for_each creates resources indexed by key. This seemingly small difference has major implications for how Terraform handles changes.
Count works well for creating N identical resources, but causes problems when you remove items from the middle of a list—all subsequent indices shift, causing Terraform to destroy and recreate resources. For_each avoids this by keying resources by name, so removing one item only affects that specific resource.
count: Create multiple resources by index
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}" # web-0, web-1, web-2
}
}
# Reference: aws_instance.web[0], aws_instance.web[1]for_each: Create resources by key
variable "instances" {
default = {
web = "t3.micro"
api = "t3.small"
worker = "t3.medium"
}
}
resource "aws_instance" "server" {
for_each = var.instances
ami = "ami-0c55b159cbfafe1f0"
instance_type = each.value
tags = {
Name = each.key
}
}
# Reference: aws_instance.server["web"], aws_instance.server["api"]When to use which:
| Use Case | Recommendation |
|---|---|
| N identical resources | count |
| Resources with unique identity | for_each |
| Might remove items from middle | for_each |
| List of objects | for_each with toset() or tomap() |
The count index problem:
# With count = ["a", "b", "c"]
# Removing "b" causes "c" to shift from index 2 to 1
# Terraform sees: destroy old [2], modify [1]
# Result: Unintended recreation
# With for_each = toset(["a", "b", "c"])
# Removing "b" only affects resource["b"]
# Resources "a" and "c" unchangedHow do data sources work in Terraform?
Data sources let you query existing infrastructure or external information to use in your configuration. Unlike resources which create and manage infrastructure, data sources are read-only—they fetch information that already exists.
Common uses include getting the current account ID or reading infrastructure managed elsewhere. A dynamic “latest image” lookup can change between plans and trigger replacement, so production workflows often promote an explicitly tested image ID instead. Data sources are evaluated during planning when their inputs are known and can expose sensitive results in state.
# Get latest Amazon Linux AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Use it
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
# ...
}
# Get current AWS account ID
data "aws_caller_identity" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}State Management Questions
State management is arguably the most critical aspect of Terraform operations.
What is Terraform state and why is it important?
State is a JSON file that maintains the mapping between your configuration and real infrastructure. Without state, Terraform couldn't know which real resources correspond to which configuration blocks, what order to create or update resources, or what the current values of resource attributes are.
The state file contains resource IDs that let Terraform interact with the cloud provider API, dependency information for determining operation order, and cached attribute values that reduce API calls. Understanding state deeply is essential for troubleshooting and disaster recovery.
{
"resources": [
{
"type": "aws_instance",
"name": "web",
"instances": [
{
"attributes": {
"id": "i-1234567890abcdef0",
"ami": "ami-0c55b159cbfafe1f0",
"public_ip": "54.123.45.67"
}
}
]
}
]
}Why state matters:
- Maps config to real resource IDs
- Tracks dependencies for ordering
- Caches attributes to reduce API calls
- Detects drift from desired state
Why should you use remote state backends?
Local state is convenient for isolated learning, but collaboration needs a controlled shared authority. Choose a remote backend or HCP Terraform that fits the required locking, authentication, encryption, audit, availability, and recovery model.
Remote does not mean safe by default. Locking support varies and may be opt-in; encryption, object versioning, retention, backups, cross-region recovery, and least-privilege access must be configured. Readers of state can often read secrets, while writers can influence future infrastructure changes. Keep state out of Git, restrict human access, protect state and saved plans as production secrets, and test restore procedures.
S3 Backend (AWS):
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}Enable S3 bucket versioning and grant only the required state and .tflock object permissions. DynamoDB-based S3 locking is deprecated; it can coexist temporarily during migration but should not be the design for a new backend.
GCS Backend (GCP):
terraform {
backend "gcs" {
bucket = "my-terraform-state"
prefix = "prod/network"
}
}Terraform Cloud:
terraform {
cloud {
organization = "my-org"
workspaces {
name = "prod-network"
}
}
}How does state locking work?
For operations that can write state, a supporting backend acquires a lock before changing the snapshot. A competing operation waits or fails according to its lock timeout. Locking serializes Terraform writers for that state; it does not stop console changes, another state file managing the same object, a provider-side controller, or an operator from changing remote infrastructure.
The S3 backend can use an S3 lock file with use_lockfile = true. Its older DynamoDB locking mechanism is deprecated. Other backends have their own semantics, so verify support rather than claiming every remote backend locks automatically. Use force-unlock only after proving that the owning run is dead and the lock ID belongs to this state; unlocking a live apply can create concurrent writers.
sequenceDiagram
participant A as Developer A
participant S as State Backend
participant B as Developer B
A->>S: terraform apply
S-->>A: Lock acquired
Note over A,S: Lock held by A
B->>S: terraform apply
S--xB: BLOCKED (lock held)
Note over B: Waiting for lock...
A->>S: Apply complete, release lock
S-->>A: Lock released
B->>S: Retry - acquire lock
S-->>B: Lock acquired
Note over B,S: B proceeds with applyWhat state commands should you know for interviews?
Terraform provides several commands for inspecting and manipulating state. These are essential for troubleshooting, refactoring, and disaster recovery. Interviewers often ask about specific commands and when you'd use them.
Prefer configuration-driven moved, removed, and import blocks because their intent is reviewed and repeatable. state list, state show, and state pull are useful for inspection. Direct state mv, state rm, state push, and force-unlock are exceptional surgery: back up state, stop competing runs, use the correct workspace, record the change, and verify a fresh plan.
# List resources in state
terraform state list
# Show specific resource
terraform state show aws_instance.web
# Pull remote state locally
terraform state pull
# Exceptional one-off surgery; normally prefer configuration blocks
terraform state mv OLD_ADDRESS NEW_ADDRESS
terraform state rm ADDRESS
terraform force-unlock LOCK_IDHow do you import existing resources into Terraform?
Importing associates an existing remote object with one Terraform resource address. It does not generate a correct lifecycle policy, discover every dependency, or prove that the written configuration is safe. Ensure one remote object is bound to one address, back up state, and review the resulting plan.
Configuration-driven import blocks can be committed, reviewed, applied, and used with for_each where supported. Terraform can generate a starting resource block with the plan -generate-config-out workflow, but provider-generated configuration still needs review. The legacy CLI command remains useful for one-off imports.
resource "aws_instance" "existing" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
import {
to = aws_instance.existing
id = "i-1234567890abcdef0"
}Run terraform plan, inspect every proposed change, adjust the configuration, and apply the reviewed import. A no-change plan is desirable after adoption but may require representing provider defaults carefully rather than copying every computed attribute.
Module Questions
Modules are the primary way to organize and reuse Terraform code.
Why should you use Terraform modules?
Modules package related resources together into reusable, shareable components. Instead of copying the same VPC configuration into every project, you define it once as a module and call it with different parameters. This reduces duplication, enforces consistency, and makes large configurations manageable.
Good modules hide implementation complexity behind a simple interface. Users don't need to understand all the resources involved—they just provide the required inputs and consume the outputs. This encapsulation also makes it easier to update implementations without affecting every project that uses the module.
Key benefits:
- Reusability: Write once, use many times
- Encapsulation: Hide complexity behind simple interface
- Consistency: Enforce standards across teams
- Versioning: Control updates and changes
How should you structure a Terraform module?
Module structure follows conventions that make modules predictable and easy to use. Every module needs at least main.tf for resources, variables.tf for inputs, and outputs.tf for values other configurations can use. Additional files like versions.tf for provider requirements and README.md for documentation are best practices.
Following these conventions means anyone familiar with Terraform can quickly understand your module's interface and implementation.
modules/
└── vpc/
├── main.tf # Resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Provider requirements
└── README.md # Documentation
Example module:
# modules/vpc/variables.tf
variable "name" {
description = "VPC name"
type = string
}
variable "cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "azs" {
description = "Availability zones"
type = list(string)
}
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = var.name
}
}
resource "aws_subnet" "public" {
count = length(var.azs)
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.cidr, 8, count.index)
availability_zone = var.azs[count.index]
tags = {
Name = "${var.name}-public-${var.azs[count.index]}"
}
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}Using the module:
module "vpc" {
source = "./modules/vpc"
name = "production"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Reference outputs
resource "aws_instance" "web" {
subnet_id = module.vpc.public_subnet_ids[0]
# ...
}What module sources does Terraform support?
Terraform can load modules from various sources, giving you flexibility in how you organize and share code. Local paths are simplest for development. The Terraform Registry provides public and private modules with versioning. Git repositories (GitHub, GitLab, Bitbucket) work for private modules with tag-based versioning.
Each source type has trade-offs. Local paths are convenient but don't version well. Registry modules have excellent versioning but require publishing. Git sources balance flexibility and versioning but require careful reference management.
# Local path
module "vpc" {
source = "./modules/vpc"
}
# Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
}
# GitHub
module "vpc" {
source = "github.com/org/repo//modules/vpc?ref=v1.0.0"
}
# S3 bucket
module "vpc" {
source = "s3::https://s3-eu-west-1.amazonaws.com/bucket/vpc.zip"
}How should you version modules in production?
Version pinning is essential for stable production infrastructure. Without it, module updates could unexpectedly change your infrastructure. Always specify exact versions or constrained ranges in production configurations.
Exact versions provide the most stability but require manual updates. Pessimistic constraints (~>) allow patch updates while preventing breaking changes. Test module updates in non-production environments before promoting version changes to production.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0" # Exact version
# Or version constraints
# version = "~> 5.0" # >= 5.0.0, < 6.0.0
# version = ">= 5.0" # >= 5.0.0
}Workspace and Environment Questions
Managing multiple environments is a common challenge in Terraform.
How do Terraform workspaces work?
Terraform CLI workspaces are separate state instances inside one initialized working directory and backend. They are distinct from HCP Terraform workspaces, which have their own configuration, variables, state, run history, and settings.
CLI workspaces can create temporary or closely related copies of one configuration. They are not a strong isolation mechanism for production versus development because they share backend configuration and commonly share credentials and access controls. Avoid extensive terraform.workspace conditionals that turn one root module into several hidden architectures.
# List workspaces
terraform workspace list
# Create workspace
terraform workspace new staging
# Switch workspace
terraform workspace select production
# Show current
terraform workspace show
# Delete workspace
terraform workspace delete stagingUsing workspace in config:
resource "aws_instance" "web" {
instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
tags = {
Environment = terraform.workspace
}
}When should you use directory structure instead of workspaces?
The directory approach creates stronger isolation between environments by giving each its own configuration files, backend configuration, and potentially different module versions. This isolation reduces the risk of accidentally applying to the wrong environment and makes environment-specific customization more explicit.
Use separate root configurations/backends when environments need different credentials, policy, approvals, ownership, provider versions, release cadence, or blast radius—even if they call the same reusable modules. CLI workspaces are reasonable for ephemeral branch environments or near-identical copies under one trust boundary.
terraform/
├── modules/
│ └── app/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ │ └── ...
│ └── prod/
│ └── ...
Each environment has its own:
- State file (different backend key)
- Variable values
- Provider configuration if needed
What are the trade-offs between workspaces and directories?
This question tests your ability to evaluate solutions based on specific requirements rather than following a single pattern blindly. Both approaches have valid use cases, and senior engineers understand when to use each.
| Aspect | Workspaces | Directories |
|---|---|---|
| State isolation | Separate states in one backend | Separately configured states/backends |
| Code duplication | None | Some (can use modules) |
| Variable differences | Conditional logic | Separate tfvars |
| Accidental cross-apply | Possible (wrong workspace) | Harder (different directory) |
| Best for | Temporary/related copies under one trust boundary | Environments needing independent control |
Recommendation: give production an explicit root configuration, state, credentials, policy, and approval path. Use CLI workspaces only when shared backend trust is intentional.
Best Practices Questions
Following established patterns separates professional Terraform users from beginners.
How should you organize Terraform code in a project?
Code organization affects maintainability and collaboration. The standard pattern separates concerns into distinct files: main.tf for resources, variables.tf for inputs, outputs.tf for exports, and so on. This convention makes large configurations navigable and helps team members find what they're looking for quickly.
Consistent organization across projects reduces cognitive load when switching between codebases and makes onboarding new team members faster.
project/
├── main.tf # Primary resources
├── variables.tf # All variable declarations
├── outputs.tf # All outputs
├── versions.tf # Terraform and provider versions
├── providers.tf # Provider configurations
├── locals.tf # Local values
├── data.tf # Data sources
└── terraform.tfvars # Variable values (don't commit secrets)
versions.tf:
terraform {
required_version = "~> 1.16.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0" # Example: use the constraint your test matrix approves
}
}
}What naming conventions should you follow?
Consistent naming makes configurations readable and maintainable. Terraform conventions use lowercase with underscores for all identifiers. Resource names should be descriptive but concise, indicating what the resource is for. Variables and outputs follow the same pattern.
Following community conventions means your code is immediately readable by other Terraform users and tools that expect standard patterns will work correctly.
# Resources: descriptive, lowercase, underscores
resource "aws_instance" "web_server" { }
resource "aws_security_group" "web_sg" { }
# Variables: lowercase, underscores
variable "instance_type" { }
variable "environment_name" { }
# Outputs: lowercase, underscores, descriptive
output "load_balancer_dns" { }
# Locals: lowercase, underscores
locals {
common_tags = { }
}How should you handle secrets in Terraform?
Do not hardcode secrets or place them in committed tfvars, shell history, CLI arguments, or PR comments. Prefer short-lived workload identity for provider authentication. Treat local/remote state, saved plans, crash logs, and CI artifacts as sensitive because provider schemas can persist secret attributes.
sensitive = true redacts normal CLI and UI rendering; it does not remove the value from state or plan files. Likewise, reading a secret through a data source or generating it with a managed resource can persist the value. Encrypt state and plan storage, apply least privilege, audit access, version/recover state securely, and limit artifact retention.
Terraform 1.10+ supports ephemeral variables/resources and Terraform 1.11+ supports provider-defined write-only arguments. When the selected provider resource offers a write-only field, combine it with an ephemeral value so the secret is available during the operation but omitted from plan and state. Support is resource-specific and often needs a separate version argument so Terraform knows when to send a changed value.
Never do this:
# BAD - secrets in code
resource "aws_db_instance" "db" {
password = "supersecret123" # NO!
}Redaction only—the value may still persist:
variable "db_password" {
type = string
sensitive = true
}Non-persistent path when the provider supports it:
variable "db_password" {
type = string
sensitive = true
ephemeral = true
}
resource "aws_db_instance" "db" {
# Provider-supported write-only argument
password_wo = var.db_password
password_wo_version = var.db_password_version
}External secret stores are valuable, but a normal data-source value passed to an ordinary managed argument can still land in state. Verify the whole data path rather than assuming “stored in Vault” means “never stored by Terraform.”
How do you integrate Terraform with CI/CD?
CI/CD should create an auditable promotion path, not merely run apply -auto-approve after a push. Pin Terraform and providers, commit .terraform.lock.hcl, authenticate with short-lived workload identity, serialize each state, and separate read-only validation/plan permissions from apply permissions.
Platform-neutral pipeline shape:
# Validate the reviewed checkout with pinned providers
terraform fmt -check -recursive
terraform init -lockfile=readonly
terraform validate
# Run module tests in an isolated account/project.
# terraform test may create real infrastructure and cost money.
terraform test
# Create and render a machine-readable, saved plan
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
# Policy/security checks and human environment approval happen here.
# Apply the exact approved artifact, not a newly generated plan.
terraform apply tfplanSaved plan files can contain cleartext sensitive values and authorize concrete infrastructure actions. Encrypt them, restrict readers, bind them to the exact commit/workspace/toolchain, set short retention, and never accept an apply artifact from an untrusted fork. Re-plan after state, code, variable, provider, or approval context changes.
Best practices for CI/CD:
- Run
terraform fmt -checkto enforce formatting - Run
terraform validatefor syntax errors - Run
terraform testwith controlled credentials, cleanup, and cost limits - Run speculative plans with appropriately limited credentials
- Require approval before
applyto production - Use OIDC for cloud authentication (no long-lived keys)
- Apply the reviewed saved plan and protect it as a secret artifact
- Add policy, security, cost, and destructive-change gates appropriate to the estate
Troubleshooting and Scenario Questions
Scenario questions test your practical experience with real-world problems.
How do you handle state drift?
Drift is a difference between the configuration/state model and provider-observed remote objects. It can come from a console edit, another controller, provider defaults, autoscaling, incident response, or two Terraform states managing the same object—not only careless manual work.
A normal terraform plan refreshes managed objects in memory and proposes reconciliation. Classify each difference: update configuration to adopt it, apply to revert it, use an intentional ignore_changes boundary for externally owned attributes, import an unmanaged object, or repair an ownership conflict. If the goal is only to update state/outputs from remote facts, review terraform plan -refresh-only and then deliberately apply refresh-only. The deprecated terraform refresh aliases an automatically approved state mutation and is unsafe as routine advice.
# Detect and review drift plus configuration changes
terraform plan
# Review a state-only reconciliation when that is the intended outcome
terraform plan -refresh-only
terraform apply -refresh-onlyPrevention:
- Define break-glass changes and reconcile them afterward
- Detect multiple owners and constrain routine write access
- Enable drift detection alerts
- Regular
terraform planin CI
What do you do when Terraform apply fails halfway?
Terraform apply is not a transaction and has no general rollback. Providers report successful operations as the graph executes; Terraform normally persists the latest known state, but a crash or ambiguous provider timeout can leave uncertain remote outcomes. Stop competing runs, preserve logs/state versions, inspect the provider and remote system, and create a fresh plan before acting.
Fix the root cause and re-apply only after reviewing the new plan. If an object must be recreated, use a reviewed -replace=ADDRESS plan/apply. terraform taint is deprecated because it mutates state before teammates can review the replacement. Use direct state surgery or backup restore only when authoritative evidence shows state is wrong; restoring an old snapshot can forget real resources created later.
terraform state list
terraform plan
# Force a reviewed replacement only when required
terraform plan -replace="aws_instance.web" -out=tfplan
terraform apply tfplanHow do you rename a resource without destroying it?
By default, an address rename looks like destroy-plus-create. Prefer a configuration-driven moved block: it is versioned, visible to every workspace/module consumer, appears in plan, and preserves the remote object binding. Keep compatibility moved blocks in reusable modules long enough for consumers to traverse the upgrade path. terraform state mv is a one-off operational alternative for older versions or exceptional surgery.
# Before
resource "aws_instance" "web" { }
# After
resource "aws_instance" "application" { }moved {
from = aws_instance.web
to = aws_instance.application
}Run plan in every affected state and confirm it reports an address move rather than replacement. A moved block cannot make incompatible resource types interchangeable unless the provider explicitly supports that move.
How do you migrate from local to remote state?
Before migration, pause all writers, back up and securely checksum the source state, configure destination encryption/access/locking/versioning, and verify the selected workspace. terraform init -migrate-state can copy state, but it does not design those controls or make a concurrent migration safe.
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
}
}terraform init -migrate-stateReview the migration prompt, then verify terraform state pull and a fresh no-surprise plan.
After verification, revoke access to the obsolete state and securely remove stray local copies. Keep the migration backup under the same secret-handling and retention policy as production state.
Quick Reference
What are the essential Terraform commands?
| Command | Purpose |
|---|---|
terraform init | Initialize working directory |
terraform plan | Preview changes |
terraform plan -out=tfplan | Save the reviewed execution plan |
terraform apply tfplan | Apply that exact saved plan |
terraform destroy | Destroy infrastructure |
terraform fmt | Format code |
terraform validate | Validate syntax |
terraform test | Execute Terraform test files; may create real resources |
terraform plan -refresh-only | Review state-only reconciliation |
terraform state list | List resources in state |
import { ... } | Declare a reviewable import in configuration |
terraform output | Show outputs |
What are common Terraform patterns?
# Conditional resource
count = var.create_resource ? 1 : 0
# Conditional attribute
instance_type = var.env == "prod" ? "t3.large" : "t3.micro"
# Dynamic blocks
dynamic "ingress" {
for_each = var.ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
}
}
# Depends on (explicit dependency)
depends_on = [aws_iam_role_policy.example]
# Reviewable address refactor
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}
# Reviewable adoption of an existing object
import {
to = aws_instance.existing
id = "i-1234567890abcdef0"
}
# Lifecycle rules
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [tags]
}Lifecycle flags are not generic safety switches. create_before_destroy can be impossible under naming/quota constraints, prevent_destroy protects only while the block remains in configuration, and broad ignore_changes can hide meaningful drift. Explain the ownership contract behind each use.
Frequently Asked Questions
What is Terraform state and why is it important?
Terraform state stores bindings between resource instances in configuration and remote objects, plus metadata and cached attributes used to plan changes. Treat every state snapshot and saved plan as sensitive. For collaboration, choose a remote backend with access control, encryption, recovery/versioning, and locking where supported and explicitly enabled; a remote backend does not provide every safeguard automatically.
What is the difference between Terraform and Ansible?
Terraform builds a dependency graph and plans provider-managed desired-state changes, while Ansible commonly executes idempotent tasks through inventories and playbooks. Both can provision and configure resources, so the boundary is not absolute. Choose from ownership, lifecycle, state, preview, provider/module ecosystem, agent model, and team operations; some organizations use Terraform for long-lived infrastructure and Ansible for guest configuration.
How do you handle secrets in Terraform?
Keep credentials and secret values out of source code, CLI arguments, logs, and ordinary tfvars. Sensitive only redacts normal CLI/UI display; the value can still exist in state and plan files. Use short-lived workload identity, secure state and plan storage, least privilege, and provider-supported ephemeral values or write-only arguments when the value must not persist. Reading a secret with a data source can still place it in state.
What is the difference between count and for_each?
Count creates resources by index number (0, 1, 2) - good for identical resources. For_each creates resources by key - better for distinct items. Count causes issues when removing middle items (indices shift, causing recreation). For_each is more stable since resources are keyed by name. Use count for simple multiples, for_each when items have identity or might change independently.
How do you manage multiple environments with Terraform?
Use reusable modules with separate root configurations and backends when environments need different credentials, access controls, ownership, or lifecycle. CLI workspaces are separate state instances in one working directory and backend, useful for temporary or closely related copies but not a strong security boundary. Keep production state, credentials, approvals, and blast radius explicitly isolated.
What happens if Terraform state gets out of sync with real infrastructure?
A normal plan refreshes managed objects in memory and shows proposed reconciliation. Decide whether configuration should adopt the remote change, Terraform should revert it, or policy should accept an exception. If state itself should adopt remote facts without changing infrastructure, review a refresh-only plan and apply it deliberately; the terraform refresh command is deprecated. Import is for unmanaged objects, not a general drift repair tool.
Sources
- Terraform 1.16 documentation - current language, CLI, workflow, and release documentation.
- Terraform state - state purpose, storage, access, and locking guidance.
- Terraform S3 backend - S3 lock files, permissions, versioning, and deprecated DynamoDB locking.
- Manage sensitive data - sensitive redaction, ephemeral values, state, and write-only arguments.
- Terraform import blocks - configuration-driven imports and supported arguments.
- Refactor modules with moved blocks - reviewable address changes without replacement.
- Terraform CLI workspaces - workspace scope and isolation limitations.
- Terraform test - test execution, assertions, cleanup, and real-infrastructure warning.
- Refresh-only mode - reviewed state reconciliation and deprecated
refreshbehavior. - Recreate resources -
-replaceworkflow and deprecatedtaintcommand.
Related Articles
This guide connects to the broader DevOps interview preparation:
Cloud Platforms:
- AWS Interview Guide - AWS resources and services
- Azure Interview Guide - Azure ARM comparison
- GCP Interview Guide - GCP infrastructure and service concepts
DevOps Fundamentals:
- CI/CD & GitHub Actions Interview Guide - Terraform in pipelines
- Docker Interview Guide - Container infrastructure
- Kubernetes Interview Guide - K8s provider
Architecture:
- System Design Interview Guide - Infrastructure patterns
- Networking Interview Guide - VPC and network resources
