Terraform: Infrastructure as Code - Manage cloud infrastructure with Terraform, including provisioning, state management, and best pract...
DevOps & Deployment

Terraform: Infrastructure as Code

Manage cloud infrastructure with Terraform, including provisioning, state management, and best practices. Master Infrastructure as Code for scalable deployments.

TechDevDex Team
12/1/2024
26 min
#Terraform#Infrastructure as Code#DevOps#Cloud#Automation#AWS#Azure#GCP

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

bash
# 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

bash
# 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

text
# 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

text
# 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

text
resource "resource_type" "resource_name" {
  # Configuration arguments
  argument1 = "value1"
  argument2 = "value2"
  
  # Nested blocks
  nested_block {
    nested_argument = "value"
  }
}

Resource Dependencies

text
# 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

text
# 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

text
# 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

text
# 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

text
# Default local state
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Remote State

S3 Backend

text
# 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

text
# backend.tf
terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state"
    storage_account_name = "terraformstate"
    container_name       = "tfstate"
    key                  = "infrastructure.terraform.tfstate"
  }
}

State Operations

bash
# 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

text
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

text
# 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

text
# 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

text
# 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

text
# 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

text
# 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

text
# 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

text
# 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

text
# 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

bash
# 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

text
# 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

text
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

text
# 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

text
# 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

yaml
# .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

yaml
# .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

bash
# Force unlock state
terraform force-unlock <lock-id>

# Check state
terraform state list
terraform state show <resource>

Provider Issues

bash
# Update providers
terraform init -upgrade

# Clean provider cache
rm -rf .terraform
terraform init

Resource Conflicts

bash
# 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

bash
# 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.