Terraform - Complete Beginner's Guide

Terraform - Complete Beginner's Guide

Table of Contents

  1. What is Terraform?
  2. Why Use Terraform?
  3. Core Concepts
  4. Installation
  5. Your First Terraform Project
  6. Terraform Language Basics (HCL)
  7. Essential Commands
  8. Working with AWS
  9. State Management
  10. Best Practices
  11. Common Patterns
  12. Troubleshooting
  13. Glossary

What is Terraform?

Terraform is a tool that lets you describe your infrastructure (servers, databases, networks) in code, and then automatically creates, updates, or deletes those resources for you.

Real-World Analogy

Think of Terraform like a blueprint for a house:

  • Without Terraform: You manually tell the construction crew every detail, one step at a time. If you want to build the same house again, you have to repeat everything.
  • With Terraform: You create a blueprint once. Terraform reads the blueprint and builds exactly what you described. Want another identical house? Use the same blueprint!

Infrastructure as Code (IaC)

Terraform is part of a concept called Infrastructure as Code:

Traditional Infrastructure Infrastructure as Code
Click buttons in a console Write code in files
Hard to repeat exactly Easy to repeat perfectly
No version history Full version control with Git
"It worked on my machine" Same result everywhere
Documentation gets outdated Code IS the documentation

Why Use Terraform?

Key Benefits

Without Terraform vs With Terraform

Scenario Without Terraform With Terraform
Create 50 servers Click 50 times, prone to mistakes Run one command
Update all servers Update each one manually Change code, run command
Recreate environment Hope you remember all the steps Run the same code
Audit changes Check CloudTrail logs, guess who did what Check Git history
Share with team Write documentation (often outdated) Share the code

Core Concepts

1. Providers

Providers are plugins that let Terraform talk to cloud platforms or other services.

Example: To create AWS resources, you need the AWS provider:

# This tells Terraform to use the AWS provider
provider "aws" {
  region = "us-east-1"
}

2. Resources

Resources are the infrastructure components you want to create.

Example: Creating an EC2 instance:

# This creates a virtual server
resource "aws_instance" "my_server" {
  ami           = "ami-0c55b159cbfafe1f0"  # Amazon Linux 2
  instance_type = "t2.micro"               # Server size

  tags = {
    Name = "MyFirstServer"
  }
}

3. State

State is how Terraform keeps track of what it has created.

Important: The state file is critical! It's how Terraform knows what already exists.

4. Plan and Apply

Terraform uses a two-step process:

  1. Plan: Shows what Terraform WILL do (create, modify, delete)
  2. Apply: Actually makes the changes

This is like a "preview" before committing - you can see exactly what will happen before it happens.

5. Modules

Modules are reusable packages of Terraform code.

Think of modules like LEGO blocks - pre-built pieces you can combine to build bigger things.


Installation

Windows

# Option 1: Using Chocolatey (recommended)
choco install terraform

# Option 2: Using Winget
winget install HashiCorp.Terraform

# Option 3: Manual download
# 1. Go to https://www.terraform.io/downloads
# 2. Download the Windows ZIP
# 3. Extract to a folder (e.g., C:\terraform)
# 4. Add that folder to your PATH

macOS

# Using Homebrew (recommended)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

Linux

# Ubuntu/Debian
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common

wget -O- https://apt.releases.hashicorp.com/gpg | \
    gpg --dearmor | \
    sudo tee /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

Verify Installation

terraform --version
# Should output something like: Terraform v1.6.0

Your First Terraform Project

Let's create a simple project that creates an S3 bucket in AWS.

Project Structure

my-first-terraform/
├── main.tf          # Main configuration
├── variables.tf     # Input variables
├── outputs.tf       # Output values
└── terraform.tfvars # Variable values (don't commit secrets!)

Step 1: Create the Project Folder

mkdir my-first-terraform
cd my-first-terraform

Step 2: Create main.tf

# main.tf - The main configuration file

# Tell Terraform we're using AWS
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure the AWS provider
provider "aws" {
  region = var.aws_region
}

# Create an S3 bucket
resource "aws_s3_bucket" "my_bucket" {
  bucket = var.bucket_name

  tags = {
    Name        = "My First Terraform Bucket"
    Environment = "Learning"
  }
}

