Skip to content
15 min read

Terraform Fundamentals Through a Network Lens

Getting started with Terraform for network engineering platforms

NetworkIaCTerraform

Terraform Fundamentals Through a Network Lens

Part 2 of the “Terraform for Network Engineers” series

From Network Diagrams to Code

In Part 1, we explored why Infrastructure as Code matters for network engineers. Now it’s time to roll up our sleeves and understand how Terraform actually works with real network infrastructure. But instead of diving into yet another cloud provider example, we’re going to learn Terraform concepts using the platforms you actually manage: Cisco Meraki, Juniper Mist, and network IPAM systems like NetBox.

Think of this as learning a new network protocol. You don’t start by memorizing packet formats—you start by understanding what problem the protocol solves and how it fits into your existing knowledge. That’s exactly how we’ll approach Terraform, but using the network platforms you work with every day.

The Network Engineer’s Mental Model

Before we dive into provider-specific syntax, let’s establish a mental model that maps Terraform concepts to networking concepts you already understand.

Terraform Configuration = Network Design Document

Your Terraform configuration files are like comprehensive network design documents, but with a crucial difference: they’re executable. Instead of creating a Visio diagram that someone then has to interpret and manually configure, you’re creating a precise specification that Terraform can automatically implement across your Meraki dashboard, Mist console, and NetBox instance.

Resources = Network Components

In networking, you work with organizations, networks, access policies, switch templates, and IP prefixes. In Terraform, these become resources—the fundamental building blocks that represent real infrastructure components in your management platforms.

Dependencies = Configuration Relationships

Just as you can’t assign a switch template before the template exists, or create a network without an organization, Terraform resources have dependencies. Terraform automatically figures out the correct order to create resources, just like you mentally plan the sequence when configuring network management platforms.

State = Network Management Database

Your Meraki dashboard, Mist console, and NetBox all maintain databases of your current network configuration. Terraform’s state file serves the same purpose—it’s Terraform’s understanding of what currently exists across all these platforms.

Providers = Platform API Clients

Just as you use different management interfaces for Meraki, Mist, and NetBox, Terraform uses providers to interact with each platform’s API. Each provider handles the specific authentication and API calls needed for that platform.

Setting Up Your First Real Network Infrastructure

Let’s start with something every network engineer does: setting up the foundational elements in your network management platforms. We’ll configure a basic multi-platform setup that includes Meraki organization setup, Mist site configuration, and NetBox IP management.

Configuring Providers and Prerequisites

First, let’s look at a Terraform configuration that sets up all three platforms:

# Configure Terraform and required providers
terraform {
  required_version = ">= 1.0"
  required_providers {
    meraki = {
      source  = "cisco-en/meraki"
      version = "~> 0.2.0"
    }
    mist = {
      source  = "Juniper/mist"
      version = "~> 0.2.0"
    }
    netbox = {
      source  = "e-breuninger/netbox"
      version = "~> 3.0"
    }
  }
}

# Configure the Meraki Provider
provider "meraki" {
  meraki_api_key = var.meraki_api_key
  meraki_base_url = "https://api.meraki.com/api/v1"
}

# Configure the Mist Provider
provider "mist" {
  username = var.mist_username
  password = var.mist_password
  # Or use API token
  # api_token = var.mist_api_token
}

# Configure the NetBox Provider
provider "netbox" {
  server_url = var.netbox_url
  api_token  = var.netbox_token
}

Network Analogy: This is like configuring your network management stations with the right credentials and connection parameters for each platform. You’re telling Terraform how to authenticate and connect to Meraki, Mist, and NetBox.

Defining Variables for Security

Never hardcode credentials! Let’s define variables properly:

# Meraki configuration
variable "meraki_api_key" {
  description = "Meraki API key"
  type        = string
  sensitive   = true
}

variable "meraki_organization_name" {
  description = "Name of the Meraki organization"
  type        = string
  default     = "My Company"
}

# Mist configuration
variable "mist_username" {
  description = "Mist username"
  type        = string
  sensitive   = true
}

variable "mist_password" {
  description = "Mist password"
  type        = string
  sensitive   = true
}

variable "mist_organization_name" {
  description = "Name of the Mist organization"
  type        = string
  default     = "My Company"
}

