Skip to main content

Terraform Complete Tutorial:

 

Terraform Complete Tutorial: From Zero to Production

📅 Published: June 2026
⏱️ Estimated Reading Time: 35 minutes
🏷️ Tags: Terraform, Infrastructure as Code, DevOps, Complete Tutorial, IaC


Introduction: What is Terraform?

Terraform is an open-source Infrastructure as Code (IaC) tool created by HashiCorp. It allows you to define and provision cloud infrastructure using a simple, declarative configuration language called HCL (HashiCorp Configuration Language).

Why Terraform matters:

  • You write code, not click buttons in cloud consoles

  • Your infrastructure is versioned in Git

  • Changes are reviewed via pull requests

  • You can destroy and recreate everything reliably

  • Works across AWS, Azure, GCP, and 1000+ providers

Real-world example: Instead of clicking through AWS Console to create a server, you write:

hcl
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}

Then run terraform apply and the server appears.


Part 1: Installation and Setup

Install Terraform

macOS:

bash
brew install terraform

Linux (Ubuntu/Debian):

bash
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

Windows (Chocolatey):

powershell
choco install terraform

Verify installation:

bash
terraform version

Configure AWS Credentials

Create an IAM user in AWS with programmatic access. Then set environment variables:

bash
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_DEFAULT_REGION="us-east-1"

Part 2: Your First Terraform Configuration

The Core Commands

bash
terraform init      # Downloads providers, initializes working directory
terraform plan      # Shows what will change (preview)
terraform apply     # Creates/modifies infrastructure
terraform destroy   # Deletes all resources
terraform fmt       # Formats code consistently
terraform validate  # Checks syntax

Your First Configuration

Create a file called main.tf:

hcl
# main.tf - My first Terraform configuration

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

provider "aws" {
  region = "us-east-1"
}

# Create an S3 bucket
resource "aws_s3_bucket" "my_first_bucket" {
  bucket = "my-unique-bucket-name-12345"
  
  tags = {
    Name        = "My First Terraform Bucket"
    Environment = "Learning"
    ManagedBy   = "Terraform"
  }
}

# Output the bucket name
output "bucket_name" {
  value = aws_s3_bucket.my_first_bucket.bucket
}

Run It

bash
terraform init
terraform plan
terraform apply

Type yes when prompted. Your S3 bucket is now created!

bash
terraform destroy  # Clean up when done

Part 3: Understanding HCL Syntax

Blocks, Arguments, and Identifiers

Every Terraform file has three building blocks:

hcl
resource "aws_instance" "web_server" {  # Block type + labels
  ami           = "ami-12345"            # Argument (key = value)
  instance_type = "t2.micro"             # Argument
  
  tags = {                               # Map argument
    Name = "web-server"
  }
}

Data Types

TypeExampleWhen to Use
string"hello"Text values
number42, 3.14Numeric values
booltrue, falseYes/no settings
list["a", "b", "c"]Ordered collection
map{name = "john", age = 30}Key-value pairs
object{name = string, age = number}Structured data

Expressions

hcl
# String interpolation
bucket = "my-app-${var.environment}-data"

# Conditional
instance_type = var.env == "prod" ? "t3.large" : "t2.micro"

# Functions
subnet_ids = aws_subnet.public[*].id  # Splat expression

Part 4: Terraform State

What is State?

State is Terraform's memory. It maps your configuration to real infrastructure.

json
// terraform.tfstate (simplified)
{
  "resources": [
    {
      "type": "aws_instance",
      "name": "web",
      "instances": [{
        "attributes": {
          "id": "i-1234567890abcdef0",
          "public_ip": "54.123.45.67"
        }
      }]
    }
  ]
}

Remote State (ALWAYS use this for teams)

hcl
# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-company-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-locks"
    encrypt        = true
  }
}

Why remote state matters:

  • Team members share the same state

  • State locking prevents corruption

  • Encrypted and backed up

  • Auditable via CloudTrail

