Back to blog

Cisco / Automation  ·  Tutorial  ·  August 2026

Cisco IOS
Managing a Fleet with Ansible

Configuring one switch by hand is fine. Configuring twenty the same way is how drift and typos creep in. Ansible turns your config into version-controlled code you push to every device at once — with a dry-run that shows exactly what will change before it touches anything.

Cisco IOS Ansible Automation IaC cisco.ios NetDevOps

playbook.yml in git · one source of truth ansible SW-01 SW-02 SW-03 … SW-20 identical config, every device Agentless: Ansible just SSHes in and runs IOS commands. No software on the switches.

EEM automates a single device reacting to its own events. Ansible does the opposite: it orchestrates change across many devices from one place. It's agentless — nothing is installed on the switches; Ansible simply SSHes in (using the access you set up in First Contact) and runs IOS commands via the cisco.ios collection. Your intended configuration lives as YAML in git, so every change is reviewed, versioned, and repeatable.

The payoff is threefold: consistency (the same hardening baseline on all 20 switches, not 20 slightly-different hand jobs), speed (add a VLAN to the whole site in one run), and safety (--check mode shows the exact diff before anything is applied, and git gives you a rollback). This is the tool that turns "we manage some switches" into "we operate a fleet." Even for the modest sites we run, the day you need to push a security fix to everything at once, you'll be glad the config is code.

Prerequisites

01

Inventory — Describe the Fleet

Ansible needs to know what devices exist and how to reach them. The inventory lists hosts and groups; group_vars holds the connection settings shared by all Cisco devices.

inventory.ini — the device list
# Group switches so a playbook can target "all switches at once"
[access_switches]
sw-01 ansible_host=192.168.99.11
sw-02 ansible_host=192.168.99.12
sw-03 ansible_host=192.168.99.13

[core_switches]
core-01 ansible_host=192.168.99.1

[switches:children]
access_switches
core_switches
group_vars/switches.yml — how to connect
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: ansible
ansible_password: "{{ vault_ios_password }}"     # from Vault (section 05)
ansible_become: true
ansible_become_method: enable
ansible_become_password: "{{ vault_enable_password }}"

02

Read-Only First — Gather Facts & Back Up

Never start automation by writing. Prove connectivity with a read-only run: gather facts from every device, and back up every config — a safe, useful first playbook.

backup.yml — pull every running-config (read-only)
---
- name: Back up all switch configs
  hosts: switches
  gather_facts: false
  tasks:
    - name: Grab facts (model, version, uptime)
      cisco.ios.ios_facts:

    - name: Save running-config to a per-host file
      cisco.ios.ios_config:
        backup: true
        backup_options:
          filename: "{{ inventory_hostname }}.cfg"
          dir_path: ./backups/
Run it, then commit the backups to git
you@laptop:~$ ansible-playbook -i inventory.ini backup.yml
PLAY [Back up all switch configs] ****
ok: [sw-01]   ok: [sw-02]   ok: [core-01] ...
you@laptop:~$ git add backups/ && git commit -m "nightly config snapshot"
# Now every device's config is versioned. Diffing two days = "what changed".

03

Idempotent Change with ios_config

The heart of it: describe the config you WANT, and Ansible makes only the changes needed to get there. Run it twice and the second run reports "ok" (no change) — that's idempotency, and it's why this is safe to run repeatedly.

ntp.yml — ensure NTP + logging on every device
---
- name: Standardize NTP and logging fleet-wide
  hosts: switches
  gather_facts: false
  tasks:
    - name: Apply the standard management lines
      cisco.ios.ios_config:
        lines:
          - ntp server 0.gr.pool.ntp.org
          - ntp server 1.gr.pool.ntp.org
          - logging host 192.168.1.50
          - service timestamps log datetime msec localtime
        save_when: modified          # write mem ONLY if something changed
    
The magic: run it, then run it again
you@laptop:~$ ansible-playbook -i inventory.ini ntp.yml
changed: [sw-01]   changed: [sw-02] ...     ← first run APPLIES the config

you@laptop:~$ ansible-playbook -i inventory.ini ntp.yml
ok: [sw-01]   ok: [sw-02] ...               ← second run: nothing to do
# The playbook is the desired state. It doesn't blindly re-type commands —
# it compares and only acts on drift. Safe to run on a schedule.
⚠ Gotcha — ios_config Matches Lines Literally; Order & Parents Matter

ios_config compares your lines against the running-config text. For commands under a parent (an interface, a line vty), you must give the parents: context, or Ansible applies them at the wrong level. And it matches exact strings — ip address 10.0.0.1 255.255.255.0 and the same with a trailing space are "different." When a task keeps reporting changed every run even though nothing really changes, that's the tell: your line doesn't textually match what IOS stores (IOS may re-render it). Use --check --diff to see exactly what Ansible thinks differs, and match IOS's own formatting.