# NetBox configuration
variable "netbox_url" {
  description = "NetBox server URL"
  type        = string
  default     = "https://netbox.company.com"
}

variable "netbox_token" {
  description = "NetBox API token"
  type        = string
  sensitive   = true
}

# Site information
variable "site_name" {
  description = "Name of the site being configured"
  type        = string
  default     = "headquarters"
}

variable "site_address" {
  description = "Physical address of the site"
  type        = string
  default     = "123 Main St, Anytown, ST 12345"
}

Working with Existing Organizations

Before you can create networks and sites, you need to reference your existing organizations. This is where data sources come in:

# Get existing Meraki organization
data "meraki_organizations" "main" {
  # This will return all organizations you have access to
}

# Find our specific organization
locals {
  meraki_org_id = [for org in data.meraki_organizations.main.organizations :
    org.id if org.name == var.meraki_organization_name][0]
}

# Get existing Mist organization
data "mist_org" "main" {
  name = var.mist_organization_name
}

# Verify NetBox connection and get site types
data "netbox_site_groups" "all" {}

Network Analogy: This is like querying your existing management systems to understand what organizations, tenants, or administrative domains you have access to before you start creating new resources.

Creating Your First Network Resources

Now let’s create some actual network infrastructure across all three platforms:

# Create a network in Meraki
resource "meraki_networks" "headquarters" {
  organization_id = local.meraki_org_id
  name           = "${var.site_name}-network"
  product_types  = ["appliance", "switch", "wireless"]
  tags           = ["terraform", "headquarters"]
  notes          = "Created and managed by Terraform"
}

# Configure basic network settings
resource "meraki_networks_appliance_settings" "headquarters" {
  network_id                = meraki_networks.headquarters.network_id
  client_tracking_method    = "MAC address"
  deployment_mode          = "routed"
  dynamic_dns_enabled      = true
}

# Create a site in Mist
resource "mist_site" "headquarters" {
  org_id      = data.mist_org.main.id
  name        = var.site_name
  country_code = "US"
  timezone    = "America/New_York"
  address     = var.site_address

  # Site-specific settings
  setting = {
    # Enable location services
    rtsa = {
      enabled = true
    }
    # Configure wireless settings
    wlan = {
      enabled = true
    }
  }
}

# Create a site in NetBox for IP management
resource "netbox_site" "headquarters" {
  name        = var.site_name
  slug        = replace(lower(var.site_name), " ", "-")
  description = "Headquarters site managed by Terraform"
  physical_address = var.site_address
  status      = "active"

  tags = ["terraform", "headquarters"]
}

# Create a VLAN group in NetBox for this site
resource "netbox_vlan_group" "headquarters" {
  name        = "${var.site_name}-vlans"
  slug        = "${replace(lower(var.site_name), " ", "-")}-vlans"
  site_id     = netbox_site.headquarters.id
  description = "VLAN group for ${var.site_name}"
}

Understanding Resources and Dependencies

Let’s break down what’s happening here:

Resource Declaration: Each resource block declares a piece of infrastructure. The format is:

resource "provider_resource_type" "local_name" {
  # configuration parameters
}

Real-World Dependencies: Notice how resources reference each other:

  • The Meraki network settings depend on the network being created first
  • The NetBox VLAN group references the site ID
  • The Mist site references the organization data

Cross-Platform Integration: While these resources exist in different platforms, Terraform manages them as a cohesive infrastructure definition.

Working with VLANs and Network Segmentation

Let’s add network segmentation across platforms:

# Define VLAN configuration
locals {
  vlans = {
    management = {
      id          = 10
      name        = "Management"
      subnet      = "192.168.10.0/24"
      description = "Network management VLAN"
    }
    users = {
      id          = 100
      name        = "Users"
      subnet      = "10.0.100.0/24"
      description = "User devices VLAN"
    }
    iot = {
      id          = 200
      name        = "IoT"
      subnet      = "10.0.200.0/24"
      description = "IoT devices VLAN"
    }
  }
}

