Terraform Complete Guide — Beginner to Advanced (Production Ready)

A simple, easy-language guide to learn Terraform from scratch and use it in real production projects.


Table of Contents

  1. What is Terraform
  2. Why Use Terraform (IaC Concept)
  3. Installation
  4. Basic Terraform Workflow
  5. Terraform File Structure
  6. Providers
  7. Resources
  8. Variables
  9. Outputs
  10. Data Sources
  11. State File
  12. Remote Backend (Production Must)
  13. State Locking
  14. Modules
  15. Workspaces
  16. Provisioners
  17. Functions & Expressions
  18. Loops: count, for_each, for
  19. Conditional Expressions
  20. Terraform Import
  21. Terraform Commands Cheat Sheet
  22. Production Best Practices
  23. Folder Structure for Real Projects
  24. Multi-Environment Setup (Dev/Staging/Prod)
  25. Secrets Management
  26. CI/CD with Terraform
  27. Terraform Cloud / Enterprise
  28. Common Errors & Fixes
  29. Practice Project Example

1. What is Terraform

Terraform is a tool made by HashiCorp that lets you create, manage, and destroy cloud infrastructure (servers, databases, networks, etc.) using code instead of clicking buttons in a console.

Simple example: Instead of going to AWS console and manually creating an EC2 server, you write a file saying "I want 1 EC2 server with this configuration" and run one command.


2. Why Use Terraform (IaC Concept)

Old Way (Manual) Terraform Way (IaC)
Click in console Write code
Hard to repeat Easy to repeat (copy-paste config)
No history of changes Code is in Git, full history
Human errors common Consistent every time
Hard to destroy everything One command destroys everything

Benefits:


3. Installation

Linux/Mac

brew install terraform

Manual (any OS)

  1. Download from https://developer.hashicorp.com/terraform/downloads
  2. Unzip and move the binary to a folder in your PATH (e.g., /usr/local/bin)

Verify

terraform -version

4. Basic Terraform Workflow

Terraform always follows these steps:

Write Code  →  terraform init  →  terraform plan  →  terraform apply  →  terraform destroy
Command What it does
terraform init Downloads providers/plugins, sets up working directory
terraform plan Shows what will be created/changed/deleted (dry run)
terraform apply Actually creates/updates the infrastructure
terraform destroy Deletes everything Terraform created

Mind it: Always run plan before apply to see what's changing. Never skip this in production.


5. Terraform File Structure

A typical project has these files:

project/
├── main.tf          # main resources
├── variables.tf     # input variable definitions
├── outputs.tf       # output values
├── providers.tf     # provider configuration
├── terraform.tfvars # variable values
└── versions.tf      # terraform & provider version constraints

All .tf files in the same folder are automatically combined by Terraform — file names don't matter, but this naming convention is standard practice.


6. Providers

A provider is a plugin that lets Terraform talk to a specific cloud (AWS, Azure, GCP, etc.)

# providers.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-south-1"
}

7. Resources

A resource is the actual infrastructure object you want to create — server, database, bucket, etc.

resource "aws_instance" "my_server" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"

  tags = {
    Name = "MyFirstServer"
  }
}

Syntax breakdown:

resource "<PROVIDER_TYPE>" "<LOCAL_NAME>" {
  argument = value
}

8. Variables

Variables make your code reusable instead of hardcoding values.

# variables.tf
variable "instance_type" {
  description = "Type of EC2 instance"
  type        = string
  default     = "t2.micro"
}

variable "region" {
  type    = string
  default = "ap-south-1"
}

Use it in main.tf:

resource "aws_instance" "my_server" {
  ami           = "ami-0abcdef1234567890"
  instance_type = var.instance_type
}

Set values 3 ways:

  1. terraform.tfvars file:
instance_type = "t3.medium"
  1. Command line:
terraform apply -var="instance_type=t3.medium"
  1. Environment variable:
export TF_VAR_instance_type="t3.medium"

9. Outputs

Outputs show useful information after apply — like the IP address of a created server.

