Automate Cisco network device configuration using Terraform, the IOSXE provider, and Drone CI/CD pipelines. This project demonstrates infrastructure-as-code principles applied to network automation, with NetBox as the source of truth for device inventory and interface configuration.
- NetBox integration - Device inventory and interface data pulled dynamically from NetBox
- Infrastructure as Code for Cisco IOSXE devices using Terraform
- CI/CD automation with Drone pipelines
- Remote state management with MinIO (S3-compatible)
- Modular configuration for scalability
- NETCONF-based device management
NetBox (Source of Truth)
↓
GitHub → Drone Pipeline → MinIO (State)
↓
NETCONF/YANG
↓
Cisco Network Devices
(P-01, P-02, DR-01, DR-02)
Key Design Patterns:
- NetBox-driven: Devices, interfaces, and IPs are fetched from NetBox at plan time
- Single
iosxeprovider manages all devices via NETCONF - Modules use
for_eachwith device names as keys - Explicit config saving via
iosxe_save_configresource
- NetBox instance with devices, interfaces, and IP addresses configured
- Cisco IOS-XE devices with NETCONF enabled
- Drone CI/CD server (optional, for automated deployments)
- MinIO server for Terraform state storage (S3-compatible)
- Terraform >= 1.0
Devices in NetBox should have:
- Primary IP assigned (management IP for NETCONF)
- Site assigned (used for SNMP location)
- Interfaces defined (MGMT interfaces are excluded automatically)
- IP addresses with descriptions matching interface descriptions
conf t
netconf-yang
git clone https://github.com/your-org/Network-CICD.git
cd Network-CICDCreate secret.tfvars with your credentials:
username = "admin" #IOSXE device username
password = "cisco" #IOSXE device password
netbox_url = "http://netbox.example.com:8000"
netbox_token = "your-netbox-api-token"drone secret add --repository your-org/Network-CICD --name tf_username --data "admin"
drone secret add --repository your-org/Network-CICD --name tf_password --data "cisco"
drone secret add --repository your-org/Network-CICD --name netbox_url --data "http://netbox:8000"
drone secret add --repository your-org/Network-CICD --name netbox_token --data "your-token"
drone secret add --repository your-org/Network-CICD --name MINIO_ACCESS_KEY --data "your-key"
drone secret add --repository your-org/Network-CICD --name MINIO_SECRET_KEY --data "your-secret"
drone secret add --repository your-org/Network-CICD --name MINIO_ENDPOINT --data "http://minio:9000"Via Drone:
drone build create your-org/Network-CICD --event pushLocally:
export AWS_ACCESS_KEY_ID="your-key"
export AWS_SECRET_ACCESS_KEY="your-secret"
export AWS_ENDPOINT_URL_S3="http://minio-host:9000"
terraform init
terraform plan -var-file=secret.tfvars
terraform apply -var-file=secret.tfvarsNetwork-CICD/
├── main.tf # Provider configuration (IOSXE + NetBox)
├── netbox.tf # NetBox data sources and transformations
├── backend.tf # Remote state (MinIO S3)
├── devices.tf # Device inventory (from NetBox)
├── variables.tf # Input variables (credentials, NetBox)
├── .drone.yml # CI/CD pipeline (4 stages)
├── destroy.sh # Safe cleanup with interface shutdown
└── modules/
├── snmp/ # SNMP configuration
├── banner/ # Login banner configuration
└── interfaces/ # Interface configuration
chmod +x destroy.sh
./destroy.shImportant: Always use destroy.sh instead of terraform destroy directly. The script:
- Shuts down managed interfaces via SSH (workaround for provider bug #219 where interfaces remain up after destroy)
- Runs
terraform destroy - Saves configurations to startup-config via SSH
This ensures a clean teardown and prevents interfaces from being left in an active state.
-
Create
modules/new-module/directory with:<feature>.tforinterface_template.tf- resource definitionsvariables.tf- module inputs (device or interface maps)versions.tf- provider version constraints
-
Add module call in
main.tf:
module "new_module" {
source = "./modules/new-module"
routers = local.routers # or wan_interfaces = local.wan_interfaces
}- Add to
depends_oniniosxe_save_configresource to ensure configs are saved
Pattern: Modules iterate with for_each over maps, using device name (each.key) as the device parameter.
Full walkthrough and deep dive available at netcask.com