# Create VLANs in Meraki
resource "meraki_networks_appliance_vlans" "site_vlans" {
  for_each = local.vlans

  network_id        = meraki_networks.headquarters.network_id
  vlan_id          = each.value.id
  name             = each.value.name
  subnet           = each.value.subnet
  appliance_ip     = cidrhost(each.value.subnet, 1)

  # DHCP settings
  dhcp_handling           = "Run a DHCP server"
  dhcp_lease_time         = "1 day"
  dns_nameservers        = "upstream_dns"

  # Reserved IP ranges (first 10 IPs)
  reserved_ip_ranges {
    start = cidrhost(each.value.subnet, 2)
    end   = cidrhost(each.value.subnet, 10)
    comment = "Reserved for infrastructure"
  }
}

# Create corresponding VLANs in NetBox for documentation
resource "netbox_vlan" "site_vlans" {
  for_each = local.vlans

  name        = each.value.name
  vid         = each.value.id
  description = each.value.description
  site_id     = netbox_site.headquarters.id
  group_id    = netbox_vlan_group.headquarters.id
  status      = "active"
  role        = "production"

  tags = ["terraform", each.key]
}

# Create IP prefixes in NetBox for each VLAN
resource "netbox_prefix" "vlan_prefixes" {
  for_each = local.vlans

  prefix      = each.value.subnet
  description = "${each.value.description} subnet"
  site_id     = netbox_site.headquarters.id
  vlan_id     = netbox_vlan.site_vlans[each.key].id
  status      = "active"
  is_pool     = false

  tags = ["terraform", each.key, "vlan-subnet"]
}

Understanding the for_each Pattern

The for_each pattern is crucial for managing multiple similar resources:

# Instead of creating each VLAN individually:
resource "meraki_networks_appliance_vlans" "management" { ... }
resource "meraki_networks_appliance_vlans" "users" { ... }
resource "meraki_networks_appliance_vlans" "iot" { ... }

# Use for_each to create them from a map:
resource "meraki_networks_appliance_vlans" "site_vlans" {
  for_each = local.vlans
  # each.key = "management", "users", "iot"
  # each.value = the VLAN configuration object
}

Network Analogy: This is like using a configuration template to create multiple VLANs with consistent settings but different parameters.

Creating Wireless Networks

Let’s add wireless configuration across platforms:

# Define SSIDs
locals {
  ssids = {
    corporate = {
      name           = "Corporate"
      auth_mode      = "psk"
      encryption_mode = "wpa"
      psk            = var.corporate_wifi_psk
      vlan_id        = local.vlans.users.id
    }
    guest = {
      name           = "Guest"
      auth_mode      = "open"
      encryption_mode = "none"
      vlan_id        = 300  # Separate guest VLAN
    }
  }
}

# Create wireless networks in Meraki
resource "meraki_networks_wireless_ssids" "site_ssids" {
  for_each = local.ssids

  network_id = meraki_networks.headquarters.network_id
  number     = each.key == "corporate" ? 0 : 1

  name                = each.value.name
  enabled            = true
  auth_mode          = each.value.auth_mode
  encryption_mode    = each.value.encryption_mode
  psk                = lookup(each.value, "psk", null)

  # VLAN settings
  use_vlan_tagging = true
  default_vlan_id  = each.value.vlan_id

  # Security settings
  wpa_encryption_mode = "WPA2 only"
  splashing_enabled   = each.key == "guest"
}

# Create corresponding WLANs in Mist
resource "mist_site_wlan" "site_wlans" {
  for_each = local.ssids

  site_id = mist_site.headquarters.id
  ssid    = each.value.name

  # Authentication settings
  auth = {
    type = each.value.auth_mode == "psk" ? "psk" : "open"
    psk  = lookup(each.value, "psk", null)
  }

  # VLAN settings
  vlan_enabled = true
  vlan_id      = each.value.vlan_id

  # Enable for guest networks
  portal_enabled = each.key == "guest"
}

Variables for Sensitive Data

Create a separate file terraform.tfvars.example:

# Copy this file to terraform.tfvars and fill in your values

# Meraki configuration
meraki_api_key = "your_meraki_api_key_here"
meraki_organization_name = "Your Organization Name"

# Mist configuration
mist_username = "your_mist_username"
mist_password = "your_mist_password"
mist_organization_name = "Your Organization Name"

