Terraform Fundamentals Through a Network Lens
Getting started with Terraform for network engineering platforms
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:
-
API Access:
- Meraki API key with organization access
- Mist credentials or API token
- NetBox API token
-
Organization Setup:
- Existing Meraki organization
- Existing Mist organization
- NetBox instance with proper permissions
-
Credentials File:
- Copy
terraform.tfvars.exampletoterraform.tfvars - Fill in your actual credentials and settings
- Copy
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
- Start with foundations: Ensure your organizations and sites exist before creating networks
- Use data sources: Query existing infrastructure before creating new resources
- Manage credentials safely: Never commit API keys or passwords to version control
- Plan dependencies: Understand how resources depend on each other across platforms
- 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