Skip to content

Terraform for Network Infrastructure as Code: A Complete Guide

Terraform has revolutionized how organizations manage network infrastructure by treating it as code. This comprehensive guide explores how to use Terraform for network automation, from basic concepts to advanced networking scenarios.

What is Infrastructure as Code (IaC)?

Infrastructure as Code is the practice of managing and provisioning computing infrastructure through machine-readable definition files rather than physical hardware configuration or interactive configuration tools. Terraform is one of the most popular IaC tools, offering:

  • Declarative Configuration: Define desired state rather than procedural steps
  • Version Control: Track infrastructure changes in Git
  • Automation: Eliminate manual configuration processes
  • Consistency: Ensure identical environments across deployments
  • Scalability: Manage complex infrastructure efficiently

Getting Started with Terraform

Installation

# Download and install Terraform
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt update && sudo apt install terraform

# Verify installation
terraform version

Basic Project Structure

network-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
├── providers.tf
└── modules/
    ├── vpc/
    ├── subnets/
    └── security/

Core Concepts

Providers

Providers are plugins that Terraform uses to interact with cloud providers, SaaS providers, and other APIs.

# providers.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

provider "azurerm" {
  features {}
}

Variables

Define input variables for your configuration:

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-west-2"
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "environment" {
  description = "Environment name"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

Resources

Resources are the infrastructure objects that Terraform manages:

# main.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  }
}

resource "aws_subnet" "public" {
  count             = length(var.public_subnets)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.public_subnets[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name        = "${var.environment}-public-${count.index + 1}"
    Environment = var.environment
  }
}

Network Infrastructure Examples

VPC and Subnet Configuration

# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(var.tags, {
    Name = "${var.name}-vpc"
  })
}

resource "aws_subnet" "public" {
  count             = length(var.public_subnets)
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.public_subnets[count.index]
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.name}-public-${count.index + 1}"
  })
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnets)
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnets[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(var.tags, {
    Name = "${var.name}-private-${count.index + 1}"
  })
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = merge(var.tags, {
    Name = "${var.name}-igw"
  })
}

resource "aws_nat_gateway" "this" {
  count         = length(var.public_subnets)
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = merge(var.tags, {
    Name = "${var.name}-nat-${count.index + 1}"
  })
}

resource "aws_eip" "nat" {
  count = length(var.public_subnets)
  vpc   = true

  tags = merge(var.tags, {
    Name = "${var.name}-eip-${count.index + 1}"
  })
}

Security Groups and Network ACLs

# modules/security/main.tf
resource "aws_security_group" "web" {
  name_prefix = "${var.name}-web-"
  vpc_id      = var.vpc_id

  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 = var.allowed_ssh_cidr
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(var.tags, {
    Name = "${var.name}-web-sg"
  })
}

resource "aws_security_group" "database" {
  name_prefix = "${var.name}-db-"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.web.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(var.tags, {
    Name = "${var.name}-db-sg"
  })
}

Load Balancer Configuration

# modules/alb/main.tf
resource "aws_lb" "this" {
  name               = "${var.name}-alb"
  internal           = var.internal
  load_balancer_type = "application"
  security_groups    = var.security_groups
  subnets            = var.subnets

  enable_deletion_protection = var.enable_deletion_protection

  tags = merge(var.tags, {
    Name = "${var.name}-alb"
  })
}

resource "aws_lb_target_group" "this" {
  count    = length(var.target_groups)
  name     = "${var.name}-tg-${count.index}"
  port     = var.target_groups[count.index].port
  protocol = var.target_groups[count.index].protocol
  vpc_id   = var.vpc_id

  health_check {
    enabled             = true
    healthy_threshold   = 2
    interval            = 30
    matcher             = "200"
    path                = var.target_groups[count.index].health_check_path
    port                = "traffic-port"
    protocol            = var.target_groups[count.index].protocol
    timeout             = 5
    unhealthy_threshold = 2
  }

  tags = merge(var.tags, {
    Name = "${var.name}-tg-${count.index}"
  })
}

resource "aws_lb_listener" "this" {
  count             = length(var.listeners)
  load_balancer_arn = aws_lb.this.arn
  port              = var.listeners[count.index].port
  protocol          = var.listeners[count.index].protocol

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.this[var.listeners[count.index].target_group_index].arn
  }
}

Advanced Networking Scenarios

Multi-Region Deployment

# multi-region/main.tf
module "vpc_us_west" {
  source = "../modules/vpc"

  providers = {
    aws = aws.us-west-2
  }

  name = "us-west-2"
  vpc_cidr = "10.1.0.0/16"
  public_subnets = ["10.1.1.0/24", "10.1.2.0/24"]
  private_subnets = ["10.1.10.0/24", "10.1.11.0/24"]
  availability_zones = ["us-west-2a", "us-west-2b"]
}

module "vpc_us_east" {
  source = "../modules/vpc"

  providers = {
    aws = aws.us-east-1
  }

  name = "us-east-1"
  vpc_cidr = "10.2.0.0/16"
  public_subnets = ["10.2.1.0/24", "10.2.2.0/24"]
  private_subnets = ["10.2.10.0/24", "10.2.11.0/24"]
  availability_zones = ["us-east-1a", "us-east-1b"]
}

