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.
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.
pip install ansibleansible-galaxy collection install cisco.ios01
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.
# 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
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
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.
---
- 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/
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
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.
--- - 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
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.
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
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.
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
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
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.
# 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
Both automate, but they solve different problems. Reach for the right one.
| EEM (on-box) | Ansible (off-box) | |
|---|---|---|
| Runs | On the device itself | From a control machine over SSH |
| Scope | One device reacting to its own events | Many devices, orchestrated change |
| Works when mgmt net is down? | Yes — it's local | No — needs reachability |
| Best for | Autonomous reflexes (port recovery, failover cleanup) | Consistency, rollout, config-as-code, backups |
| State lives | In the device config | In git |
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
cisco.ios — nothing installed on the switches. Your config becomes reviewed, versioned YAML in git.ios_facts and a config-backup playbook before you ever write to a device. Commit the backups to git — instant "what changed" history.parents: for sub-mode commands, or tasks report changed forever. --diff shows what Ansible thinks differs.--limit, then apply. Four gears, each safer than the last.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 →