04

Check Mode — See Before You Touch

The single most important habit. --check runs the whole playbook without applying anything and --diff shows the exact lines it would add or remove. This is the "measure twice, cut once" of network automation.

Dry-run a change across the whole fleet
you@laptop:~$ ansible-playbook -i inventory.ini add-vlan.yml --check --diff
TASK [Add VLAN 50 everywhere] ****
--- before
+++ after
@@ -12,0 +13,2 @@
+vlan 50
+ name IOT
changed: [sw-01]   changed: [sw-02] ...    (check mode — NOTHING applied)

# You see EXACTLY what would change on every device. Happy? Drop --check:
you@laptop:~$ ansible-playbook -i inventory.ini add-vlan.yml --diff
# Roll out gradually with --limit to test on one device first:
you@laptop:~$ ansible-playbook -i inventory.ini add-vlan.yml --limit sw-01
💡 The Safe Rollout Pattern

Professional fleet change goes in four gears, each safer for having the last: (1) --check --diff to preview across everything; (2) --limit sw-01 to apply to one canary device and verify it in the real network; (3) apply to the rest; (4) commit the playbook to git as the record. If step 2 goes wrong, only one device is affected and your git-committed backups (section 02) restore it. This is how you push a change to 20 switches without the 2am pucker — the same discipline as the upgrade post, applied to config.

05

Secrets — Ansible Vault

Never commit a device password to git in the clear. Ansible Vault encrypts the sensitive variables so the repo is safe to store and share, and the playbook decrypts them at runtime with a password you supply.

Encrypt the credentials, use them normally
# Create an encrypted vars file holding the passwords
you@laptop:~$ ansible-vault create group_vars/all/vault.yml
# (opens an editor; you type:)
vault_ios_password: "TheSSHPassword"
vault_enable_password: "TheEnableSecret"

# The file on disk is now encrypted gibberish — safe to commit to git.
# Run any playbook, supplying the vault password to decrypt at runtime:
you@laptop:~$ ansible-playbook -i inventory.ini ntp.yml --ask-vault-pass
# Best practice: SSH KEYS to the devices instead of passwords where possible,
# and vault only what must be a secret. Never a plaintext password in the repo.

06

EEM vs Ansible — Which Tool

Both automate, but they solve different problems. Reach for the right one.

 EEM (on-box)Ansible (off-box)
RunsOn the device itselfFrom a control machine over SSH
ScopeOne device reacting to its own eventsMany devices, orchestrated change
Works when mgmt net is down?Yes — it's localNo — needs reachability
Best forAutonomous reflexes (port recovery, failover cleanup)Consistency, rollout, config-as-code, backups
State livesIn the device configIn git
💡 MikroTik ↔ Cisco — and Beyond

Ansible isn't Cisco-specific — the same control machine manages MikroTik (community.routeros), Linux servers, and cloud in one workflow, which is exactly why it's the right investment for a mixed shop like the ones we run. RouterOS has its own on-box scripting (the EEM analog), but for pushing a consistent baseline across Cisco and MikroTik and the servers, one Ansible repo beats three different tools. Config-as-code is the same idea everywhere: the intended state is text, reviewed and versioned, and the tooling reconciles reality to it.

Takeaways

  1. Ansible is agentless orchestration. It SSHes in and runs IOS via cisco.ios — nothing installed on the switches. Your config becomes reviewed, versioned YAML in git.
  2. Start read-only. Prove connectivity and value with ios_facts and a config-backup playbook before you ever write to a device. Commit the backups to git — instant "what changed" history.
  3. ios_config is idempotent. Describe desired state; Ansible applies only the drift. Run twice, the second run is a no-op — safe to schedule.
  4. Match IOS's exact line formatting, and give parents: for sub-mode commands, or tasks report changed forever. --diff shows what Ansible thinks differs.
  5. --check --diff before every real run. Preview the exact change across the whole fleet, canary with --limit, then apply. Four gears, each safer than the last.
  6. Vault your secrets. Never commit a plaintext device password; prefer SSH keys, and encrypt what must be secret with Ansible Vault.
  7. EEM for local reflexes, Ansible for fleet change. One reacts on-box when the network's down; the other pushes consistent config-as-code from a control machine — across Cisco, MikroTik, and servers alike.

Managing more than a handful of devices in Crete?

NOCTIS builds config-as-code with Ansible — versioned backups, consistent hardening baselines, and safe fleet-wide rollouts — for multi-site businesses on Cisco, MikroTik, or both. One security fix, every device, one reviewed change.

Book a Discovery Call →