Terraform Fundamentals
Terraform is an infrastructure as code tool that provisions and manages cloud resources through declarative configuration files. It supports multiple cloud providers and enables version-controlled, automated infrastructure management.
What is Terraform
Terraform manages infrastructure lifecycle through code, supporting AWS, Azure, GCP, and hundreds of other providers.
Core Workflow
Terraform follows a consistent three-step workflow:
# 1. Initialize working directory
terraform init
# 2. Preview changes
terraform plan
# 3. Apply changes
terraform apply
This workflow provides safety through explicit preview before making changes.
graph LR
A[Write Configuration] --> B[terraform init]
B --> C[terraform plan]
C --> D{Review Changes}
D -->|Approve| E[terraform apply]
D -->|Reject| F[Modify Code]
F --> C
E --> G[Infrastructure Updated]
style E fill:#e1f5ff
style G fill:#d4f1d4
Declarative Configuration
Terraform uses HashiCorp Configuration Language (HCL):
# main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
tags = {
Name = "web-server"
Environment = "production"
}
}
resource "aws_security_group" "web" {
name = "web-server-sg"
description = "Security group for web server"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
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"]
}
}
Terraform reads these declarations and creates the specified resources.
Terraform Basics
Understanding fundamental concepts enables effective Terraform usage.
Providers
Providers connect Terraform to infrastructure platforms:
# Configure AWS provider
provider "aws" {
region = "us-east-1"
access_key = var.aws_access_key
secret_key = var.aws_secret_key
}
# Configure Azure provider
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
}
# Configure multiple provider instances
provider "aws" {
alias = "us_west"
region = "us-west-2"
}
# Use alternate provider
resource "aws_instance" "west_coast" {
provider = aws.us_west
ami = "ami-0f87f5c6c0c8e4c0f"
instance_type = "t3.medium"
}
Providers translate Terraform configuration into API calls for specific platforms.
Resources
Resources define infrastructure components:
# VPC resource
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "main-vpc"
}
}
# Subnet resource referencing VPC
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-1a"
}
}
# Internet gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
Resource blocks have type (aws_vpc), local name (main), and configuration arguments.
Data Sources
Data sources query existing infrastructure:
# Lookup existing VPC
data "aws_vpc" "existing" {
id = "vpc-12345678"
}
# Find latest Amazon Linux AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Use data source in resource
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.medium"
subnet_id = data.aws_subnet.public.id
}
Data sources enable referencing resources not managed by the current Terraform configuration.
Variables and Outputs
Variables parameterize configurations, while outputs expose values.
Input Variables
Define reusable parameters:
# variables.tf
variable "environment" {
description = "Environment name (dev, staging, production)"
type = string
default = "dev"
}
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "Instance count must be between 1 and 10."
}
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.medium"
}
variable "allowed_cidr_blocks" {
description = "CIDR blocks allowed to access resources"
type = list(string)
default = ["10.0.0.0/16"]
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {
ManagedBy = "Terraform"
}
}
Use variables in configuration:
# main.tf
resource "aws_instance" "app" {
count = var.instance_count
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
tags = merge(
var.tags,
{
Name = "app-server-${count.index + 1}"
Environment = var.environment
}
)
}
Provide variable values multiple ways:
# Command line
terraform apply -var="environment=production" -var="instance_count=3"
# Variable file
terraform apply -var-file="production.tfvars"
# Environment variables
export TF_VAR_environment=production
export TF_VAR_instance_count=3
terraform apply
Variable file example:
# production.tfvars
environment = "production"
instance_count = 5
instance_type = "t3.large"
allowed_cidr_blocks = [
"10.0.0.0/16",
"172.16.0.0/12"
]
tags = {
ManagedBy = "Terraform"
Environment = "production"
CostCenter = "engineering"
}
Output Values
Expose information from resources:
# outputs.tf
output "instance_ids" {
description = "IDs of created instances"
value = aws_instance.app[*].id
}
output "instance_public_ips" {
description = "Public IP addresses of instances"
value = aws_instance.app[*].public_ip
}
output "load_balancer_dns" {
description = "DNS name of load balancer"
value = aws_lb.main.dns_name
}
output "database_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
sensitive = true # Hide from console output
}
View outputs:
# Show all outputs
terraform output
# Show specific output
terraform output instance_public_ips
# Output as JSON
terraform output -json
Outputs enable sharing information between Terraform configurations and external tools.
State Management
Terraform tracks infrastructure state to determine required changes.
Local State
Default state storage in local file:
# Creates terraform.tfstate
terraform apply
# State file contents (JSON)
{
"version": 4,
"terraform_version": "1.5.0",
"serial": 3,
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"instances": [
{
"attributes": {
"id": "i-0123456789abcdef0",
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.medium"
}
}
]
}
]
}
Local state works for single-user scenarios but causes issues with teams.
Remote State
Store state remotely for team collaboration:
# backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/infrastructure.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Configure backend:
# Initialize with remote backend
terraform init
# Migrate existing local state to remote
terraform init -migrate-state
Remote state benefits:
- Team Collaboration: Shared state for multiple engineers
- State Locking: Prevents concurrent modifications
- Encryption: Protects sensitive data
- Versioning: Maintains state history
sequenceDiagram
participant A as Engineer A
participant B as Engineer B
participant S3 as S3 Backend
participant DDB as DynamoDB Lock
A->>DDB: Acquire lock
DDB->>A: Lock granted
A->>S3: Read state
A->>A: Calculate changes
A->>S3: Update state
A->>DDB: Release lock
B->>DDB: Acquire lock
Note over B,DDB: Waits for A to finish
DDB->>B: Lock granted
B->>S3: Read state
State Commands
Manage state directly:
# List resources in state
terraform state list
# Show specific resource
terraform state show aws_instance.web
# Move resource to different name
terraform state mv aws_instance.web aws_instance.app
# Remove resource from state (doesn't destroy)
terraform state rm aws_instance.old
# Import existing resource
terraform import aws_instance.web i-0123456789abcdef0
# Replace resource (force recreation)
terraform apply -replace="aws_instance.web"
Modules
Modules organize and reuse Terraform code.
Creating Modules
Module structure:
modules/
└── vpc/
├── main.tf
├── variables.tf
├── outputs.tf
└── README.md
Module implementation:
# modules/vpc/variables.tf
variable "cidr_block" {
description = "CIDR block for VPC"
type = string
}
variable "environment" {
description = "Environment name"
type = string
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
}
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.cidr_block, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-${var.availability_zones[count.index]}"
Environment = var.environment
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.environment}-igw"
Environment = var.environment
}
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "cidr_block" {
description = "VPC CIDR block"
value = aws_vpc.main.cidr_block
}
Using Modules
Reference modules in configuration:
# main.tf
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
environment = "production"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Use module outputs
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.medium"
subnet_id = module.vpc.public_subnet_ids[0]
tags = {
Name = "app-server"
}
}
output "vpc_id" {
value = module.vpc.vpc_id
}
Modules from registry:
# Use public module from Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "production-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
enable_vpn_gateway = false
tags = {
Environment = "production"
}
}
Terraform Workflow
Production Terraform usage follows structured workflows.
Development Cycle
# 1. Write configuration
vim main.tf
# 2. Format code
terraform fmt
# 3. Initialize (download providers)
terraform init
# 4. Validate syntax
terraform validate
# 5. Plan changes
terraform plan -out=tfplan
# 6. Review plan output
# ... review proposed changes ...
# 7. Apply plan
terraform apply tfplan
# 8. Verify infrastructure
aws ec2 describe-instances --filters "Name=tag:Name,Values=web-server"
Workspace Management
Workspaces isolate multiple instances of infrastructure:
# List workspaces
terraform workspace list
# Create new workspace
terraform workspace new staging
# Switch workspace
terraform workspace select production
# Show current workspace
terraform workspace show
Use workspace in configuration:
locals {
environment = terraform.workspace
instance_count = {
dev = 1
staging = 2
production = 5
}
}
resource "aws_instance" "app" {
count = local.instance_count[local.environment]
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.medium"
tags = {
Name = "app-${local.environment}-${count.index + 1}"
Environment = local.environment
}
}
Drift Detection
Identify manual changes outside Terraform:
# Compare state with actual infrastructure
terraform plan -refresh-only
# Shows resources modified outside Terraform
terraform plan
# Example output:
# Note: Objects have changed outside of Terraform
#
# aws_instance.web has changed:
# ~ resource "aws_instance" "web" {
# ~ instance_type = "t3.medium" -> "t3.large"
# }
# Import manual changes to state
terraform apply -refresh-only
# Or revert to Terraform configuration
terraform apply
Common Patterns
Effective Terraform usage follows established patterns.
Resource Dependencies
Explicit dependencies:
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.medium"
# Explicit dependency
depends_on = [
aws_security_group.web,
aws_subnet.public
]
}
Implicit dependencies through references:
resource "aws_security_group" "web" {
vpc_id = aws_vpc.main.id # Implicit dependency on VPC
}
resource "aws_instance" "web" {
security_groups = [aws_security_group.web.id] # Implicit dependency
}
Conditional Resources
Create resources conditionally:
variable "enable_monitoring" {
type = bool
default = false
}
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
count = var.enable_monitoring ? 1 : 0
alarm_name = "high-cpu-alarm"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = 80
}
Dynamic Blocks
Generate repeated nested blocks:
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = [
{
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP"
},
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS"
}
]
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Security group for web servers"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
description = ingress.value.description
}
}
}
Common Pitfalls
State File Loss: Losing state files prevents Terraform from managing infrastructure. Always use remote backends with backups.
Manual Changes: Modifying infrastructure outside Terraform causes drift. Either import changes or revert them.
Large State Files: Managing too many resources in single state causes slow operations. Split into multiple configurations with separate states.
Hardcoded Values: Embedding environment-specific values prevents reuse. Use variables and modules for flexibility.
Key Takeaways
- Terraform manages infrastructure through declarative configuration, following init-plan-apply workflow for safe changes
- Providers connect Terraform to platforms, resources define infrastructure components, and data sources query existing infrastructure
- Variables parameterize configurations for reusability, outputs expose values for external consumption
- State tracks infrastructure, requiring remote backends for team collaboration with locking to prevent conflicts
- Modules organize and reuse code, supporting local modules or public registry modules for common patterns
- Production workflows include formatting, validation, planning, and applying changes with workspace isolation for environments
- Avoid state file loss with remote backends, prevent manual changes causing drift, split large configurations, and use variables instead of hardcoded values