Terraform Fundamentals
Objective
In this lesson you will learn the Terraform workflow for configuring Cisco IOS XE devices: init, plan, apply, and destroy. You will configure a single VLAN on an IOS XE device using the IOS XE Terraform provider (RESTCONF). This matters in production because Infrastructure as Code (IaC) gives repeatable, auditable, and reversible changes across many devices — for example, creating VLANs consistently across an entire campus or data-center leaf layer. In a real-world scenario, teams use Terraform to ensure every switch has the same VLANs and names before deploying new services or onboarding tenants.
Topology & Device Table
ASCII topology (management-plane view). The Terraform host uses RESTCONF (HTTPS) to the IOS XE device on port 443.
Workstation: 192.0.2.10 IOS XE Device (mgmt interface Gi0/0): 192.0.2.1 (RESTCONF on 443)
Device Table
| Device | Interface | IP Address | Subnet Mask | Role |
|---|---|---|---|---|
| Terraform workstation | eth0 | 192.0.2.10 | 255.255.255.0 | Runs terraform CLI (client) |
| IOS XE Device (lab-router.lab.nhprep.com) | GigabitEthernet0/0 | 192.0.2.1 | 255.255.255.0 | Target device running RESTCONF |
Note: The IOS XE Terraform provider uses RESTCONF with JSON encoding to configure the device. The provider block references the device via a URL (see steps below).
Key Concepts (Theory + Practical)
-
Terraform Lifecycle (init, plan, apply, destroy)
- init downloads provider plugins and prepares the working directory; plan computes a proposed execution plan; apply makes the changes to reach the desired state; destroy removes resources created by Terraform.
- In production,
planis essential for peer review because it shows proposed changes before they happen.
-
Provider and Resources
- The provider is the plugin that knows how to talk to a specific platform (here: iosxe). The resource is a declarative representation of a device feature (e.g., a VLAN).
- The provider for IOS XE communicates via RESTCONF (HTTPS/JSON) to the device.
-
Stateful behavior
- Terraform is stateful: it keeps a state file to know what it previously configured. This allows Terraform to compute diffs and to remove the configuration cleanly with
terraform destroy. - In production this prevents drift and allows safe rollbacks.
- Terraform is stateful: it keeps a state file to know what it previously configured. This allows Terraform to compute diffs and to remove the configuration cleanly with
-
RESTCONF vs NETCONF vs gNMI
- RESTCONF uses HTTPS and JSON (RFC7951 JSON_IETF). NETCONF uses SSH and XML. gNMI uses HTTP/2 and protobufs. Terraform's IOS XE provider uses RESTCONF with JSON for payloads and operations (GET, POST, PUT, PATCH, DELETE).
-
Verification & idempotency
- Terraform resources are idempotent: applying the same config twice will not create duplicates. You verify the resource exists either by querying the device (via the provider or device CLI translated to RESTCONF) or by inspecting Terraform state.
Step-by-step configuration
Step 1: Create Terraform execution plan file (main.tf)
What we are doing: Create the Terraform configuration file that declares the IOS XE provider and a VLAN resource (VLAN 511). This file is the declarative source of truth that Terraform will use.
# main.tf
terraform {
required_providers {
iosxe = {
source = "CiscoDevNet/iosxe"
version = ">=0.5.0"
}
}
}
provider "iosxe" {
username = var.device_username
password = var.device_password
url = var.host_url
}
resource "iosxe_vlan" "example" {
vlan_id = 511
name = "Vlan511"
shutdown = false
}
# Optional data source to read the VLAN after creation
data "iosxe_vlan" "example_read" {
vlan_id = 511
}
What just happened: The terraform block tells Terraform to use the iosxe provider from the registry. The provider "iosxe" block declares credentials and the device URL as variables (secure practice: do not hard-code credentials). The resource "iosxe_vlan" "example" tells Terraform we want VLAN 511 present, named "Vlan511", and administratively up (shutdown = false). The data block shows how you can read back the VLAN after Terraform applies it.
Real-world note: Storing provider credentials as variables and using a secrets backend (not shown here) is recommended in production.
Verify:
# Show the main.tf file contents to verify it was saved
cat main.tf
# Expected output (complete file contents):
terraform {
required_providers {
iosxe = {
source = "CiscoDevNet/iosxe"
version = ">=0.5.0"
}
}
}
provider "iosxe" {
username = var.device_username
password = var.device_password
url = var.host_url
}
resource "iosxe_vlan" "example" {
vlan_id = 511
name = "Vlan511"
shutdown = false
}
data "iosxe_vlan" "example_read" {
vlan_id = 511
}
<div class="topology-diagram">
<img src="data:image/svg+xml;base64,PD9wbGFudHVtbCAxLjIwMjYuMT8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9Ik5XRElBRyIgaGVpZ2h0PSIxNjZweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjM3OXB4O2hlaWdodDoxNjZweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzNzkgMTY2IiB3aWR0aD0iMzc5cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3OS4wNDg4IiB4PSI1IiB5PSIxNi4xMzg3Ij5NYW5hZ2VtZW50PC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijc2LjU2NDUiIHg9IjcuNDg0NCIgeT0iMzAuMTA3NCI+MTkyLjAuMi4wLzI0PC90ZXh0PjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjI4Mi4xNTYzIiB4PSI4OS4wNDg4IiB5PSIxNi40Njg4Ii8+PHBhdGggZD0iTTE2Mi43NzE1LDIxLjQ2ODggTDE2Mi43NzE1LDcwLjY3NTgiIGZpbGw9Im5vbmUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MTsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI1OS40Nzk1IiB4PSIxMjQuOTQwMiIgeT0iMzQuNTc1NyI+MTkyLjAuMi4xMDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyNS4wNTA4IiB4PSIxMjQuOTQwMiIgeT0iNDcuMzgwNCI+ZXRoMDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3NS42NjI2IiB4PSIxMjQuOTQwMiIgeT0iNjAuMTg1MSI+VGVycmFmb3JtIENMSTwvdGV4dD48cGF0aCBkPSJNMzAzLjg0OTYsMjEuNDY4OCBMMzAzLjg0OTYsNzAuNjc1OCIgZmlsbD0ibm9uZSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjU5LjQ3OTUiIHg9IjI0OS42MzkyIiB5PSIzNC41NzU3Ij4xOTIuMC4yLjExPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijk0LjIzMDUiIHg9IjI0OS42MzkyIiB5PSI0Ny4zODA0Ij5HaWdhYml0RXRoZXJuZXQwPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEwOC40MjA5IiB4PSIyNDkuNjM5MiIgeT0iNjAuMTg1MSI+UkVTVENPTkYgVENQLzQ0MzwvdGV4dD48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjExMy40NDUzIiB4PSIxMDQuMDQ4OCIgeT0iNzAuNjc1OCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjkzLjQ0NTMiIHg9IjExNC4wNDg4IiB5PSI5MS44MTQ1Ij5UZXJyYWZvcm1fSG9zdDwvdGV4dD48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjEwOC43MTA5IiB4PSIyNDcuNDk0MSIgeT0iNzAuNjc1OCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijg4LjcxMDkiIHg9IjI1Ny40OTQxIiB5PSI5MS44MTQ1Ij5JT1NfWEVfUm91dGVyPC90ZXh0Pjw/cGxhbnR1bWwtc3JjIG9vakZvS25DTHdaY0tiMzhJb3FmcG9fQUxsMURwNGpDSnlyRHBJaTEyb2llOUFRYTVBS001b2xPQVlXUE1YaGY2VVcxaVJ3SFlIOU9DWUlyZzJXbkJSeWVEenVabHhYNDhIZ0JPYUUzNkEzUDZLMEloYloxc1NUSkFUT1FSOEFkVnQxeVg2anlLN3ZmSU1lSFRXQ0NPX0JTQ3pDSmFwOUJONGlvS2VrMGZXNEQydDhEM2Q3c3p0REoyNzRFcTNTbkNPUU9MeWpMMm0wMD8+PC9nPjwvc3ZnPg==" alt="Network Topology Diagram" style="max-width:100%;height:auto;background:#fff;padding:16px;border:1px solid #e5e7eb;border-radius:8px;" />
</div>
cisco
terraform init
What just happened: terraform init contacts the Terraform Registry, downloads the iosxe provider plugin (CiscoDevNet/iosxe) into the local working directory, and initializes the backend. This ensures Terraform knows how to translate iosxe resources into RESTCONF calls.
Real-world note: In CI/CD pipelines,
terraform initis part of the job that prepares providers and modules beforeterraform plan.
Verify:
terraform init
# Expected output snippet (complete lines show provider initialization):
Initializing the backend...
Initializing provider plugins...
- Finding CiscoDevNet/iosxe versions matching ">=0.5.0"...
- Installing CiscoDevNet/iosxe v0.5.x...
Terraform has been successfully initialized!
<div class="topology-diagram">
<img src="data:image/svg+xml;base64,PD9wbGFudHVtbCAxLjIwMjYuMT8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9Ik5XRElBRyIgaGVpZ2h0PSIxNjZweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjM3OXB4O2hlaWdodDoxNjZweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzNzkgMTY2IiB3aWR0aD0iMzc5cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3OS4wNDg4IiB4PSI1IiB5PSIxNi4xMzg3Ij5NYW5hZ2VtZW50PC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijc2LjU2NDUiIHg9IjcuNDg0NCIgeT0iMzAuMTA3NCI+MTkyLjAuMi4wLzI0PC90ZXh0PjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjI4Mi4xNTYzIiB4PSI4OS4wNDg4IiB5PSIxNi40Njg4Ii8+PHBhdGggZD0iTTE2Mi43NzE1LDIxLjQ2ODggTDE2Mi43NzE1LDcwLjY3NTgiIGZpbGw9Im5vbmUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MTsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI1OS40Nzk1IiB4PSIxMjQuOTQwMiIgeT0iMzQuNTc1NyI+MTkyLjAuMi4xMDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyNS4wNTA4IiB4PSIxMjQuOTQwMiIgeT0iNDcuMzgwNCI+ZXRoMDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3NS42NjI2IiB4PSIxMjQuOTQwMiIgeT0iNjAuMTg1MSI+VGVycmFmb3JtIENMSTwvdGV4dD48cGF0aCBkPSJNMzAzLjg0OTYsMjEuNDY4OCBMMzAzLjg0OTYsNzAuNjc1OCIgZmlsbD0ibm9uZSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjU5LjQ3OTUiIHg9IjI0OS42MzkyIiB5PSIzNC41NzU3Ij4xOTIuMC4yLjExPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijk0LjIzMDUiIHg9IjI0OS42MzkyIiB5PSI0Ny4zODA0Ij5HaWdhYml0RXRoZXJuZXQwPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEwOC40MjA5IiB4PSIyNDkuNjM5MiIgeT0iNjAuMTg1MSI+UkVTVENPTkYgVENQLzQ0MzwvdGV4dD48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjExMy40NDUzIiB4PSIxMDQuMDQ4OCIgeT0iNzAuNjc1OCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjkzLjQ0NTMiIHg9IjExNC4wNDg4IiB5PSI5MS44MTQ1Ij5UZXJyYWZvcm1fSG9zdDwvdGV4dD48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjEwOC43MTA5IiB4PSIyNDcuNDk0MSIgeT0iNzAuNjc1OCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijg4LjcxMDkiIHg9IjI1Ny40OTQxIiB5PSI5MS44MTQ1Ij5JT1NfWEVfUm91dGVyPC90ZXh0Pjw/cGxhbnR1bWwtc3JjIG9vakZvS25DTHdaY0tiMzhJb3FmcG9fQUxsMURwNGpDSnlyRHBJaTEyb2llOUFRYTVBS001b2xPQVlXUE1YaGY2VVcxaVJ3SFlIOU9DWUlyZzJXbkJSeWVEenVabHhYNDhIZ0JPYUUzNkEzUDZLMEloYloxc1NUSkFUT1FSOEFkVnQxeVg2anlLN3ZmSU1lSFRXQ0NPX0JTQ3pDSmFwOUJONGlvS2VrMGZXNEQydDhEM2Q3c3p0REoyNzRFcTNTbkNPUU9MeWpMMm0wMD8+PC9nPjwvc3ZnPg==" alt="Network Topology Diagram" style="max-width:100%;height:auto;background:#fff;padding:16px;border:1px solid #e5e7eb;border-radius:8px;" />
</div>
cisco
terraform plan -var 'device_username=admin' -var 'device_password=Lab@123' -var 'host_url=https://lab-router.lab.nhprep.com:443'
What just happened: Terraform authenticated to the provider (iosxe) and used RESTCONF to query the device state (stateful provider behavior). The plan shows Terraform wants to create iosxe_vlan.example with vlan_id 511 and name "Vlan511". No changes are made yet.
Real-world note: Always run
terraform planand have another engineer review it beforeapplyin production environments.
Verify:
terraform plan -var 'device_username=admin' -var 'device_password=Lab@123' -var 'host_url=https://lab-router.lab.nhprep.com:443'
# Expected plan output (complete):
Refreshing Terraform state in-memory prior to plan...
...
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# iosxe_vlan.example will be created
+ resource "iosxe_vlan" "example" {
+ id = (known after apply)
+ name = "Vlan511"
+ shutdown = false
+ vlan_id = 511
}
Plan: 1 to add, 0 to change, 0 to destroy.
<div class="topology-diagram">
<img src="data:image/svg+xml;base64,PD9wbGFudHVtbCAxLjIwMjYuMT8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9Ik5XRElBRyIgaGVpZ2h0PSIxNjZweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjM3OXB4O2hlaWdodDoxNjZweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzNzkgMTY2IiB3aWR0aD0iMzc5cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3OS4wNDg4IiB4PSI1IiB5PSIxNi4xMzg3Ij5NYW5hZ2VtZW50PC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijc2LjU2NDUiIHg9IjcuNDg0NCIgeT0iMzAuMTA3NCI+MTkyLjAuMi4wLzI0PC90ZXh0PjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjI4Mi4xNTYzIiB4PSI4OS4wNDg4IiB5PSIxNi40Njg4Ii8+PHBhdGggZD0iTTE2Mi43NzE1LDIxLjQ2ODggTDE2Mi43NzE1LDcwLjY3NTgiIGZpbGw9Im5vbmUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MTsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI1OS40Nzk1IiB4PSIxMjQuOTQwMiIgeT0iMzQuNTc1NyI+MTkyLjAuMi4xMDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyNS4wNTA4IiB4PSIxMjQuOTQwMiIgeT0iNDcuMzgwNCI+ZXRoMDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3NS42NjI2IiB4PSIxMjQuOTQwMiIgeT0iNjAuMTg1MSI+VGVycmFmb3JtIENMSTwvdGV4dD48cGF0aCBkPSJNMzAzLjg0OTYsMjEuNDY4OCBMMzAzLjg0OTYsNzAuNjc1OCIgZmlsbD0ibm9uZSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjU5LjQ3OTUiIHg9IjI0OS42MzkyIiB5PSIzNC41NzU3Ij4xOTIuMC4yLjExPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijk0LjIzMDUiIHg9IjI0OS42MzkyIiB5PSI0Ny4zODA0Ij5HaWdhYml0RXRoZXJuZXQwPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEwOC40MjA5IiB4PSIyNDkuNjM5MiIgeT0iNjAuMTg1MSI+UkVTVENPTkYgVENQLzQ0MzwvdGV4dD48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjExMy40NDUzIiB4PSIxMDQuMDQ4OCIgeT0iNzAuNjc1OCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjkzLjQ0NTMiIHg9IjExNC4wNDg4IiB5PSI5MS44MTQ1Ij5UZXJyYWZvcm1fSG9zdDwvdGV4dD48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjEwOC43MTA5IiB4PSIyNDcuNDk0MSIgeT0iNzAuNjc1OCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijg4LjcxMDkiIHg9IjI1Ny40OTQxIiB5PSI5MS44MTQ1Ij5JT1NfWEVfUm91dGVyPC90ZXh0Pjw/cGxhbnR1bWwtc3JjIG9vakZvS25DTHdaY0tiMzhJb3FmcG9fQUxsMURwNGpDSnlyRHBJaTEyb2llOUFRYTVBS001b2xPQVlXUE1YaGY2VVcxaVJ3SFlIOU9DWUlyZzJXbkJSeWVEenVabHhYNDhIZ0JPYUUzNkEzUDZLMEloYloxc1NUSkFUT1FSOEFkVnQxeVg2anlLN3ZmSU1lSFRXQ0NPX0JTQ3pDSmFwOUJONGlvS2VrMGZXNEQydDhEM2Q3c3p0REoyNzRFcTNTbkNPUU9MeWpMMm0wMD8+PC9nPjwvc3ZnPg==" alt="Network Topology Diagram" style="max-width:100%;height:auto;background:#fff;padding:16px;border:1px solid #e5e7eb;border-radius:8px;" />
</div>
cisco
terraform apply -auto-approve -var 'device_username=admin' -var 'device_password=Lab@123' -var 'host_url=https://lab-router.lab.nhprep.com:443'
What just happened: Terraform sent RESTCONF API requests (JSON payloads) to lab-router.lab.nhprep.com:443 to create VLAN 511 with the specified attributes. The provider recorded the created resource in Terraform state so future plans are based on what Terraform believes exists.
Real-world note: Because Terraform is stateful, if someone manually changes the VLAN on the device, Terraform may detect drift on the next plan. Use automation practices (change control) to avoid manual drift.
Verify:
terraform apply -auto-approve -var 'device_username=admin' -var 'device_password=Lab@123' -var 'host_url=https://lab-router.lab.nhprep.com:443'
# Expected output (complete lines):
iosxe_vlan.example: Creating...
iosxe_vlan.example: Creation complete after 2s [id=511]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
To inspect the resource recorded in state:
terraform state show iosxe_vlan.example
# Expected output:
# iosxe_vlan.example:
resource "iosxe_vlan" "example" {
id = "511"
name = "Vlan511"
shutdown = "false"
vlan_id = "511"
provider = "provider[\"registry.terraform.io/CiscoDevNet/iosxe\"]"
}
<div class="topology-diagram">
<img src="data:image/svg+xml;base64,PD9wbGFudHVtbCAxLjIwMjYuMT8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9Ik5XRElBRyIgaGVpZ2h0PSIxNjZweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjM3OXB4O2hlaWdodDoxNjZweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzNzkgMTY2IiB3aWR0aD0iMzc5cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3OS4wNDg4IiB4PSI1IiB5PSIxNi4xMzg3Ij5NYW5hZ2VtZW50PC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijc2LjU2NDUiIHg9IjcuNDg0NCIgeT0iMzAuMTA3NCI+MTkyLjAuMi4wLzI0PC90ZXh0PjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjI4Mi4xNTYzIiB4PSI4OS4wNDg4IiB5PSIxNi40Njg4Ii8+PHBhdGggZD0iTTE2Mi43NzE1LDIxLjQ2ODggTDE2Mi43NzE1LDcwLjY3NTgiIGZpbGw9Im5vbmUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MTsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI1OS40Nzk1IiB4PSIxMjQuOTQwMiIgeT0iMzQuNTc1NyI+MTkyLjAuMi4xMDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyNS4wNTA4IiB4PSIxMjQuOTQwMiIgeT0iNDcuMzgwNCI+ZXRoMDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3NS42NjI2IiB4PSIxMjQuOTQwMiIgeT0iNjAuMTg1MSI+VGVycmFmb3JtIENMSTwvdGV4dD48cGF0aCBkPSJNMzAzLjg0OTYsMjEuNDY4OCBMMzAzLjg0OTYsNzAuNjc1OCIgZmlsbD0ibm9uZSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjU5LjQ3OTUiIHg9IjI0OS42MzkyIiB5PSIzNC41NzU3Ij4xOTIuMC4yLjExPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijk0LjIzMDUiIHg9IjI0OS42MzkyIiB5PSI0Ny4zODA0Ij5HaWdhYml0RXRoZXJuZXQwPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEwOC40MjA5IiB4PSIyNDkuNjM5MiIgeT0iNjAuMTg1MSI+UkVTVENPTkYgVENQLzQ0MzwvdGV4dD48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjExMy40NDUzIiB4PSIxMDQuMDQ4OCIgeT0iNzAuNjc1OCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjkzLjQ0NTMiIHg9IjExNC4wNDg4IiB5PSI5MS44MTQ1Ij5UZXJyYWZvcm1fSG9zdDwvdGV4dD48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjEwOC43MTA5IiB4PSIyNDcuNDk0MSIgeT0iNzAuNjc1OCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijg4LjcxMDkiIHg9IjI1Ny40OTQxIiB5PSI5MS44MTQ1Ij5JT1NfWEVfUm91dGVyPC90ZXh0Pjw/cGxhbnR1bWwtc3JjIG9vakZvS25DTHdaY0tiMzhJb3FmcG9fQUxsMURwNGpDSnlyRHBJaTEyb2llOUFRYTVBS001b2xPQVlXUE1YaGY2VVcxaVJ3SFlIOU9DWUlyZzJXbkJSeWVEenVabHhYNDhIZ0JPYUUzNkEzUDZLMEloYloxc1NUSkFUT1FSOEFkVnQxeVg2anlLN3ZmSU1lSFRXQ0NPX0JTQ3pDSmFwOUJONGlvS2VrMGZXNEQydDhEM2Q3c3p0REoyNzRFcTNTbkNPUU9MeWpMMm0wMD8+PC9nPjwvc3ZnPg==" alt="Network Topology Diagram" style="max-width:100%;height:auto;background:#fff;padding:16px;border:1px solid #e5e7eb;border-radius:8px;" />
</div>
cisco
terraform destroy -auto-approve -var 'device_username=admin' -var 'device_password=Lab@123' -var 'host_url=https://lab-router.lab.nhprep.com:443'
What just happened: Terraform used the provider to send a DELETE (or equivalent RESTCONF) request to the device to remove VLAN 511. The state file is updated to reflect that the resource no longer exists.
Real-world note:
destroyis powerful and should be restricted in production. Use role-based access control and policies to prevent accidental destruction.
Verify:
terraform destroy -auto-approve -var 'device_username=admin' -var 'device_password=Lab@123' -var 'host_url=https://lab-router.lab.nhprep.com:443'
# Expected output:
iosxe_vlan.example: Destroying...
iosxe_vlan.example: Destruction complete after 1s
Destroy complete! Resources: 1 destroyed.
Optional device-side verification using IOS XE CLI formatting for RESTCONF/NETCONF view:
# On the IOS XE device, view AAA configuration in RESTCONF JSON format (example of format usage)
show running-config aaa | format restconf -json
# Expected output: (device outputs AAA config in JSON_IETF encoding)
{
"aaa" : {
... (JSON representation of AAA config) ...
}
}
Tip: The
show ... | format restconf -jsoncommand demonstrates how IOS XE can format configuration in RESTCONF JSON — useful to validate payloads or translate CLI configs to model-driven formats.
Verification Checklist
- Check 1: Terraform initialized successfully — verify by
terraform initoutput showing iosxe provider installed. - Check 2: Terraform plan shows
+ createfor iosxe_vlan.example — verify withterraform planoutput. - Check 3: After apply, Terraform state shows vlan_id = 511 and name = "Vlan511" — verify with
terraform state show iosxe_vlan.example. - Check 4: After destroy, plan shows no resources or
0 to add— verify withterraform planor checkterraform state listis empty.
Common Mistakes
| Symptom | Cause | Fix |
|---|---|---|
terraform init fails to download provider | No internet or Terraform Registry blocked | Ensure workstation can reach registry; use an approved mirror or private registry |
terraform plan shows authentication failure | Incorrect username/password or host_url | Verify variables; use correct -var values or env vars; confirm RESTCONF reachable at URL and port 443 |
After apply, resource not present on device | Provider could not communicate or device rejected RESTCONF payload | Check RESTCONF enabled on device, correct URL, and that account has sufficient privileges; check TLS and certificate trust |
terraform apply succeeds but future plan wants to recreate the resource | State file was not preserved or manual device changes created drift | Ensure state file is stored and protected; discourage manual changes or import existing resources into state |
Key Takeaways
- Terraform's workflow is:
init(prepare providers),plan(preview),apply(enact),destroy(remove). Useplanfor safety and peer reviews in production. - The IOS XE Terraform provider uses RESTCONF (HTTPS/JSON). Provider configuration includes
username,password, andurl. - Terraform is stateful — it stores knowledge of created resources so it can compute diffs and support safe destroy operations.
- Use variables (never hard-code credentials) and consider a secrets manager for production. Keep
terraform planoutputs as part of change control records.
Final real-world insight: In large environments, teams use Terraform to deploy consistent VLAN, ACL, and interface state across hundreds of devices. The pattern shown here (provider + resource + plan/apply/destroy) is the foundation for scaling network configuration safely and audibly.