← Home☁️ AWS
🏗️ Complete Hands-On Reference

Terraform Complete Guide

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
ConstraintMeaningExample
= 5.0.0Exact version onlyStrictest
~> 5.0Allow 5.x but not 6.0Recommended
>= 5.0, < 6.0RangeFlexible 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
TypeExampleAccess
string\"t3.micro\"var.instance_type
number8080var.app_port
booltruevar.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)
-var-file: terraform apply -var-file="prod.tfvars"
terraform.tfvars or *.auto.tfvars (auto-loaded)
TF_VAR_ environment variables: export TF_VAR_environment=staging
Variable default value in variables.tf (LOWEST)
TFVARS# terraform.tfvars — values per environment environment = "production" app_port = 443 allowed_cidrs = ["10.0.0.0/8"] instance_tags = { team = "sre" project = "api-gateway" }
💡 Interview Favourite

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 } }
Featurecountfor_each
IndexNumeric (0,1,2)Key-based (\"api\",\"worker\")
Remove middleShifts indexes — riskyOnly affects that key — safe
Best forIdentical resourcesResources 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.
Remote Exec — Run Commands on EC2
HCLresource "aws_instance" "app_server" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro" key_name = "deploy-key" connection { type = "ssh" user = "ec2-user" private_key = file("~/.ssh/deploy-key.pem") host = self.public_ip } provisioner "remote-exec" { inline = [ "sudo yum update -y", "sudo yum install -y nginx", "sudo systemctl start nginx", "sudo systemctl enable nginx" ] } }
Local Exec — Run Commands on Your Machine
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.
Standard Module Structure
DIRECTORYmodules/ networking/ main.tf # VPC, subnets, route tables variables.tf # Input parameters outputs.tf # Exposed values (vpc_id, subnet_ids) versions.tf # Required provider versions README.md # Documentation
Calling a Module
HCL# Root module calls the networking child module module "vpc" { source = "./modules/networking" vpc_cidr = "10.0.0.0/16" environment = "production" az_count = 3 } # Access module outputs resource "aws_instance" "api" { subnet_id = module.vpc.private_subnet_ids[0] }
Registry Modules
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.
S3 Backend — The Standard
HCLterraform { backend "s3" { bucket = "mycompany-terraform-state" key = "prod/api-service/terraform.tfstate" region = "ap-south-1" dynamodb_table = "terraform-locks" encrypt = true } }
State Locking — How It Works
🔒
Lock Acquired
Engineer A runs apply → Terraform creates a lock record in DynamoDB → state file is locked.
🚫
Lock Blocked
Engineer B runs apply → sees "Error acquiring state lock" → must wait for A to finish.
🔓
Lock Released
Engineer A's apply completes → lock is removed → Engineer B can now proceed.
💡
Native S3 Lock
Terraform 1.10+ supports use_lockfile = true — no DynamoDB needed. S3 handles locking natively.
ComponentAWS ServicePurpose
State StorageS3 BucketStore terraform.tfstate securely
State Locking (classic)DynamoDB TablePrevent concurrent modifications
State Locking (new)S3 Lock FileNative S3 locking (Terraform 1.10+)
EncryptionS3 SSE / KMSEncrypt state at rest
terraform_remote_state — Read Other Team's Outputs
HCL# Security team reads networking team's VPC ID data "terraform_remote_state" "network" { backend = "s3" config = { bucket = "mycompany-terraform-state" key = "prod/networking/terraform.tfstate" region = "ap-south-1" } } # Use the fetched value resource "aws_instance" "api" { subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id }
Essential .gitignore for Terraform
GITIGNORE.terraform/ *.tfstate *.tfstate.* crash.log *.tfvars # May contain secrets !example.tfvars # Except template files
⚠️ State Contains Secrets

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
WorkflowHow It WorksBest For
VCS-DrivenPush to GitHub → auto plan/applyProduction teams
CLI-DrivenRun locally, execute remotelyDevelopers testing
API-DrivenTriggered by automation/CIAdvanced 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
LevelBehaviorOverride?
Hard MandatoryBlocks apply completelyNo — cannot override
Soft MandatoryBlocks by defaultYes — authorized users can override
AdvisoryShows warning onlyAlways 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