State Commands

bash
terraform state list                    # List all resources
terraform state show aws_instance.web   # Show resource details
terraform state mv old_name new_name    # Rename resource in state
terraform state rm aws_s3_bucket.old    # Remove from state (doesn't delete)

Part 5: Variables and Outputs

Input Variables

hcl
# variables.tf
variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "dev"
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  # No default = required
}

variable "tags" {
  description = "Resource tags"
  type        = map(string)
  default = {
    Environment = "dev"
    ManagedBy   = "Terraform"
  }
}

# With validation
variable "environment" {
  type = string
  
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

# Sensitive variable (for secrets)
variable "db_password" {
  type      = string
  sensitive = true
}

Setting variable values:

bash
# Command line
terraform apply -var="instance_type=t3.micro"

# Environment variable
export TF_VAR_instance_type="t3.micro"

# terraform.tfvars file (gitignored)
instance_type = "t3.micro"
environment   = "production"

# terraform.tfvars.example (committed)
instance_type = "t2.micro"  # Example value
environment   = "dev"

Output Values

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

output "bucket_arn" {
  value = aws_s3_bucket.data.arn
}

output "instance_ids" {
  value = aws_instance.web[*].id  # List of IDs
}

# Sensitive output
output "db_password" {
  value     = random_password.db.result
  sensitive = true
}

Local Values (Internal Calculations)

hcl
locals {
  # Derived values
  name_prefix = "${var.project}-${var.environment}"
  common_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
    Project     = var.project
  }
  
  # Conditional logic
  instance_type = var.environment == "prod" ? "t3.large" : "t2.micro"
  instance_count = var.environment == "prod" ? 3 : 1
}

# Usage
resource "aws_instance" "web" {
  count         = local.instance_count
  instance_type = local.instance_type
  tags          = local.common_tags
}

Part 6: Loops and Conditionals

Count (Simple Loop)

hcl
# Create multiple instances
resource "aws_instance" "web" {
  count = 3
  
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  
  tags = {
    Name = "web-server-${count.index + 1}"
  }
}

# Access instances
# aws_instance.web[0], aws_instance.web[1], aws_instance.web[2]

# Conditional resource (create if true)
resource "aws_instance" "bastion" {
  count = var.create_bastion ? 1 : 0
  
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}

For_Each (Stable, Recommended)

hcl
# Create from a map
variable "users" {
  type = map(object({
    groups = list(string)
    path   = string
  }))
  default = {
    "alice" = {
      groups = ["developers", "ops"]
      path   = "/"
    }
    "bob" = {
      groups = ["developers"]
      path   = "/"
    }
  }
}

resource "aws_iam_user" "this" {
  for_each = var.users
  
  name = each.key
  path = each.value.path
}

Dynamic Blocks (For Nested Configurations)

hcl
variable "ingress_rules" {
  type = list(object({
    port        = number
    cidr_blocks = list(string)
  }))
}

resource "aws_security_group" "web" {
  name = "web-sg"
  
  dynamic "ingress" {
    for_each = var.ingress_rules
    
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

Part 7: Modules (Reusable Infrastructure)

What is a Module?

A module is a container for multiple resources. It's how you share and reuse infrastructure code.

Module Structure

text
modules/aws-vpc/
├── main.tf       # Resource definitions
├── variables.tf  # Input variables
├── outputs.tf    # Return values
└── README.md     # Documentation

Creating a Module

modules/aws-vpc/variables.tf:

hcl
variable "name" {
  description = "VPC name"
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "enable_nat_gateway" {
  description = "Enable NAT gateway"
  type        = bool
  default     = false
}

modules/aws-vpc/main.tf:

hcl
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = {
    Name = var.name
  }
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)
  
  vpc_id     = aws_vpc.this.id
  cidr_block = var.public_subnet_cidrs[count.index]
  
  tags = {
    Name = "${var.name}-public-${count.index + 1}"
  }
}

modules/aws-vpc/outputs.tf:

hcl
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.this.id
}

