Ansible's appeal is that it needs no agent: it connects over SSH or HTTP and runs modules on the control node. For network devices, that is the only option most of them offer.
The trap is that Ansible will happily run a list of CLI commands and call it automation. That is a script with extra indentation, and it fails the same way a script does.
Connection types
| Type | Used for | Notes |
|---|---|---|
network_cli |
SSH CLI | Most switches; relies on parsing |
netconf |
YANG devices | Structured, safer |
httpapi |
REST APIs | Firewalls, controllers |
local |
Vendor-specific | Legacy modules |
Prefer netconf or httpapi where supported. network_cli works, but the module has to parse text,
which is where fragility lives.
Idempotence, concretely
| Task | Idempotent? |
|---|---|
ios_config with a line |
Yes — checks before applying |
ios_command with show run |
Yes, but changes nothing |
cli_command looping over config lines |
No — applies every time |
The distinction: modules that describe state are idempotent; modules that execute are not. Use the former for configuration and the latter for verification.
A playbook that is safe to run twice
- hosts: switches
gather_facts: false
vars:
vlans:
- { id: 104, name: student }
- { id: 106, name: staff }
tasks:
- name: ensure VLANs exist
ios_vlan:
vlan_id: "{{ item.id }}"
name: "{{ item.name }}"
state: present
loop: "{{ vlans }}"
- name: back up running config
ios_config:
backup: yes
Two properties matter: running it twice changes nothing the second time, and it takes a backup before it touches anything.
Rollout control
| Setting | Effect |
|---|---|
serial: 1 |
One device at a time; stop on failure |
max_fail_percentage |
Abort the batch if too many fail |
--check |
Report what would change |
--diff |
Show the difference |
--limit |
Restrict to a group or host |
For a district, serial: 1 with a small max_fail_percentage is the difference between a bad change on
one switch and a bad change on forty.
Check mode is not a guarantee
Some modules do not fully support --check and will report changes they would not make, or miss changes
they would. Treat check mode as a hint, and always have a backup plus a rollback path.
Worked example: adding a VLAN to 40 switches
- Add the VLAN to
group_vars/all/vlans.yml. - Dry run with
--check --diff --limit test-switch. - Apply to the test switch; verify the VLAN exists and traffic passes.
- Run with
serial: 5across the rest, stopping on the first failure. - Compare the backup files to confirm only the intended change.
The variable file is the source of truth; the playbook is the mechanism.
What breaks in production
- Command lists instead of state modules, so every run re-applies and drift is invisible.
- No backup, so rollback means retyping from memory.
- Parallel execution across a whole site, turning one bad template into an outage.
- Credentials in the repo, or in plaintext inventory.
- Parsing output that changes between firmware versions.
- No
--limitdiscipline, so a test run becomes a production run.
Next
Ansible is one option. Continue with Python Libraries Worth Using for the cases where a playbook is the wrong shape.