Introduction to Terraform
Terraform is an open-source Infrastructure as Code (IaC) tool created by HashiCorp that enables you to safely and predictably provision and manage infrastructure across various cloud providers and services. Using a declarative configuration language called HashiCorp Configuration Language (HCL), Terraform allows you to define infrastructure components in human-readable configuration files that can be versioned, shared, and reused.
As one of the most widely adopted IaC tools in the industry, Terraform has become the de facto standard for multi-cloud infrastructure provisioning. Its provider-based architecture supports hundreds of infrastructure platforms, including major cloud providers like AWS, Azure, and Google Cloud, as well as SaaS platforms, database services, and more.
The Shopping List Analogy
Working with Terraform can be compared to grocery shopping with a list:
- Configuration Files are like your shopping list – they declare what you want, not how to get it.
- Terraform Plan is like reviewing your shopping list before shopping, checking what you already have at home and what you still need to buy.
- Terraform Apply is the actual shopping trip, where you acquire the items on your list.
- Terraform State is like your pantry inventory – it tracks what you currently have.
- Terraform Providers are like different stores (grocery store, hardware store, etc.) where you can acquire different types of items.
- Terraform Modules are like pre-made shopping lists for specific occasions (dinner party, camping trip) that you can reuse and customize.
Just as a shopping list lets you efficiently buy exactly what you need without forgetting items, Terraform lets you consistently provision exactly the infrastructure you need without manual errors or omissions.
Terraform vs. Other IaC Tools
To understand Terraform's positioning in the IaC ecosystem, it's helpful to compare it with other common tools used for infrastructure management.
| Tool | Primary Focus | Approach | Language | Multi-Cloud |
|---|---|---|---|---|
| Terraform | Infrastructure provisioning | Declarative | HCL (HashiCorp Configuration Language) | Yes (strong) |
| AWS CloudFormation | AWS-specific provisioning | Declarative | JSON/YAML | No (AWS only) |
| Azure Resource Manager | Azure-specific provisioning | Declarative | JSON/Bicep | No (Azure only) |
| Google Deployment Manager | GCP-specific provisioning | Declarative | YAML/Python | No (GCP only) |
| Ansible | Configuration management | Procedural | YAML | Yes |
| Pulumi | Infrastructure provisioning | Declarative | General programming languages (Python, TypeScript, etc.) | Yes |
| AWS CDK | AWS-specific provisioning | Imperative to Declarative | TypeScript, Python, Java, etc. | No (AWS only) |
Key Terraform Differentiators
- Provider Ecosystem: Vast ecosystem with hundreds of providers for different platforms
- State Management: Sophisticated state tracking to manage infrastructure lifecycle
- Cloud Agnostic: Same workflow and language across all cloud providers
- Resource Graph: Creates a dependency graph for efficient provisioning
- Plan Phase: Preview changes before applying them for safety
- Community: Large, active community and extensive documentation
- HCL: Purpose-built declarative language that balances readability and functionality
When to Choose Terraform
Terraform is particularly well-suited for:
- Multi-cloud or hybrid-cloud environments
- Infrastructure that spans multiple service providers
- Teams looking for a cloud-agnostic approach to IaC
- Projects requiring a balance of readability and expressiveness
- Scenarios needing detailed dependency management
- When you want to leverage a vast ecosystem of community modules
Consider alternatives when:
- You're working exclusively within a single cloud provider and prefer native tooling
- You need more focus on configuration management than provisioning
- Your team strongly prefers using general-purpose programming languages
Installing and Setting Up Terraform
Installation Options
Terraform can be installed in several ways depending on your operating system and preferences:
Manual Installation
For macOS (using Homebrew):
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
For Windows (using Chocolatey):
choco install terraform
For Linux (using apt on Ubuntu/Debian):
# Add HashiCorp GPG key
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
# Add HashiCorp repository
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
# Update and install Terraform
sudo apt-get update && sudo apt-get install terraform
Manual Download and Installation
For any platform:
- Download the appropriate package from the Terraform downloads page
- Extract the downloaded zip file
- Move the terraform binary to a directory included in your system's PATH
# Example for Linux/macOS after downloading
unzip terraform_*.zip
sudo mv terraform /usr/local/bin/
Verifying Installation
After installation, verify that Terraform is correctly installed by checking the version:
terraform version
You should see output similar to:
Terraform v1.5.4
on darwin_amd64
Editor Setup
Setting up your editor for Terraform development improves productivity:
Visual Studio Code
Install the official HashiCorp Terraform extension for syntax highlighting, validation, and autocompletion:
- Open VS Code
- Go to Extensions (Ctrl+Shift+X or Cmd+Shift+X)
- Search for "HashiCorp Terraform"
- Click Install
Other Editors
- JetBrains IDEs: Install the "HashiCorp Terraform/HCL Language Support" plugin
- Vim: Use plugins like vim-terraform or vim-polyglot
- Emacs: Install terraform-mode
- Sublime Text: Install Terraform syntax highlighting package
Editor Recommendations
- Enable format on save for automatic HCL formatting
- Set up linting for Terraform best practices
- Configure tab spacing to 2 spaces (Terraform convention)
- If using VS Code, enable the Terraform language server for enhanced features
Terraform Fundamentals
Core Terraform Concepts
Providers
Providers are plugins that enable Terraform to interact with specific platforms and services.
# AWS provider configuration
provider "aws" {
region = "us-west-2"
profile = "production"
}
# Using multiple providers
provider "azurerm" {
features {}
subscription_id = "your-subscription-id"
}
Resources
Resources are the infrastructure components you want to create and manage, such as virtual machines, networks, or DNS records.
# Example AWS EC2 instance resource
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
Environment = "Production"
}
}
# Example S3 bucket resource
resource "aws_s3_bucket" "data_bucket" {
bucket = "my-company-data-bucket"
acl = "private"
versioning {
enabled = true
}
}
Data Sources
Data sources allow Terraform to fetch information from existing infrastructure or external sources.
# Fetch information about an existing VPC
data "aws_vpc" "existing_vpc" {
id = "vpc-0123456789abcdef0"
}
# Use data from the existing VPC
resource "aws_subnet" "new_subnet" {
vpc_id = data.aws_vpc.existing_vpc.id
cidr_block = "10.0.1.0/24"
tags = {
Name = "NewSubnet"
}
}
Variables
Variables provide a way to parameterize your Terraform configurations.
# Defining variables in variables.tf
variable "region" {
description = "AWS region to deploy resources"
type = string
default = "us-west-2"
}
variable "instance_count" {
description = "Number of EC2 instances to create"
type = number
default = 2
}
variable "environment" {
description = "Deployment environment"
type = string
default = "development"
validation {
condition = contains(["development", "staging", "production"], var.environment)
error_message = "Environment must be one of: development, staging, production."
}
}
# Using variables in main.tf
provider "aws" {
region = var.region
}
resource "aws_instance" "server" {
count = var.instance_count
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.environment == "production" ? "t2.medium" : "t2.micro"
tags = {
Name = "server-${count.index + 1}"
Environment = var.environment
}
}
Outputs
Outputs expose specific values from your infrastructure that can be used by other Terraform configurations or external systems.
# Define outputs
output "instance_ip_addresses" {
description = "Public IP addresses of created instances"
value = aws_instance.server[*].public_ip
}
output "s3_bucket_arn" {
description = "ARN of the created S3 bucket"
value = aws_s3_bucket.data_bucket.arn
sensitive = true # Marks as sensitive to hide in console output
}
State
Terraform state is a snapshot of your infrastructure that tracks resource metadata and dependencies.
# Configure remote state storage (typically in a backend.tf file)
terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "project/environment/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Modules
Modules are reusable components that encapsulate groups of resources, making your Terraform code more organized and maintainable.
# Using a module
module "vpc" {
source = "./modules/vpc"
name = "main-vpc"
cidr_block = "10.0.0.0/16"
availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]
tags = {
Environment = var.environment
Project = "MyProject"
}
}
# Reference module outputs
resource "aws_instance" "app_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
subnet_id = module.vpc.private_subnet_ids[0]
tags = {
Name = "AppServer"
}
}
The Terraform Workflow
Terraform follows a consistent workflow regardless of which cloud provider or services you're using.
Key Workflow Commands
terraform init
Initializes a Terraform working directory, downloading providers and modules.
# Basic initialization
terraform init
# Upgrade modules and plugins
terraform init -upgrade
# Initialize with a specific backend configuration
terraform init -backend-config=prod.backend.hcl
terraform plan
Creates an execution plan showing what Terraform will do when you apply the configuration.
# Create a plan
terraform plan
# Save the plan to a file
terraform plan -out=tfplan
# Create a plan for specific resources
terraform plan -target=aws_instance.web_server
# Create a plan with variable values
terraform plan -var="instance_count=5" -var="environment=staging"
terraform apply
Applies the changes required to reach the desired state of the configuration.
# Apply changes with confirmation
terraform apply
# Apply changes without confirmation
terraform apply -auto-approve
# Apply a specific saved plan
terraform apply tfplan
# Apply changes for specific resources
terraform apply -target=aws_instance.web_server
terraform destroy
Destroys all resources managed by the current Terraform configuration.
# Destroy with confirmation
terraform destroy
# Destroy without confirmation
terraform destroy -auto-approve
# Destroy specific resources
terraform destroy -target=aws_instance.web_server
Additional Useful Commands
# Validate configuration syntax
terraform validate
# Format configuration according to conventions
terraform fmt
# Show current state
terraform show
# List all resources in state
terraform state list
# Import existing infrastructure into Terraform
terraform import aws_instance.imported i-1234567890abcdef0
# Get plugin or module updates
terraform get -update
# View outputs
terraform output
# View specific output
terraform output instance_ip_addresses
Best Practices for Terraform Workflow
- Always Run Plan First: Review planned changes before applying them
- Use Version Control: Commit Terraform configurations to track changes over time
- Use Remote State: Store state in a shared, secure backend for team collaboration
- State Locking: Ensure only one person can modify state at a time
- Use Workspaces: Separate state for different environments
- Output Plans: Save and review plans for complex changes
- Target Carefully: Be cautious with -target as it can break dependencies
Your First Terraform Project
Let's create a simple Terraform project to provision basic AWS infrastructure.
Project Structure
first-terraform-project/
├── main.tf # Main configuration file
├── variables.tf # Variable declarations
├── outputs.tf # Output declarations
├── providers.tf # Provider configurations
└── .gitignore # Git ignore file
Step 1: Set Up Provider Configuration
Create a providers.tf file:
# providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = var.aws_region
# Uncomment if not using AWS CLI credentials
# access_key = var.aws_access_key
# secret_key = var.aws_secret_key
}
Step 2: Define Variables
Create a variables.tf file:
# variables.tf
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-west-2"
}
variable "vpc_cidr_block" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "subnet_cidr_block" {
description = "CIDR block for the subnet"
type = string
default = "10.0.1.0/24"
}
variable "availability_zone" {
description = "Availability zone for the subnet"
type = string
default = "us-west-2a"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "ami_id" {
description = "AMI ID for the EC2 instance"
type = string
default = "ami-0c55b159cbfafe1f0" # Amazon Linux 2 in us-west-2
}
variable "environment" {
description = "Deployment environment"
type = string
default = "dev"
}
Step 3: Create Main Configuration
Create a main.tf file:
# main.tf
# Create a VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr_block
tags = {
Name = "main-vpc"
Environment = var.environment
}
}
# Create a subnet in the VPC
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_cidr_block
availability_zone = var.availability_zone
map_public_ip_on_launch = true
tags = {
Name = "public-subnet"
Environment = var.environment
}
}
# Create an internet gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
Environment = var.environment
}
}
# Create a route table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "public-route-table"
Environment = var.environment
}
}
# Associate the route table with the subnet
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
# Create a security group
resource "aws_security_group" "web" {
name = "web-sg"
description = "Allow inbound HTTP and SSH traffic"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTP"
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow SSH"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = {
Name = "web-sg"
Environment = var.environment
}
}
# Create an EC2 instance
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
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 = "web-server"
Environment = var.environment
}
}
Step 4: Define Outputs
Create an outputs.tf file:
# outputs.tf
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}
output "subnet_id" {
description = "ID of the created subnet"
value = aws_subnet.public.id
}
output "instance_id" {
description = "ID of the created EC2 instance"
value = aws_instance.web.id
}
output "public_ip" {
description = "Public IP address of the EC2 instance"
value = aws_instance.web.public_ip
}
output "website_url" {
description = "URL to access the web server"
value = "http://${aws_instance.web.public_ip}"
}
Step 5: Create .gitignore
Create a .gitignore file:
# .gitignore
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
# Exclude all .tfvars files, which are likely to contain sensitive data
*.tfvars
# Ignore override files as they are usually used for local settings
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc
Step 6: Initialize and Apply
Now let's execute the Terraform workflow:
# Initialize Terraform
terraform init
# Validate the configuration
terraform validate
# Format the configuration
terraform fmt
# Create an execution plan
terraform plan
# Apply the configuration
terraform apply
What's Happening Behind the Scenes
When you run these commands, Terraform:
- During Init:
- Creates a .terraform directory to store plugins and modules
- Downloads the AWS provider plugin
- Sets up the backend for state storage
- During Plan:
- Reads the current state or initializes a new one
- Refreshes state to match real-world resources
- Determines what actions are necessary to achieve desired state
- Creates a dependency graph of resources
- Shows what will be created, modified, or destroyed
- During Apply:
- Executes the plan in the correct order based on dependencies
- Makes API calls to AWS to create the VPC, subnet, etc.
- Updates the state file with the IDs and properties of created resources
- Displays output values specified in outputs.tf
The result is a complete network setup with a web server running Apache, accessible via its public IP.
Step 7: Verify and Clean Up
# Check outputs
terraform output
# Access the web server
echo "Open http://$(terraform output -raw public_ip) in your browser"
# When finished, destroy the resources
terraform destroy
Terraform Language Features
HCL Syntax Essentials
HashiCorp Configuration Language (HCL) is the syntax used in Terraform files.
Blocks and Arguments
# Block structure
resource "aws_instance" "example" { # Block type, resource type, and name label
ami = "ami-0c55b159cbfafe1f0" # Argument
instance_type = "t2.micro" # Another argument
# Nested block
ebs_block_device {
device_name = "/dev/sdh"
volume_size = 100
}
}
Data Types
# String
variable "region" {
type = string
default = "us-west-2"
}
# Number
variable "port" {
type = number
default = 80
}
# Boolean
variable "enable_logging" {
type = bool
default = true
}
# List
variable "availability_zones" {
type = list(string)
default = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
# Map
variable "tags" {
type = map(string)
default = {
Environment = "Development"
Project = "Example"
}
}
# Complex types
variable "subnet_config" {
type = object({
cidr_block = string
availability_zone = string
public = bool
tags = map(string)
})
default = {
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
public = true
tags = {
Name = "Public Subnet"
}
}
}
Expressions and Functions
# String interpolation
resource "aws_instance" "example" {
tags = {
Name = "web-server-${var.environment}" # String interpolation
}
}
# Conditional expression
resource "aws_instance" "example" {
instance_type = var.environment == "production" ? "t2.medium" : "t2.micro"
}
# For expressions
resource "aws_subnet" "example" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = var.availability_zones[count.index]
}
# Built-in functions
locals {
upper_name = upper(var.name)
timestamp = formatdate("YYYY-MM-DD", timestamp())
subnets = cidrsubnets(var.vpc_cidr, 8, 8, 8, 8)
}
Advanced Terraform Features
Count and For Each
# Using count
resource "aws_instance" "server" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "server-${count.index + 1}"
}
}
# Using for_each with a map
resource "aws_instance" "server" {
for_each = {
web = "t2.micro"
api = "t2.small"
db = "t2.medium"
}
ami = "ami-0c55b159cbfafe1f0"
instance_type = each.value
tags = {
Name = "${each.key}-server"
}
}
# Using for_each with a set
resource "aws_security_group_rule" "ingress" {
for_each = toset(["80", "443", "22"])
type = "ingress"
security_group_id = aws_security_group.example.id
from_port = each.value
to_port = each.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
Dynamic Blocks
# Without dynamic blocks (repetitive)
resource "aws_security_group" "example" {
name = "example"
description = "Example security group"
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"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
}
# With dynamic blocks (more concise)
variable "ingress_rules" {
default = [
{
port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
]
}
resource "aws_security_group" "example" {
name = "example"
description = "Example security group"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Locals
# Using locals for calculated values
locals {
common_tags = {
Environment = var.environment
Project = var.project_name
Owner = "Terraform"
ManagedBy = "Terraform"
}
is_production = var.environment == "production"
instance_type = local.is_production ? "t2.medium" : "t2.micro"
availability_zones = slice(data.aws_availability_zones.available.names, 0, var.az_count)
}
resource "aws_instance" "example" {
ami = var.ami_id
instance_type = local.instance_type
tags = merge(
local.common_tags,
{
Name = "example-instance"
}
)
}
Lifecycle Rules
# Lifecycle rules modify how Terraform handles resource changes
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
lifecycle {
create_before_destroy = true # Create replacement before destroying original
prevent_destroy = true # Prevent accidental destruction
ignore_changes = [
tags, # Ignore changes to tags
user_data # Ignore changes to user_data
]
}
}
HCL Style Guidelines
- Indentation: Use 2 spaces for indentation
- Block Spacing: Add a blank line between blocks
- Naming: Use snake_case for resource names and variables
- String Values: Double quotes are preferred over single quotes
- Alignment: Align equal signs for readability
- Comments: Use # for comments (not // or /*...*/)
- Ordering: Place required arguments first, followed by optional ones
- Autoformatting: Run terraform fmt before committing code
Terraform Modules
Modules are containers for multiple resources that are used together, allowing you to create reusable components.
Module Structure
modules/vpc/
├── main.tf # Main configuration
├── variables.tf # Input variables
├── outputs.tf # Output values
└── README.md # Documentation
Creating a Basic Module
Module Definition (modules/vpc/main.tf)
# main.tf in modules/vpc
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_support = var.enable_dns_support
enable_dns_hostnames = var.enable_dns_hostnames
tags = merge(
var.tags,
{
Name = var.name
}
)
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index % length(var.availability_zones)]
map_public_ip_on_launch = true
tags = merge(
var.tags,
{
Name = "${var.name}-public-${count.index + 1}"
Type = "Public"
}
)
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index % length(var.availability_zones)]
tags = merge(
var.tags,
{
Name = "${var.name}-private-${count.index + 1}"
Type = "Private"
}
)
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(
var.tags,
{
Name = "${var.name}-igw"
}
)
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = merge(
var.tags,
{
Name = "${var.name}-public-rt"
}
)
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.this.id
tags = merge(
var.tags,
{
Name = "${var.name}-private-rt"
}
)
}
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
Module Variables (modules/vpc/variables.tf)
# variables.tf in modules/vpc
variable "name" {
description = "Name prefix for the VPC resources"
type = string
}
variable "cidr_block" {
description = "CIDR block for the VPC"
type = string
}
variable "enable_dns_support" {
description = "Enable DNS support in the VPC"
type = bool
default = true
}
variable "enable_dns_hostnames" {
description = "Enable DNS hostnames in the VPC"
type = bool
default = true
}
variable "availability_zones" {
description = "List of availability zones to use"
type = list(string)
}
variable "public_subnet_cidrs" {
description = "List of CIDR blocks for public subnets"
type = list(string)
default = []
}
variable "private_subnet_cidrs" {
description = "List of CIDR blocks for private subnets"
type = list(string)
default = []
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
Module Outputs (modules/vpc/outputs.tf)
# outputs.tf in modules/vpc
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}
output "internet_gateway_id" {
description = "ID of the created internet gateway"
value = aws_internet_gateway.this.id
}
output "public_route_table_id" {
description = "ID of the public route table"
value = aws_route_table.public.id
}
output "private_route_table_id" {
description = "ID of the private route table"
value = aws_route_table.private.id
}
Using the Module
# Using the VPC module in main.tf
module "vpc" {
source = "./modules/vpc"
name = "example-vpc"
cidr_block = "10.0.0.0/16"
availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
tags = {
Environment = "Development"
Project = "Example"
}
}
# Using module outputs
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
subnet_id = module.vpc.public_subnet_ids[0]
tags = {
Name = "web-server"
}
}
Module Sources
Terraform modules can be sourced from various locations:
# Local path
module "vpc" {
source = "./modules/vpc"
}
# Git repository
module "vpc" {
source = "git::https://github.com/example/terraform-aws-vpc.git?ref=v1.2.0"
}
# Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
}
# S3 bucket
module "vpc" {
source = "s3::https://s3-eu-west-1.amazonaws.com/examplebucket/modules/vpc/v1.2.0.zip"
}
# HTTP URL
module "vpc" {
source = "https://example.com/modules/vpc.zip"
}
Module Best Practices
- Pin Versions: Always specify module versions for stability
- Flat Structure: Avoid deeply nested modules
- Composability: Design small, focused modules that can be combined
- Documentation: Include README.md with usage examples and input/output descriptions
- Input Validation: Use variable validation to prevent misuse
- Default Values: Provide sensible defaults where appropriate
- Avoid Hardcoding: Parameterize values that might change
- Module Testing: Include examples or tests to verify functionality
State Management in Terraform
Understanding Terraform state is crucial for effectively working with infrastructure at scale.
What is Terraform State?
- State is Terraform's record of managed resources and their properties
- Stores mapping between real-world resources and your configuration
- Tracks metadata like resource dependencies
- Used to determine what needs to be created, updated, or deleted
- Can contain sensitive information (passwords, IPs, etc.)
Local vs. Remote State
Local State (Default)
- Stored in terraform.tfstate file in workspace directory
- Simple to set up - no additional configuration needed
- Works well for personal projects or experimentation
- Problematic for team collaboration
- No built-in locking mechanism
- Risk of loss or accidental commitment to version control
Remote State
- Stored in a shared backend (S3, Azure Blob, GCS, etc.)
- Enables team collaboration
- Provides state locking to prevent concurrent modifications
- Increased security (encryption, access controls)
- Better disaster recovery
- Remote operations (plan/apply in a consistent environment)
Configuring Remote State
AWS S3 and DynamoDB Backend
# backend.tf
terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "myproject/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# Create these resources first (in a separate bootstrap configuration)
resource "aws_s3_bucket" "terraform_state" {
bucket = "terraform-state-bucket"
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Azure Storage Backend
# backend.tf
terraform {
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "terraformstate12345"
container_name = "tfstate"
key = "myproject.terraform.tfstate"
}
}
Google Cloud Storage Backend
# backend.tf
terraform {
backend "gcs" {
bucket = "terraform-state-bucket"
prefix = "myproject"
}
}
State Management Commands
# List resources in the state
terraform state list
# Show details of a specific resource
terraform state show aws_instance.web
# Move a resource within the state (rename)
terraform state mv aws_instance.old_name aws_instance.new_name
# Remove a resource from state (without destroying it)
terraform state rm aws_instance.web
# Pull current state and output to stdout
terraform state pull
# Update state from a file
terraform state push terraform.tfstate
# Import existing infrastructure into state
terraform import aws_instance.imported i-1234567890abcdef0
# Force a refresh of state against real resources
terraform refresh
Workspaces
Terraform workspaces allow you to manage multiple distinct sets of infrastructure using the same configuration files.
# List workspaces
terraform workspace list
# Create a new workspace
terraform workspace new development
# Switch workspace
terraform workspace select production
# Show current workspace
terraform workspace show
# Delete a workspace
terraform workspace delete staging
Using Workspaces in Configuration
# Use workspace name for environment-specific configuration
resource "aws_instance" "example" {
instance_type = terraform.workspace == "production" ? "t2.medium" : "t2.micro"
tags = {
Environment = terraform.workspace
}
}
State Management Best Practices
- Always Use Remote State: For all but the simplest personal projects
- Secure Your State: Ensure state is encrypted and access-controlled
- Use State Locking: Prevent concurrent modifications
- Separate State by Environment: Use workspaces or separate state files
- Backup State: Enable versioning for state storage
- Handle Sensitive Data Carefully: Mark outputs as sensitive where needed
- Limit State Manipulation: Use terraform state commands sparingly
Learning Activities
Activity 1: Create a Multi-Tier Web Application
Extend the basic web server example to create a multi-tier application:
- Create a new Terraform project with a VPC module
- Add a web tier with EC2 instances in a public subnet
- Add an application tier with EC2 instances in a private subnet
- Add a database tier using RDS in a private subnet
- Set up security groups with appropriate rules for each tier
- Configure an Application Load Balancer for the web tier
- Use variables to parameterize the configuration
- Define outputs to provide useful information
Activity 2: Remote State and CI/CD
Set up a proper state management and CI/CD workflow:
- Create an S3 bucket and DynamoDB table for remote state
- Configure your Terraform project to use remote state
- Set up multiple workspaces (dev, staging, prod)
- Create environment-specific variable files
- Set up a basic CI/CD pipeline (GitHub Actions or similar)
- Implement a plan stage that runs on pull requests
- Add an apply stage that runs on merge to main branch
Activity 3: Custom Module Development
Create a reusable custom module:
- Identify a common infrastructure pattern in your project
- Extract it into a reusable module with variables and outputs
- Add validation and sensible defaults
- Create a README.md with usage examples
- Use the module in your infrastructure configuration
- Consider publishing the module to a private or public registry
Key Takeaways
- Terraform is a powerful tool for defining infrastructure as code using a declarative approach
- The core Terraform workflow consists of write, plan, apply, and refine cycles
- HCL provides a balance of readability and functionality for infrastructure definition
- Modules enable code reuse and help organize complex infrastructure
- Remote state management is essential for team collaboration and infrastructure security
- Understanding resource dependencies and lifecycle is key to effective Terraform use
- The rich provider ecosystem makes Terraform valuable across multi-cloud environments
- Terraform integrates well with modern software development practices like version control and CI/CD