# NetBox configuration
netbox_url = "https://your-netbox-server.com"
netbox_token = "your_netbox_api_token"

# Site configuration
site_name = "headquarters"
site_address = "123 Main St, Your City, ST 12345"

# WiFi credentials
corporate_wifi_psk = "your_corporate_wifi_password"

Outputs for Integration

Extract useful information that other systems or team members might need:

# Meraki outputs
output "meraki_network_id" {
  description = "Meraki network ID"
  value       = meraki_networks.headquarters.network_id
}

output "meraki_vlans" {
  description = "Created Meraki VLANs"
  value = {
    for k, v in meraki_networks_appliance_vlans.site_vlans :
    k => {
      id     = v.vlan_id
      name   = v.name
      subnet = v.subnet
    }
  }
}

# Mist outputs
output "mist_site_id" {
  description = "Mist site ID"
  value       = mist_site.headquarters.id
}

# NetBox outputs
output "netbox_site_id" {
  description = "NetBox site ID"
  value       = netbox_site.headquarters.id
}

output "netbox_prefixes" {
  description = "Created NetBox prefixes"
  value = {
    for k, v in netbox_prefix.vlan_prefixes :
    k => {
      id     = v.id
      prefix = v.prefix
      vlan_id = v.vlan_id
    }
  }
}

# Combined network summary
output "network_summary" {
  description = "Complete network configuration summary"
  value = {
    site_name = var.site_name
    meraki = {
      network_id = meraki_networks.headquarters.network_id
      org_id     = local.meraki_org_id
    }
    mist = {
      site_id = mist_site.headquarters.id
      org_id  = data.mist_org.main.id
    }
    netbox = {
      site_id = netbox_site.headquarters.id
    }
    vlans = local.vlans
  }
}

The Terraform Workflow with Real Infrastructure

Now let’s walk through the Terraform workflow using our multi-platform network example.

Step 1: Prerequisites Setup

Before running Terraform, ensure you have:

  1. API Access:

    • Meraki API key with organization access
    • Mist credentials or API token
    • NetBox API token
  2. Organization Setup:

    • Existing Meraki organization
    • Existing Mist organization
    • NetBox instance with proper permissions
  3. Credentials File:

    • Copy terraform.tfvars.example to terraform.tfvars
    • Fill in your actual credentials and settings

Step 2: Initialize (terraform init)

terraform init

This downloads the Meraki, Mist, and NetBox providers.

Step 3: Validate Configuration

# Check syntax
terraform validate

# Format code consistently
terraform fmt

Step 4: Plan (terraform plan)

terraform plan

Sample output for our multi-platform configuration:

Terraform will perform the following actions:

  # meraki_networks.headquarters will be created
  + resource "meraki_networks" "headquarters" {
      + id               = (known after apply)
      + name             = "headquarters-network"
      + network_id       = (known after apply)
      + organization_id  = "123456"
      + product_types    = ["appliance", "switch", "wireless"]
    }

  # mist_site.headquarters will be created
  + resource "mist_site" "headquarters" {
      + id           = (known after apply)
      + name         = "headquarters"
      + org_id       = "abcd1234-5678-9012-3456-789012345678"
      + country_code = "US"
      + timezone     = "America/New_York"
    }

  # netbox_site.headquarters will be created
  + resource "netbox_site" "headquarters" {
      + id     = (known after apply)
      + name   = "headquarters"
      + slug   = "headquarters"
      + status = "active"
    }

Plan: 15 to add, 0 to change, 0 to destroy.

Step 5: Apply Changes

terraform apply

Review the plan and type yes to proceed. Terraform will:

  • Create the Meraki network and configure VLANs
  • Set up the Mist site and WLANs
  • Create NetBox documentation entries
  • Show outputs with all the created resource IDs

Step 6: Verify Configuration

Check your platforms:

  • Meraki Dashboard: Verify network, VLANs, and SSIDs
  • Mist Console: Confirm site and WLAN configuration
  • NetBox: Check site, VLANs, and IP prefixes

Practical Exercise: Build Your First Multi-Platform Network

Let’s put these concepts together. Create a new directory and follow along:

1. Create the Directory Structure

mkdir terraform-network-lab
cd terraform-network-lab

# Create the main configuration
touch main.tf
touch variables.tf
touch outputs.tf
touch terraform.tfvars.example

2. Start with a Simple Configuration

Begin with just one platform to master the workflow:

main.tf (NetBox only to start):

terraform {
  required_providers {
    netbox = {
      source  = "e-breuninger/netbox"
      version = "~> 3.0"
    }
  }
}

provider "netbox" {
  server_url = var.netbox_url
  api_token  = var.netbox_token
}

resource "netbox_site" "lab" {
  name        = "terraform-lab"
  slug        = "terraform-lab"
  description = "Learning Terraform with NetBox"
  status      = "planned"
}

resource "netbox_vlan" "management" {
  name    = "Management"
  vid     = 10
  site_id = netbox_site.lab.id
  status  = "active"
}

variables.tf:

variable "netbox_url" {
  description = "NetBox server URL"
  type        = string
}

variable "netbox_token" {
  description = "NetBox API token"
  type        = string
  sensitive   = true
}

outputs.tf:

output "site_info" {
  value = {
    id   = netbox_site.lab.id
    name = netbox_site.lab.name
    slug = netbox_site.lab.slug
  }
}

3. Test the Workflow

# Initialize
terraform init

# Plan
terraform plan

# Apply (if you have NetBox access)
terraform apply

# Check state
terraform show

# Clean up
terraform destroy

4. Expand to Multiple Platforms

Once comfortable with the workflow, add Meraki or Mist resources following the patterns shown earlier.

Common Patterns for Network Infrastructure

1. Environment-Specific Configurations

locals {
  is_production = var.environment == "production"

  # Different settings based on environment
  vlan_config = var.environment == "production" ? {
    management = { id = 10, subnet = "192.168.10.0/24" }
    users      = { id = 100, subnet = "10.0.100.0/24" }
    iot        = { id = 200, subnet = "10.0.200.0/24" }
  } : {
    management = { id = 10, subnet = "10.10.10.0/24" }
    users      = { id = 100, subnet = "10.10.100.0/24" }
  }
}

2. Site-Specific Variations

variable "site_config" {
  description = "Site-specific configuration"
  type = object({
    name           = string
    timezone       = string
    country_code   = string
    wireless_power = string
  })

  default = {
    name           = "headquarters"
    timezone       = "America/New_York"
    country_code   = "US"
    wireless_power = "high"
  }
}

3. Conditional Resource Creation

# Only create guest network if enabled
resource "meraki_networks_wireless_ssids" "guest" {
  count = var.enable_guest_wifi ? 1 : 0

  network_id = meraki_networks.main.network_id
  number     = 1
  name       = "Guest"
  auth_mode  = "open"
}

Error Handling and Troubleshooting

Network infrastructure providers have specific troubleshooting patterns:

1. API Authentication Issues

# Test API connectivity
curl -H "X-Cisco-Meraki-API-Key: YOUR_KEY" \
     "https://api.meraki.com/api/v1/organizations"

# Enable detailed logging
export TF_LOG=DEBUG
terraform plan

2. Organization/Site Prerequisites

# Verify organization access
terraform console
> data.meraki_organizations.main.organizations
> data.mist_org.main

3. Resource Dependencies

# Check resource dependencies
terraform graph | dot -Tpng > graph.png

4. State Management Issues

# Import existing resources
terraform import netbox_site.existing 123
terraform import meraki_networks.existing L_123456789

Key Takeaways

  1. Start with foundations: Ensure your organizations and sites exist before creating networks
  2. Use data sources: Query existing infrastructure before creating new resources
  3. Manage credentials safely: Never commit API keys or passwords to version control
  4. Plan dependencies: Understand how resources depend on each other across platforms
  5. Test incrementally: Start with one platform and expand gradually

The concepts you’ve learned here using real network infrastructure providers form the foundation for everything else we’ll build in this series. You’re now managing actual network devices and services as code, not just cloud resources.


Next: Part 3 - “Advanced Network Infrastructure Patterns” - We’ll explore multi-site deployments, complex network topologies, and integration patterns that work with your existing infrastructure.

// Comments