output "subnet_ids" {
  description = "IDs of public subnets"
  value       = aws_subnet.public[*].id
}

Using the Module

hcl
module "vpc" {
  source = "./modules/aws-vpc"
  
  name     = "production"
  vpc_cidr = "10.0.0.0/16"
}

# Reference outputs
resource "aws_instance" "web" {
  subnet_id = module.vpc.subnet_ids[0]
  # ...
}

Remote Modules

hcl
# From Terraform Registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
  
  name = "my-vpc"
  cidr = "10.0.0.0/16"
}

# From Git
module "vpc" {
  source = "git::https://github.com/company/terraform-aws-vpc.git?ref=v1.2.0"
}

Part 8: Data Sources

What are Data Sources?

Data sources read information from providers without creating anything.

hcl
# Get the latest Ubuntu AMI
data "aws_ami" "ubuntu" {
  most_recent = true
  
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
  
  owners = ["099720109477"]
}

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

# Get existing VPC
data "aws_vpc" "existing" {
  filter {
    name   = "tag:Name"
    values = ["production-vpc"]
  }
}

# Get availability zones
data "aws_availability_zones" "available" {
  state = "available"
}

Part 9: Dependencies

Implicit Dependencies (Automatic)

hcl
# Terraform knows: subnet depends on VPC
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id = aws_vpc.main.id  # ← Implicit dependency
  cidr_block = "10.0.1.0/24"
}

Explicit Dependencies (depends_on)

hcl
resource "aws_s3_bucket" "data" {
  bucket = "my-app-data"
  
  # Policy references the bucket in its ARN
  # But the policy doesn't reference the bucket directly
  depends_on = [aws_s3_bucket_policy.data_policy]
}

Part 10: Terraform Functions

Common Functions

hcl
# String functions
lower("HELLO")           # "hello"
upper("hello")           # "HELLO"
format("instance-%03d", 5)  # "instance-005"
join("-", ["a", "b", "c"])  # "a-b-c"

# List functions
length(["a", "b", "c"])  # 3
element(["a", "b", "c"], 1)  # "b"
slice(["a", "b", "c"], 0, 2)  # ["a", "b"]
concat([1, 2], [3, 4])   # [1, 2, 3, 4]

# Map functions
keys({name = "John", age = 30})   # ["name", "age"]
values({name = "John", age = 30}) # ["John", 30]
merge({a = 1}, {b = 2})           # {a = 1, b = 2}

# File functions
file("${path.module}/user_data.sh")
fileexists("${path.module}/config.json")

# JSON/YAML
jsonencode({name = "John", age = 30})
yamlencode({name = "John"})

# Network
cidrsubnet("10.0.0.0/16", 8, 2)  # "10.0.2.0/24"

Part 11: Terraform Workspaces

Managing Multiple Environments

bash
# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# Switch workspaces
terraform workspace select prod

# List workspaces
terraform workspace list

# Use workspace name in configuration
resource "aws_s3_bucket" "data" {
  bucket = "my-app-${terraform.workspace}"
}

⚠️ Warning: Workspaces share provider configuration. For different AWS accounts, use directory structure instead.


Part 12: Terraform in CI/CD

GitHub Actions Example

yaml
# .github/workflows/terraform.yml
name: Terraform

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

jobs:
  terraform:
    runs-on: ubuntu-latest
    environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3
      with:
        terraform_version: 1.6.0
    
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/terraform
        aws-region: us-west-2
    
    - name: Terraform Init
      run: terraform init
    
    - name: Terraform Plan
      id: plan
      run: terraform plan -no-color
      continue-on-error: true
    
    - name: Comment Plan on PR
      uses: actions/github-script@v6
      if: github.event_name == 'pull_request'
      with:
        script: |
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: `#### Terraform Plan 📖\n\`\`\`\n${process.env.PLAN}\n\`\`\``
          })
      env:
        PLAN: ${{ steps.plan.outputs.stdout }}
    
    - name: Terraform Apply
      if: github.ref == 'refs/heads/main' && github.event_name == 'push'
      run: terraform apply -auto-approve

