Terraform: Infrastructure as Code
Manage cloud infrastructure with Terraform, including provisioning, state management, and best practices. Master Infrastructure as Code for scalable deployments.
Terraform: Infrastructure as Code
Terraform is a powerful Infrastructure as Code (IaC) tool that enables you to define, provision, and manage cloud infrastructure using declarative configuration files. This comprehensive guide covers everything from basic concepts to advanced production practices.
What is Terraform?
Core Concepts
Infrastructure as Code (IaC)
- Definition: Managing infrastructure through code and configuration files
- Benefits: Version control, reproducibility, consistency, automation
- Approach: Declarative configuration describing desired state
- Tools: Terraform, CloudFormation, Pulumi, Ansible
Terraform Workflow
- Write: Define infrastructure in configuration files
- Plan: Preview changes before applying
- Apply: Create, update, or destroy infrastructure
- Destroy: Remove infrastructure when no longer needed
Key Benefits
- Multi-Cloud Support: Works with AWS, Azure, GCP, and others
- State Management: Tracks infrastructure state and changes
- Dependency Resolution: Automatically handles resource dependencies
- Modularity: Reusable modules and components
- Collaboration: Team-based infrastructure management
- Cost Optimization: Infrastructure visibility and optimization
Getting Started
Installation
Local Installation
# Download Terraform
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
# Extract and install
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Verify installation
terraform version
Package Manager Installation
# 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
# macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
Basic Configuration
Provider Configuration
# main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-west-2"
default_tags {
tags = {
Environment = "production"
Project = "my-project"
ManagedBy = "terraform"
}
}
}
Basic Resources
# Create 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 Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
# Create Subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
map_public_ip_on_launch = true
tags = {
Name = "public-subnet"
}
}
Core Concepts
Resources
Resource Syntax
resource "resource_type" "resource_name" {
# Configuration arguments
argument1 = "value1"
argument2 = "value2"
# Nested blocks
nested_block {
nested_argument = "value"
}
}
Resource Dependencies
# Explicit dependency
resource "aws_instance" "web" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t2.micro"
subnet_id = aws_subnet.public.id
depends_on = [aws_internet_gateway.main]
}
# Implicit dependency (using reference)
resource "aws_security_group" "web" {
name_prefix = "web-"
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"]
}
}
Variables
Variable Declaration
# variables.tf
variable "region" {
description = "AWS region"
type = string
default = "us-west-2"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_count" {
description = "Number of instances"
type = number
default = 2
}
variable "tags" {
description = "Common tags"
type = map(string)
default = {}
}
Variable Usage
# main.tf
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-0c02fb55956c7d316"
instance_type = "t2.micro"
subnet_id = aws_subnet.public.id
tags = merge(var.tags, {
Name = "web-instance-${count.index + 1}"
})
}
Outputs
Output Declaration
# outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = aws_subnet.public[*].id
}
output "instance_public_ips" {
description = "Public IP addresses of the instances"
value = aws_instance.web[*].public_ip
}
State Management
Local State
# Default local state
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
Remote State
S3 Backend
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Azure Backend
# backend.tf
terraform {
backend "azurerm" {
resource_group_name = "terraform-state"
storage_account_name = "terraformstate"
container_name = "tfstate"
key = "infrastructure.terraform.tfstate"
}
}
State Operations
# List resources in state
terraform state list
# Show resource details
terraform state show aws_instance.web[0]
# Move resource
terraform state mv aws_instance.web aws_instance.web_new
# Remove resource from state
terraform state rm aws_instance.web[1]
# Import existing resource
terraform import aws_instance.web i-1234567890abcdef0
Modules
Module Structure
modules/
āāā vpc/
ā āāā main.tf
ā āāā variables.tf
ā āāā outputs.tf
ā āāā versions.tf
āāā ec2/
ā āāā main.tf
ā āāā variables.tf
ā āāā outputs.tf
ā āāā versions.tf
āāā rds/
āāā main.tf
āāā variables.tf
āāā outputs.tf
āāā versions.tf
Module Definition
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = var.enable_dns_hostnames
enable_dns_support = var.enable_dns_support
tags = merge(var.tags, {
Name = var.name
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(var.tags, {
Name = "${var.name}-igw"
})
}
Module Variables
# modules/vpc/variables.tf
variable "cidr_block" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "enable_dns_hostnames" {
description = "Enable DNS hostnames"
type = bool
default = true
}
variable "enable_dns_support" {
description = "Enable DNS support"
type = bool
default = true
}
variable "name" {
description = "Name of the VPC"
type = string
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {}
}
Module Outputs
# modules/vpc/outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr_block" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "internet_gateway_id" {
description = "ID of the Internet Gateway"
value = aws_internet_gateway.main.id
}
Using Modules
# main.tf
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
name = "main-vpc"
tags = {
Environment = "production"
Project = "my-project"
}
}
module "ec2" {
source = "./modules/ec2"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
instance_count = 2
instance_type = "t2.micro"
tags = {
Environment = "production"
Project = "my-project"
}
}
Advanced Features
Data Sources
# Get latest AMI
data "aws_ami" "latest" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Get availability zones
data "aws_availability_zones" "available" {
state = "available"
}
# Use data source
resource "aws_instance" "web" {
ami = data.aws_ami.latest.id
instance_type = "t2.micro"
availability_zone = data.aws_availability_zones.available.names[0]
}
Local Values
# locals.tf
locals {
common_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "terraform"
}
name_prefix = "${var.project_name}-${var.environment}"
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = [
"10.0.1.0/24",
"10.0.2.0/24"
]
private_subnet_cidrs = [
"10.0.10.0/24",
"10.0.20.0/24"
]
}
Conditional Logic
# Conditional resources
resource "aws_instance" "web" {
count = var.create_web_instances ? var.instance_count : 0
ami = var.ami_id
instance_type = var.instance_type
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-${count.index + 1}"
})
}
# Conditional configuration
resource "aws_security_group" "web" {
name_prefix = "${local.name_prefix}-web-"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = var.allowed_cidrs
}
}
}
Loops and Iteration
# for_each loop
resource "aws_subnet" "public" {
for_each = toset(data.aws_availability_zones.available.names)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, index(data.aws_availability_zones.available.names, each.key))
availability_zone = each.key
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-${each.key}"
})
}
# count loop
resource "aws_instance" "web" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-${count.index + 1}"
})
}
Workspaces
Workspace Management
# List workspaces
terraform workspace list
# Create workspace
terraform workspace new staging
# Select workspace
terraform workspace select staging
# Show current workspace
terraform workspace show
# Delete workspace
terraform workspace delete old-workspace
Workspace-Specific Configuration
# Use workspace in configuration
resource "aws_instance" "web" {
ami = "ami-0c02fb55956c7d316"
instance_type = terraform.workspace == "prod" ? "t3.medium" : "t2.micro"
tags = {
Name = "web-${terraform.workspace}"
Environment = terraform.workspace
}
}
Best Practices
File Organization
infrastructure/
āāā environments/
ā āāā dev/
ā ā āāā main.tf
ā ā āāā variables.tf
ā ā āāā terraform.tfvars
ā āāā staging/
ā ā āāā main.tf
ā ā āāā variables.tf
ā ā āāā terraform.tfvars
ā āāā prod/
ā āāā main.tf
ā āāā variables.tf
ā āāā terraform.tfvars
āāā modules/
ā āāā vpc/
ā āāā ec2/
ā āāā rds/
āāā shared/
āāā backend.tf
āāā providers.tf
Security Best Practices
# Use data sources for sensitive information
data "aws_secretsmanager_secret" "db_password" {
name = "database-password"
}
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = data.aws_secretsmanager_secret.db_password.id
}
# Use least privilege IAM roles
resource "aws_iam_role" "ec2_role" {
name = "ec2-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
# Attach minimal required policies
resource "aws_iam_role_policy_attachment" "ec2_s3_read" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
Cost Optimization
# Use spot instances for non-critical workloads
resource "aws_spot_instance_request" "web" {
count = var.use_spot_instances ? var.instance_count : 0
ami = var.ami_id
instance_type = var.instance_type
spot_price = "0.05"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-spot-${count.index + 1}"
})
}
# Use lifecycle rules for cost optimization
resource "aws_s3_bucket" "logs" {
bucket = "${local.name_prefix}-logs"
lifecycle_rule {
id = "log_retention"
enabled = true
expiration {
days = 30
}
noncurrent_version_expiration {
noncurrent_days = 7
}
}
}
CI/CD Integration
GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.6.0
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve
GitLab CI
# .gitlab-ci.yml
stages:
- validate
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}
TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_COMMIT_REF_SLUG}
before_script:
- cd ${TF_ROOT}
- terraform --version
- terraform init
validate:
stage: validate
script:
- terraform validate
- terraform fmt -check
plan:
stage: plan
script:
- terraform plan
artifacts:
reports:
terraform: ${TF_ROOT}/plan.cache
apply:
stage: apply
script:
- terraform apply -auto-approve
when: manual
only:
- main
Troubleshooting
Common Issues
State Lock Issues
# Force unlock state
terraform force-unlock <lock-id>
# Check state
terraform state list
terraform state show <resource>
Provider Issues
# Update providers
terraform init -upgrade
# Clean provider cache
rm -rf .terraform
terraform init
Resource Conflicts
# Import existing resource
terraform import aws_instance.web i-1234567890abcdef0
# Move resource in state
terraform state mv aws_instance.web aws_instance.web_new
Debugging
# Enable debug logging
export TF_LOG=DEBUG
terraform apply
# Enable trace logging
export TF_LOG=TRACE
terraform apply
# Save logs to file
export TF_LOG_PATH=terraform.log
terraform apply
Conclusion
Terraform is a powerful tool for managing infrastructure as code, enabling teams to provision, modify, and destroy cloud resources in a consistent, repeatable manner. By following best practices for state management, module organization, and security, you can build robust, maintainable infrastructure that scales with your needs.
The key to successful Terraform adoption is starting with simple configurations and gradually adding complexity as you become more comfortable with the tool. With proper planning and implementation, Terraform can significantly improve your infrastructure management capabilities.