# outputs.tf
output "instance_ip" {
  description = "Public IP of EC2 instance"
  value       = aws_instance.my_server.public_ip
}

After apply, you'll see:

Outputs:
instance_ip = "13.234.56.78"

View again anytime:

terraform output
terraform output instance_ip

10. Data Sources

data blocks let you read/fetch existing infrastructure info without managing it.

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
}

Use case: Getting latest AMI ID, existing VPC ID, existing subnet ID, etc.


11. State File

Terraform keeps track of everything it creates in a file called terraform.tfstate.

terraform state list          # list all resources in state
terraform state show <resource>  # show details of one resource

⚠️ Local state (default) is dangerous for teams — only one person can work at a time, and if the file is lost, Terraform "forgets" everything it created. This is why production uses remote backend (next section).


12. Remote Backend (Production Must)

Instead of storing terraform.tfstate on your laptop, store it in a shared remote location like AWS S3.

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "project/terraform.tfstate"
    region         = "ap-south-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Why this matters in production:

After adding/changing backend config, run:

terraform init -reconfigure

13. State Locking

When someone runs terraform apply, Terraform locks the state so no one else can run apply at the same time (prevents conflicts/corruption).

terraform force-unlock <LOCK_ID>

⚠️ Only use force-unlock if you're 100% sure no one else is running apply.


14. Modules

A module is a reusable, self-contained group of .tf files — like a function in programming.

Folder structure:

modules/
└── ec2-instance/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf

main.tf  (root - calls the module)

modules/ec2-instance/main.tf

resource "aws_instance" "this" {
  ami           = var.ami
  instance_type = var.instance_type
  tags = {
    Name = var.name
  }
}

modules/ec2-instance/variables.tf

variable "ami" {}
variable "instance_type" {}
variable "name" {}

Root main.tf (calling the module)

module "web_server" {
  source        = "./modules/ec2-instance"
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"
  name          = "WebServer"
}

Why use modules:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
  # ...inputs
}

15. Workspaces

Workspaces let you use the same code to manage multiple environments with separate state files.

terraform workspace new dev
terraform workspace new prod
terraform workspace list
terraform workspace select dev

In code, reference current workspace:

resource "aws_instance" "server" {
  instance_type = terraform.workspace == "prod" ? "t3.large" : "t2.micro"
}

⚠️ Note: Many production teams prefer separate folders per environment (covered in section 24) over workspaces, because workspaces share the same backend config and can be risky if mistakes happen (e.g., applying prod changes while in dev workspace by accident).


16. Provisioners

Provisioners run scripts/commands on a resource after creation. Use only as last resort — prefer cloud-init, user_data, or configuration management tools (Ansible) instead.

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"

  provisioner "remote-exec" {
    inline = [
      "sudo apt update",
      "sudo apt install -y nginx"
    ]
  }
}

Common types:


17. Functions & Expressions

Terraform has built-in functions for strings, numbers, lists, maps, etc.

# String functions
upper("hello")            # "HELLO"
lower("HELLO")             # "hello"
join(",", ["a","b","c"])   # "a,b,c"
split(",", "a,b,c")        # ["a","b","c"]

# Numeric
max(5, 10, 3)              # 10
min(5, 10, 3)              # 3

# Collections
length(["a","b","c"])      # 3
contains(["a","b"], "a")   # true

# File
file("user_data.sh")       # reads file content as string

# Type conversion
tostring(123)
tonumber("123")

18. Loops: count, for_each, for

count — create multiple copies of a resource

resource "aws_instance" "server" {
  count         = 3
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"

  tags = {
    Name = "Server-${count.index}"
  }
}

for_each — create resources from a map/set (better when items have unique names)

variable "servers" {
  default = {
    web = "t2.micro"
    db  = "t3.medium"
  }
}

resource "aws_instance" "server" {
  for_each      = var.servers
  ami           = "ami-0abcdef1234567890"
  instance_type = each.value

  tags = {
    Name = each.key
  }
}

