Terraform Introduction: Modern Infrastructure as Code
Introduction
Terraform has revolutionized how we provision and manage infrastructure by introducing a declarative approach to infrastructure-as-code. Created by HashiCorp, Terraform allows you to define your entire infrastructure using human-readable configuration files, version control your infrastructure alongside your application code, and safely apply changes across multiple cloud providers and services through a single consistent workflow.
Unlike traditional imperative scripts that describe step-by-step how to create infrastructure, Terraform uses a declarative model where you define what your infrastructure should look like, and Terraform figures out how to make it happen. This paradigm shift enables teams to treat infrastructure as code, implementing the same practices used in software development: version control, code review, testing, and continuous deployment.
In today's multi-cloud world, Terraform's provider ecosystem supports hundreds of services including AWS, Azure, Google Cloud, Kubernetes, GitHub, Datadog, and many more. This comprehensive guide will introduce you to Terraform's core concepts, walk you through installation and setup, and provide practical examples to get you started with infrastructure automation.
Why Choose Terraform for Infrastructure as Code?
Key Advantages
Cloud-Agnostic: Terraform works with all major cloud providers through its provider plugin system. Write infrastructure code once and deploy it anywhere, or manage multi-cloud infrastructure from a single tool.
Declarative Syntax: HashiCorp Configuration Language (HCL) is human-readable and expressive, making infrastructure code easy to understand and maintain.
State Management: Terraform maintains a state file that tracks your infrastructure, enabling it to determine what changes are needed and prevent drift.
Plan Before Apply: The terraform plan command shows exactly what changes will be made before they're executed, preventing unexpected modifications to production infrastructure.
Resource Graph: Terraform builds a dependency graph of all resources, allowing parallel creation and proper ordering of dependent resources.
Modular and Reusable: Create reusable modules to standardize infrastructure patterns across your organization.
Immutable Infrastructure: Terraform encourages immutable infrastructure practices, where servers are replaced rather than modified in place.
Terraform vs Other Tools
Terraform vs Ansible: Ansible excels at configuration management and application deployment, while Terraform specializes in infrastructure provisioning. They complement each other well in a complete DevOps workflow.
Terraform vs CloudFormation: CloudFormation is AWS-specific, while Terraform works across multiple cloud providers. Terraform's syntax is also more concise and easier to read.
Terraform vs Pulumi: Pulumi uses general-purpose programming languages (Python, TypeScript, Go) while Terraform uses HCL. Terraform has a larger community and more mature ecosystem.
Prerequisites
Before starting with Terraform, ensure you have:
- Operating System: Linux, macOS, or Windows
- Command Line Knowledge: Basic familiarity with terminal/command prompt
- Cloud Provider Account: AWS, Azure, GCP, or other supported provider
- API Credentials: Access keys or service account credentials for your cloud provider
- Version Control: Git installed and basic Git knowledge
- Text Editor: VS Code, Vim, or any code editor (VS Code with Terraform extension recommended)
- Basic Networking: Understanding of IP addresses, subnets, and firewalls
Recommended Knowledge
- Basic understanding of cloud computing concepts
- Familiarity with infrastructure components (VMs, networks, storage)
- Experience with command-line tools
- Understanding of version control with Git
Installation and Setup
Installing Terraform on Linux
Ubuntu/Debian:
# Add HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
# Add HashiCorp repository
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
# Update and install
sudo apt update && sudo apt install terraform
# Verify installation
terraform version
CentOS/Rocky Linux:
# Add HashiCorp repository
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
# Install Terraform
sudo yum install terraform
# Verify installation
terraform version
Manual Installation (Universal):
# Download latest version
TERRAFORM_VERSION="1.7.0"
wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip
# Extract and install
unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Verify installation
terraform version
# Expected output:
# Terraform v1.7.0
# on linux_amd64
Installing Terraform on macOS
# Using Homebrew (recommended)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Verify installation
terraform version
# Update Terraform
brew upgrade hashicorp/tap/terraform
Installing Terraform on Windows
Using Chocolatey:
# Install via Chocolatey
choco install terraform
# Verify installation
terraform version
Manual Installation:
- Download the Windows 64-bit ZIP from https://www.terraform.io/downloads
- Extract the ZIP file
- Move
terraform.exeto a directory in your PATH (e.g.,C:\Windows\System32) - Open Command Prompt and run:
terraform version
Setting Up Shell Completion
# Bash
terraform -install-autocomplete
echo 'complete -C /usr/local/bin/terraform terraform' >> ~/.bashrc
source ~/.bashrc
# Zsh
terraform -install-autocomplete
echo 'complete -o nospace -C /usr/local/bin/terraform terraform' >> ~/.zshrc
source ~/.zshrc
Configuring Cloud Provider Credentials
AWS Configuration:
# Install AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Configure AWS credentials
aws configure
# Enter your AWS Access Key ID, Secret Access Key, region, and output format
# Verify configuration
aws sts get-caller-identity
# Alternative: Environment variables
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_DEFAULT_REGION="us-east-1"
Azure Configuration:
# Install Azure CLI
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# Login to Azure
az login
# Set subscription
az account set --subscription "your-subscription-id"
# Create service principal for Terraform
az ad sp create-for-rbac --name "terraform" --role="Contributor" --scopes="/subscriptions/your-subscription-id"
# Note the output for use in Terraform configuration
Google Cloud Configuration:
# Install Google Cloud SDK
curl https://sdk.cloud.google.com | bash
exec -l $SHELL
# Initialize and authenticate
gcloud init
gcloud auth application-default login
# Set project
gcloud config set project your-project-id
Core Terraform Concepts
Infrastructure as Code
Terraform treats infrastructure as code, which means:
- Infrastructure is defined in human-readable configuration files
- Infrastructure configuration is version-controlled like application code
- Changes are tracked, reviewed, and can be rolled back
- Infrastructure can be tested, validated, and continuously deployed
HashiCorp Configuration Language (HCL)
HCL is Terraform's configuration language, designed to be both human-readable and machine-friendly:
# Basic HCL syntax
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
Environment = "Production"
}
}
# Variables
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 2
}
# Outputs
output "instance_ip" {
description = "Public IP of the instance"
value = aws_instance.web.public_ip
}
Terraform Workflow
The standard Terraform workflow consists of three main commands:
- terraform init: Initialize the working directory, download providers
- terraform plan: Preview changes before applying
- terraform apply: Execute the planned changes
# Initialize Terraform
terraform init
# Validate configuration
terraform validate
# Preview changes
terraform plan
# Apply changes
terraform apply
# Destroy infrastructure
terraform destroy
Resources and Providers
Resources are the most important element in Terraform. Each resource block describes one or more infrastructure objects:
resource "resource_type" "resource_name" {
argument1 = value1
argument2 = value2
}
Providers are plugins that allow Terraform to interact with cloud platforms and other services:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
State Files
Terraform stores information about your infrastructure in a state file (terraform.tfstate). This file:
- Maps real-world resources to your configuration
- Tracks metadata and resource dependencies
- Improves performance by caching resource attributes
- Must be shared among team members (use remote state)
Important: Never commit terraform.tfstate to version control. Use remote state backends instead.
Your First Terraform Configuration
Let's create a simple infrastructure on AWS:
Project Structure
# Create project directory
mkdir terraform-introduction
cd terraform-introduction
# Create configuration files
touch main.tf variables.tf outputs.tf terraform.tfvars
Recommended project structure:
terraform-introduction/
├── main.tf # Main infrastructure configuration
├── variables.tf # Variable definitions
├── outputs.tf # Output definitions
├── terraform.tfvars # Variable values (don't commit secrets)
├── providers.tf # Provider configurations
└── README.md # Documentation
Main Configuration (main.tf)
# main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
Environment = var.environment
ManagedBy = "Terraform"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
Environment = var.environment
}
}
# Public Subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-subnet"
Environment = var.environment
}
}
# 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 = "${var.project_name}-public-rt"
Environment = var.environment
}
}
# Route Table Association
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
# Security Group
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH from specific IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.admin_ip]
}
egress {
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-web-sg"
Environment = var.environment
}
}
# Data source for AMI
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Data source for availability zones
data "aws_availability_zones" "available" {
state = "available"
}
# EC2 Instance
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
key_name = var.key_name
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y nginx
echo "<h1>Hello from Terraform</h1>" > /var/www/html/index.html
systemctl start nginx
systemctl enable nginx
EOF
root_block_device {
volume_size = 20
volume_type = "gp3"
}
tags = {
Name = "${var.project_name}-web-server"
Environment = var.environment
ManagedBy = "Terraform"
}
lifecycle {
create_before_destroy = true
}
}
# Elastic IP
resource "aws_eip" "web" {
instance = aws_instance.web.id
domain = "vpc"
tags = {
Name = "${var.project_name}-web-eip"
Environment = var.environment
}
depends_on = [aws_internet_gateway.main]
}
Variables Configuration (variables.tf)
# variables.tf
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "us-east-1"
}
variable "project_name" {
description = "Project name for resource naming"
type = string
default = "myproject"
}
variable "environment" {
description = "Environment name"
type = string
default = "development"
validation {
condition = contains(["development", "staging", "production"], var.environment)
error_message = "Environment must be development, staging, or production."
}
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidr" {
description = "CIDR block for public subnet"
type = string
default = "10.0.1.0/24"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "key_name" {
description = "SSH key name"
type = string
}
variable "admin_ip" {
description = "IP address allowed SSH access"
type = string
}
variable "tags" {
description = "Additional tags for resources"
type = map(string)
default = {}
}
Outputs Configuration (outputs.tf)
# outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_id" {
description = "ID of the public subnet"
value = aws_subnet.public.id
}
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "Public IP of the EC2 instance"
value = aws_eip.web.public_ip
}
output "instance_public_dns" {
description = "Public DNS of the EC2 instance"
value = aws_instance.web.public_dns
}
output "security_group_id" {
description = "ID of the security group"
value = aws_security_group.web.id
}
output "web_url" {
description = "URL to access the web server"
value = "http://${aws_eip.web.public_ip}"
}
Variable Values (terraform.tfvars)
# terraform.tfvars
# DO NOT commit this file if it contains sensitive data
aws_region = "us-east-1"
project_name = "terraform-intro"
environment = "development"
instance_type = "t2.micro"
key_name = "my-ssh-key"
admin_ip = "YOUR_IP_ADDRESS/32" # Replace with your IP
tags = {
Project = "Terraform Introduction"
Owner = "DevOps Team"
}
Deploying Your First Infrastructure
Step 1: Initialize Terraform
# Navigate to project directory
cd terraform-introduction
# Initialize Terraform
terraform init
# Expected output:
# Initializing the backend...
# Initializing provider plugins...
# - Finding hashicorp/aws versions matching "~> 5.0"...
# - Installing hashicorp/aws v5.x.x...
# Terraform has been successfully initialized!
Step 2: Validate Configuration
# Check syntax and validate configuration
terraform validate
# Expected output:
# Success! The configuration is valid.
# Format code (optional but recommended)
terraform fmt -recursive
Step 3: Plan Infrastructure Changes
# Preview what Terraform will create
terraform plan
# Save plan to file for review
terraform plan -out=tfplan
# Review the plan
# You'll see a list of resources to be created, modified, or destroyed
Step 4: Apply Configuration
# Apply the configuration
terraform apply
# Or apply saved plan
terraform apply tfplan
# Terraform will prompt for confirmation
# Type 'yes' to proceed
# Expected output shows resource creation progress:
# aws_vpc.main: Creating...
# aws_vpc.main: Creation complete after 2s
# aws_internet_gateway.main: Creating...
# ...
# Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Step 5: Verify Deployment
# Show current state
terraform show
# List all resources
terraform state list
# Get specific output values
terraform output instance_public_ip
terraform output web_url
# Test the web server
curl $(terraform output -raw web_url)
Step 6: Make Changes
Edit main.tf to add tags or modify resources:
resource "aws_instance" "web" {
# ... existing configuration ...
tags = {
Name = "${var.project_name}-web-server"
Environment = var.environment
ManagedBy = "Terraform"
UpdatedAt = timestamp() # Add new tag
}
}
Apply the changes:
# Plan changes
terraform plan
# Apply changes
terraform apply
Step 7: Destroy Infrastructure
# Preview what will be destroyed
terraform plan -destroy
# Destroy all resources
terraform destroy
# Terraform will prompt for confirmation
# Type 'yes' to proceed
# Verify destruction
terraform show # Should show empty state
Terraform Commands Reference
Essential Commands
# Initialize working directory
terraform init
# Validate configuration
terraform validate
# Format configuration files
terraform fmt
# Show execution plan
terraform plan
# Apply changes
terraform apply
# Destroy infrastructure
terraform destroy
# Show current state
terraform show
# List resources in state
terraform state list
# Show specific resource
terraform state show aws_instance.web
# Get output values
terraform output
# Refresh state
terraform refresh
# Import existing resource
terraform import aws_instance.web i-1234567890abcdef0
# Taint resource (mark for recreation)
terraform taint aws_instance.web
# Untaint resource
terraform untaint aws_instance.web
# Create workspace
terraform workspace new production
# List workspaces
terraform workspace list
# Switch workspace
terraform workspace select production
Advanced Commands
# Show resource graph
terraform graph | dot -Tpng > graph.png
# Validate and check syntax
terraform validate
# Apply with auto-approve (use carefully)
terraform apply -auto-approve
# Apply specific resource
terraform apply -target=aws_instance.web
# Plan with variable override
terraform plan -var="instance_type=t2.small"
# Apply with variable file
terraform apply -var-file="production.tfvars"
# Show providers
terraform providers
# Show version
terraform version
# Upgrade providers
terraform init -upgrade
Best Practices
1. Version Control
# .gitignore for Terraform
*.tfstate
*.tfstate.backup
*.tfvars
.terraform/
.terraform.lock.hcl
crash.log
override.tf
override.tf.json
Always commit:
*.tffiles*.tf.jsonfiles.terraform.lock.hcl(provider version lock)README.md
Never commit:
*.tfstatefiles*.tfvarswith secrets.terraform/directory
2. Remote State
Use remote state for team collaboration:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-lock"
}
}
3. Use Variables
Make configurations reusable:
# Bad: Hardcoded values
resource "aws_instance" "web" {
instance_type = "t2.micro"
ami = "ami-0c55b159cbfafe1f0"
}
# Good: Using variables
resource "aws_instance" "web" {
instance_type = var.instance_type
ami = data.aws_ami.ubuntu.id
}
4. Use Data Sources
Query existing infrastructure:
data "aws_vpc" "existing" {
default = true
}
resource "aws_subnet" "app" {
vpc_id = data.aws_vpc.existing.id
# ...
}
5. Tag Everything
locals {
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
}
}
resource "aws_instance" "web" {
# ...
tags = merge(local.common_tags, {
Name = "web-server"
Role = "frontend"
})
}
6. Use Modules
Create reusable components:
module "vpc" {
source = "./modules/vpc"
cidr_block = var.vpc_cidr
environment = var.environment
}
module "web_servers" {
source = "./modules/ec2"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
}
7. Implement Lifecycle Rules
resource "aws_instance" "web" {
# ...
lifecycle {
create_before_destroy = true
prevent_destroy = true # Production protection
ignore_changes = [tags.UpdatedAt]
}
}
Troubleshooting
Common Issues and Solutions
Issue: Provider not found
# Solution: Run terraform init
terraform init
Issue: Resource already exists
# Solution: Import existing resource
terraform import aws_instance.web i-1234567890abcdef0
Issue: State lock error
# Solution: Force unlock (use carefully)
terraform force-unlock LOCK_ID
Issue: Invalid credentials
# Solution: Verify credentials
aws sts get-caller-identity
# Or set environment variables
export AWS_ACCESS_KEY_ID="your-key"
export AWS_SECRET_ACCESS_KEY="your-secret"
Issue: Dependency errors
# Solution: Use explicit depends_on
resource "aws_eip" "web" {
instance = aws_instance.web.id
depends_on = [aws_internet_gateway.main]
}
Debug Mode
# Enable detailed logging
export TF_LOG=DEBUG
terraform apply
# Log to file
export TF_LOG_PATH=./terraform.log
terraform apply
# Disable logging
unset TF_LOG
unset TF_LOG_PATH
Conclusion
Terraform is a powerful tool for managing infrastructure as code, providing a consistent workflow across all cloud providers and services. This introduction covered the fundamental concepts, installation, configuration, and basic usage patterns that form the foundation of Terraform expertise.
Key takeaways:
- Terraform uses declarative configuration to define infrastructure
- The standard workflow is: init, plan, apply
- State management is crucial for tracking infrastructure
- Variables and outputs make configurations reusable
- Remote state enables team collaboration
- Following best practices ensures maintainable infrastructure code
Next steps:
- Explore Terraform modules for code reusability
- Implement remote state with backend configuration
- Learn about workspaces for environment management
- Study advanced features like provisioners and dynamic blocks
- Integrate Terraform with CI/CD pipelines
- Explore Terraform Cloud for team collaboration
With Terraform in your DevOps toolkit, you can build, change, and version infrastructure safely and efficiently. Continue practicing with different cloud providers and gradually build more complex infrastructure configurations.


