Skip to content
Log in Sign up My account

Terraform for Network Engineers

Terraform describes infrastructure in declarative files, compares them with what exists, and makes the difference. Network teams meet it first in cloud networking (VPCs, transit gateways, firewalls), then in controllers such as ACI and Meraki, and increasingly on IOS XE devices. This article explains what infrastructure as code means for a network, how providers and state work, the plan/apply workflow, and walks through a small working example.

What IaC means for a network

With the CLI, the device's running config is the only record of intent. With infrastructure as code:

  • The desired state lives in text files in Git.
  • A tool reads those files, reads the real state, and computes the changes.
  • Review happens on the files before anything is touched.
  • The same files can build a lab, a staging copy and production.

Terraform is declarative. You write there is a VPC with this CIDR and these subnets, not create a VPC, then create subnet 1. Terraform works out the order from references between resources. Ansible and Netmiko are procedural by comparison: they run steps against a device that already exists. Terraform fits things that are created and destroyed through an API, which describes cloud networks and controllers well and traditional box-by-box CLI devices less well.

Core concepts

graph LR
    CODE["Configuration<br/>*.tf files"] --> PLAN["terraform plan"]
    STATE["State file"] --> PLAN
    REAL["Real infrastructure<br/>via provider API"] --> PLAN
    PLAN --> APPLY["terraform apply"]
    APPLY --> REAL
    APPLY --> STATE

Providers are plugins that talk to an API. You declare which ones you need and Terraform downloads them on terraform init. Providers relevant to network work:

Provider Manages Notes
hashicorp/aws, hashicorp/azurerm, hashicorp/google Cloud VPCs and VNets, routes, security groups, gateways Mature, widely used
CiscoDevNet/aci ACI tenants, VRFs, bridge domains, EPGs, contracts via the APIC Official Cisco DevNet provider
CiscoDevNet/iosxe IOS XE devices over RESTCONF Needs RESTCONF enabled; resource coverage grows with releases
Meraki provider Meraki Dashboard organisations, networks, VLANs, SSIDs Check the registry for the currently maintained provider

Search the Terraform Registry for the current provider name, version and supported resources before you commit to one. Provider coverage differs a lot between products.

Resources are the things you manage. Data sources read existing things without managing them. Variables parameterise a configuration, outputs expose values, and modules package reusable groups of resources.

State is a file in which Terraform records what it created and the real IDs. Terraform uses it to know that aws_vpc.main in your code is vpc-0abc... in the cloud. State matters for three reasons:

  1. Losing it means Terraform no longer knows what it owns.
  2. It can contain secrets in plain text, including passwords passed to providers.
  3. Two people applying at the same time can corrupt it.

So do not keep state on a laptop for shared work. Use a remote backend with locking, such as an S3 bucket with a lock mechanism, Azure Storage, or a Terraform Cloud or HCP Terraform workspace, and restrict who can read it.

Warning

Never commit terraform.tfstate or .tfvars files that hold credentials to Git. Add them to .gitignore. Pass secrets through environment variables (TF_VAR_name), a secrets manager, or the provider's own environment variables.

The workflow

terraform init       # download providers, configure the backend
terraform fmt        # format the files
terraform validate   # check syntax and internal consistency
terraform plan       # show what would change, change nothing
terraform apply      # apply the plan, asks for confirmation
terraform destroy    # remove everything in this configuration

plan is the central habit. It shows each resource marked as create (+), update in place (~), or destroy (-), including destroy-and-recreate (-/+). Read it every time. In a pipeline, save the plan and apply exactly that plan so nothing changes between review and apply:

terraform plan -out=tfplan
terraform apply tfplan

Example: a small VPC with two subnets

This uses AWS because the resources are stable and anyone can try it in a free-tier-sized account. The same pattern applies to any provider. Check the AWS provider documentation for the current version constraint.

main.tf:

terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.region
}

variable "region" {
  type    = string
  default = "eu-west-1"
}

variable "subnets" {
  type = map(object({
    cidr = string
    az   = string
  }))
  default = {
    app = { cidr = "10.20.1.0/24", az = "eu-west-1a" }
    db  = { cidr = "10.20.2.0/24", az = "eu-west-1b" }
  }
}

resource "aws_vpc" "main" {
  cidr_block           = "10.20.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = { Name = "lab-vpc" }
}

resource "aws_subnet" "this" {
  for_each          = var.subnets
  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az

  tags = { Name = "lab-${each.key}" }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
}

resource "aws_route_table_association" "app" {
  subnet_id      = aws_subnet.this["app"].id
  route_table_id = aws_route_table.public.id
}

output "vpc_id" {
  value = aws_vpc.main.id
}

Notice that nothing says "create the VPC first". aws_subnet.this refers to aws_vpc.main.id, so Terraform builds a dependency graph and orders the work itself. for_each over a map gives you one subnet per entry, and adding a third entry to the map adds a third subnet on the next apply without touching the others.

Run it:

terraform init
terraform plan -out=tfplan
terraform apply tfplan

Now change the db subnet CIDR and plan again. Terraform will show the subnet as -/+ (replace), because AWS cannot change a subnet's CIDR in place. That is the kind of detail the plan exposes before you break something.

Example: ACI in a few lines

The ACI provider follows the same shape, with the APIC as the target:

terraform {
  required_providers {
    aci = {
      source = "CiscoDevNet/aci"
    }
  }
}

provider "aci" {
  username = var.apic_user
  password = var.apic_password
  url      = "https://apic.example.net"
  insecure = true   # lab only
}

resource "aci_tenant" "demo" {
  name = "demo"
}

resource "aci_vrf" "prod" {
  tenant_dn = aci_tenant.demo.id
  name      = "prod"
}

Resource names and required arguments change between provider versions, so treat this as a shape and read the provider documentation for the version you pin.

Drift, import and destroy

  • Drift is when someone changes a resource by hand. terraform plan shows it as a difference, and the next apply puts it back. That is the same drift-correction idea as a NAPALM replace, applied at the API level.
  • Import brings an existing resource under Terraform's control. Write the resource block, then run terraform import <address> <id>. Recent versions also support import blocks in configuration. Plan afterwards and keep editing the code until the plan shows no changes.
  • Destroy deletes everything in the state. Protect important resources with lifecycle { prevent_destroy = true } and restrict who can run it in production.

Where Terraform fits and where it does not

Terraform suits:

  • Cloud network foundations, built once and changed rarely.
  • Controllers with solid providers (ACI, Meraki, SD-WAN managers) where the API is the interface.
  • Environments you create and tear down, such as labs and per-project networks.

It is a weaker fit for:

  • Brownfield estates of CLI-only switches and routers, where Ansible or NAPALM is more natural.
  • Anything where resource coverage in the provider is thin. If the provider lacks the resource you need, you cannot manage it declaratively.
  • Teams that will not guard the state file. Unprotected state is a security and reliability problem.

OpenTofu is an open-source fork of Terraform that reads the same configuration language and works with most of the same providers. Terraform itself is distributed under the HashiCorp Business Source License, so read the licence terms if you embed it in a product. For internal use of either tool, the practical differences are small.

Run terraform plan from a CI pipeline on every pull request and apply only from the main branch after approval, as described in Git and CI/CD for network configs.

References