Part 13: Security Best Practices

Never Commit Secrets

bash
# ❌ NEVER
variable "db_password" {
  default = "SuperSecret123"
}

# ✅ DO THIS
variable "db_password" {
  sensitive = true
}
# Pass via TF_VAR_db_password environment variable

Use Secrets Manager

hcl
data "aws_secretsmanager_secret" "db_password" {
  name = "prod/database/password"
}

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = data.aws_secretsmanager_secret.db_password.id
}

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

Static Analysis

bash
# Checkov - security scanning
checkov -d .

# tfsec - focused security scanner
tfsec .

# tflint - Terraform linter
tflint

Part 14: Common Commands Cheat Sheet

bash
# Setup
terraform init                      # Initialize directory
terraform init -reconfigure         # Reconfigure backend
terraform init -upgrade            # Upgrade providers

# Validation
terraform fmt                       # Format code
terraform fmt -check               # Check formatting
terraform validate                 # Validate syntax

# Planning
terraform plan                     # Show changes
terraform plan -out=tfplan         # Save plan to file
terraform plan -var="key=value"    # Set variable

# Apply
terraform apply                    # Apply changes (prompts)
terraform apply -auto-approve      # Apply without prompt
terraform apply tfplan             # Apply saved plan

# Destroy
terraform destroy                  # Delete all resources
terraform destroy -target=aws_instance.web  # Delete specific resource

# State
terraform state list               # List resources
terraform state show aws_instance.web  # Show resource
terraform state mv old new         # Move resource
terraform state rm aws_s3_bucket.old  # Remove from state

# Workspaces
terraform workspace new dev        # Create workspace
terraform workspace select dev     # Switch workspace
terraform workspace list           # List workspaces

# Output
terraform output                   # Show outputs
terraform output -json             # JSON format
terraform output instance_ip       # Specific output

Part 15: Real-World Examples

Example 1: Complete Web Application

hcl
# variables.tf
variable "environment" {
  type    = string
  default = "dev"
}

variable "instance_count" {
  type    = number
  default = 2
}

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

provider "aws" {
  region = "us-west-2"
}

# VPC
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags = { Name = "${var.environment}-vpc" }
}

# Subnets
resource "aws_subnet" "public" {
  count      = 2
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.${count.index}.0/24"
  tags = { Name = "${var.environment}-public-${count.index + 1}" }
}

# Security Group
resource "aws_security_group" "web" {
  name   = "${var.environment}-web-sg"
  vpc_id = aws_vpc.main.id
  
  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"]
  }
}

# EC2 Instances
resource "aws_instance" "web" {
  count         = var.instance_count
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.public[count.index % 2].id
  vpc_security_group_ids = [aws_security_group.web.id]
  
  tags = {
    Name = "${var.environment}-web-${count.index + 1}"
  }
}

# Data source for AMI
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

# Load Balancer
resource "aws_lb" "web" {
  name               = "${var.environment}-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.web.id]
  subnets            = aws_subnet.public[*].id
}

resource "aws_lb_target_group" "web" {
  name     = "${var.environment}-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main.id
}

resource "aws_lb_listener" "web" {
  load_balancer_arn = aws_lb.web.arn
  port              = 80
  protocol          = "HTTP"
  
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.web.arn
  }
}

resource "aws_lb_target_group_attachment" "web" {
  count            = var.instance_count
  target_group_arn = aws_lb_target_group.web.arn
  target_id        = aws_instance.web[count.index].id
  port             = 80
}

# Outputs
output "load_balancer_dns" {
  value = aws_lb.web.dns_name
}

output "instance_ips" {
  value = aws_instance.web[*].public_ip
}

Part 16: Troubleshooting Common Errors