# Make the bucket private (best practice)
resource "aws_s3_bucket_public_access_block" "my_bucket_access" {
  bucket = aws_s3_bucket.my_bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Step 3: Create variables.tf

# variables.tf - Define input variables

variable "aws_region" {
  description = "The AWS region to create resources in"
  type        = string
  default     = "us-east-1"
}

variable "bucket_name" {
  description = "The name of the S3 bucket (must be globally unique)"
  type        = string
}

Step 4: Create outputs.tf

# outputs.tf - Define what information to display after apply

output "bucket_name" {
  description = "The name of the created bucket"
  value       = aws_s3_bucket.my_bucket.id
}

output "bucket_arn" {
  description = "The ARN of the created bucket"
  value       = aws_s3_bucket.my_bucket.arn
}

Step 5: Create terraform.tfvars

# terraform.tfvars - Set variable values

aws_region  = "us-east-1"
bucket_name = "my-unique-bucket-name-12345"  # Must be globally unique!

Step 6: Run Terraform!

# Step 1: Initialize (download providers)
terraform init

# Step 2: See what will be created
terraform plan

# Step 3: Create the resources
terraform apply

# Type "yes" when prompted

# Step 4: When done, destroy the resources (optional)
terraform destroy

Terraform Language Basics (HCL)

Terraform uses HCL (HashiCorp Configuration Language). It's designed to be human-readable.

Basic Syntax

# This is a comment

# Block syntax
block_type "block_label" "block_name" {
  argument1 = "value1"
  argument2 = 123

  nested_block {
    nested_argument = "nested_value"
  }
}

Data Types

# String
name = "my-server"

# Number
count = 3

# Boolean
enabled = true

# List (array)
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

# Map (dictionary)
tags = {
  Name        = "MyServer"
  Environment = "Production"
}

Variables

# Defining a variable
variable "instance_type" {
  description = "The type of EC2 instance"
  type        = string
  default     = "t2.micro"
}

# Using a variable
resource "aws_instance" "example" {
  instance_type = var.instance_type
}

Variable Types

# String
variable "name" {
  type = string
}

# Number
variable "instance_count" {
  type = number
}

# Boolean
variable "enable_monitoring" {
  type = bool
}

# List of strings
variable "availability_zones" {
  type = list(string)
}

# Map of strings
variable "tags" {
  type = map(string)
}

# Object with specific structure
variable "server_config" {
  type = object({
    name          = string
    instance_type = string
    disk_size     = number
  })
}

Locals

Locals are like variables, but computed within your configuration:

locals {
  # Combine values
  full_name = "${var.project_name}-${var.environment}"

  # Common tags to apply everywhere
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

# Using locals
resource "aws_instance" "example" {
  tags = local.common_tags
}

Expressions and Functions

# Conditional expression
instance_type = var.environment == "production" ? "t2.large" : "t2.micro"

# String interpolation
name = "server-${var.environment}"

# Common functions
upper_name = upper(var.name)              # "HELLO"
list_length = length(var.my_list)         # 3
joined = join("-", var.my_list)           # "a-b-c"
first_item = element(var.my_list, 0)      # "a"

For Loops

# Create multiple resources
resource "aws_instance" "servers" {
  count = 3

  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    Name = "server-${count.index}"  # server-0, server-1, server-2
  }
}

# For each with a map
resource "aws_instance" "named_servers" {
  for_each = {
    web   = "t2.micro"
    api   = "t2.small"
    db    = "t2.medium"
  }

  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = each.value

  tags = {
    Name = each.key  # web, api, db
  }
}

Essential Commands

Command Reference

Command What It Does When to Use
terraform init Downloads providers and modules First time, or after adding providers
terraform validate Checks syntax is correct Before planning
terraform fmt Formats code nicely Before committing
terraform plan Shows what will change Before applying
terraform apply Creates/updates resources When ready to deploy
terraform destroy Deletes all resources When cleaning up
terraform show Shows current state To see what exists
terraform output Shows output values To get resource info
terraform state list Lists all resources in state To see tracked resources

Command Examples

# Initialize a new or existing project
terraform init

# Check if code is valid
terraform validate

# Format all .tf files
terraform fmt

# Preview changes without making them
terraform plan

# Save the plan to a file
terraform plan -out=myplan.tfplan

# Apply a saved plan (no confirmation needed)
terraform apply myplan.tfplan

# Apply with auto-approve (skip confirmation)
terraform apply -auto-approve

# Destroy specific resource
terraform destroy -target=aws_instance.my_server

# Refresh state from real infrastructure
terraform refresh

# Import existing resource into state
terraform import aws_instance.my_server i-1234567890abcdef0

Working with AWS

Setting Up AWS Credentials

Terraform needs credentials to access your AWS account:

# Windows PowerShell
$env:AWS_ACCESS_KEY_ID="your-access-key"
$env:AWS_SECRET_ACCESS_KEY="your-secret-key"

# Linux/macOS
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"

Method 2: AWS CLI Profile

# First, configure AWS CLI
aws configure

# Then in Terraform
provider "aws" {
  region  = "us-east-1"
  profile = "default"  # or your profile name
}
# DON'T DO THIS - credentials in code!
provider "aws" {
  region     = "us-east-1"
  access_key = "AKIA..."  # Never commit this!
  secret_key = "..."      # Never commit this!
}

Common AWS Resources

EC2 Instance (Virtual Server)

# Create a simple EC2 instance
resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"  # Amazon Linux 2
  instance_type = "t2.micro"

  # Enable public IP
  associate_public_ip_address = true

  # User data script (runs on first boot)
  user_data = <<-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl start httpd
              systemctl enable httpd
              echo "Hello from Terraform!" > /var/www/html/index.html
              EOF

  tags = {
    Name = "WebServer"
  }
}

