Infrastructure as Code from HCL basics to modules, remote state, workspaces, Sentinel policies, and production-grade patterns.
12
Chapters
60+
Examples
100%
Free
01🏗️
Introduction to Terraform
Infrastructure as Code Fundamentals
Terraform is an open-source Infrastructure as Code tool by HashiCorp. It lets you define cloud resources in declarative configuration files, version-control them, and deploy entire environments reproducibly.
Why Terraform?
📝
Declarative
Define WHAT you want, not HOW. Terraform figures out the execution order and dependencies.
☁️
Multi-Cloud
Works with AWS, Azure, GCP, Kubernetes, Docker, and 3000+ providers from a single tool.
📊
Plan Before Apply
terraform plan shows exactly what will change before you touch real infrastructure.
🔄
Idempotent
Running the same code twice produces the same result. Safe to re-run at any time.
Core Terraform Workflow
$ terraform initDownload providers and initialize the working directory
$ terraform planPreview changes without modifying anything
$ terraform applyCreate or update infrastructure to match your code
$ terraform destroyTear down all resources managed by this configuration
HCL — HashiCorp Configuration Language
HCL# main.tf — Create a web server
resource "aws_instance" "web_app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-app-server"
Environment = "staging"
Team = "platform"
}
}
💡 Key Concept
Terraform stores a record of every resource it creates in a state file. This is how it knows what exists, what changed, and what to destroy.
02🔌
Providers & Versioning
Connect to Cloud Platforms
Providers are plugins that let Terraform talk to APIs — AWS, Azure, GCP, Kubernetes, GitHub, and thousands more. Pinning versions prevents unexpected upgrades from breaking your infrastructure.
Provider Configuration
HCLterraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.80"
}
}
}
provider "aws" {
region = "ap-south-1"
}
# Multiple regions using alias
provider "aws" {
alias = "singapore"
region = "ap-southeast-1"
}
Version Constraints
Constraint
Meaning
Example
= 5.0.0
Exact version only
Strictest
~> 5.0
Allow 5.x but not 6.0
Recommended
>= 5.0, < 6.0
Range
Flexible within major
Multi-Region with Alias
HCL# Deploy a load balancer in Mumbai
resource "aws_lb" "primary" {
provider = aws
name = "primary-lb"
}
# Deploy a replica in Singapore
resource "aws_lb" "replica" {
provider = aws.singapore
name = "replica-lb"
}
💡 Lock File
.terraform.lock.hcl records the exact provider versions used. Commit this file to Git so your team uses identical versions. Delete and re-init only when you intentionally want to upgrade.
⚠️ Aliased Providers in Modules
Aliased providers are NOT inherited automatically by child modules. You must explicitly pass them using the providers argument in the module block.
03💾
State Management
How Terraform Tracks Your Infrastructure
The state file (terraform.tfstate) is Terraform's memory. It maps your HCL code to real cloud resources, stores attributes like IDs and IPs, and determines what needs to change on the next apply.
Desired State vs Current State
📝
Desired State
What your .tf files define — the infrastructure you WANT to exist.
☁️
Current State
What actually exists in the cloud right now, recorded in the state file.
🔄
Reconciliation
terraform plan compares both. If they differ, it generates a change plan.
⚠️
Drift
When someone changes infrastructure manually outside Terraform — the state file becomes stale.
EXAMPLE# You wrote this (Desired State):
resource "aws_instance" "api" {
instance_type = "t3.small"
}
# But someone manually changed it in AWS Console to t3.xlarge
# terraform plan detects the drift:
# ~ instance_type = "t3.xlarge" -> "t3.small"
# terraform apply fixes it back to your code
State Commands
$ terraform state listList all resources in state
$ terraform state show aws_instance.apiShow details of a specific resource
$ terraform state pullDownload remote state to stdout
$ terraform state rm aws_instance.oldRemove resource from state (keeps real resource)
$ terraform state mv aws_instance.old aws_instance.newRename a resource in state
⚠️ Never Edit State Manually
The state file may contain passwords, tokens, and database credentials in plain text. Never commit terraform.tfstate to Git. Use remote backends instead.
04🔤
Variables & Data Types
Parameterize Everything
Variables eliminate hardcoding. Define them in variables.tf, assign values in terraform.tfvars, and your code becomes reusable across dev, staging, and production.
Variable Definition & Usage
HCL# variables.tf
variable "environment" {
type = string
description = "Deployment environment"
default = "dev"
}
variable "app_port" {
type = number
default = 8080
}
variable "allowed_cidrs" {
type = list(string)
default = ["10.0.0.0/8", "172.16.0.0/12"]
}
variable "instance_tags" {
type = map(string)
default = {
team = "platform"
project = "api-gateway"
}
}
Data Types
Type
Example
Access
string
\"t3.micro\"
var.instance_type
number
8080
var.app_port
bool
true
var.enable_monitoring
list
[\"a\",\"b\",\"c\"]
var.zones[0]
map
{dev=\"t3.micro\"}
var.sizes[\"dev\"]
Variable Assignment — Priority Order (Highest to Lowest)
✓-var flag on CLI: terraform apply -var="environment=prod" (WINS)
Variable precedence is one of the most asked Terraform questions. Remember: CLI -var always wins, default value always loses.
05📤
Outputs & References
Share Values Between Resources
Outputs expose resource attributes after apply — useful for displaying IPs, passing values to other modules, or feeding into CI/CD pipelines. Cross-resource references let one resource use another's attributes.
Output Blocks
HCLoutput "api_public_ip" {
description = "Public IP of the API server"
value = aws_instance.api.public_ip
}
output "db_endpoint" {
description = "Database connection string"
value = aws_db_instance.main.endpoint
sensitive = true
}
# After apply, Terraform displays:
# api_public_ip = "52.66.123.45"
# db_endpoint = <sensitive>
Cross-Resource References
HCL# Security Group
resource "aws_security_group" "api_sg" {
name = "api-firewall"
ingress {
from_port = 443
to_port = 443
cidr_blocks = ["0.0.0.0/0"]
}
}
# EC2 references the Security Group
resource "aws_instance" "api" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.api_sg.id]
}
# Elastic IP references the EC2
resource "aws_eip" "api_ip" {
instance = aws_instance.api.id
domain = "vpc"
}
String Interpolation
HCL# Combine dynamic values with strings
resource "aws_security_group_rule" "allow_eip" {
cidr_blocks = ["${aws_eip.api_ip.public_ip}/32"]
}
# In tags
tags = {
Name = "${var.project}-${var.environment}-api"
}
💡 Resource Attributes
After creation, every resource exposes attributes like id, arn, public_ip, endpoint. These are stored in state and usable by other resources. Check provider docs for available attributes.
06🔢
Count & For Each
Create Multiple Resources
The count meta-argument creates multiple copies of a resource. count.index gives each copy a unique number starting from 0. for_each is the modern alternative using maps or sets.
Count with Index
HCL# Create 3 web servers with unique names
resource "aws_instance" "web_fleet" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server-${count.index}"
}
}
# Creates: web-server-0, web-server-1, web-server-2
# Terraform addresses: aws_instance.web_fleet[0], [1], [2]
Count with Variable List
HCLvariable "developer_names" {
type = list(string)
default = ["priya", "rahul", "meena"]
}
resource "aws_iam_user" "devs" {
count = length(var.developer_names)
name = var.developer_names[count.index]
}
# Creates IAM users: priya, rahul, meena
for_each — Better Alternative
HCLvariable "app_configs" {
type = map(object({
instance_type = string
port = number
}))
default = {
api = { instance_type = "t3.small", port = 8080 }
worker = { instance_type = "t3.medium", port = 9090 }
gateway = { instance_type = "t3.micro", port = 443 }
}
}
resource "aws_instance" "apps" {
for_each = var.app_configs
instance_type = each.value.instance_type
ami = "ami-0c55b159cbfafe1f0"
tags = {
Name = "${each.key}-server"
Port = each.value.port
}
}
Feature
count
for_each
Index
Numeric (0,1,2)
Key-based (\"api\",\"worker\")
Remove middle
Shifts indexes — risky
Only affects that key — safe
Best for
Identical resources
Resources with different configs
⚠️ Count Pitfall
If you have count=3 and remove the middle item from a list, resources shift — Terraform may destroy and recreate the wrong ones. Use for_each for named resources.
07⚡
Provisioners
Execute Scripts on Resources
Provisioners run commands on local or remote machines after resource creation. They bridge the gap between creating a VM and configuring it — installing packages, deploying apps, running bootstrap scripts.
HCLresource "aws_instance" "app_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
provisioner "local-exec" {
command = "echo ${self.public_ip} >> inventory.txt"
}
# Runs only when resource is destroyed
provisioner "local-exec" {
when = destroy
command = "echo 'Server destroyed' >> cleanup.log"
}
}
💡 Provisioners vs Config Management
Provisioners are for bootstrap tasks. For complex server configuration, use Ansible, Chef, or cloud-init. HashiCorp recommends minimizing provisioner use.
⚠️ Exam Note
Provisioners are NOT part of the newer HashiCorp Terraform certification exams. But they are heavily used in real-world DevOps work.
08📦
Modules
Reusable Infrastructure Packages
Modules are reusable packages of Terraform code. Instead of copying 200 lines of VPC config into every project, you create a module once and call it with different parameters.
Root Module vs Child Module
🏠
Root Module
Your main working directory where you run terraform apply. The entry point.
📦
Child Module
A module called by another module. Lives in a subdirectory or external source.
🌐
Registry Module
Pre-built modules from Terraform Registry — community or verified by HashiCorp.
HCL# Use a community module from Terraform Registry
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.0"
cluster_name = "my-cluster"
cluster_version = "1.28"
}
# Publishing requires: public GitHub repo,
# naming: terraform-<PROVIDER>-<NAME>,
# and semantic version tags (v1.0.0)
💡 Module Best Practice
Always expose outputs from child modules. The root module can only access child module values through defined outputs — not internal resource attributes directly.
09🗄️
Remote Backends & Locking
Centralized State for Teams
Local state files don't work for teams — if your laptop crashes, the state is gone. Remote backends store state in S3, Azure Blob, or GCS, with locking to prevent concurrent modifications.
terraform.tfstate stores database passwords, API keys, and connection strings in plain text. NEVER commit it to Git. Use remote backends with encryption.
10🔀
Workspaces
Multiple Environments, One Codebase
Workspaces let you maintain separate state files for dev, staging, and production using the same Terraform code. Each workspace gets its own isolated state.
Workspace Commands
$ terraform workspace listShow all workspaces (* marks active)
$ terraform workspace showPrint current workspace name
$ terraform workspace new stagingCreate and switch to staging workspace
$ terraform workspace select prodSwitch to an existing workspace
$ terraform workspace delete stagingDelete a workspace
Using Workspaces in Code
HCL# Adjust resources based on workspace
locals {
instance_sizes = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
}
resource "aws_instance" "api" {
instance_type = local.instance_sizes[terraform.workspace]
ami = "ami-0c55b159cbfafe1f0"
tags = {
Name = "api-${terraform.workspace}"
Environment = terraform.workspace
}
}
💡 Workspace State
Each workspace stores its state in a separate file. In S3 backend, states go to env:/dev/terraform.tfstate, env:/prod/terraform.tfstate, etc.
11🏢
HCP Terraform & Governance
Cloud Platform, Sentinel, Drift Detection
HCP Terraform (formerly Terraform Cloud) is HashiCorp's managed platform for remote execution, team collaboration, policy enforcement, private registries, and infrastructure governance.
HCP Hierarchy
🏢
Organization
Top-level container. Billing, users, and teams live here.
📂
Project
Groups related workspaces. Organizes by team or application.
⚙️
Workspace
Individual Terraform project — stores state, variables, and run history.
Workspace Workflow Types
Workflow
How It Works
Best For
VCS-Driven
Push to GitHub → auto plan/apply
Production teams
CLI-Driven
Run locally, execute remotely
Developers testing
API-Driven
Triggered by automation/CI
Advanced pipelines
Sentinel — Policy as Code
SENTINEL# Deny any EC2 instance without required tags
import "tfplan/v2" as tfplan
mandatory_tags = ["environment", "team", "cost-center"]
all_ec2 = filter tfplan.resource_changes as _, rc {
rc.type is "aws_instance"
}
violations = filter all_ec2 as _, instance {
not all mandatory_tags as tag {
tag in (instance.change.after.tags else {})
}
}
main = rule { length(violations) is 0 }
Sentinel Enforcement Levels
Level
Behavior
Override?
Hard Mandatory
Blocks apply completely
No — cannot override
Soft Mandatory
Blocks by default
Yes — authorized users can override
Advisory
Shows warning only
Always passes
Drift Detection & Health
🔍
Drift Detection
HCP automatically compares actual infrastructure against state. Alerts when someone changes resources manually outside Terraform.
💚
Continuous Validation
Monitors runtime health — checks if websites respond, SSL certificates are valid, endpoints are reachable.
💰
Cost Estimation
Before apply, HCP estimates monthly cost changes. Catches expensive resource launches before they happen.
Terraform Import & Removed Block
HCL# Import: Bring manually-created resources under Terraform management
# Terraform 1.5+ can auto-generate config:
terraform plan -generate-config-out=imported.tf
# Removed Block: Stop managing a resource WITHOUT destroying it
removed {
from = aws_instance.legacy_server
lifecycle {
destroy = false
}
}
# Better than 'terraform state rm' — declarative, Git-friendly, auditable
💡 Removed Block
Modern replacement for terraform state rm. It's declarative (lives in code), trackable in Git, and safe for CI/CD pipelines. The old imperative approach was risky and unauditable.
12💼
Interview Questions
Top Terraform Questions & Answers
The most commonly asked Terraform interview questions — from entry-level to senior DevOps positions.
Core Concepts
❓
What is Terraform State?
A JSON file that maps your HCL code to real infrastructure. It stores resource IDs, attributes, and metadata so Terraform knows what exists.
❓
Desired vs Current State?
Desired = what's in .tf files. Current = what exists in the cloud. terraform plan compares both and shows differences.
❓
What is Terraform Drift?
When actual infrastructure differs from Terraform code because someone changed things manually (AWS Console, CLI, scripts). Detected by terraform plan.
❓
Why not commit tfstate to Git?
State files contain sensitive data (passwords, tokens, database credentials) in plain text. Use remote backends with encryption instead.
Variables & Providers
❓
Variable Precedence?
CLI -var (highest) > -var-file > terraform.tfvars > auto.tfvars > TF_VAR_ env var > default value (lowest).
❓
List vs Map?
List = ordered collection accessed by index [0]. Map = key-value pairs accessed by key [\"prod\"]. Lists for similar items, maps for named configs.
❓
What is Provider Alias?
Allows multiple configurations of the same provider (e.g., AWS in two regions). Used with alias argument. Must be explicitly passed to modules.
❓
What is .terraform.lock.hcl?
Records exact provider versions used. Ensures team consistency. Commit to Git. Different from .tfstate.lock.info which is state locking.
State & Backends
❓
Where store state in AWS?
S3 bucket for storage + DynamoDB table for locking (classic). Terraform 1.10+ supports native S3 locking with use_lockfile = true.
❓
What is State Locking?
Prevents two engineers from running apply simultaneously. DynamoDB creates a lock record. Second user gets "Error acquiring state lock".
❓
terraform state rm vs Removed Block?
state rm is imperative (run manually, not tracked). Removed block is declarative (in code, Git-tracked, CI/CD safe). Use removed block.
❓
terraform_remote_state?
Data source that reads outputs from another Terraform project's state. Enables cross-team resource sharing (e.g., networking → security).
Modules & Advanced
❓
Root vs Child Module?
Root = main directory where you run terraform apply. Child = any module called by another module. Child modules expose values through outputs.
❓
count vs for_each?
count uses numeric indexes (0,1,2) — removing middle item shifts everything. for_each uses keys — removing one item only affects that resource.
❓
What are Workspaces?
Separate state files for different environments using same code. terraform workspace new dev creates isolated dev state.
❓
What is Sentinel?
Policy-as-code framework in HCP Terraform. Enforces rules before apply (e.g., block untagged EC2). Three levels: hard-mandatory, soft-mandatory, advisory.
✓Always pin provider versions with ~> constraint
✓Never commit terraform.tfstate to Git
✓Use remote backend (S3 + DynamoDB) for team collaboration
✓Use for_each over count for named resources
✓Use modules for reusable infrastructure
✓Run terraform plan before every apply
✓Use workspaces or separate state files per environment
✓Always run terraform destroy to avoid surprise cloud charges