Terraform - Complete Beginner's Guide
Terraform - Complete Beginner's Guide
Table of Contents
- What is Terraform?
- Why Use Terraform?
- Core Concepts
- Installation
- Your First Terraform Project
- Terraform Language Basics (HCL)
- Essential Commands
- Working with AWS
- State Management
- Best Practices
- Common Patterns
- Troubleshooting
- 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:
- Plan: Shows what Terraform WILL do (create, modify, delete)
- 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:
Method 1: Environment Variables (Recommended for local development)
# 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
}
Method 3: In Provider Block (NOT recommended - security risk)
# 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
- Practice: Build simple projects (S3 bucket, EC2 instance)
- Learn Modules: Create reusable modules
- Remote State: Set up S3 backend for team collaboration
- CI/CD: Integrate Terraform with GitHub Actions or GitLab CI
- 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