VPC (Virtual Network)

# Create a VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "main-vpc"
  }
}

# Create a public subnet
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet"
  }
}

# Create an Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw"
  }
}

Security Group (Firewall)

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow web traffic"
  vpc_id      = aws_vpc.main.id

  # Allow HTTP from anywhere
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Allow HTTPS from anywhere
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Allow SSH from your IP only
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["YOUR.IP.ADDRESS/32"]
  }

  # Allow all outbound traffic
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "web-security-group"
  }
}

S3 Bucket

resource "aws_s3_bucket" "data" {
  bucket = "my-unique-data-bucket-12345"

  tags = {
    Name = "DataBucket"
  }
}

# Enable versioning
resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id

  versioning_configuration {
    status = "Enabled"
  }
}

# Block public access
resource "aws_s3_bucket_public_access_block" "data" {
  bucket = aws_s3_bucket.data.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

RDS Database

resource "aws_db_instance" "database" {
  identifier           = "my-database"
  engine               = "mysql"
  engine_version       = "8.0"
  instance_class       = "db.t3.micro"
  allocated_storage    = 20
  storage_type         = "gp2"

  db_name              = "myapp"
  username             = "admin"
  password             = var.db_password  # Use a variable, not hardcoded!

  skip_final_snapshot  = true  # For dev only; keep snapshots in production

  tags = {
    Name = "MyDatabase"
  }
}

State Management

What is State?

The state file (terraform.tfstate) tracks:

  • What resources Terraform has created
  • The current configuration of those resources
  • Relationships between resources

Local vs Remote State

Setting Up Remote State (S3)

# First, create the S3 bucket and DynamoDB table manually or in a separate config

# Then configure the backend
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "project/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"  # For state locking
  }
}

State Commands

# List all resources in state
terraform state list

# Show details of a specific resource
terraform state show aws_instance.my_server

# Remove a resource from state (doesn't delete the actual resource)
terraform state rm aws_instance.my_server

# Move a resource to a new name
terraform state mv aws_instance.old_name aws_instance.new_name

# Pull remote state to local
terraform state pull

# Push local state to remote
terraform state push

Best Practices

1. Project Structure

project/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── production/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
├── modules/
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── ec2/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── README.md

2. Naming Conventions

# Use lowercase with underscores
resource "aws_instance" "web_server" { }    # ✅ Good
resource "aws_instance" "WebServer" { }     # ❌ Bad
resource "aws_instance" "web-server" { }    # ❌ Bad

# Be descriptive
resource "aws_s3_bucket" "application_logs" { }  # ✅ Good
resource "aws_s3_bucket" "bucket1" { }           # ❌ Bad

3. Use Variables Wisely

# Good: Use variables for things that change
variable "environment" {
  type = string
}

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

# Bad: Hardcoding values
resource "aws_instance" "web" {
  instance_type = "t2.micro"  # Should be a variable
}

4. Version Constraints

terraform {
  required_version = ">= 1.0.0, < 2.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"  # Allows 5.x but not 6.0
    }
  }
}

5. Security Practices

6. .gitignore for Terraform

# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log
crash.*.log

# Exclude all .tfvars files, which might contain secrets
*.tfvars
*.tfvars.json

# Ignore override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Ignore CLI configuration files
.terraformrc
terraform.rc

Common Patterns

Pattern 1: Multi-Environment Setup

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

locals {
  env_config = {
    dev = {
      instance_type = "t2.micro"
      instance_count = 1
    }
    staging = {
      instance_type = "t2.small"
      instance_count = 2
    }
    production = {
      instance_type = "t2.large"
      instance_count = 3
    }
  }

  config = local.env_config[var.environment]
}