for — transform lists/maps (in expressions, not resources)

variable "names" {
  default = ["web", "db", "cache"]
}

output "upper_names" {
  value = [for n in var.names : upper(n)]
}
# Output: ["WEB", "DB", "CACHE"]

count vs for_each: Use for_each when items can be identified by unique keys (avoids issues when removing items from middle of a list with count).


19. Conditional Expressions

Terraform's ternary operator: condition ? true_value : false_value

variable "environment" {
  default = "dev"
}

resource "aws_instance" "server" {
  ami           = "ami-0abcdef1234567890"
  instance_type = var.environment == "prod" ? "t3.large" : "t2.micro"
}

Conditionally create a resource (using count):

resource "aws_instance" "extra_server" {
  count         = var.environment == "prod" ? 1 : 0
  ami           = "ami-0abcdef1234567890"
  instance_type = "t2.micro"
}

20. Terraform Import

If a resource was created manually (outside Terraform), you can bring it under Terraform's management.

terraform import aws_instance.my_server i-0abcd1234efgh5678

Steps:

  1. Write a matching resource block in your .tf file (can be empty initially)
  2. Run terraform import <resource_address> <real_resource_id>
  3. Run terraform plan and adjust your code until plan shows no changes

21. Terraform Commands Cheat Sheet

Command Purpose
terraform init Initialize working directory, download providers
terraform validate Check syntax errors
terraform fmt Auto-format code nicely
terraform plan Preview changes
terraform apply Apply changes
terraform apply -auto-approve Apply without confirmation prompt
terraform destroy Delete all managed resources
terraform show Show current state in readable format
terraform state list List resources in state
terraform output Show output values
terraform graph Generate dependency graph
terraform taint <resource> Mark resource for recreation on next apply
terraform refresh Sync state with real infrastructure
terraform workspace list List workspaces

22. Production Best Practices

  1. Always use remote backend (S3 + DynamoDB, or Terraform Cloud)
  2. Never commit .tfstate or .tfvars with secrets to Git
  3. Use .gitignore:
    *.tfstate
    *.tfstate.backup
    .terraform/
    *.tfvars
    
  4. Pin provider/module versions (~> 5.0) — avoid surprise breaking changes
  5. Always run terraform plan and review before apply
  6. Use modules for reusable, DRY (Don't Repeat Yourself) code
  7. Use separate state files per environment (dev/staging/prod)
  8. Use remote variable/secret stores (AWS Secrets Manager, Vault) — not hardcoded passwords
  9. Tag all resources (Environment, Owner, Project) for cost tracking
  10. Use CI/CD pipelines for apply — avoid manual applies from laptops in prod
  11. Enable state file versioning (S3 versioning) for rollback capability
  12. Use terraform fmt and terraform validate in CI before merging code
  13. Limit blast radius — don't put everything in one giant state file; split by component (network, database, app)
  14. Use policy-as-code (Sentinel/OPA) for compliance checks in larger orgs

23. Folder Structure for Real Projects

terraform-project/
├── modules/
│   ├── vpc/
│   ├── ec2/
│   ├── rds/
│   └── s3/
│
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       ├── terraform.tfvars
│       └── backend.tf
│
└── README.md

Each environment folder calls the shared modules with different variable values.


24. Multi-Environment Setup (Dev/Staging/Prod)

environments/dev/main.tf

module "vpc" {
  source   = "../../modules/vpc"
  cidr     = "10.0.0.0/16"
  env_name = "dev"
}

module "ec2" {
  source        = "../../modules/ec2"
  instance_type = "t2.micro"
  env_name      = "dev"
}

environments/prod/main.tf

module "vpc" {
  source   = "../../modules/vpc"
  cidr     = "10.1.0.0/16"
  env_name = "prod"
}

module "ec2" {
  source        = "../../modules/ec2"
  instance_type = "t3.large"
  env_name      = "prod"
}

environments/dev/backend.tf

terraform {
  backend "s3" {
    bucket = "my-tf-state"
    key    = "dev/terraform.tfstate"
    region = "ap-south-1"
  }
}

environments/prod/backend.tf

terraform {
  backend "s3" {
    bucket = "my-tf-state"
    key    = "prod/terraform.tfstate"
    region = "ap-south-1"
  }
}

Result: Same module code, different state files, different sizes/configs per environment. Applying to dev never affects prod.


25. Secrets Management

Never do this:

# BAD - hardcoded password
resource "aws_db_instance" "db" {
  password = "MySuperSecret123"
}

Better options:

Option 1: Environment variables

export TF_VAR_db_password="MySuperSecret123"
variable "db_password" {
  type      = string
  sensitive = true
}

Option 2: AWS Secrets Manager (best for production)

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/db/password"
}

