Most engineers think…
Most engineers meet Ansible for Cisco and think "it's just SSH that types my CLI commands for me — so I'll dump all my show run lines into ios_config and call it automation."
Wrong — and that habit makes brittle, non-idempotent playbooks that re-push the same lines every run. The real model is two-tier: use the resource modules (ios_vlans, ios_interfaces, ios_l2_interfaces) for anything with structured state, because they read the device, compare to your desired state, and push only the difference — and drop to ios_config only for the raw lines that have no resource module. Idempotency and the merged/replaced/overridden choice are the whole game, not the SSH.
① The cisco.ios collection — the modules that matter
Meet Sneha, an L1 network engineer at Infosys who looks after 40 branch switches. Last quarter she added one VLAN to all of them by hand — 40 SSH sessions, 40 chances to fat-finger a name. This quarter she does it with one Ansible playbook. The thing that makes that possible is the cisco.ios collection — a set of modules built specifically to drive Cisco IOS and IOS-XE devices. You install it once with ansible-galaxy collection install cisco.ios and it brings everything below.
There are two families of modules and knowing which to reach for is half the skill. First, the generic ones. ios_config is the workhorse — you hand it the exact CLI lines and (optionally) their parents, and it pushes them. ios_command runs read-only show commands and hands back the output for you to inspect or assert on. ios_facts collects structured facts (version, interfaces, serial) so the rest of your play can branch on them.
Second — and this is where most people level up — the resource modules: ios_vlans, ios_interfaces, ios_l2_interfaces, ios_l3_interfaces, ios_ospfv2 and more. Each manages one slice of config as structured data with a state: of merged, replaced or overridden. They don't blindly type your lines — they read what's on the box, compare it to the state you declared, and push only the difference. That is what makes them idempotent by design.
The rule of thumb you'll repeat in interviews: if there's a resource module for it, use the resource module; only drop to ios_config for raw lines that have none. Want VLANs, access/trunk ports, IP on interfaces, OSPF? Use ios_vlans / ios_l2_interfaces / ios_l3_interfaces / ios_ospfv2 — you get a clean diff, structured state, and idempotency for free. Want to set a banner, an NTP server, or some box-specific line nobody wrote a module for? That's the honest job for ios_config.
The five modules you'll actually type
Tap each — these cover ~90% of day-one Cisco automation tasks.
Pushes raw IOS lines into a section (with parents). The workhorse for whatever has no resource module. So: powerful, but you own idempotency.
Runs show/exec commands read-only and returns the text. So: your verify and assert tool — it never changes a thing.
Manages VLANs as data with merged/replaced/overridden. So: declare the VLANs you want, it pushes only the delta.
Sets access VLAN, trunk mode, allowed and native VLANs per port. So: your switchport config without typing switchport lines.
Dumping show run lines into ios_config is like the watchman re-writing every resident's name in the register every single day — noisy and error-prone. A resource module is the smart register that already knows who lives there: you say "flat 30 should have a pass," it checks the page, and only writes if flat 30 isn't already listed. Same outcome, but it only touches what actually changed — that's idempotency.
Rahul at TCS needs to set an NTP server line on 30 switches. There is no dedicated resource module for it. Which module is the right, honest choice — and what must he own himself?
Pause & Predict
Predict: you run an ios_vlans task with state: merged twice in a row. What does the SECOND run report as changed, and why? Type your guess.
② Connection + auth — how Ansible actually reaches IOS
A switch is not a Linux box, so the default SSH-then-Python model doesn't apply — there's no Python on a Catalyst. Instead Ansible opens a persistent SSH session and drives the CLI directly. That mode is network_cli, and you select it (plus the IOS dialect) in your inventory. Three variables do almost all the work.
ansible_connection: ansible.netcommon.network_cli says "talk to a CLI device, not a server." ansible_network_os: cisco.ios.ios says "that CLI speaks Cisco IOS" — this is the exact platform string, and getting it wrong is the classic first-day error. Then credentials: ansible_user and ansible_password for the SSH login. That gets you to user EXEC (the switch> prompt) — but config needs privileged mode.
To reach privileged EXEC (enable mode) you set ansible_become: true and ansible_become_method: enable — for network_cli, enable is the only valid become method. If your devices have an enable secret, supply it as ansible_become_password. Behind the scenes Ansible types enable, sends that password, and you land at the switch# prompt where config lives.
Never commit passwords in plaintext. Two clean options: prompt for them at runtime with --ask-pass --ask-become-pass (or vars_prompt in the play), or — the production answer — keep them in an Ansible Vault-encrypted vars file and run with --ask-vault-pass. The enable secret and the SSH password both belong in vault, referenced as normal variables. This is the same principle as never hardcoding an API key: the secret lives in an encrypted store, the playbook only references it.
all:
children:
access_switches:
hosts:
SW-Mumbai-01: { ansible_host: 10.10.1.1 }
SW-Mumbai-02: { ansible_host: 10.10.1.2 }
vars:
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: netadmin
ansible_password: "{{ vault_ssh_password }}"
ansible_become: true
ansible_become_method: enable
ansible_become_password: "{{ vault_enable_secret }}"# sanity check the connection + privileged mode:
$ ansible access_switches -m cisco.ios.ios_command \
-a "commands='show privilege'" --ask-vault-pass
SW-Mumbai-01 | SUCCESS => { "stdout": ["Current privilege level is 15"] }
SW-Mumbai-02 | SUCCESS => { "stdout": ["Current privilege level is 15"] }- name: Base config on access switches
hosts: access_switches
gather_facts: false
tasks:
- name: Hostname + NTP (raw lines via ios_config)
cisco.ios.ios_config:
lines:
- hostname {{ inventory_hostname }}
- ntp server 10.0.0.5
save_when: modified
- name: A management loopback on each switch
cisco.ios.ios_l3_interfaces:
config:
- name: Loopback0
ipv4:
- address: 10.255.1.1/32
state: mergedPLAY [Base config on access switches] ****************** TASK [Hostname + NTP (raw lines via ios_config)] ******* changed: [SW-Mumbai-01] changed: [SW-Mumbai-02] TASK [A management loopback on each switch] ************ changed: [SW-Mumbai-01] PLAY RECAP ********************************************* SW-Mumbai-01 : ok=2 changed=2 unreachable=0 failed=0
Symptom: ansible-playbook logs in fine but the task fails with unable to elevate privilege to enable mode or a connection timeout. Cause is almost always one of three: (1) you forgot ansible_become: true / become_method: enable, so you're stuck at switch> user EXEC; (2) the ansible_become_password (enable secret) is wrong or missing; or (3) ansible_network_os is misspelt — it must be exactly cisco.ios.ios. Fix: confirm all three, then prove it with show privilege returning level 15.
Aditya at Wipro can SSH to a switch and run show commands via Ansible, but every config task fails with 'unable to elevate privilege to enable mode'. Which inventory fix is correct?
Pause & Predict
Predict: a teammate hardcodes the enable secret directly in the playbook as plaintext and commits it to Git. Beyond the obvious leak, name the better pattern and one runtime flag it needs. Type your guess.
③ Structured config with resource modules — merged vs replaced vs overridden
Now the idea that separates someone who "uses Ansible" from someone who's dangerous with it on a Friday evening: the state of a resource module. Every resource module (ios_vlans, ios_interfaces, …) takes a state: that decides how your declared config is reconciled with what's on the box. Three matter most — and the difference between them is the difference between a clean change and an outage.
merged (the default) merges your config in: it adds or updates the VLANs/interfaces you list and leaves everything else untouched. replaced takes each resource you listed and rewrites it to exactly match your data (so an attribute you removed from your data is removed on the box) — but resources you didn't list are left alone. overridden is the dangerous one: it forces the entire set to match your data, so any VLAN or interface-setting on the device that is not in your list gets deleted.
Here's the trap that has caused real outages. You write an ios_vlans task listing only VLAN 30 with state: overridden, meaning "make VLAN 30." But on a switch that already carries VLANs 10, 20 and 30, overridden reads "the complete VLAN set should be just 30" — so it deletes 10 and 20, instantly orphaning every access port in those VLANs. The same line with state: merged would have simply ensured 30 exists and left 10 and 20 alone. Default to merged; reach for overridden only when you truly want the device to mirror your list exactly (and even then, dry-run it first).
- name: Ensure VLAN 30 exists (SAFE: merged, not overridden)
cisco.ios.ios_vlans:
config:
- vlan_id: 30
name: guest
state: active
state: merged
# backup the whole running-config first, just in case
register: vlan_result
- name: Show what changed
ansible.builtin.debug:
var: vlan_result.commands# dry-run with --check --diff shows the exact lines, applies nothing: $ ansible-playbook -i inventory vlans.yml --check --diff TASK [Ensure VLAN 30 exists] *************************** --- before +++ after @@ -1,2 +1,4 @@ +vlan 30 + name guest changed: [SW-Mumbai-01]
On ios_config set backup: yes — it writes a timestamped copy of the running-config to backup/ (filename defaults to <hostname>_config.<date>@<time>) before touching anything, so you always have a rollback. And mind save_when: it controls the write-mem. save_when: modified copies running-config to startup-config only if it changed (the sensible default for change tasks), always saves every run, never leaves startup-config alone (handy if you want a change that vanishes on reload during testing). If you never set it and never write mem, your change is gone after the next power-cut.
▶ Watch one VLAN change flow through a resource module
You declare VLAN 30 with state: merged. Follow how the module turns your data into exactly the right CLI — and where overridden would have gone wrong. Press Play for the healthy path, then Break it to see the failure.
Priya at ICICI faces this
Priya pushes an ios_vlans task to add VLAN 30 on 8 access switches. Minutes later the NOC reports users on VLANs 10 and 20 are offline across all 8 switches — exactly the VLANs she never touched.
She used state: overridden with only VLAN 30 in her config. overridden forces the device's whole VLAN set to match her list, so VLANs 10 and 20 (absent from the list) were deleted — orphaning every access port assigned to them.
She checks the task's returned commands and the running-config diff and sees 'no vlan 10' / 'no vlan 20' were issued. The change log confirms state: overridden where it should have been merged.
ansible-playbook -i inventory vlans.yml --check --diff (and: git show on the task to confirm state: overridden)Re-declare VLANs 10, 20, 30 explicitly (or switch the task to state: merged) and re-run. Because she had backup: yes on the companion ios_config task, she also restores from backup/SW-Mumbai-01_config.
show vlan brief on each switch lists 10, 20 and 30; users on 10 and 20 are back; a re-run reports changed=0, proving the desired state is now stable.
Karthik at HCL wants a task that makes interface Gi1/0/5 EXACTLY match his declared settings (removing any extra config on THAT port) but must not touch any other interface on the switch. Which state fits best?
Pause & Predict
Predict: you set backup: yes on an ios_config task and run it with --check. Does a backup file get written, and does the device change? Type your guess.
④ Operational plays — push VLAN 30 + SVI + trunk to 12 switches, verified
Time for the real change every campus engineer does: add VLAN 30 (guest), give it an SVI gateway, and allow VLAN 30 on the uplink trunk — across 12 access switches. By hand that's 12 logins and roughly 60 lines typed without a typo. As a playbook it's one file, dry-run first, applied once, verified automatically. We'll build it the safe way and verify with ios_command + assert.
The structure mirrors the change itself: ios_vlans (state merged) creates VLAN 30 everywhere; ios_l3_interfaces puts the SVI (Vlan30) gateway on the distribution switch; ios_l2_interfaces adds 30 to the trunk's allowed_vlans without clobbering the existing list; then ios_command reads show vlan brief and an assert proves VLAN 30 is present before we call the change done.
- name: Roll out VLAN 30 (guest) + SVI + trunk
hosts: access_switches
gather_facts: false
tasks:
- name: 1) Create VLAN 30 everywhere (merged = safe add)
cisco.ios.ios_vlans:
config:
- vlan_id: 30
name: guest
state: active
state: merged
- name: 2) SVI gateway for VLAN 30 (distribution only)
cisco.ios.ios_l3_interfaces:
config:
- name: Vlan30
ipv4:
- address: 10.30.0.1/24
state: merged
when: "'distribution' in group_names"
- name: 3) Allow VLAN 30 on the uplink trunk
cisco.ios.ios_l2_interfaces:
config:
- name: GigabitEthernet1/0/48
trunk:
allowed_vlans: [10, 20, 30]
native_vlan: 99
state: merged
- name: 4) Verify VLAN 30 landed
cisco.ios.ios_command:
commands: show vlan brief | include 30
register: vlanchk
- name: 5) Fail loudly if it didn't
ansible.builtin.assert:
that: "'guest' in vlanchk.stdout[0]"
fail_msg: "VLAN 30 missing on {{ inventory_hostname }}"PLAY RECAP ********************************************* SW-Mumbai-01 : ok=5 changed=3 failed=0 SW-Mumbai-02 : ok=5 changed=3 failed=0 ... SW-Mumbai-12 : ok=5 changed=3 failed=0 # second run, proving idempotency: SW-Mumbai-01 : ok=5 changed=0 failed=0
Symptom: after your trunk task, the uplink only allows the VLANs in your allowed_vlans and dropped the rest — half the campus goes dark. Cause: switchport trunk allowed vlan (without add) replaces the whole allowed list on IOS. With ios_l2_interfaces, declare the complete intended list (here [10, 20, 30]) so the module reconciles correctly, or use the device's ... allowed vlan add 30 semantics. Never assume the module appends — confirm with show interfaces trunk and always --check --diff first.
Rollback on IOS isn't a magic 'undo' — your rollback IS the backup file (backup: yes wrote it) plus the fact that a corrected playbook re-run converges the device to the right state. Stage risky changes with --limit SW-Mumbai-01 to one box, prove it, then drop the limit. One real idempotency trap: banners. A banner motd pushed via ios_config often shows changed on every run because IOS stores the delimiters/newlines slightly differently than you typed them — match the on-device form exactly (or use the banner-specific handling) so re-runs report no change.
For your certification path, this lesson sits on two blueprints. On RHCE EX294, the objective "manage advanced features" includes using Ansible modules to gather facts and configure devices, and the whole exam is graded on writing correct, idempotent playbooks with proper inventory and variables — exactly the discipline here. On the Cisco DevNet (DEVASC 200-901) side, the "Network Fundamentals / Application Deployment & Automation" domains expect you to read and reason about an Ansible playbook that configures network devices and to know the value of infrastructure-as-code. Nail the merged/replaced/overridden table below and you can answer both.
Pushing the change by hand to 12 switches is ringing 12 restaurants one by one and re-reading the same order — slow, and you'll misspeak on the 9th call. The playbook is placing one Swiggy group order: you specify the items once (your desired state), the app fans it out to every kitchen, confirms each, and tells you which ones failed. --check is previewing the cart before you pay; assert is the delivery photo proving it actually arrived.
Meera at Airtel is about to push an overridden ios_vlans change to all 12 production switches in a 30-minute window. What's the single most important thing she does BEFORE hitting apply?
Pause & Predict
Predict: your VLAN rollout playbook runs clean and reports changed=3 per switch. You run the exact same playbook again an hour later. What should the RECAP say, and what does it prove? Type your guess.
🤖 Ask the AI Tutor
Tap any question — instant, scoped to this lesson. No login, no waiting.
Pre-curated from Ansible docs + community Q&A, scoped to this lesson. For a live prod issue, paste your export into chat.techclick.in.
📝 Wrap-up assessment — six more
You've answered 4 inline. Six left. 70% (7 of 10) marks the lesson complete on your profile. Tap Submit all answers at the end.
🧠 In your own words
Type one line: In one line, why is state: merged the safe default and state: overridden the one that causes outages? Then compare to the expert version.
🗣 Teach a friend
Best way to lock it in — explain it in one line to a teammate. Tap to generate a paste-ready summary.
📖 Glossary
- cisco.ios collection
- The Ansible collection of modules and plugins that drive Cisco IOS and IOS-XE devices.
- ios_config
- Generic module that pushes raw IOS config lines (with optional parents) into a section. The workhorse.
- ios_command
- Read-only module that runs show/exec commands and returns the text — never changes config.
- ios_facts
- Gathers structured device facts (version, interfaces, serial) into ansible_facts.
- Resource module
- A module that manages one logical resource (VLANs, interfaces, OSPF) declaratively, pushing only the diff.
- network_cli
- Connection plugin that keeps a persistent SSH CLI session to a network device instead of copying Python over.
- ansible_network_os
- Inventory var naming the device dialect; for IOS it is exactly cisco.ios.ios.
- become / enable
- Privilege escalation; for network_cli the only method is enable, which enters IOS privileged EXEC (the # prompt).
- merged
- Default state — adds or updates only what you declare, leaving all other config on the device untouched.
- replaced
- Rewrites each listed resource to exactly match your data, but leaves resources you didn't list alone.
- overridden
- Forces the whole resource set to match your data; anything on the device not in your list is deleted.
- save_when
- ios_config option controlling write-mem: always / modified / changed / never (copy running-config to startup-config).
- Idempotency
- Re-running the same playbook produces no change the second time (changed=0) — the goal for all config automation.
- Ansible Vault
- Built-in AES-256 secret store for encrypting passwords/vars; unlocked at runtime with --ask-vault-pass.
📚 Sources
- Ansible Community Documentation — "IOS Platform Options" (exact inventory vars: ansible_connection=ansible.netcommon.network_cli, ansible_network_os=cisco.ios.ios, ansible_become=true, ansible_become_method=enable, ansible_become_password; enable is the only valid become method for network_cli). docs.ansible.com/ansible/latest/network/user_guide/platform_ios.html
- Ansible Community Documentation — cisco.ios.ios_vlans / ios_l2_interfaces / ios_l3_interfaces resource modules (state choices merged/replaced/overridden/deleted/gathered/rendered/parsed/purged and what overridden deletes; config keys vlan_id, name, state, trunk.allowed_vlans, native_vlan). docs.ansible.com/ansible/latest/collections/cisco/ios/ios_vlans_module.html
- Ansible Community Documentation — cisco.ios.ios_config module (lines, parents, match, replace, backup + backup_options, save_when choices always/modified/changed/never, diff_against; note that abbreviated commands are not idempotent). docs.ansible.com/projects/ansible/latest/collections/cisco/ios/ios_config_module.html
- Ansible Community Documentation — "Network Resource Modules" user guide (the merged/replaced/overridden model, gather→diff→push, when to use a resource module vs config). docs.ansible.com/ansible/latest/network/user_guide/network_resource_modules.html
- Ansible Forum + Cisco Community — practitioner threads on privilege escalation ("unable to elevate privilege to enable mode") and ios_l2_interfaces trunk configuration gotchas; plus the netcommon deprecation notice migrating network_cli from paramiko to ansible-pylibssh (libssh). forum.ansible.com/t/deprecation-notice-paramiko-deprecated-for-network-cli-migrate-to-libssh-ansible-pylibssh/45782 · community.cisco.com/t5/tools/ansible-privilege-escalation/td-p/4908549 · github.com/ansible-collections/cisco.ios/issues/1061
- Red Hat EX294 (RHCE) exam objectives — "manage advanced features / use modules for system tasks" and the idempotent-playbook grading model; and Cisco DevNet Associate DEVASC 200-901 blueprint — automation & infrastructure-as-code domain (read/reason about playbooks that configure network devices). redhat.com/en/services/training/ex294-red-hat-certified-engineer-rhce-exam-red-hat-enterprise-linux · developer.cisco.com/certification/devnet-associate/
What's next?
You can automate a switch fabric now. Next we point the same playbook discipline at the security edge — using the paloaltonetworks.panos and fortinet.fortios collections to manage firewall objects, rules and commits as code.