resource "aws_instance" "app" {
  count         = local.config.instance_count
  instance_type = local.config.instance_type
  # ...
}

Pattern 2: Using Modules

# Using a module
module "vpc" {
  source = "./modules/vpc"

  vpc_cidr     = "10.0.0.0/16"
  environment  = var.environment
}

module "web_servers" {
  source = "./modules/ec2"

  instance_count = 3
  instance_type  = "t2.micro"
  subnet_id      = module.vpc.public_subnet_id
  security_group = module.vpc.web_security_group_id
}

Pattern 3: Data Sources (Reading Existing Resources)

# Get the latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

# Use the AMI
resource "aws_instance" "web" {
  ami = data.aws_ami.amazon_linux.id
  # ...
}

Pattern 4: Dynamic Blocks

variable "ingress_rules" {
  type = list(object({
    port        = number
    description = string
  }))
  default = [
    { port = 80, description = "HTTP" },
    { port = 443, description = "HTTPS" },
  ]
}

resource "aws_security_group" "dynamic" {
  name = "dynamic-sg"

  dynamic "ingress" {
    for_each = var.ingress_rules

    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = ingress.value.description
    }
  }
}

Troubleshooting

Common Errors and Solutions

Error: Provider not installed

Error: Could not load plugin

Solution: Run terraform init

Error: Resource already exists

Error: Error creating S3 bucket: BucketAlreadyExists

Solution:

  • Use a unique name
  • Or import the existing resource: terraform import aws_s3_bucket.my_bucket my-bucket-name

Error: State lock

Error: Error locking state: ConditionalCheckFailedException

Solution:

  • Wait for the other operation to complete
  • Or force unlock (careful!): terraform force-unlock LOCK_ID

Error: Cycle detected

Error: Cycle: aws_instance.a, aws_instance.b

Solution: Break the circular dependency by using depends_on or restructuring

Debugging Tips

# Enable detailed logging
export TF_LOG=DEBUG
terraform apply

# Log to a file
export TF_LOG_PATH=./terraform.log

# Only log specific components
export TF_LOG=TRACE
export TF_LOG_CORE=TRACE
export TF_LOG_PROVIDER=TRACE

Glossary

Term Definition
Apply Command that creates/updates resources to match your configuration
Backend Where Terraform stores its state (local or remote)
Data Source A way to query existing resources or external data
Destroy Command that deletes all resources managed by Terraform
HCL HashiCorp Configuration Language - Terraform's syntax
Init Command that initializes a Terraform project
Module A reusable package of Terraform configuration
Output Values exported from your Terraform configuration
Plan Command that shows what changes Terraform will make
Provider Plugin that lets Terraform interact with APIs
Resource An infrastructure component managed by Terraform
State Terraform's record of managed resources
Variable Input parameters for your Terraform configuration

Quick Reference Card

┌─────────────────────────────────────────────────────────────┐
│                    TERRAFORM CHEAT SHEET                     │
├─────────────────────────────────────────────────────────────┤
│ COMMANDS                                                     │
│   terraform init      - Initialize project                   │
│   terraform plan      - Preview changes                      │
│   terraform apply     - Apply changes                        │
│   terraform destroy   - Delete all resources                 │
│   terraform fmt       - Format code                          │
│   terraform validate  - Check syntax                         │
├─────────────────────────────────────────────────────────────┤
│ FILES                                                        │
│   main.tf            - Main configuration                    │
│   variables.tf       - Variable definitions                  │
│   outputs.tf         - Output definitions                    │
│   terraform.tfvars   - Variable values                       │
│   terraform.tfstate  - State file (don't edit!)             │
├─────────────────────────────────────────────────────────────┤
│ SYNTAX                                                       │
│   resource "type" "name" { }  - Create a resource           │
│   variable "name" { }         - Define a variable           │
│   output "name" { }           - Define an output            │
│   module "name" { }           - Use a module                │
│   data "type" "name" { }      - Query existing data         │
│   var.name                    - Reference a variable        │
│   local.name                  - Reference a local value     │
└─────────────────────────────────────────────────────────────┘

Next Steps

  1. Practice: Build simple projects (S3 bucket, EC2 instance)
  2. Learn Modules: Create reusable modules
  3. Remote State: Set up S3 backend for team collaboration
  4. CI/CD: Integrate Terraform with GitHub Actions or GitLab CI
  5. Explore: Check out Terraform Registry for modules and providers

Remember: Start simple! Create one resource, understand how it works, then build more complex configurations. The best way to learn Terraform is by doing.

See Also

  • AWS.md - Understanding AWS services that Terraform manages