Network Automation with Ansible¶
Ansible is often the first tool network engineers reach for when they move from typing
commands into a terminal to automating them. This article covers how Ansible configures
Cisco IOS XE devices — the state key that governs every change, idempotent resource
modules, secrets, and how to preview a change before it touches production.
Why Ansible fits network work¶
Ansible is agentless. Nothing is installed on the router or switch; the module logic
runs on your control node and pushes CLI or API calls to each device over a connection
plugin — network_cli (SSH CLI, the usual choice for IOS XE), netconf, or httpapi.
That means you can automate a large brownfield estate without deploying software to every
box first.
A playbook is a YAML file of one or more plays. Each play targets a group of hosts and
lists tasks; each task calls exactly one module. Data — inventory, group_vars,
secrets — is deliberately kept separate from task logic, so the same playbook can drive
dev, staging, and production unchanged.
The state key decides everything¶
Cisco IOS XE content lives in the cisco.ios collection, whose resource modules
(ios_vlans, ios_interfaces, ios_ospfv2, ios_acls, …) are declarative and
idempotent. The state key tells the module how to reconcile your input against what is
already on the device:
| State | What it does |
|---|---|
merged |
Adds/updates only what you list; leaves everything else intact. The safe default. |
replaced |
Reconciles a single resource subsection to match your input. |
overridden |
Replaces the entire resource — silently removes anything you didn't list. |
deleted |
Removes the specified configuration. |
gathered |
Reads current config back as structured data (no change). |
rendered |
Produces the CLI commands without connecting to a device. |
When a task says "add X without disturbing anything else," the answer is almost always
merged, never overridden. Reserve overridden for greenfield or fully-known devices
where your definition is genuinely complete.
Worked example: VLANs, interfaces, OSPF, ACLs¶
Every example below uses state: merged, so it is additive and safe to re-run.
# vlans.yml — ensure three access VLANs exist on every device in the group
---
- name: Configure access VLANs
hosts: ios_xe
gather_facts: false
tasks:
- name: Ensure VLANs are present
cisco.ios.ios_vlans:
config:
- { name: SALES, vlan_id: 10 }
- { name: VOICE, vlan_id: 20 }
- { name: MGMT, vlan_id: 99 }
state: merged
# interfaces.yml — describe an uplink, then make Gi3 an access port
---
- name: Configure interfaces
hosts: ios_xe
gather_facts: false
tasks:
- name: Set description and enable the uplink
cisco.ios.ios_interfaces:
config:
- name: GigabitEthernet2
description: Uplink to core
enabled: true
state: merged
- name: Make Gi3 an access port in VLAN 10
cisco.ios.ios_l2_interfaces:
config:
- name: GigabitEthernet3
access:
vlan: 10
state: merged
The same pattern extends to routing and filtering — ios_ospfv2 with a processes list
for OSPF, and ios_acls with an aces list for an extended ACL. Because every module is
declarative, you describe the end state and let Ansible compute the delta.
For asset management, gather structured inventory instead of pushing config:
# facts.yml — collect model, serial, and version as one CSV row per host
---
- name: Collect device inventory
hosts: ios_xe
gather_facts: false
tasks:
- name: Gather IOS facts
cisco.ios.ios_facts:
gather_subset: all
- name: Save inventory to a file per host
ansible.builtin.copy:
content: "{{ ansible_net_serialnum }},{{ ansible_net_version }},{{ ansible_net_model }}"
dest: "inventory/{{ inventory_hostname }}.csv"
delegate_to: localhost
Prefer resource modules over ios_config¶
Use ios_config only for settings that lack a dedicated resource module. It is idempotent,
but you own the matching logic — and there is a classic gotcha: abbreviated commands are
not idempotent. If your config lines don't match the running-config exactly (including
indentation), the module reports a change on every run. Resource modules avoid this because
they parse the device's structured state for you.
Secrets and safe dry runs¶
Never hard-code passwords. Encrypt a vars file with Ansible Vault — it is ciphertext at rest, so the encrypted file can live in Git alongside your playbooks:
ansible-vault encrypt secrets.yml
ansible-playbook site.yml --ask-vault-pass
Before touching production, preview the change. --check simulates without applying, and
--diff shows the exact line-by-line delta:
ansible-playbook vlans.yml --check --diff
Don't confuse these with --syntax-check (parses YAML only) or -v (verbosity). A related
tip from the field: run the resource module in gathered state first to record current
state, and use rendered to review the generated commands offline before you run for real.
changed=0 is the proof of idempotency
A correctly written resource-module playbook reports changed=0 on its second run:
the device already matches the declared state, so nothing is done. If repeat runs keep
reporting changes, the task isn't truly idempotent.
Key takeaways¶
- Ansible is agentless — modules run on the control node; only CLI/API calls reach the device.
- Playbooks are YAML; the unit of work is a task calling one module.
- Prefer resource modules over raw
ios_config— they're idempotent and return structured data. - The
statekey is decisive:mergedadds,overriddenremoves anything not listed. - Keep data apart from logic (
group_vars/host_vars), store secrets in Ansible Vault, and always preview with--check --diff.
Sources: Ansible Network Resource Modules docs, cisco.ios.ios_config module, How to Use Ansible Network Resource Modules.