ErrorLikely CauseSolution
Invalid credentialsAWS keys missing/wrongCheck AWS_ACCESS_KEY_ID env vars
Resource already existsName conflictUse unique names or random_string
Error acquiring state lockAnother terraform runningWait or terraform force-unlock
Invalid for_each argumentValue is list, not map/setUse tomap() or toset()
Cycle: aws_vpc.main, aws_subnet.privateCircular dependencyRestructure or use depends_on
Backend configuration changedBackend config updatedterraform init -reconfigure

Quick Reference Card

bash
# Essential commands
terraform init      # First command
terraform plan      # Always before apply
terraform apply     # Create/update
terraform destroy   # Delete everything

# Key concepts
# Block = container with { }
# Argument = name = value
# Resource = actual infrastructure
# Data source = read-only info
# Module = reusable component
# State = Terraform's memory

Learn More

Practice Terraform with hands-on exercises in our interactive labs:
https://devops.trainwithsky.com/

Comments

Popular posts from this blog

📊 Monitoring & Logging in Kubernetes – Tools like Prometheus, Grafana, and Fluentd

  Monitoring & Logging in Kubernetes – Tools like Prometheus, Grafana, and Fluentd Monitoring and logging are essential for maintaining a healthy and well-performing Kubernetes cluster. In this guide, we’ll cover why monitoring is important, key monitoring tools like Prometheus and Grafana, and logging tools like Fluentd to help you gain visibility into your cluster’s performance and logs. Shape Your Future with AI & Infinite Knowledge...!! Want to Generate Text-to-Voice, Images & Videos? http://www.ai.skyinfinitetech.com Read In-Depth Tech & Self-Improvement Blogs http://www.skyinfinitetech.com Watch Life-Changing Videos on YouTube https://www.youtube.com/@SkyInfinite-Learning Transform Your Skills, Business & Productivity – Join Us Today! 🚀 Introduction In today’s fast-paced cloud-native environment, Kubernetes has emerged as the de-facto container orchestration platform. But deploying and managing applications in Kubernetes is just half the ba...

How to Use SKY TTS: The Complete, Step-by-Step Guide for 2025

 What is SKY TTS? SKY TTS  is a free, next-generation  AI audio creation platform  that brings together high-quality  Text-to-Speech ,  Speech-to-Text , and a full suite of professional  audio editing tools  in one seamless experience. Our vision is simple — to make advanced audio technology  free, accessible, and effortless  for everyone. From creators and educators to podcasters, developers, and businesses, SKY TTS helps users produce  studio-grade voice content  without expensive software or technical skills. With support for  70+ languages, natural voices, audio enhancement, waveform generation, and batch automation , SKY TTS has become a trusted all-in-one toolkit for modern digital audio workflows. Why Choose SKY TTS? Instant Conversion:  Enjoy rapid text-to-speech generation, even with large documents. Advanced Voice Settings:   Adjust speed, pitch, and style for a personalized listening experience. Multi-...

Introduction to Terraform – The Future of Infrastructure as Code

  Introduction to Terraform – The Future of Infrastructure as Code In today’s fast-paced DevOps world, managing infrastructure manually is outdated . This is where Terraform comes in—a powerful Infrastructure as Code (IaC) tool that allows you to define, provision, and manage cloud infrastructure efficiently . Whether you're working with AWS, Azure, Google Cloud, or on-premises servers , Terraform provides a declarative, automation-first approach to infrastructure deployment. Shape Your Future with AI & Infinite Knowledge...!! Read In-Depth Tech & Self-Improvement Blogs http://www.skyinfinitetech.com Watch Life-Changing Videos on YouTube https://www.youtube.com/@SkyInfinite-Learning Transform Your Skills, Business & Productivity – Join Us Today! In today’s digital-first world, agility and automation are no longer optional—they’re essential. Companies across the globe are rapidly shifting their operations to the cloud to keep up with the pace of innovatio...