# VPC Peering
resource "aws_vpc_peering_connection" "us_west_to_us_east" {
  provider = aws.us-west-2
  vpc_id   = module.vpc_us_west.vpc_id
  peer_vpc_id = module.vpc_us_east.vpc_id
  peer_region = "us-east-1"

  tags = {
    Name = "us-west-2-to-us-east-1"
  }
}

Transit Gateway Configuration

# modules/transit-gateway/main.tf
resource "aws_ec2_transit_gateway" "this" {
  description = "${var.name} Transit Gateway"

  default_route_table_association = "enable"
  default_route_table_propagation = "enable"

  tags = merge(var.tags, {
    Name = "${var.name}-tgw"
  })
}

resource "aws_ec2_transit_gateway_vpc_attachment" "this" {
  count = length(var.vpc_attachments)

  subnet_ids         = var.vpc_attachments[count.index].subnet_ids
  transit_gateway_id = aws_ec2_transit_gateway.this.id
  vpc_id             = var.vpc_attachments[count.index].vpc_id

  tags = merge(var.tags, {
    Name = "${var.name}-tgw-attachment-${count.index}"
  })
}

resource "aws_ec2_transit_gateway_route_table" "this" {
  count = length(var.route_tables)

  transit_gateway_id = aws_ec2_transit_gateway.this.id

  tags = merge(var.tags, {
    Name = "${var.name}-tgw-rt-${count.index}"
  })
}

Best Practices

1. Use Modules for Reusability

# modules/network/main.tf
module "vpc" {
  source = "../vpc"

  name = var.name
  vpc_cidr = var.vpc_cidr
  public_subnets = var.public_subnets
  private_subnets = var.private_subnets
  availability_zones = var.availability_zones
}

module "security" {
  source = "../security"

  name = var.name
  vpc_id = module.vpc.vpc_id
  allowed_ssh_cidr = var.allowed_ssh_cidr
}

2. Implement State Management

# backend.tf
terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "network/terraform.tfstate"
    region = "us-west-2"

    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

3. Use Data Sources for Dynamic Configuration

# data.tf
data "aws_availability_zones" "available" {
  state = "available"
}

data "aws_ami" "latest_amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

4. Implement Proper Tagging

# locals.tf
locals {
  common_tags = {
    Environment = var.environment
    Project     = var.project
    Owner       = var.owner
    ManagedBy   = "Terraform"
  }
}

CI/CD Integration

GitLab CI/CD Pipeline

# .gitlab-ci.yml
stages:
  - validate
  - plan
  - apply

variables:
  TF_ROOT: ${CI_PROJECT_DIR}/terraform

validate:
  stage: validate
  script:
    - cd $TF_ROOT
    - terraform init
    - terraform validate
    - terraform fmt -check

plan:
  stage: plan
  script:
    - cd $TF_ROOT
    - terraform init
    - terraform plan -out=plan.tfplan
  artifacts:
    paths:
      - $TF_ROOT/plan.tfplan
    expire_in: 1 week

apply:
  stage: apply
  script:
    - cd $TF_ROOT
    - terraform init
    - terraform apply plan.tfplan
  when: manual
  only:
    - main

GitHub Actions Workflow

# .github/workflows/terraform.yml
name: Terraform

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  terraform:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v1

    - name: Terraform Init
      run: terraform init

    - name: Terraform Validate
      run: terraform validate

    - name: Terraform Plan
      run: terraform plan
      if: github.event_name == 'pull_request'

Monitoring and Validation

Output Configuration

# outputs.tf
output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.main.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 "load_balancer_dns" {
  description = "The DNS name of the load balancer"
  value       = aws_lb.main.dns_name
}

Validation Rules

# validation.tf
variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Must be a valid CIDR block."
  }
}

variable "subnet_cidrs" {
  description = "List of subnet CIDR blocks"
  type        = list(string)

  validation {
    condition = alltrue([
      for cidr in var.subnet_cidrs : can(cidrhost(cidr, 0))
    ])
    error_message = "All subnet CIDR blocks must be valid."
  }
}

Troubleshooting Common Issues

1. State Management Issues

# Refresh state
terraform refresh

# Import existing resources
terraform import aws_vpc.main vpc-12345678

# Move resources
terraform state mv aws_subnet.old aws_subnet.new

2. Dependency Issues

# Use depends_on for explicit dependencies
resource "aws_instance" "web" {
  ami           = data.aws_ami.latest.id
  instance_type = "t3.micro"

  depends_on = [aws_security_group.web]
}

3. Variable Validation

variable "instance_type" {
  description = "EC2 instance type"
  type        = string

  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
  }
}

Conclusion

Terraform provides a powerful and flexible platform for managing network infrastructure as code. By following the practices outlined in this guide, you can build robust, scalable, and maintainable network infrastructure.

Key takeaways: - Use modules for reusability and maintainability - Implement proper state management - Follow naming conventions and tagging strategies - Integrate with CI/CD pipelines - Implement validation and testing - Document your infrastructure code

Additional Resources


This guide provides a comprehensive overview of using Terraform for network infrastructure as code. For more advanced topics, check out our other articles on specific Terraform modules and best practices.