What is Terraform?
BeginnerTerraform is an open-source Infrastructure as Code (IaC) tool by HashiCorp. It lets you define cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share.
Terraform is declarative — you describe the desired end state and Terraform figures out how to achieve it. This is different from imperative tools (scripts) where you describe the steps to take.
Why Terraform?
- Multi-cloud — Works with AWS, Azure, GCP, and 3000+ providers
- Declarative — Describe what you want, not how to get there
- State management — Tracks the real state of your infrastructure
- Plan before apply — Preview changes before making them
- Modular — Reusable modules for common patterns
- Version controlled — Infrastructure changes go through code review
Installation & Setup
Beginner# Install Terraform on Ubuntu/Debian
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
# Verify
terraform version
# Enable tab completion
terraform -install-autocomplete
HCL Basics
BeginnerHashiCorp Configuration Language (HCL) is Terraform's configuration language.
# Block types in Terraform
# Provider block — configures a cloud provider
provider "aws" {
region = "us-east-1"
}
# Resource block — defines infrastructure
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "WebServer"
}
}
# Variable block — parameterizes configuration
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
# Output block — exports values
output "public_ip" {
value = aws_instance.web.public_ip
}
Your First Terraform Configuration
Beginner# main.tf — Create an EC2 instance with a security group
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_security_group" "web_sg" {
name_prefix = "web-sg-"
description = "Allow HTTP and SSH"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
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 = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
vpc_security_group_ids = [aws_security_group.web_sg.id]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Hello from Terraform!</h1>" > /var/www/html/index.html
EOF
tags = {
Name = "WebServer"
Environment = "dev"
ManagedBy = "terraform"
}
}
output "web_url" {
value = "http://${aws_instance.web.public_ip}"
}
Core Commands
Beginner# Initialize — download providers and modules
terraform init
# Format — auto-format your .tf files
terraform fmt -recursive
# Validate — check syntax and configuration
terraform validate
# Plan — preview changes (dry run)
terraform plan
# Apply — create/update infrastructure
terraform apply
# Apply with auto-approve (CI/CD)
terraform apply -auto-approve
# Show current state
terraform show
# List resources in state
terraform state list
# Destroy all resources
terraform destroy
Variables & Outputs
Intermediate# variables.tf
variable "environment" {
description = "Deployment environment"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Must be dev, staging, or production."
}
}
variable "instance_count" {
description = "Number of instances"
type = number
default = 2
}
variable "allowed_cidrs" {
description = "List of allowed CIDRs"
type = list(string)
default = ["10.0.0.0/16"]
}
variable "tags" {
description = "Resource tags"
type = map(string)
default = {
ManagedBy = "terraform"
Project = "devops-mastery"
}
}
# Using variables
resource "aws_instance" "web" {
count = var.instance_count
instance_type = var.environment == "production" ? "t3.medium" : "t3.micro"
tags = merge(var.tags, { Name = "web-${count.index + 1}" })
}
# terraform.tfvars (environment-specific values)
environment = "production"
instance_count = 3
allowed_cidrs = ["10.0.0.0/16", "172.16.0.0/12"]
# Or use -var flag
terraform apply -var="environment=staging"
# Or environment variables
export TF_VAR_environment="production"
State Management
IntermediateTerraform state maps your config to real-world resources. It's stored in terraform.tfstate by default.
Never edit the state file manually. Never commit terraform.tfstate to version control — it may contain secrets. Always use remote state in team environments.
# State commands
terraform state list # List all resources
terraform state show aws_instance.web # Show resource details
terraform state mv aws_instance.web aws_instance.app # Rename resource
terraform state rm aws_instance.web # Remove from state (doesn't destroy)
terraform state pull # Download remote state
terraform import aws_instance.web i-12345 # Import existing resource
Modules
IntermediateModules are reusable packages of Terraform configuration — the building blocks of your infrastructure.
# Module structure:
# modules/vpc/
# ├── main.tf
# ├── variables.tf
# ├── outputs.tf
# └── README.md
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.tags, {
Name = "${var.project}-vpc"
})
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = { Name = "${var.project}-public-${count.index + 1}" }
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
# Using the module in root config
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
azs = ["us-east-1a", "us-east-1b"]
project = "devops-mastery"
tags = var.tags
}
# Reference module outputs
resource "aws_instance" "web" {
subnet_id = module.vpc.public_subnet_ids[0]
}
Data Sources
IntermediateData sources let you query existing infrastructure that wasn't created by your Terraform config.
# Look up 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"]
}
}
# Get current AWS account info
data "aws_caller_identity" "current" {}
# Look up an existing VPC
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["Production-VPC"]
}
}
# Use data source results
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
subnet_id = data.aws_vpc.existing.id
}
Workspaces
AdvancedWorkspaces allow you to manage multiple environments with the same configuration.
# Create and switch workspaces
terraform workspace new staging
terraform workspace new production
terraform workspace select staging
# List workspaces
terraform workspace list
# Use workspace in configuration
resource "aws_instance" "web" {
instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"
count = terraform.workspace == "production" ? 3 : 1
tags = {
Environment = terraform.workspace
}
}
Remote State & Locking
Advanced# backend.tf — S3 + DynamoDB for remote state with locking
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# Create the backend resources first (bootstrap)
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-bucket"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Best Practices
Advanced- Use remote state with locking (S3 + DynamoDB)
- Structure your project — Separate files:
main.tf,variables.tf,outputs.tf,providers.tf - Use modules for reusable components
- Pin provider versions — Avoid unexpected breaking changes
- Use
terraform fmtandterraform validatein CI - Tag all resources for cost tracking and organization
- Use
prevent_destroylifecycle on critical resources - Never hardcode secrets — Use variables, AWS Secrets Manager, or Vault
- Keep state files small — Break large infrastructures into multiple state files
- Review plans in PRs — Use
terraform planoutput in pull request comments
CI/CD with Terraform
Advanced# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
pull_request:
paths: ['infrastructure/**']
push:
branches: [main]
paths: ['infrastructure/**']
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infrastructure
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform Init
run: terraform init
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
- name: Comment Plan on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const plan = `${{ steps.plan.outputs.stdout }}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Terraform Plan\n\`\`\`\n${plan}\n\`\`\``
});
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplan