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:

  1. Download the Windows 64-bit ZIP from https://www.terraform.io/downloads
  2. Extract the ZIP file
  3. Move terraform.exe to a directory in your PATH (e.g., C:\Windows\System32)
  4. 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:

  1. terraform init: Initialize the working directory, download providers
  2. terraform plan: Preview changes before applying
  3. 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:

  • *.tf files
  • *.tf.json files
  • .terraform.lock.hcl (provider version lock)
  • README.md

Never commit:

  • *.tfstate files
  • *.tfvars with 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:

  1. Explore Terraform modules for code reusability
  2. Implement remote state with backend configuration
  3. Learn about workspaces for environment management
  4. Study advanced features like provisioners and dynamic blocks
  5. Integrate Terraform with CI/CD pipelines
  6. 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.