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.

Inventory, playbook, module, device: the path a task takes.

Worked example: adding a VLAN to 40 switches

  1. Add the VLAN to group_vars/all/vlans.yml.
  2. Dry run with --check --diff --limit test-switch.
  3. Apply to the test switch; verify the VLAN exists and traffic passes.
  4. Run with serial: 5 across the rest, stopping on the first failure.
  5. 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 --limit discipline, 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.

  • Ansible
  • Automation
  • Idempotence
  • Playbooks