M1 โ Terraform & Infrastructure as Code¶
Core question: How do you turn a 400-click AWS console session into a 10-line code review that any teammate can read, reproduce, and audit?
โฑ๏ธ Time: ~60 min padho + 30 min lab ยท ๐๏ธ Level: BeginnerโIntermediate ยท ๐ Pehle chahiye: M0 โ Foundations
Is module ke baad tum kar paoge: - Terraform lifecycle chalao:
init โ plan โ apply โ destroyโ ek real AWS resource pe end-to-end - tfstate file ka role samjho aur drift vs lost-state ka farak interview mein confidently explain karo - Modules se multi-environment infrastructure organize karo โ dev aur prod ki state alag rakho
โก 60-second hook โ pehle KARO, phir padho (no cloud, no cost)
mkdir -p /tmp/tf-hook && cd /tmp/tf-hook
printf 'resource "local_file" "hello" {\n filename = "hello.txt"\n content = "Terraform ne mujhe banaya."\n}\n' > main.tf
terraform init -input=false >/dev/null && terraform apply -auto-approve
cat hello.txt
terraform apply -auto-approve # โ dobara chalao. "No changes." โ YE line is module ka hero hai
โฉ๏ธ Recall gate โ shuru karne se pehle¶
Pichhle modules se 3 sawaal. Pehle memory se jawab do, phir kholo. (Yeh retrieve karna hi lifetime yaad rakhta hai โ dobara padhna nahi.)
- (M0) "Idempotency" aur "reconciliation" mein kya farak hai? Dono ko ek line mein define karo.
- (M0) "Provisioning" aur "configuration management" alag kyun hain โ dono ka ek-ek real-world example do.
- (M0) "Pets vs cattle" DevOps mein kya hota hai โ aur yeh distinction scale ke liye kyun zaroori hai?
Jawab
- Idempotency = ek operation safely baar baar chalao, result same (single op, safe re-run). Reconciliation = continuous loop jo hamesha desired state enforce karta rehta hai. 2. Provisioning = raw machine banana, e.g., EC2 spin up (Terraform); configuration = uss machine ke andar software daalna, e.g., nginx install (Ansible). Alag concerns, alag tools. 3. Pets = unique, hand-configured servers jo replace nahi ho sakte; cattle = identical, interchangeable nodes jo bina soche replace ho jaate โ automation aur scale ke liye zaroori.
Module map: 00-INDEX | 01-M0 | 02-M1 | 03-M2-ansible | 04-M3-docker | 05-M4-kubernetes-core | 06-M5-sizing-and-cost | 07-M6-cicd | 08-M7-gitops | 09-connected-system
The 60-Second Version¶
Infrastructure as Code (IaC) means your servers, networks, databases, and firewalls are described in text files โ not clicked into existence in a web console. Terraform is the dominant IaC tool. It reads your .tf files, compares what you asked for against what actually exists in AWS (using a local record called the state file), and makes only the changes needed to close that gap.
Three words explain how Terraform works:
- Declarative โ you describe the destination, not the steps to get there.
- State โ Terraform keeps a diary of what it built; that diary is the link between your code and the real world.
- Idempotent โ run it a hundred times; you still get the same result, not a hundred copies.
Why This Exists โ What It Replaced¶
Before IaC, teams provisioned infrastructure by clicking through cloud consoles manually. This pattern has a name: ClickOps.
ClickOps problems:
| Problem | Consequence |
|---|---|
| No record of what was clicked | "Who opened port 22 to the world?" โ nobody knows |
| Knowledge lives in one engineer's head | That engineer leaves โ knowledge leaves |
| Reproducing an environment takes days | "Just spin up a staging copy" โ impossible |
| Audits are guesswork | Compliance teams find untracked servers |
| Snowflake servers | Every machine slightly different; none documented |
A snowflake server is one that has been hand-configured until it is unique โ special, fragile, and irreplaceable. When it breaks, nobody knows how to rebuild it. IaC eliminates snowflakes: if you can read the .tf file, you can rebuild the entire environment from scratch.
๐ฎ๐ณ Hinglish intuition: Pehle infra banane ka koi recipe nahi tha โ chef apne mood se banata tha, next day kuch aur. IaC = har dish ka likha hua recipe โ koi bhi bana sake, har baar same.
Core Concepts¶
Infrastructure as Code (IaC)¶
IaC means expressing infrastructure intent in code files that live in Git. The result:
- Reviewable โ a pull request shows exactly what will change before it changes.
- Reproducible โ
terraform applyin any account recreates the same environment. - Auditable โ Git history shows who changed what and when.
- Versionable โ roll back infrastructure the same way you roll back application code.
Declarative vs Imperative¶
| Style | You Write | Tool Does |
|---|---|---|
| Declarative (Terraform) | "I want 3 EC2 servers" | Figures out the steps itself |
| Imperative (bash script) | "Step 1: create VPC. Step 2: create subnet. Step 3: ..." | Runs exactly what you wrote, blindly |
The declarative approach means Terraform handles the complexity of ordering, dependencies, and partial states. You describe the destination; Terraform plans the route.
State โ The Diary¶
Terraform maintains a file called terraform.tfstate. This JSON file is the link between your code and the real world. It records:
- Every resource Terraform has ever created
- The real cloud identifiers (e.g.,
i-0abc1234for an EC2 instance) - All resource attributes at the time of the last apply
Without the state file, Terraform cannot know what it has already built. It becomes blind.
๐ฎ๐ณ Hinglish intuition: tfstate = Terraform ki diary ๐. "Aaj maine kya banaya, iska ID kya hai" โ sab likha hai. Diary gayi to andha ho gaya.
Critical warning: The state file contains sensitive data in plaintext โ including database passwords. Never commit it to Git. Always store it in a remote backend with encryption enabled.
Idempotent¶
Idempotent means the operation can be safely repeated. If your code says count = 3 and you already have 3 servers, running terraform apply does nothing. Terraform is performing a SET operation (desired = 3), not an ADD operation (actual += 3).
๐ฎ๐ณ Hinglish intuition: Light switch โ ek baar dabao ON hota. Sau baar dabao, ON hi rehta. += nahi hota.
The Command Lifecycle¶
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โ โ โ โ โ โ โ โ
โ terraform โโโโโโบโ terraform โโโโโโบโ terraform โโโโโโบโ terraform โ
โ init โ โ plan โ โ apply โ โ destroy โ
โ โ โ โ โ โ โ โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
Download Preview Execute Tear down
providers & what will the plan all managed
set up backend change (real API calls) resources
(once per project) (safe โ no (costs money,
changes made) changes state)
flowchart LR
HCL["Write HCL<br/>(.tf files)"]:::ci --> PLAN["terraform plan<br/>(preview changes)"]:::ci
PLAN -->|"if approved"| APPLY["terraform apply<br/>(real API calls)"]:::infra
APPLY --> CLOUD[("Real Cloud<br/>EC2 VPC RDS")]:::run
CLOUD -. "state tracks<br/>resources" .-> STATE[("tfstate<br/>state file")]:::store
STATE -. "compared on<br/>next plan" .-> PLAN
classDef infra fill:#fce4ec,stroke:#d81b60,color:#880e4f;
classDef ci fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
classDef run fill:#e0f2f1,stroke:#00897b,color:#004d40;
classDef store fill:#fff3e0,stroke:#ef6c00,color:#e65100;
classDef shared fill:#fff9c4,stroke:#f9a825,color:#4a3800;
The Terraform loop: HCL declares desired state, plan previews changes against tfstate, apply modifies real cloud, and the state file is the map that tracks what was built.
๐ฎ๐ณ Hinglish intuition:
plan= bill dekhna ๐งพ.apply= payment ๐ณ. Bill padhe bina payment mat karo.
Providers and Resources¶
A provider is a plugin that knows how to talk to a specific cloud or service. hashicorp/aws handles AWS API calls; hashicorp/google handles GCP. You declare the provider once; Terraform downloads it during init.
A resource is a single piece of infrastructure managed by Terraform. Each resource block describes one thing to create: one VPC, one security group, one EC2 instance.
A backend tells Terraform where to store the state file. The default is a local file on disk. The production-grade choice is a remote backend (S3).
Annotated HCL Example¶
HCL (HashiCorp Configuration Language) is Terraform's own file format โ readable, structured, designed to describe infrastructure rather than write algorithms.
# โโ 1. Tell Terraform which cloud provider to use โโโโโโโโโโโโโโโโโโโโโโโโโโ
provider "aws" {
region = "ap-south-1" # Mumbai region
}
# โโ 2. Remote backend โ state lives in S3, not on your laptop โโโโโโโโโโโโโโ
terraform {
backend "s3" {
bucket = "mycompany-tfstate" # S3 bucket (create manually first)
key = "prod/terraform.tfstate" # path inside the bucket
region = "ap-south-1"
use_lockfile = true # S3-native state locking (Terraform 1.10+)
encrypt = true # server-side encryption
}
}
# โโ 3. A variable โ input, not hardcoded โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
variable "my_ip" {
description = "Your office IP for SSH access"
type = string
}
# โโ 4. A VPC (Virtual Private Cloud) โ your private network on AWS โโโโโโโโโ
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16" # CIDR = IP range (Classless Inter-Domain Routing)
enable_dns_hostnames = true
tags = { Name = "prod-vpc" }
}
# โโ 5. Security Group (SG) โ firewall rules โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id # reference to the VPC above (implicit dependency)
ingress { # inbound: allow SSH only from your IP
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["${var.my_ip}/32"] # /32 = exactly one IP; use IPv4 here (not IPv6)
}
egress { # outbound: allow everything
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# โโ 6. An EC2 instance โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
resource "aws_instance" "web" {
ami = "ami-0f5ee92e2d63afc18" # AMI = Amazon Machine Image, OS blueprint for EC2 โ โ ๏ธ AMI IDs are region-specific and get deregistered; real code should use a `data "aws_ami"` lookup (see Senior Insights table)
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id # aws_subnet.public shown in the full example โ omitted here for brevity
vpc_security_group_ids = [aws_security_group.web.id]
tags = { Name = "web-server" }
}
# โโ 7. Output โ values surfaced after apply, used by Ansible next โโโโโโโโโโ
output "web_public_ip" {
value = aws_instance.web.public_ip
description = "Pass this to Ansible inventory"
}
Key observations:
- aws_vpc.main.id references another resource by type + name โ Terraform builds a dependency graph from these and applies in the correct order automatically.
- var.my_ip keeps sensitive or environment-specific values out of the code itself.
- The output block surfaces the IP that Ansible will need (see 03-M2-ansible.md).
State โ The Thing You Must Never Lose¶
Why Remote Backend + Lock¶
๐ง War story: Ek engineer ka tfstate laptop pe tha โ naye teammate ne
terraform applykiya, state nahi thi, usne 5 duplicate servers bana diye. Tab tak pata chala jab AWS bill double aa gaya. Poori kahani + lesson โ Interview Bank.
If the state file lives on your laptop, two problems immediately appear the moment a second person joins the team:
Problem 1 โ Sharing: Your teammate has no state file. Their Terraform thinks nothing exists. They run apply and create duplicate infrastructure: two VPCs, two databases, ten servers instead of five.
Problem 2 โ Corruption: Two people run apply simultaneously. Both read the state, make changes, and write back. The writes overlap. The state file is now corrupt โ partial entries, missing IDs, inconsistencies.
Solution: Store the state in S3 (solves sharing) and use a DynamoDB table as a distributed lock (solves simultaneous apply).
# Create the S3 bucket (once, manually โ Terraform cannot bootstrap its own backend)
aws s3 mb s3://mycompany-tfstate --region ap-south-1
# That's it. With `use_lockfile = true`, S3 handles locking natively โ no separate table needed.
Locking, current best practice: Terraform 1.10+ does state locking natively in S3 via
use_lockfile = true. The old pattern โ a separate DynamoDB table withdynamodb_table = "tf-lock"โ still works but is now legacy; use it only when maintaining pre-1.10 configs. Interviewers notice which one you reach for.๐ฎ๐ณ Hinglish intuition: S3 = shared almari (sab padhein). Lock (
use_lockfile) = taala ๐ โ ek waqt mein ek hi likhเฅ. Pehle taala alag DynamoDB me tha; ab S3 khud laga leta.
Secrets in State¶
Terraform writes every resource attribute to the state file in plaintext. This includes:
- Database passwords passed via
variable "db_password" { sensitive = true } - Private key material
- API tokens injected into resources
Non-negotiable rules:
1. Add *.tfstate* to .gitignore โ before the first commit, not after.
2. Enable encrypt = true in the S3 backend block.
3. Apply an IAM policy restricting who can read the S3 bucket.
4. If a state file is accidentally committed, rotate every credential it contains immediately.
๐ฎ Predict pehle (socho, phir aage padho): tfstate file delete ho gayi. Agla
terraform plankya sochta hai ki exist karta hai โ aur kyun ye potentially catastrophic hai?
Drift vs Lost-State โ The Critical Distinction¶
These two situations look similar but require completely different responses.
| Drift | Lost-State | |
|---|---|---|
| What happened | Someone changed real infrastructure outside Terraform (manual console click, AWS auto-scaling, another tool) | The tfstate file was deleted or corrupted |
| State file | Intact โ Terraform still knows what it built | Gone โ Terraform is blind |
| Terraform's view | Sees mismatch between state and reality | Sees nothing at all โ thinks zero resources exist |
Result of apply |
Reverts reality back to what code specifies | Creates everything from scratch โ you now have double the infrastructure |
| Concrete example | Someone manually opened port 80 in the console; plan shows -/+ change |
State deleted; 5 servers running; apply โ 5 new servers โ total 10 |
| Your reflex | Run terraform plan โ it shows the drift; run apply to revert |
Stop. Run terraform import for each existing resource to reconstruct state |
| Recovery command | terraform apply (straightforward) |
terraform import aws_instance.web i-0abc1234 (one resource at a time) |
flowchart LR
subgraph DRIFT["Drift"]
direction TB
D1["tfstate has 3 EC2"]:::store --> D2["reality has 4<br/>1 manually added"]:::shared
D2 --> D3["plan: 1 created<br/>outside Terraform"]:::ci
D3 --> D4["SAFE โ re-apply<br/>reconciles to code"]:::ok
end
subgraph LOST["Lost-State"]
direction TB
L1["tfstate EMPTY or lost"]:::warn --> L2["reality has 5 EC2<br/>Terraform BLIND"]:::warn
L2 --> L3["plan: 5 to add<br/>thinks nothing exists"]:::warn
L3 --> L4["DANGER โ apply = duplicates<br/>import FIRST"]:::infra
end
classDef ci fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
classDef store fill:#fff3e0,stroke:#ef6c00,color:#e65100;
classDef ok fill:#e0f2f1,stroke:#00897b,color:#004d40;
classDef shared fill:#fff9c4,stroke:#f9a825,color:#4a3800;
classDef warn fill:#fce4ec,stroke:#d81b60,color:#880e4f;
classDef infra fill:#fce4ec,stroke:#d81b60,color:#880e4f;
drift = state sach se thoda peeche; lost state = Terraform andha โ recovery bilkul ulti
๐ฎ๐ณ Hinglish intuition: - Drift = diary intact, par duniya badal gayi. TF diary padh ke duniya ko wapas theek kar deta. - Lost-state = diary jal gayi โ ๏ธ. TF andha โ bhool gaya kya banaya tha. 5 naye bana deta.
Orphaned resource: A resource that exists in reality but has no entry in the state file. After lost-state, all your existing servers become orphans. Terraform neither manages nor destroys them โ they run up your AWS bill invisibly. Fix: terraform import.
๐ฎ๐ณ Hinglish intuition: Orphaned resource = bina maalik ki gaay ๐ โ zinda hai, khana kha rahi hai, par kisi ki zimmedari mein nahi.
user_data and Provisioners โ Where to Draw the Line¶
user_data / cloud-init: A script you pass to an EC2 instance that runs once at first boot. It is valid for simple, one-time setup tasks โ installing a single package, writing a config file at launch time.
resource "aws_instance" "web" {
# ... other args
user_data = <<-EOF
#!/bin/bash
apt-get update -y
apt-get install -y nginx
EOF
}
๐ฎ๐ณ Hinglish intuition: user_data = "ghar ban-te waqt bijli ka connection lagwa do" โ ek baar, construction ke time.
Provisioner (avoid): Terraform has remote-exec and local-exec provisioners that run shell commands against a resource after it is created. They are a trap:
- They run only once at creation โ not on re-apply (not idempotent).
- Terraform has no visibility into whether they succeeded.
- They blur the boundary between Terraform (what exists) and Ansible (what is configured inside).
- HashiCorp's own documentation calls them a "last resort."
Rule: Terraform provisions the machine. Ansible configures what is inside it. Do not mix them. See 03-M2-ansible.md for how Ansible takes the output IPs from Terraform and configures the servers.
The lifecycle block (small but critical)¶
Every resource block accepts an optional lifecycle meta-argument that controls how Terraform creates, updates, and destroys that resource. Three arguments cover the situations you will actually hit in production:
resource "aws_db_instance" "prod" {
# ...
lifecycle {
prevent_destroy = true # guard: `terraform destroy` / accidental removal is BLOCKED
create_before_destroy = true # make the replacement BEFORE deleting the old = no downtime
ignore_changes = [tags["LastModified"]] # stop fighting drift on fields changed outside TF
}
}
prevent_destroy = true โ a hard stop. If any plan would destroy this resource, Terraform errors the plan instead of executing it. A mistaken terraform destroy or a resource replacement triggered by a config change cannot complete while this flag is set. Use it on any production database, S3 bucket, or resource whose accidental deletion would cause data loss. Remove it explicitly only when you intend a real destroy.
create_before_destroy = true โ for resources that must be replaced (not updated in place), Terraform normally destroys the old resource first and then creates the new one, leaving a gap. With this flag, the new resource is fully provisioned before the old one is removed โ zero-downtime replacement. Essential for launch templates, ACM certificates, and anything with a downstream dependency that would break during the gap.
ignore_changes โ when something outside Terraform legitimately modifies an attribute, TF treats the difference as drift and tries to revert it on every plan. ignore_changes tells Terraform: "stop watching this field." Common cases: an autoscaler sets desired_capacity; a tag like LastModified is written by a Lambda or a cost-allocation tool. Without this, you get a perpetual diff that apply never fully resolves.
๐ฎ๐ณ Hinglish intuition:
prevent_destroy= taala ๐ jo sirf intentional remove pe khulta hai โterraform planhi fail kar deta hai galti se.create_before_destroy= naya bridge banao phir purana girao โ traffic ruka nahi.ignore_changes= "yeh field bahar se manage hoti hai, TF tujhe koi matlab nahi" โ perpetual diff hamesha ke liye gayab.
Remote backend migration + terraform import (daily prod work)¶
Concepts padi, ab woh do kaam jo hamesha milenge production mein โ pehli baar team join karo ya purana ClickOps infra haath mein aaye.
LAB A โ Local โ S3 Remote Backend Migration¶
Why: Akele kaam karte waqt local state chalta hai. Jaise hi doosra engineer aaya โ state share nahi hoti, duplicate infra banta hai, concurrent apply pe state corrupt hoti hai. Team ka pehla rule: never local state in a team.
Step 1 โ Backend block likhna โ terraform {} ke andar backend section daalo:
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
backend "s3" {
bucket = "mycompany-tfstate" # manually banao pehle โ neeche dekho
key = "microshop/dev/terraform.tfstate" # har environment ka alag key
region = "ap-south-1"
use_lockfile = true # S3-native locking (Terraform 1.10+)
encrypt = true # server-side encryption always on
}
}
DynamoDB โ S3 locking: Pehle
dynamodb_table = "tf-lock"se alag table banana padta tha. Terraform 1.10+ meinuse_lockfile = truene woh khatam kar diya โ S3 khud locking sambhalti hai. Nayi configs mein DynamoDB locking legacy hai; maintain karo purani configs ke liye, nayi ke liye nahi.
Step 2 โ State bucket bootstrap (ek baar, manually) โ bucket pehle banao, Terraform khud nahi bana sakta:
# Terraform apna backend bucket khud nahi bana sakta โ chicken-egg problem
aws s3 mb s3://mycompany-tfstate --region ap-south-1
# Versioning on karo โ galat apply ke baad purani state wapas lane ke liye
aws s3api put-bucket-versioning \
--bucket mycompany-tfstate \
--versioning-configuration Status=Enabled
Step 3 โ Existing local state migrate karo โ ek command, Terraform baki sambhalega:
Terraform ek prompt dikhayega โ type yes:
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the old "local" backend to the
newly configured "s3" backend. No existing state was found in the new backend.
Do you want to copy this state to the new backend?
Enter a value: yes
Successfully configured the backend "s3"!
Local terraform.tfstate S3 mein copy ho gaya. Ab koi bhi teammate terraform init kare โ same remote state milegi, same lock.
Chicken-egg gotcha + must-do checklist
S3 bucket pehle banana padega โ Terraform apna state bucket khud bootstrap nahi kar sakta (state kahan rakhega pehle se pata nahi hota). Standard approach:
- Bucket console ya CLI se banao (ek baar, manually โ ya ek alag "bootstrap" Terraform run mein)
- Versioning on karo โ accidental overwrite ke baad point-in-time recovery milti hai
.gitignoremein add karo*.tfstate*aur.terraform/โ pehle commit se pehle, kabhi nahi commit karna
๐ฎ๐ณ Hinglish takeaway: Local state = akele ki pocket mein team ka ATM card โ team nahi chal sakti. S3 backend = shared bank locker,
use_lockfile = true= ek waqt mein sirf ek hi andar jaata hai.
LAB B โ terraform import (ClickOps Infra ko Terraform ke Under Laana)¶
Scenario: Company ka ek S3 bucket manually console se banaya tha (ClickOps). Terraform ko pata hi nahi. terraform plan chalao โ plan kehta hai "yeh bucket create karunga" โ apply kiya to duplicate ban jaata. Fix: import karo.
Way 1 โ CLI import (classic) โ terraform import <resource_address> <real_cloud_id>:
# PEHLE resource block likho (neeche dekho), PHIR import karo
terraform import aws_s3_bucket.legacy my-existing-bucket-name
aws_s3_bucket.legacy: Importing from ID "my-existing-bucket-name"...
aws_s3_bucket.legacy: Import prepared!
aws_s3_bucket.legacy: Refreshing state... [id=my-existing-bucket-name]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
Way 2 โ import {} block (Terraform 1.5+, teams ke liye preferred)
# main.tf mein yeh block daalo โ plan preview mein dikhega, PR mein review hogi
import {
to = aws_s3_bucket.legacy
id = "my-existing-bucket-name"
}
resource "aws_s3_bucket" "legacy" {
bucket = "my-existing-bucket-name"
# tags aur baaki attributes plan ke output se milao โ neeche dekho
}
terraform plan # import preview + config diff saath dikhega
terraform apply # import run hoga + state update hoga
CLI import quietly hota hai; import {} block PR mein visible hai, reproducible hai โ team workflows ke liye better choice.
Sahi workflow โ yahi sequence miss hoti hai sabse zyada:
1. resource block likhna โ pehle โ minimum bucket name toh daalo
โ
2. terraform import โ state mein entry aayi, real infra chhuu nahi
โ
3. terraform plan โ config aur reality ka diff dikhega
โ
4. config tweak karo โ plan output dekh ke attributes milao (tags, acl, etc.)
โ
5. terraform plan โ "No changes. Your infrastructure matches the configuration."
โ
DONE โ ab Terraform manage kar raha hai
Import sirf state mein ID dalta hai โ matching config tumhe hand-write karni padti hai. Terraform nahi jaanta "iska HCL kya hona chahiye." "Import successful!" message = sirf state mein gaya, config match nahi ka matlab kaam adhoora.
Import Danger Zone
- Galat resource address pe import:
aws_s3_bucket.legacyki jagahaws_s3_bucket.prodpe import kiya โ prod bucket ki state overwrite โ aglaplanunexpected destroy/recreate dikha sakta hai. Address double-check karo pehle. - Plan clean nahi, apply kar diya: Agar
terraform planabhi bhi changes dikha raha hai (config aur reality mismatch) aur tum apply karo โ real infra touch ho sakti hai. Zero-diff plan aaye tab hi apply karo. Yahi rule hai: plan โ fix โ plan โ zero-diff โ apply.
๐ฎ๐ณ Hinglish takeaway: Import = purani gaay ko apni diary (state) mein register karna โ woh usi jagah rahi, ab Terraform ki zimmedari mein hai. Plan zero-diff dikhaye tabhi apply karo, warna Terraform galti se real infra chhoo sakta hai.
Modules and Environment Isolation¶
The DRY Problem¶
Without modules, a typical team does this: copy main.tf to main-prod.tf, tweak a few values, and now has two files diverging forever. A bug fix in one has to be manually applied to the other. This is a DRY (Don't Repeat Yourself) violation.
Modules solve it by creating reusable black boxes.
Modules โ Black Box Pattern¶
A module is a directory containing Terraform files with a defined interface:
- Inputs = variables (
variables.tfinside the module) - Outputs = outputs (
outputs.tfinside the module) - Internal logic =
main.tf(the caller does not need to understand it)
๐ฎ๐ณ Hinglish intuition: Module = car engine. Accelerator dabao (input) โ car chale (output). Engine kaise kaam karta โ driver ko jaanne ki zaroorat nahi.
Module: modules/vpc/
โโโ main.tf โ creates VPC, subnets, IGW, route tables
โโโ variables.tf โ accepts: project, env, cidr_block
โโโ outputs.tf โ exposes: vpc_id, subnet_ids, sg_id
The Wiring Diagram โ Root Orchestrates, Modules Do Not Talk to Each Other¶
environments/dev/main.tf (ROOT โ the driver)
โ
โโโ module "vpc"
โ source = "../../modules/vpc"
โ vpc_cidr = "10.0.0.0/16"
โ โ
โ outputs: vpc_id, subnet_ids, sg_id
โ โ
โโโ module "ec2" โโโโโ (root passes vpc outputs as ec2 inputs)
โ source = "../../modules/ec2"
โ subnet_id = module.vpc.subnet_public_a_id
โ sg_id = module.vpc.sg_id
โ โ
โ outputs: master_ip, worker_ips
โ
โโโ module "rds" โโโโโ (root passes vpc outputs as rds inputs)
source = "../../modules/rds"
subnet_ids = [module.vpc.subnet_public_a_id, module.vpc.subnet_public_b_id]
sg_id = module.vpc.sg_id
flowchart TD
subgraph ROOT["ROOT โ main.tf"]
R["main.tf<br/>orchestrator"]:::ci
end
subgraph VPC_MOD["MODULE VPC"]
VPC["inputs: cidr<br/>outputs: vpc_id subnet_id"]:::store
end
subgraph EC2_MOD["MODULE EC2"]
EC2["inputs: subnet_id<br/>from module.vpc"]:::run
end
subgraph RDS_MOD["MODULE RDS"]
RDS["inputs: subnet_ids<br/>from module.vpc"]:::run
end
R -->|"cidr=10.0.0.0/16"| VPC
VPC -->|"vpc_id + subnet_id"| R
R -->|"subnet_id"| EC2
R -->|"subnet_ids"| RDS
NOTE["Modules NEVER call<br/>each other โ Root wires them"]:::shared
classDef ci fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
classDef store fill:#fff3e0,stroke:#ef6c00,color:#e65100;
classDef run fill:#e0f2f1,stroke:#00897b,color:#004d40;
classDef shared fill:#fff9c4,stroke:#f9a825,color:#4a3800;
data ka rasta RootโVPCโRootโEC2 hai, VPCโEC2 seedha kabhi nahi
The vpc module and the ec2 module do not reference each other directly. The root main.tf wires them: it takes the output of one and passes it as the input of another. This keeps modules reusable and independently testable.
Environment Isolation โ Separate State Keys Per Environment¶
S3 bucket: mycompany-tfstate/
โโโ microshop/dev/terraform.tfstate โ dev's diary
โโโ microshop/prod/terraform.tfstate โ prod's diary (completely separate)
Running terraform destroy from environments/dev/ destroys only what is in the dev state. Prod is untouched. This is the only production-safe pattern.
Also use non-overlapping CIDR blocks per environment so VPC peering and VPN connections remain possible:
flowchart TD
Dev["env/dev<br/>cidr=10.0.0.0/16"]:::ci -->|"calls"| M["module vpc<br/>(shared code)"]:::shared
Stg["env/staging<br/>cidr=10.2.0.0/16"]:::ci -->|"calls"| M
Prod["env/prod<br/>cidr=10.1.0.0/16"]:::ci -->|"calls"| M
M -->|"provisions"| VPC[("VPC per env<br/>isolated resources")]:::run
Dev -.->|"own state"| SD[("dev tfstate")]:::store
Prod -.->|"own state"| SP[("prod tfstate")]:::store
classDef infra fill:#fce4ec,stroke:#d81b60,color:#880e4f;
classDef ci fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
classDef run fill:#e0f2f1,stroke:#00897b,color:#004d40;
classDef store fill:#fff3e0,stroke:#ef6c00,color:#e65100;
classDef shared fill:#fff9c4,stroke:#f9a825,color:#4a3800;
One shared module "vpc" called by dev, staging, and prod with different CIDRs; each environment writes to its own isolated tfstate.
Dev vs Prod โ Key Differences¶
| Setting | Dev | Prod | Why Different |
|---|---|---|---|
| EC2 instance type | t3.micro | t3.medium | Free-tier vs real load |
| Worker node count | 0 (single-node) | 2 | HA and capacity |
| RDS instance class | db.t3.micro | db.t3.small | User volume |
| RDS Multi-AZ | false | true | AZ failure = 60s auto-failover |
| RDS backup retention | 0 days | 7 days | Point-in-time recovery |
| skip_final_snapshot | true | false | See Mistake 3 below |
| Storage encryption | false | true | GDPR / SOC 2 compliance |
| SSH access | your IP /32 | bastion CIDR only | Attack surface reduction |
| ECR tag mutability | MUTABLE | IMMUTABLE | No accidental overwrite in prod |
Five Common Module/Environment Mistakes¶
Mistake 1 โ Same CIDR in dev and prod
Dev: 10.0.0.0/16
Prod: 10.0.0.0/16 โ identical
Problem: VPC peering setup โ routing conflict โ connectivity fails
Fix: Plan your CIDR allocation before writing a single line of Terraform
Mistake 2 โ Hardcoded environment name
# Wrong
tags = { Name = "microshop-dev-vpc" }
# Correct
tags = { Name = "${var.project}-${var.env}-vpc" }
Mistake 3 โ skip_final_snapshot = true in prod
This is the data-loss war story. When someone runs terraform destroy against production by mistake (wrong terminal window, wrong workspace), RDS deletes the database. If skip_final_snapshot = true, there is no automatic snapshot. The data is gone permanently. There is no undo.
Always set skip_final_snapshot = false in production. Verify that a snapshot exists before any destroy on RDS.
Mistake 4 โ One shared tfstate key for all environments
# Wrong โ all envs write to the same file
key = "terraform.tfstate"
# Correct
# dev: key = "microshop/dev/terraform.tfstate"
# prod: key = "microshop/prod/terraform.tfstate"
Mistake 5 โ Wrong output path when wiring modules
# Wrong โ module doesn't expose an output named "subnet_id"
subnet_id = module.vpc.subnet_id
# Correct โ match the exact name in the module's outputs.tf
subnet_id = module.vpc.subnet_public_a_id
Always check the module's outputs.tf before referencing its values in root.
Real Production Example¶
The URL Shortener capstone (12-capstone-url-shortener.md) uses this Terraform structure. Here is the directory layout:
infra/
โโโ backend.tf โ S3 + DynamoDB backend declaration
โโโ main.tf โ VPC / subnets / IGW / route tables / SG / EC2 / RDS / ECR
โโโ variables.tf โ db_password (sensitive = true), my_ip
โโโ outputs.tf โ master_ip, worker_ips, rds_endpoint, ecr_url
# Modular version (production-grade):
infra/
โโโ modules/
โ โโโ vpc/ (main.tf, variables.tf, outputs.tf)
โ โโโ ec2/
โ โโโ rds/
โ โโโ ecr/
โโโ environments/
โโโ dev/ (main.tf calls modules with dev values)
โโโ prod/ (same modules, prod values, separate state key)
The outputs.tf block exposes master_ip and worker_ips. Ansible reads these to populate its inventory file, then configures Kubernetes on the nodes. This is the Terraform โ Ansible handoff. See 03-M2-ansible.md.
Commands, Explained¶
# Download the AWS provider plugin and configure the S3 backend.
# Run once per project, or after adding a new provider.
terraform init
# Show what Terraform will create, change, or destroy.
# Reads: code + state + real AWS. Writes: nothing.
# + = create, ~ = modify in place, - = destroy, -/+ = destroy and recreate
terraform plan
# Apply the plan. Prompts for confirmation (type "yes").
# Passes -var for sensitive values not stored in code.
terraform plan -var="db_password=Secret123!"
terraform apply -var="db_password=Secret123!"
# Print the output values defined in outputs.tf.
# Use this to get IPs and endpoints after apply.
terraform output
# Show the current state as Terraform understands it.
# Use to debug drift or confirm what Terraform is tracking.
terraform show
# Re-attach an existing resource to Terraform state without destroying it.
# Use for lost-state recovery, one resource at a time.
terraform import aws_instance.web i-0abc1234def567890
# Destroy all resources in the current state. Prompts for "yes".
# Run from the right environment directory. Always confirm state is correct first.
terraform destroy
# Format all .tf files consistently. Run before every commit.
terraform fmt
# Check syntax and validate resource configurations. Run before plan.
terraform validate
plan -out: apply exactly what you reviewed¶
terraform plan -out=tfplan # save the exact plan to a file
terraform apply tfplan # apply THAT plan โ no drift between review and apply
In interactive use, plan followed immediately by apply is fine. In CI โ and especially in a prod apply gate โ there is a window between the plan you reviewed and the apply you run. If the state changed in that window (a teammate applied something, an autoscaler touched a resource), apply without a saved plan re-plans silently and acts on the new state, not what you reviewed and approved.
plan -out=tfplan captures the exact plan object at that moment. apply tfplan executes precisely that snapshot โ no re-plan, no surprises. The capstone CI (07-M6-cicd.md) pairs this with an approval gate: plan runs on the PR, the plan file is stored as a pipeline artifact, and apply uses that file in the merge job โ guaranteeing the apply matches what was reviewed.
๐ฎ๐ณ Hinglish intuition:
plan -out= jo tumne review kiya, wahi cheez apply hogi โ plan aur apply ke beech mein state change se bach jaate ho. CI mein "reviewed plan" ko artifact ki tarah pakad ke rakho, phir exactly wahi apply karo.
Beginner Mistakes vs Senior Insights¶
| Beginner Does | Senior Does | Why It Matters |
|---|---|---|
| Stores state on laptop | Remote S3 backend + DynamoDB lock from day one | First teammate = instant chaos without remote state |
Commits .tfstate to Git |
Adds *.tfstate* to .gitignore before first commit |
State contains plaintext DB passwords |
Runs apply without reading plan |
Always reads plan output; no -auto-approve in prod | One typo can cascade to replacing a production database |
Uses provisioner "remote-exec" for config |
Hands off to Ansible via outputs | Provisioners are not idempotent; they hide failures from state |
One flat main.tf for all environments |
Modules + per-environment directories + separate state keys | Without isolation, dev destroy can take prod offline |
| Same CIDR in dev and prod | Plans CIDR allocation before writing any code | Overlap makes VPC peering impossible later |
skip_final_snapshot = true everywhere |
Only in dev; always false in prod |
A mistaken destroy in prod is permanent data loss |
| Hardcodes AMI IDs | Uses data "aws_ami" to fetch latest dynamically, or pins with a comment |
AMI IDs are region-specific; hardcode breaks cross-region deploys |
Never runs terraform fmt |
Formats before every commit; runs validate before every plan |
Unformatted HCL fails peer review; invalid config wastes plan time |
Memory Shortcuts¶
| Concept | Hook |
|---|---|
| IaC | Blueprint โ anyone can build from it |
| tfstate | Diary ๐ โ Terraform's memory of what it built |
| Declarative | "Kya chahiye" โ destination, not directions |
| Idempotent | Light switch โ 100 presses = still ON, not 100ร brighter |
plan |
Bill dekhna ๐งพ โ preview before payment |
apply |
Payment ๐ณ โ real change, real cost |
| Remote backend | Shared almari โ team reads the same diary |
| Lock (DynamoDB) | Taala ๐ โ one writer at a time |
| Drift | Diary intact, duniya badal gayi โ apply fixes it |
| Lost-state | Diary jal gayi โ ๏ธ โ import to rebuild |
| Orphaned resource | Bina maalik ki gaay ๐ โ running but unmanaged |
| Module | Car engine โ inputs in, outputs out, internals hidden |
| user_data | One-time startup script โ runs at first boot only |
| Provisioner | Last resort โ not idempotent, use Ansible instead |
Mutable vs Immutable Infrastructure¶
Terraform sits at the provisioning layer, which intersects both approaches:
Mutable infrastructure: You create a server with Terraform, then Ansible logs in and installs/updates software on the running machine. The machine accumulates changes over time. This is the pattern used in this bootcamp for the Kubernetes cluster.
Immutable infrastructure: You bake everything into a machine image at build time (using Packer to create an AMI โ Amazon Machine Image, the EC2 equivalent of a Docker image). When you need a change, you build a new image and replace the old servers. Nothing is ever modified in place.
| Mutable (Terraform + Ansible) | Immutable (Terraform + Packer) | |
|---|---|---|
| Change method | ansible-playbook on running servers |
Build new AMI โ terraform apply replaces EC2 |
| Drift risk | High โ servers diverge over time | Low โ every server is identical to the image |
| Speed | Fast incremental updates | Slow to build image; fast to deploy |
| Complexity | Lower to start | Higher pipeline; right choice at scale |
| Bootcamp use | Phase 4 (Ansible + kubeadm) | Beyond this scope; Packer is the tool |
Containers (Docker + Kubernetes, covered in 04-M3-docker.md and 05-M4-kubernetes-core.md) are the most common form of immutable infrastructure in practice.
Summary¶
Terraform gives you three things that ClickOps cannot:
- A record โ everything is in
.tffiles that live in Git. The commit history is your infrastructure history. - A preview โ
planshows you exactly what will change before anything changes. This is the seatbelt. - A repeatable process โ the same code, applied twice, produces the same result. No snowflakes.
The state file is the critical dependency. Protect it: remote S3 backend, DynamoDB lock, encryption on, never in Git. Understand the difference between drift (state intact, reality changed โ recoverable with apply) and lost-state (state gone โ potentially catastrophic, recover with import).
Modules turn copy-paste into composition. Separate state keys per environment ensure that a dev destroy can never touch production.
Terraform hands off to Ansible via outputs. Terraform builds the server; Ansible configures what runs inside it.
Self-Check Quiz¶
Pehle memory se jawab do, phir neeche kholo.
-
What is the difference between declarative and imperative configuration? Give a one-line example of each in the context of Terraform vs a bash script.
-
You run
terraform applyfive times against code that declarescount = 3. How many EC2 instances exist after the fifth apply? Why? -
Your state file is in S3. Two teammates run
applyat exactly the same time. What prevents the state file from being corrupted? -
A developer logs into the AWS console and manually opens port 443 on a security group. You then run
terraform plan. What does it show, and what happens when you runterraform apply? -
The S3 bucket containing your state file is accidentally deleted. You have five production EC2 instances running. You run
terraform apply. How many EC2 instances exist afterwards, and why? -
What is an orphaned resource, and what command do you run to bring it back under Terraform management?
-
Why does HashiCorp recommend against using
provisioner "remote-exec"for server configuration? What should you use instead? -
Your module
modules/vpcexposessubnet_public_a_idin itsoutputs.tf. Inenvironments/dev/main.tf, how do you reference that value when wiring it to theec2module?
Jawab dekho
- Declarative = desired end-state describe karo ("3 servers chahiye"); tool khud steps figure out karta. Imperative = har step manually likhna padta ("Step 1: VPC banao, Step 2: subnet..."). Terraform declarative โ
resource "aws_instance" "web" { count = 3 }; bash script imperative โaws ec2 run-instances ...ek ek kar ke. - 3 instances. Terraform SET operation karta (desired = 3), ADD nahi (actual += 3). Idempotency โ 5 baar apply karo, sirf 3 servers.
- DynamoDB table distributed lock ka kaam karta hai.
applystart hote hi lock entry write hoti; doosraapplywait ya fail karta. Done hone ke baad lock release. State file concurrent writes se safe. terraform planshows~ aws_security_group.webwith port 443 as drift โ reality state se alag hai.terraform applymanually-added rule remove karta, reality ko code pe wapas laata hai.- 10 EC2 instances. State zero dikhata โ Terraform 5 naye banata. Original 5 orphaned ho jaate โ running hain, bill bana rahe hain, lekin state mein nahi. Fix: apply mat karo. Har existing instance ke liye
terraform import aws_instance.web_N i-0abc...chala ke state rebuild karo. - Orphaned resource = cloud mein exist karta hai lekin state file mein entry nahi. Command:
terraform import aws_instance.web i-0abc1234def567890โ ek ek resource, ek ek baar. - Provisioners sirf creation pe ek baar chalte hain (re-apply pe nahi) โ idempotent nahi. Unki failures state mein reflect nahi hoti. Terraform/Ansible boundary blur hoti. HashiCorp khud "last resort" kehta. Use Ansible โ idempotent, drift handle karta, state check karta.
subnet_id = module.vpc.subnet_public_a_idโ pattern:module.<module_name>.<output_name>. Module kaoutputs.tfzaroor check karo pehle; wrong name pe plan fail hoga.
Hands-On Lab¶
โ
Prove it โ bash labs/check-m1-terraform.sh
Lab ho gaya? Tick mat lagao โ machine se verify karo (validate + non-empty state + plan = No changes). โ pe exact fix-hint milega. โ The Doer's Path
Goal: Apply the core Terraform workflow on a real (free-tier) resource.
Cost: A single aws_s3_bucket resource costs $0 (storage is charged per GB stored; an empty bucket is free). Always run terraform destroy at the end.
# 1. Install Terraform (if not already done)
# https://developer.hashicorp.com/terraform/install
terraform version # should show 1.x.x
# 2. Configure AWS credentials
aws configure # enter access key, secret key, region (ap-south-1 recommended)
aws sts get-caller-identity # verify โ should return your account ID
# 3. Create a working directory
mkdir tf-lab && cd tf-lab
# 4. Write main.tf
cat > main.tf << 'EOF'
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" {
region = "ap-south-1"
}
resource "aws_s3_bucket" "lab" {
bucket = "tf-lab-yourname-2024" # must be globally unique โ add your name
tags = { Purpose = "terraform-lab" }
}
output "bucket_name" {
value = aws_s3_bucket.lab.bucket
}
EOF
# 5. Run the lifecycle
terraform init # downloads AWS provider
terraform validate # checks syntax
terraform plan # should show: Plan: 1 to add
terraform apply # type "yes" when prompted
# 6. Verify
aws s3 ls | grep tf-lab
terraform output
# 7. Experiment with drift
# Go to AWS console โ S3 โ add a tag to the bucket manually.
# Then run:
terraform plan # should show drift: the tag you added
# 8. Cleanup (important โ always destroy lab resources)
terraform destroy # type "yes"
aws s3 ls | grep tf-lab # should be empty
What you observed:
- init downloaded the provider
- plan showed the future state without making changes
- apply created the bucket and updated state
- The manually added console tag appeared as drift in the next plan
- destroy removed everything and cleared state
โ
Sahi hua to aisa dikhega: Step 5 ke baad terraform plan shows Plan: 0 to add, 0 to change, 0 to destroy (bucket already exists). Step 7 (drift experiment) mein terraform plan shows ~ aws_s3_bucket.lab with the manually-added console tag as a pending change. Final terraform destroy ke baad aws s3 ls | grep tf-lab kuch return nahi karta โ bucket gone, state cleared.
Interview Questions¶
Q1: What is infrastructure drift, and how does Terraform detect and correct it?
Drift occurs when someone modifies infrastructure outside of Terraform โ typically via the cloud console or CLI. Terraform detects it during terraform plan by querying the real AWS APIs and comparing the result against the state file. Any mismatch appears as a change in the plan output. Running terraform apply brings reality back in line with the code. The key point: drift is only detectable because the state file is intact.
Q2: Your state file is gone. You have 5 production servers running. You run terraform apply. How many servers exist afterwards, and how do you actually recover?
After apply: 10 servers. Terraform sees no state, assumes zero resources exist, creates 5 new ones. The existing 5 are now orphans โ unmanaged, still billing you, not in the new state.
Recovery: stop before applying. Run terraform import aws_instance.web_0 i-0abc... for each existing server, one at a time, to reconstruct the state file without touching the real resources. Then run plan to verify state matches reality before any apply.
Q3: Why should the state file never be stored in Git?
Terraform writes sensitive values โ including database passwords, private keys, and secret tokens โ to the state file in plaintext. Git history is permanent and often shared. Once a secret is committed, it is effectively leaked even after deletion (it remains in history). Use S3 with encryption and restricted IAM access.
Q4: What is the difference between user_data and a Terraform provisioner?
user_data (cloud-init) is a bootstrap script passed to the EC2 instance that runs once at first boot. It is handled entirely by the operating system at startup โ Terraform just delivers the script. A provisioner is a Terraform-specific construct that runs commands (via SSH or locally) after a resource is created. Provisioners are not idempotent, their failures may not be reflected in state, and they blur the boundary between Terraform and Ansible. Use user_data for one-time boot tasks; use Ansible for anything repeatable and configurable.
Q5: How do you manage multiple environments (dev, staging, prod) safely with Terraform?
Use a module-per-component structure (modules/vpc, modules/ec2, modules/rds) with a separate environment directory per environment (environments/dev, environments/prod). Each environment directory has its own backend configuration pointing to a unique S3 key. This ensures that terraform destroy in dev cannot affect prod, that state files are completely isolated, and that the same module code is reused with different variable values per environment.
Bonus scenario question: A senior engineer says "our Terraform apply ran successfully but prod is down." What are three things you check first?
- Check
terraform planoutput โ did an apply trigger a resource replacement (-/+) that caused brief downtime? - Check if
skip_final_snapshot = falseand whether an RDS replacement dropped the database. - Check whether a security group change removed an inbound rule (e.g., port 80/443) that the load balancer needs.
Production Challenge¶
Design and implement a three-environment Terraform module structure for a hypothetical e-commerce application with the following components: VPC, public and private subnets, NAT Gateway, EC2 instances for application servers, RDS PostgreSQL, and ECR.
Requirements:
- Each environment (dev, staging, prod) must have completely isolated state.
- Modules must be reusable โ adding a fourth environment should require only a new environments/qa/main.tf with different variable values.
- CIDR blocks must not overlap across environments.
- Prod must have Multi-AZ RDS, encryption, 7-day backups, and skip_final_snapshot = false.
- Dev must work within AWS free-tier limits.
- All sensitive values (passwords, IPs) must be passed via variables โ nothing hardcoded.
Deliverables: directory structure, module interfaces (variables.tf + outputs.tf for each module), and a environments/prod/main.tf showing the complete wiring. Show the backend configuration for each environment.
This challenge maps directly to what a DevOps engineer builds on day one at a Series B startup. If you can explain every decision here, you can answer any Terraform interview question.
Next: 03-M2 ยท Ansible โ Terraform just gave you empty servers. Ansible configures what goes inside them (packages, kernel settings, the container runtime) โ turning raw EC2 into cluster-ready nodes.