resource "aws_db_instance" "db" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
}

Mark variables as sensitive

variable "db_password" {
  type      = string
  sensitive = true  # hides value in plan/apply output
}

26. CI/CD with Terraform

Example using GitHub Actions:

# .github/workflows/terraform.yml
name: Terraform CI/CD

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        run: terraform init
        working-directory: environments/prod

      - name: Terraform Format Check
        run: terraform fmt -check
        working-directory: environments/prod

      - name: Terraform Validate
        run: terraform validate
        working-directory: environments/prod

      - name: Terraform Plan
        run: terraform plan
        working-directory: environments/prod

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve
        working-directory: environments/prod

Flow in real teams:

  1. Developer creates PR with Terraform changes
  2. CI runs fmt, validate, plan automatically — plan output posted as PR comment
  3. Team reviews plan output
  4. On merge to main, CI runs apply automatically (or with manual approval gate for prod)

27. Terraform Cloud / Enterprise

Terraform Cloud (by HashiCorp) is a managed service that provides:

terraform {
  cloud {
    organization = "my-company"
    workspaces {
      name = "prod-infra"
    }
  }
}

28. Common Errors & Fixes

Error Cause Fix
Error: No valid credential sources found AWS credentials not set Run aws configure or set env vars
Error acquiring the state lock Previous apply crashed, lock stuck terraform force-unlock <ID> (be careful)
Resource already exists Resource created outside Terraform Use terraform import
Error: Provider configuration not present Forgot required_providers block Add provider config and run init
Plan shows changes every time (drift) Resource changed manually outside Terraform Run terraform refresh or fix manually then re-apply
Backend configuration changed Modified backend.tf Run terraform init -reconfigure

29. Practice Project Example

A simple complete project to practice — creates an EC2 instance with a security group:

main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.region
}

resource "aws_security_group" "web_sg" {
  name        = "web-sg"
  description = "Allow HTTP and SSH"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  tags = {
    Name        = "web-server-${var.environment}"
    Environment = var.environment
  }
}

variables.tf

variable "region" {
  default = "ap-south-1"
}

variable "ami_id" {
  default = "ami-0abcdef1234567890"
}

variable "instance_type" {
  default = "t2.micro"
}

variable "environment" {
  default = "dev"
}

outputs.tf

output "instance_public_ip" {
  value = aws_instance.web.public_ip
}

output "security_group_id" {
  value = aws_security_group.web_sg.id
}

Run it:

terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
# ... test it ...
terraform destroy

Quick Recap (Beginner → Advanced Path)

  1. ✅ Install Terraform, run init/plan/apply/destroy
  2. ✅ Learn providers, resources, variables, outputs
  3. ✅ Understand state file and why it matters
  4. ✅ Move to remote backend (S3 + DynamoDB)
  5. ✅ Learn modules — write reusable code
  6. ✅ Learn loops (count, for_each) and conditionals
  7. ✅ Set up multi-environment structure (dev/staging/prod)
  8. ✅ Add secrets management (Secrets Manager/Vault)
  9. ✅ Integrate with CI/CD (GitHub Actions/GitLab CI)
  10. ✅ Explore Terraform Cloud for team collaboration & policy enforcement

Happy Terraforming! 🚀