Exhaustive Production-Grade Terraform (IaC) Masterclass: From Beginner to Staff Infrastructure Engineer
Welcome to the definitive, production-grade masterclass on HashiCorp Terraform and Infrastructure as Code (IaC). This comprehensive guide is designed for software engineers, DevOps specialists, platform engineers, and site reliability engineers (SREs). It spans from foundational declarative principles and Directed Acyclic Graph (DAG) state calculations to advanced Terragrunt patterns, state recovery, Sentinel/OPA policy enforcement, and multi-cloud architectural design.
- Stage 1: IaC Foundations & Core Architecture
- 1.1 Declarative vs Imperative Infrastructure Paradigms
- 1.2 Core vs Plugins: The gRPC Provider Architecture
- 1.3 Directed Acyclic Graph (DAG) Dependency Engine
- 1.4 The Terraform Engine Lifecycle: Init, Plan, Apply, Destroy
- Stage 2: HCL2 Syntax & Metaprogramming Mastery
- 2.1 Variables, Validations, and Sensitive Types
- 2.2 Collections, Complex Objects, and Type Constraints
- 2.3 Expressions: Splats, For Loops, Dynamic Blocks, and Coalesce
- 2.4 Built-in Functions: Collection, Cryptographic, String, and Encoding
- Stage 3: State Mechanics, Locking, and Drift Remediation
- 3.1 State File Anatomy and Resource Mapping
- 3.2 Remote Backend Locking with AWS S3 and DynamoDB
- 3.3 State Operations: Migration, Inspection, Removal, and Renaming
- 3.4 Modern Import Blocks (Terraform 1.5+) vs Legacy CLI Import
- Stage 4: Modular Architecture & Production Reusability
- 4.1 Enterprise Module Decomposition Principles
- 4.2 Module Contracts: Inputs, Outputs, and Semantic Versioning
- 4.3 Terragrunt and DRY Configuration Patterns
- 4.4 Local and Remote Module Registry Distribution
- Stage 5: Multi-Environment Orchestration & Workspaces
- 5.1 CLI Workspaces vs Directory-Isolated Stacks
- 5.2 Multi-Account and Multi-Region Architectures
- 5.3 Cross-Stack Data Sharing via
terraform_remote_stateand SSM
- Stage 6: Enterprise Security, Governance & Automated CI/CD
- 6.1 Policy as Code: Open Policy Agent (OPA/Rego) & Sentinel
- 6.2 Static Security Scanning: Checkov, tfsec, and Trivy
- 6.3 Secure Secret Injection: HashiCorp Vault & AWS KMS
- 6.4 GitOps Pipeline Automation with Atlantis and GitHub Actions
- Stage 7: Staff SRE & Cloud Architect Interview Handbook
- 7.1 Real-World High-Stakes Production Scenarios
- 7.2 25 Exhaustive Technical Interview Questions & Answers
- Community, Contributing & Support
Infrastructure as Code (IaC) is divided into two contrasting paradigms:
- Imperative (Procedural): You specify how to achieve a state through a sequence of commands (e.g., AWS CLI, Bash scripts, Python SDKs). If an imperative script fails halfway through, re-running it may attempt to duplicate resources, creating non-deterministic errors.
- Declarative: You declare what the desired end state must look like. Terraform calculates the differential between current reality and desired state, computing an execution graph to reconcile divergences.
flowchart TD
subgraph Declarative["Declarative Model (Terraform)"]
A["Desired Configuration (*.tf)"] --> C["Terraform Engine (Diff Calculation)"]
B["Recorded State (terraform.tfstate)"] --> C
C --> D["Target Cloud (AWS, Azure, GCP)"]
end
subgraph Imperative["Imperative Model (Scripts)"]
E["Script Command 1: Create VPC"] --> F["Script Command 2: Create Subnet"]
F --> G["Script Command 3: Attach IGW (Fail Point)"]
end
| Dimension | Declarative (Terraform) | Imperative (Bash / AWS CLI / Python) |
|---|---|---|
| State Tracking | Native via terraform.tfstate |
Custom manual database or none |
| Idempotency | Guaranteed by reconciliation engine | Requires manual defensive coding |
| Dependency Graph | Automatic DAG compilation | Hardcoded sequential ordering |
| Drift Detection | Native (terraform refresh / plan) |
Custom audit cron jobs |
Terraform is architected as an extensible, decoupled binary:
- Terraform Core: Written in Go, handles HCL parsing, configuration file merging, state management, and building the execution graph.
- Terraform Plugins (Providers): Separate Go binaries that communicate with Terraform Core over high-performance RPC/gRPC sockets using the HashiCorp
go-pluginsystem.
flowchart LR
A["HCL Configuration Files"] --> B["Terraform Core Engine"]
C["terraform.tfstate"] <--> B
B <--"gRPC (Unix Domain Socket / Named Pipe)"--> D["AWS Provider Plugin Binary"]
B <--"gRPC"--> E["Kubernetes Provider Plugin Binary"]
D --> F["AWS Cloud API (HTTPS/REST)"]
E --> G["K8s API Server (HTTPS/REST)"]
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.50.0"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Environment = "Production"
ManagedBy = "Terraform"
}
}
}| Line / Block | Syntax Structure | Architectural Purpose |
|---|---|---|
terraform { ... } |
Core meta-block | Establishes engine and provider requirements |
required_version |
SemVer constraint | Enforces minimum Terraform binary execution version |
required_providers |
Provider mapping block | Instructs Core where to fetch plugin binaries from registry |
source = "hashicorp/aws" |
Hostname/Namespace/Type | Fully Qualified Provider Name (FQPN) pointing to registry.terraform.io |
version = "~> 5.50.0" |
Pessimistic constraint | Allows updates up to < 6.0.0 while locking major release |
provider "aws" { ... } |
Provider instantiation | Configures authentication credentials, endpoints, and regions |
default_tags { ... } |
Provider default feature | Injects standard enterprise governance tags across all child resources |
Terraform constructs a Directed Acyclic Graph (DAG) of all resources declared in your .tf files. Nodes represent resources, data sources, and provider instances; edges represent implicit or explicit dependencies.
graph TD
VPC["aws_vpc.primary"] --> IGW["aws_internet_gateway.gw"]
VPC --> SUB["aws_subnet.public_a"]
IGW --> RT["aws_route_table.public"]
SUB --> RT
RT --> EC2["aws_instance.web_server"]
Terraform traverses this graph concurrently:
- Independent nodes are provisioned in parallel (governed by
-parallelism=10default). - Dependent nodes wait for parent resolution.
- Cycles (e.g., Node A depends on Node B, which depends on Node A) trigger fatal graph cycle errors.
Every Terraform workflow progresses through 4 core phases:
sequenceDiagram
autonumber
actor Engineer as DevOps Engineer
participant Core as Terraform Core
participant Reg as Terraform Registry
participant State as Remote State (S3)
participant Cloud as Cloud Provider API
Engineer->>Core: terraform init
Core->>Reg: Download provider binaries (.terraform/providers)
Core->>State: Initialize backend storage & check lock table
Engineer->>Core: terraform plan
Core->>State: Fetch latest state
Core->>Cloud: Refresh current resource attributes (Read APIs)
Core->>Engineer: Output speculative diff (+ create, ~ update, - destroy)
Engineer->>Core: terraform apply
Core->>State: Acquire DynamoDB mutex lock
Core->>Cloud: Send create/modify/delete API calls
Core->>State: Write updated attributes & release lock
Engineer->>Core: terraform destroy
Core->>Cloud: Reverse graph traversal to delete resources
Core->>State: Update state to empty resources
# Initialize working directory, download provider plugins, configure backend
terraform init -upgrade
# Validate HCL syntax and semantic correctness without cloud API calls
terraform validate
# Generate speculative execution plan and persist to disk for immutable execution
terraform plan -out=tfplan.binary -var="environment=production"
# Apply the pre-compiled plan atomically
terraform apply tfplan.binary
# Destroy all managed infrastructure in safe reverse dependency order
terraform destroy -auto-approve| Parameter / Flag | Purpose | Operational Best Practice |
|---|---|---|
-upgrade |
Checks registry for newer allowed provider versions | Use during scheduled maintenance sprints |
validate |
Validates attribute types and block schemas | Run in pre-commit hooks and CI linters |
-out=tfplan.binary |
Locks speculative plan into binary artifact | Mandatory in enterprise CI/CD to prevent drift between plan and apply |
-auto-approve |
Skips interactive yes/no terminal prompt |
Strictly reserved for automated CI/CD worker agents |
In August 2023, HashiCorp announced a license shift from open-source Mozilla Public License v2.0 (MPL 2.0) to the Business Source License v1.1 (BSL 1.1). In response, the cloud-native ecosystem formed OpenTofu, an open-source, community-governed fork under the Linux Foundation.
flowchart TD
TFOrigin["Terraform (MPL 2.0 Open Source)"] --> TF15["Terraform 1.5.7 (Last MPL Release)"]
TF15 --> TFBSL["HashiCorp Terraform 1.6+ (BSL License)"]
TF15 --> OpenTofu["OpenTofu 1.6+ (Linux Foundation - MPL 2.0)"]
TFBSL --> TFCloud["HashiCorp Cloud Platform (HCP)"]
OpenTofu --> TofuRegistry["OpenTofu Open Registry"]
| Feature / Dimension | HashiCorp Terraform | OpenTofu (Linux Foundation) |
|---|---|---|
| Licensing | Business Source License (BSL 1.1) | Mozilla Public License (MPL 2.0) |
| Governance | HashiCorp / IBM Corporate Steering | Open Governance under Linux Foundation |
| Registry | registry.terraform.io | get.opentofu.org (Decentralized fallback) |
| State Encryption | HCP Enterprise feature | Native Client-Side State Encryption (tofu) |
| CLI Compatibility | terraform <command> |
Drop-in replacement tofu <command> |
# 1. Install OpenTofu via package manager
# (Linux/macOS)
brew install opentofu
# 2. Verify drop-in CLI compatibility in existing repository
tofu init
# 3. Inspect existing state file without modifying format
tofu plan
# 4. Leverage native client-side state encryption in OpenTofu
# encryption.tofu
tofu applyIntroduced in Terraform 1.6, terraform test provides native integration and unit testing for modules without requiring external Go or Python test runners.
# tests/vpc_validation.tftest.hcl
run "verify_vpc_cidr_and_subnets" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
}
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR block does not match the configured variable input."
}
assert {
condition = length(aws_subnet.public) == 2
error_message = "Expected exactly 2 public subnets to be provisioned."
}
}| Test Attribute | Technical Purpose |
|---|---|
run "test_name" |
Defines an isolated test block with execution scope |
command = plan |
Specifies speculative test (no real cloud resources provisioned) |
command = apply |
Provisions ephemeral sandbox resources, validates assertions, and runs teardown |
assert { condition = ... } |
Boolean expression that must evaluate to true for the test step to pass |
Terraform variable declarations define clear contracts for root and child modules. Modern HCL2 supports complex custom validation blocks and data masking.
variable "environment" {
description = "Target deployment environment"
type = string
default = "staging"
validation {
condition = contains(["development", "staging", "production"], var.environment)
error_message = "Allowed values for environment are: development, staging, production."
}
}
variable "database_password" {
description = "Master password for primary database cluster"
type = string
sensitive = true
validation {
condition = length(var.database_password) >= 16
error_message = "Database password must be at least 16 characters in length."
}
}| Attribute | Significance |
|---|---|
type |
Enforces static type checking during compilation (string, number, bool, list, map, object) |
sensitive = true |
Redacts value from CLI terminal output, plan diffs, and console logs (Note: value remains plaintext in .tfstate) |
validation { ... } |
Custom expression evaluated during plan time before any cloud calls |
error_message |
User-friendly diagnostic message returned if validation expression evaluates to false |
HCL2 permits nested object typing for strict schema enforcement across large engineering teams:
variable "cluster_config" {
description = "Kubernetes node pool configuration schema"
type = object({
cluster_name = string
node_count = number
instance_type = string
zones = list(string)
auto_scaling = object({
min_nodes = number
max_nodes = number
})
tags = optional(map(string), {})
})
}Modern HCL2 offers functional metaprogramming expressions:
# Input list of objects
variable "users" {
type = list(object({
username = string
role = string
active = bool
}))
}
# For loop filtering active admins
locals {
admin_usernames = [
for u in var.users : u.username if u.role == "admin" && u.active
]
# Map transformation
user_role_map = {
for u in var.users : u.username => u.role
}
}
# Splat extraction of all subnet IDs
output "subnet_ids" {
value = aws_subnet.public[*].id
}Dynamic blocks generate repeated nested blocks programmatically without duplicating HCL code:
variable "ingress_rules" {
type = list(object({
port = number
description = string
protocol = string
cidr_blocks = list(string)
}))
default = [
{ port = 80, description = "HTTP Ingress", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
{ port = 443, description = "HTTPS Ingress", protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
]
}
resource "aws_security_group" "web" {
name = "production-web-sg"
description = "Security group for web tier"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = var.ingress_rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}| Element | Mechanics |
|---|---|
dynamic "ingress" |
Declares that nested ingress blocks will be generated dynamically |
for_each |
Iterates over the collection provided in var.ingress_rules |
content { ... } |
Body of the generated nested block |
ingress.value |
Iterator referencing the current item in the iteration |
HCL2 provides pure, side-effect-free built-in functions:
locals {
raw_subnets = [["10.0.1.0/24", "10.0.2.0/24"], ["10.0.3.0/24"]]
# Flatten nested lists
flat_subnets = flatten(local.raw_subnets)
# Merge multiple maps
combined_tags = merge(
{ Owner = "DevOps" },
{ Project = "Nebula" },
var.extra_tags
)
# Safely parse JSON or handle failure with can()
raw_config = "{\"version\": 2}"
parsed_version = can(jsondecode(local.raw_config)) ? jsondecode(local.raw_config).version : 1
# Compute cryptographic SHA256 hash of Lambda archive
code_hash = filesha256("${path.module}/dist/app.zip")
}Terraform's state file (terraform.tfstate) is an internal JSON document mapping real-world cloud resources to declared HCL blocks.
flowchart LR
A["Resource in HCL<br/>aws_s3_bucket.data"] <--> B["terraform.tfstate<br/>(Resource Metadata & IDs)"]
B <--> C["Cloud Reality<br/>AWS S3 arn:aws:s3:::data-prod"]
Key attributes tracked in terraform.tfstate:
serial: Incrementing integer tracking schema updates.lineage: Unique UUID identifying the state file life cycle.resources: Array of objects detailing provider, type, instances, attributes, and dependencies.
In multi-engineer enterprise environments, storing state locally leads to catastrophic race conditions. A distributed backend with S3 and DynamoDB ensures state persistence, encryption, versioning, and exclusive mutex locking.
sequenceDiagram
autonumber
actor Alice as Alice (CI Runner 1)
actor Bob as Bob (CI Runner 2)
participant Dynamo as DynamoDB (Mutex Lock Table)
participant S3 as S3 (Versioned State Bucket)
Alice->>Dynamo: PutItem (LockID: terraform-prod/state)
Note over Dynamo: Lock Acquired by Alice
Alice->>S3: Read terraform.tfstate
Bob->>Dynamo: PutItem (LockID: terraform-prod/state)
Dynamo-->>Bob: ConditionalCheckFailedException (Lock Held by Alice)
Note over Bob: Execution Aborted (Lock Active)
Alice->>S3: Write updated terraform.tfstate
Alice->>Dynamo: DeleteItem (LockID: terraform-prod/state)
Note over Dynamo: Lock Released
terraform {
backend "s3" {
bucket = "acme-corp-terraform-state-prod"
key = "infrastructure/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "acme-corp-terraform-locks"
}
}| Parameter | Operational Best Practice |
|---|---|
bucket |
Must have S3 Object Versioning enabled and public access blocked |
key |
Unique hierarchical path separating micro-stacks |
encrypt = true |
Enforces AWS server-side encryption (AES-256 or KMS) at rest |
dynamodb_table |
DynamoDB table with Primary Key named LockID (String) |
Manipulating state via CLI is an essential skill when refactoring without destroying production resources:
# List all resources tracked in active state
terraform state list
# Show detailed attributes of a single tracked resource
terraform state show aws_instance.web_server
# Rename a resource address without recreating the real cloud resource
terraform state mv aws_instance.old_name aws_instance.new_name
# Move resource into a child module
terraform state mv aws_security_group.web module.network.aws_security_group.web
# Remove resource from state tracking without deleting cloud resource
terraform state rm aws_s3_bucket.legacy_logsHistorically, importing existing infrastructure required running terraform import <address> <id> followed by manually writing the matching HCL block. Terraform 1.5+ introduced declarative import blocks with automated code generation.
# Declarative import block
import {
to = aws_security_group.database
id = "sg-0123456789abcdef0"
}
# Auto-generate HCL code during plan
# terraform plan -generate-config-out=generated_sg.tf| Step | Action | Command / Code |
|---|---|---|
| 1 | Declare import target | import { to = aws_vpc.main; id = "vpc-0a1b2c3d" } |
| 2 | Speculative Generation | terraform plan -generate-config-out=generated_vpc.tf |
| 3 | Review Generated Code | Inspect generated_vpc.tf for formatting and variables |
| 4 | Atomic Apply | terraform apply to register resource into state cleanly |
Modules encapsulate infrastructure patterns into reusable components. A well-designed module follows the Unix philosophy: do one thing and do it well.
graph TD
Root["Root Module (environments/production)"] --> VPCMod["module.vpc (v3.2.0)"]
Root --> DBMod["module.aurora_postgres (v2.1.0)"]
Root --> EKSMod["module.eks_cluster (v5.0.1)"]
VPCMod --> AWSVPC["AWS VPC, Subnets, Gateways"]
DBMod --> AWSRDS["AWS RDS Cluster & Parameter Groups"]
EKSMod --> AWSEKS["AWS EKS Control Plane & Node Groups"]
- Never hardcode provider blocks inside reusable child modules: The root module must provide and configure providers.
- Never hardcode backend blocks inside child modules: Backends belong strictly in root orchestrators.
- Always output the core IDs and ARNs: Enable downstream composition.
Standard module directory structure:
modules/aws-vpc/
├── README.md # Documentation generated via terraform-docs
├── main.tf # Core resource declarations
├── variables.tf # Input variable definitions with types & descriptions
├── outputs.tf # Output values with clear descriptions
├── versions.tf # Provider and Terraform version constraints
└── tests/ # Native Terraform test files (run via terraform test)
output "vpc_id" {
description = "The ID of the provisioned VPC"
value = aws_vpc.this.id
}
output "private_subnet_ids" {
description = "List of IDs of private subnets"
value = aws_subnet.private[*].id
}
output "database_subnet_group_name" {
description = "Name of database subnet group for RDS integration"
value = aws_db_subnet_group.this.name
}As enterprise infrastructure scales across dozens of accounts and environments, vanilla Terraform can lead to repetitive backend and provider declarations. Terragrunt acts as a thin wrapper that keeps HCL DRY (Don't Repeat Yourself).
flowchart TD
RootTG["terragrunt.hcl (Root: Remote Backend & Provider Gen)"] --> DevEnv["dev/vpc/terragrunt.hcl"]
RootTG --> StageEnv["stage/vpc/terragrunt.hcl"]
RootTG --> ProdEnv["prod/vpc/terragrunt.hcl"]
ProdEnv --> RemoteModule["Git: terraform-aws-modules/vpc.git?ref=v5.0.0"]
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "acme-terragrunt-state-${get_aws_account_id()}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "acme-terragrunt-locks"
}
}Modules can be sourced from various repositories:
# Sourcing from local directory
module "networking" {
source = "../../modules/networking"
}
# Sourcing from Terraform Public Registry with SemVer pin
module "security_group" {
source = "terraform-aws-modules/security-group/aws"
version = "~> 5.1.0"
}
# Sourcing from Private Git Repository over SSH with Git Tag pin
module "billing_engine" {
source = "git::git@github.com:acme-corp/infra-modules.git//payment-pipeline?ref=v2.4.1"
}There are two primary paradigms for managing multiple environments (Dev, Stage, Prod):
flowchart TD
subgraph Workspaces["Terraform Workspaces (Shared Codebase)"]
W1["Default Workspace"] --> S1["env: default"]
W2["Workspace: dev"] --> S2["env: dev"]
W3["Workspace: prod"] --> S3["env: prod"]
end
subgraph DirectoryIsolation["Directory Isolation (Recommended for Prod)"]
D1["environments/dev/*.tf"] --> St1["Dev State & AWS Account A"]
D2["environments/stage/*.tf"] --> St2["Stage State & AWS Account B"]
D3["environments/prod/*.tf"] --> St3["Prod State & AWS Account C"]
end
| Dimension | CLI Workspaces (terraform workspace) |
Directory-Isolated Stacks |
|---|---|---|
| State Storage | Single bucket under env:/<workspace_name>/ |
Completely separate S3 buckets & DynamoDB tables |
| Blast Radius | High (Code changes affect all workspaces) | Low (Each environment config is independently versioned) |
| Access Control | Hard to enforce per-environment IAM | Native AWS IAM per bucket/directory |
| Best Use Case | Ephemeral feature branches / testing | Long-lived environments (Dev, QA, Staging, Prod) |
Production cloud systems distribute workloads across isolated AWS accounts (e.g., Security, Shared Services, Dev, Prod) and multiple geographic regions.
# Default provider for Primary Region (US East)
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
# Aliased provider for Disaster Recovery Region (US West)
provider "aws" {
alias = "us_west_2"
region = "us-west-2"
}
# Resource in primary region
resource "aws_s3_bucket" "primary" {
provider = aws.us_east_1
bucket = "acme-corp-media-primary"
}
# Replica in secondary region
resource "aws_s3_bucket" "replica" {
provider = aws.us_west_2
bucket = "acme-corp-media-replica"
}To prevent monolithic blast radiuses, infrastructure should be decoupled into smaller stacks:
- Base Network Stack (VPC, Subnets, NAT Gateways)
- Data Stack (RDS, Redis, DynamoDB)
- Compute Stack (EKS, ECS, Lambda)
flowchart LR
Net["Network Stack (VPC)"] -->|"Writes SSM Parameter /vpc/id"| SSM["AWS SSM Parameter Store"]
SSM -->|"Reads Parameter via data source"| Compute["Compute Stack (EKS)"]
Net -.->|"Alternative: data.terraform_remote_state"| Compute
# Read remote state from networking stack
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "acme-corp-terraform-state-prod"
key = "infrastructure/vpc/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_security_group" "app" {
name = "app-server-sg"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
}Policy as Code enforces security guardrails before any cloud resource is modified or provisioned.
flowchart LR
Plan["terraform plan -out=tfplan.binary"] --> Show["terraform show -json tfplan.binary > plan.json"]
Show --> OPA["OPA / Conftest Engine (policy.rego)"]
OPA -->|Pass| Apply["terraform apply"]
OPA -->|Violations Detected| Block["Pipeline Failed (Merge Blocked)"]
package terraform.security
default allow = false
# Deny any unencrypted S3 bucket creation
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.server_side_encryption_configuration
msg := sprintf("Security Violation: S3 bucket '%v' must have server-side encryption enabled.", [resource.address])
}
allow {
count(deny) == 0
}Pre-commit and CI scans detect misconfigurations early in the development cycle:
# Scan repository using Checkov for compliance violations (CIS, HIPAA, PCI-DSS)
checkov -d . --framework terraform
# Fast, lightweight static security analysis with Trivy
trivy config ./infrastructure
# Lint HCL syntax, deprecated features, and unused variables with TFLint
tflint --init && tflint| Security Linter | Specialization | Integration Point |
|---|---|---|
| Checkov | Deep policy inspection across AWS/GCP/Azure compliance frameworks | CI pull request gating |
| TFLint | Catches cloud provider-specific errors (invalid instance types) | Pre-commit hook & local IDE |
| Trivy | Unified scanner for IaC, Docker images, and SBOM licenses | Containerized CI pipeline runner |
Never store secrets, database passwords, or private API keys directly in .tf code or variable files.
sequenceDiagram
autonumber
participant TF as Terraform Runner
participant Vault as HashiCorp Vault / AWS KMS
participant DB as AWS RDS Instance
TF->>Vault: Authenticate with AppRole / IAM Role
Vault-->>TF: Issue short-lived dynamic DB credentials
TF->>DB: Provision database with generated dynamic credentials
Note over TF,DB: Zero hardcoded secrets in version control
# Fetch dynamic database password from HashiCorp Vault
data "vault_generic_secret" "db_creds" {
path = "secret/data/production/database"
}
resource "aws_db_instance" "prod" {
identifier = "production-postgres"
engine = "postgres"
instance_class = "db.r6g.xlarge"
username = "dbadmin"
password = data.vault_generic_secret.db_creds.data["password"]
skip_final_snapshot = false
}Enterprise Terraform requires automated pull-request execution with full auditability.
sequenceDiagram
autonumber
actor Dev as Infrastructure Engineer
participant PR as GitHub Pull Request
participant Atl as Atlantis Server
participant AWS as AWS Cloud Environment
Dev->>PR: Open Pull Request with HCL changes
PR->>Atl: Webhook Trigger
Atl->>AWS: Run terraform plan
Atl->>PR: Post comment with plan diff
Dev->>PR: Comment: "atlantis apply"
PR->>Atl: Webhook Trigger
Atl->>AWS: Run terraform apply
Atl->>PR: Merge PR & comment success output
- Incident: An engineer changes an internal ingress security group rule. Because the VPC, subnets, database, and Kubernetes clusters are in one monolithic root
.tfstate, the plan detects an inadvertent parameter group change, attempting to recreate the production Aurora cluster. - Root Cause: Monolithic state coupling.
- Mitigation & Resolution: Immediately cancel apply. Decouple the monolith using
terraform state mvto separate network, data, and compute into distinct remote state keys.
- Incident: S3 versioned state becomes corrupted due to a partial network disconnect while releasing a DynamoDB lock.
- Remediation:
- Inspect DynamoDB locks:
aws dynamodb scan --table-name acme-corp-terraform-locks. - If locked by dead runner, release safely:
terraform force-unlock <LOCK_ID>. - Revert state to previous clean S3 object version:
aws s3api copy-object --copy-source bucket/state?versionId=<CLEAN_VER> --bucket bucket --key state. - Run
terraform refreshto reconcile real cloud resource attributes without executing changes.
- Inspect DynamoDB locks:
Answer:
terraform plan: Generates a speculative execution plan. It refreshes state against real cloud infrastructure, evaluates HCL configuration changes, and computes the delta needed to bring reality in line with configuration.terraform apply -refresh-only: Only inspects the real-world infrastructure and updates the state file to reflect external changes (drift) without proposing or applying any modifications to actual cloud resources.
Answer: During graph compilation, Terraform parses all resource blocks and inspects references. When Resource B accesses an exported attribute of Resource A (e.g.,
vpc_id = aws_vpc.primary.id), an implicit dependency edge is created ($A o B$ ). Terraform uses topological sorting (Kahn's algorithm or DFS) to determine node execution order. Nodes without incoming edges are scheduled concurrently up to the-parallelismlimit. Explicit dependencies can be enforced usingdepends_on = [aws_vpc.primary].
Q3: Why does count cause unexpected destructions when removing an item from the middle of a list, and how does for_each solve this?
Answer:
countkeys resources by an array integer index (aws_subnet.public[0],aws_subnet.public[1],aws_subnet.public[2]). Ifsubnets[1]is removed from the input list, the item previously at index 2 shifts to index 1. Terraform detects thatsubnets[1]changed its CIDR block andsubnets[2]no longer exists, causing it to destroy and recreate resources. In contrast,for_eachkeys resources by a stable unique map string key or set value (aws_subnet.public["us-east-1a"]). Removing an item only deletes that specific keyed resource, leaving all other instances untouched.
Answer: Circular dependencies occur when Resource A requires an attribute from Resource B, which simultaneously requires an attribute from Resource A. Examples include Security Group A allowing ingress from Security Group B and vice-versa. To resolve this, decompose the mutual dependency into an independent standalone resource. Instead of defining inline
ingressblocks insideaws_security_group, declare separateaws_security_group_ruleresources, breaking the dependency cycle in the DAG.
Answer:
- Reads the backend block and configures state storage (validating S3 bucket access, DynamoDB lock table existence).
- Inspects
required_providersblocks and searches the local cache directory (.terraform/providers).- Queries the Terraform Registry API to resolve SemVer constraints, downloads provider plugin binaries, verifies their SHA-256 cryptographic signatures against the
.terraform.lock.hcldependency lock file, and places the binaries in the working directory.- Downloads referenced child modules (local, Git, or Registry) into
.terraform/modules.
Answer: Introduced in Terraform 0.14, the dependency lock file pins the exact provider versions and cryptographic checksums (hashes) for all providers used in the configuration. It guarantees deterministic builds across different developer laptops and CI/CD agents, preventing supply chain attacks or accidental breaking changes caused by unpinned upstream provider releases. It must always be committed to version control.
Answer: Setting
sensitive = trueprevents Terraform from displaying the value in CLI stdout, plan outputs, and logs. However, the sensitive value is still stored in plaintext in theterraform.tfstatefile. Therefore, protecting state storage (via S3 SSE-KMS encryption, strict IAM bucket policies, and access auditing) is mandatory.
Answer: State drift occurs when real cloud resources are modified outside of Terraform (via the Cloud Console, direct API calls, or emergency hotfixes). Strategies:
- Overwrite Drift: Run
terraform applyto overwrite external changes and force cloud reality back to the declared HCL configuration.- Adopt Drift: Update the
.tfconfiguration files to match the new out-of-band attributes, then runterraform apply(which results in a 0-change plan).- Refresh Only: Run
terraform apply -refresh-onlyto update the state file to match reality without changing configuration or infrastructure.
Answer:
terraform taintwas the legacy command that marked a resource in the state file as degraded, forcing its destruction and recreation on the next apply. Since Terraform 0.15.2,terraform taintis deprecated in favor of the non-mutating CLI optionterraform apply -replace="aws_instance.web", which marks the resource for replacement during the speculative plan phase without directly altering the state file beforehand.
Answer:
- Use automated discovery tools (e.g.,
terraformerorformer2) to inspect cloud resources and generate HCL declarations and state mappings.- Use Terraform 1.5+ declarative
importblocks:import { to = aws_s3_bucket.legacy[each.key] for_each = toset(["bucket-a", "bucket-b"]) id = each.key }- Execute
terraform plan -generate-config-out=generated.tfto let Terraform construct the matching resource configurations.- Validate and sanitize generated code, then run
terraform apply.
Answer:
lifecycle { prevent_destroy = true }is a safety mechanism enforced by Terraform Core during plan time. If an execution plan would result in the destruction of that resource (either via explicit deletion, or recreation caused by changing an immutable attribute), Terraform rejects the plan with an immediate error. Note: it does not prevent destruction ifterraform destroyis explicitly run on the entire stack.
Answer: By default, when a resource requires replacement, Terraform destroys the old resource first and then creates the replacement. With
lifecycle { create_before_destroy = true }, Terraform reverses this order: it provisions the new resource, switches dependencies over, and then tears down the old resource, achieving zero-downtime updates. Pitfall: Resources with unique naming constraints (e.g., an S3 bucket or IAM role with a hardcoded static name) will fail because the new resource cannot be created while the old resource still holds that unique name.
Answer: Direct database schema migrations should never be managed via Terraform (use specialized tools like Flyway, Liquibase, or Prisma). For infrastructure-level database updates (e.g., upgrading RDS instance classes or engine versions):
- Use Multi-AZ deployments so AWS applies upgrades to the standby replica first, triggers a graceful failover (<60s), and then upgrades the former primary.
- Set
apply_immediately = falseso updates occur during scheduled maintenance windows, preventing unexpected mid-day downtime.
Q14: What is the difference between an input variable default, an environment variable TF_VAR_name, and a .tfvars file?
Answer: Terraform resolves variable values using a strict order of precedence (highest to lowest):
- CLI flags:
-varor-var-file*.auto.tfvarsor*.auto.tfvars.json(alphabetical order)terraform.tfvars.jsonterraform.tfvars- Environment variables:
TF_VAR_<variable_name>- Default value defined inside the
variabledeclaration block.
Q15: When should you use terraform_remote_state versus AWS Systems Manager (SSM) Parameter Store for cross-stack references?
Answer:
terraform_remote_state: Tight coupling. Requires read access to the upstream stack's state file. Exposes all outputs and potentially exposes sensitive state metadata.- SSM Parameter Store / AWS Secrets Manager (Recommended): Loose coupling. The upstream stack publishes output values as standard SSM parameters (
/network/vpc_id). Downstream stacks read these viadata "aws_ssm_parameter"data sources. This enforces least-privilege IAM separation and decouples team deployment lifecycles.
Answer: Enable verbose debugging logs by setting environment variables:
TF_LOG=DEBUG(orTRACE,INFO,WARN,ERROR)TF_LOG_PATH=/path/to/terraform.logTRACEoutputs the exact JSON-RPC / gRPC payloads sent between Terraform Core and the provider binary, HTTP requests/responses to cloud endpoints, and detailed graph resolution decisions.
Answer: The state file contains a complete architectural map of your cloud estate, including resource IDs, network topologies, IAM role definitions, and any variables or attributes passed to resources—even those marked
sensitive = true. If unencrypted, compromised read access allows an attacker to discover database endpoints, VPC CIDRs, and secret API keys in plaintext.
Answer:
- Unit / Syntax Testing:
terraform validateandtflint.- Static Security Testing:
checkovandtrivy.- Native Integration Testing (Terraform 1.6+): The
terraform testcommand runs.tftest.hclfiles, executing synthetic plans and applies against sandboxed environments with custom assertion blocks (assert { condition = ... }).- End-to-End Infrastructure Testing: Terratest (Go framework) provisions real cloud infrastructure, sends HTTP/SSH requests to verify functionality, and executes
t.Cleanup(terraform.Destroy).
Answer:
merge(map1, map2, ...): Combines multiple maps into a single map. If keys overlap, values from the rightmost map overwrite earlier ones.coalesce(val1, val2, ...): Takes any number of arguments and returns the first value that is not null and not an empty string"".
Answer: The Provider Protocol defines the gRPC interface between Terraform Core and provider plugins. Version 5 was based on the legacy SDKv2. Version 6 powers the modern Terraform Plugin Framework, supporting structured type systems, dynamic object attributes, nested attributes with nullability, and significantly faster serialization speeds.
Answer: Integrate Infracost into the CI/CD pipeline. Infracost parses the speculative
terraform planJSON output, queries cloud pricing APIs, and posts a comment on the GitHub pull request showing the exact monthly delta cost (e.g.,+$142.50/mo) before merging.
Answer:
terraform state rm: Deletes the resource tracking entry fromterraform.tfstate. The real cloud resource remains alive and untouched in AWS/GCP, but Terraform ceases to manage it.terraform destroy(or deleting the block and runningapply): Sends a delete API call to the cloud provider, permanently terminating the real-world infrastructure.
Q23: How do you prevent race conditions when multiple CI/CD runners trigger on simultaneous Git pushes?
Answer: Enable distributed state locking (e.g., DynamoDB table for S3 backend, or native Consul locking). When Runner A initiates a plan/apply, it writes an exclusive lock record containing its execution ID. When Runner B attempts execution, it fails to acquire the lock and halts immediately with exit code 1. Additionally, set GitHub Actions
concurrencygroups to cancel in-flight pipelines when new commits are pushed.
Answer:
movedblocks allow module authors to refactor resource addresses without requiring end users to manually run CLI commands liketerraform state mv:moved { from = aws_instance.web to = aws_instance.frontend_server }During
terraform plan, Terraform recognizes the declaration rename and migrates the state mapping internally without proposing destruction and recreation.
Answer:
- Authenticate CI/CD agents using OIDC (OpenID Connect) federated identity with AWS/GCP/Azure instead of long-lived static API credentials.
- Enforce immutable GitOps approvals via Atlantis or GitHub Actions environments.
- Gate deployments using Policy as Code (OPA/Rego) to enforce encryption, private networking, and tagging.
- Keep state files strictly encrypted with customer-managed KMS keys and audited via CloudTrail.
- Continuously audit configuration drift with scheduled read-only cron pipelines.
We welcome contributions from cloud engineers worldwide! Please review our contribution guidelines before submitting pull requests.
# 1. Clone your fork
git clone https://github.com/manthanank/learn-terraform.git
# 2. Run automated validation and test suite
node --test tests/terraform.test.mjs
# 3. Format HCL code
terraform fmt -recursive- Contributing Guidelines: CONTRIBUTING.md
- Code of Conduct: CODE_OF_CONDUCT.md
- License: MIT License