# Ansible Playbooks for Cisco IOS: — VLANs, Interfaces and Config at Scale

Source: https://ai.techclick.in/blog_ansible_cisco_ios_playbooks
Markdown: https://ai.techclick.in/blog_ansible_cisco_ios_playbooks.md
Publisher: Techclick Infosec Pvt Ltd

Ansible for Cisco IOS for L1/L2 engineers and RHCE/DevNet: the cisco.ios modules (ios_config, ios_command, ios_vlans, ios_interfaces), network_cli + enable auth, merged vs replaced vs overridden, and a real VLAN push with --check/--diff.

Ansible Playbooks for Cisco IOS: — VLANs, Interfaces and Config at Scale student learning map
                     A visual study map for Ansible Playbooks for Cisco IOS: — VLANs, Interfaces and Config at Scale showing learning path, evidence, traps, and practice sequence.

                     TECHCLICK STUDY MAP
                     Ansible Playbooks for Cisco IOS: — VLANs, Interfaces...
                     Ansible · learn the flow, prove with evidence, avoid unsafe shortcuts

   1. Start
   🎯 By the end you will be able to

   2. Understand
   Pick where you want to start

   3. Prove
   ① The cisco.ios collection — the...

   4. Practice
   ② Connection + auth — how Ansible...

                     How to use this page
                     First build the mental model, then connect the concept to a realistic production decision. Finish by testing yourself.
                     Techclick Infosec Pvt Ltd | ai.techclick.in | Training Contact: WhatsApp +91 92772 29456

             Content-specific feature visual for this lesson: use it as the 60-second map before reading the full detail.

             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.

 👉 So far: two families — generic (ios_config / ios_command / ios_facts) and resource (ios_vlans, ios_interfaces, …). Next: exactly when to use which.

 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 .

  Figure 1 — One control node, the cisco.ios modules, many switches

 An architecture diagram. On the left a control node runs ansible-playbook. In the middle the cisco.ios collection is shown as two groups of modules: a generic group with ios_config for raw lines, ios_command for read-only show, and ios_facts for facts; and a resource group with ios_vlans, ios_interfaces, ios_l2_interfaces and ios_ospfv2 that manage structured state with merged, replaced or overridden. On the right a fan-out of twelve access switches. All connections use the network_cli connection over SSH. Green marks the preferred structured, idempotent resource-module path; blue marks the generic path.

- cisco.ios — one playbook, the right module, every switch Control node ansible-playbook site.yml cisco.ios collection generic ios_config · ios_command · ios_facts resource (structured state) ios_vlans · ios_interfaces · ios_l2_interfaces · ios_ospfv2 SW-Mumbai-01 SW-Mumbai-02 … SW-Mumbai-12 12 access switches all over network_cli (SSH) — privileged via enable Pick the right tool ✓ structured config (VLAN/intf/OSPF) → resource module → clean diff + idempotent ✓ read a show / verify → ios_command (read-only, never changes) ✓ raw line with no module (banner, NTP) → ios_config (the honest workhorse) ✗ dumping show-run into ios_config → brittle, re-pushes every run risky / wipes config generic / read path raw lines (ios_config) structured / idempotent Read left to right: the control node runs a playbook; the cisco.ios modules split into generic (raw lines / read-only / facts) and resource (structured state); both reach every switch over network_cli. Green = the structured, idempotent path you should prefer. ### The five modules you'll actually type Tap each — these cover ~90% of day-one Cisco automation tasks. 🧱 ios_config tap to flip Pushes raw IOS lines into a section (with parents). The workhorse for whatever has no resource module. So: powerful, but you own idempotency. 👀 ios_command tap to flip Runs show/exec commands read-only and returns the text. So: your verify and assert tool — it never changes a thing. 🗂️ ios_vlans tap to flip Manages VLANs as data with merged/replaced/overridden. So: declare the VLANs you want, it pushes only the delta. 🔌 ios_l2_interfaces tap to flip Sets access VLAN, trunk mode, allowed and native VLANs per port. So: your switchport config without typing switchport lines. Daily-life analogy — the society gate-pass register vs shouting names 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. Quick check · Q1 of 10 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? a) ios_config — he must write the exact IOS line so the run stays idempotent b) ios_command — it will configure the NTP server for him c) ios_facts — it pushes the NTP config as a fact d) ios_vlans — NTP is a kind of VLAN setting Correct: a. ios_config is the workhorse for raw lines that have no resource module; because it just pushes lines, Rahul must write the exact IOS syntax (e.g. ntp server 10.0.0.5) so re-runs are idempotent. ios_command is read-only and changes nothing; ios_facts only gathers facts; ios_vlans manages VLANs, not NTP. 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. Reveal answer Answer: Nothing — the second run reports ok / changed=0. With merged, the module reads the device, sees the VLANs you declared already exist exactly as desired, computes an empty diff, and pushes no commands. That's idempotency: the resource module compares desired state to actual state, so re-running is a safe no-op. (If you'd used raw ios_config with the literal vlan lines, a naive match could re-push them — which is exactly why resource modules are preferred.) ## ② 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. 👉 So far: network_cli + cisco.ios.ios + become=enable get you to the # prompt. Next: where to keep those passwords so they're not sitting in plaintext. 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. inventory/hosts.yml — the group that defines HOW Ansible reaches the switches 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: "&#123;&#123; vault_ssh_password &#125;&#125;" ansible_become: true ansible_become_method: enable ansible_become_password: "&#123;&#123; vault_enable_secret &#125;&#125;" Expected output # 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"] } ram@netauto:~/playbooks $ ansible-playbook -i inventory base.yml --ask-vault-pass . (Recreated for clarity — your terminal matches this.)"> 🖥️ This is the run you'll watch — a terminal pushing hostname + NTP + a loopback to the group: ram@netauto:~/playbooks $ ansible-playbook -i inventory base.yml --ask-vault-pass . (Recreated for clarity — your terminal matches this.) ram@netauto:~/playbooks 1 Command ansible-playbook -i inventory base.yml --ask-vault-pass 2 Vault password •••••••• (unlocks vault_enable_secret) PLAY Base config on access switches 3 TASK [set hostname] changed: [SW-Mumbai-01] TASK [set NTP server] changed: [SW-Mumbai-01] TASK [Loopback0 10.255.1.1] changed: [SW-Mumbai-01] 4 PLAY RECAP ok=4 changed=3 failed=0 ▶ run base.yml — set hostname + NTP + a loopback across the whole group - 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 &#123;&#123; inventory_hostname &#125;&#125; - 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: merged Expected output PLAY [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 Common mistake — "Unable to enter configuration mode" / timeout even though SSH works 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. Quick check · Q2 of 10 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? a) Change ansible_connection to local b) Remove ansible_network_os so Ansible auto-detects c) Set ansible_become: true and ansible_become_method: enable (and the enable secret as ansible_become_password) d) Switch ansible_become_method to sudo, which IOS understands Correct: c. Config needs privileged EXEC, reached with become=true and the only valid network_cli method, enable — plus the enable secret as ansible_become_password. ansible_connection: local is the deprecated model and won't help; removing ansible_network_os breaks the IOS dialect; sudo is a Linux method IOS does not understand (only enable is valid for network_cli). 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. Reveal answer Answer: Better pattern: put the enable secret (and SSH password) in an Ansible Vault-encrypted vars file and reference them as variables — secrets in an encrypted store, never in source. At runtime you unlock it with --ask-vault-pass (or a vault password file/identity). Same idea as never hardcoding an API key: the secret stays encrypted at rest and is only decrypted in memory during the run. The plaintext-in-Git version means anyone with repo access owns enable on every switch. ## ③ 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 . 👉 So far: merged adds, replaced rewrites what you listed, overridden makes the whole set match your data. Next: why overridden bites — and how backup + save_when keep you safe. 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). Figure 2 — merged vs replaced vs overridden — same input, three very different results A comparison table. The device starts with VLAN 10 sales, VLAN 20 voice, VLAN 30 old. The playbook declares only VLAN 30 named guest. Three result columns. Under merged: VLAN 10 and 20 are kept untouched and VLAN 30 is renamed to guest. Under replaced: VLAN 10 and 20 are kept untouched and only VLAN 30 is rewritten to guest. Under overridden: VLAN 10 and 20 are deleted in red and only VLAN 30 named guest remains, matching the declared list exactly. The amber box marks where the decision happens; red marks the dangerous deletions. You declared ONLY vlan 30 → three states, three outcomes On the device now vlan 10 name sales vlan 20 name voice vlan 30 name old Your playbook declares vlan_id: 30 name: guest state: ? ← this one word decides everything (10 and 20 are NOT in your list) state: merged (default) vlan 10 sales ✓ kept vlan 20 voice ✓ kept vlan 30 guest ✎ renamed adds/updates only what you list state: replaced vlan 10 sales ✓ kept vlan 20 voice ✓ kept vlan 30 guest ⟳ rewritten rewrites ONLY the ones you listed state: overridden ⚠ vlan 10 sales ✗ DELETED vlan 20 voice ✗ DELETED vlan 30 guest ✓ remains whole set must match → wipes the rest Same one-line input. merged = safe add · overridden = outage on a live switch deleted (not in your list) kept / on device decision point (state) applied as intended The device starts with VLANs 10, 20, 30. Your playbook declares only VLAN 30 (renamed). Follow each column: merged keeps all three, replaced rewrites only 30, overridden wipes 10 and 20. The red cells are deletions you did not ask for. Where (ansible-playbook) — preview the VLAN change as a running-config diff before applying - 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 Expected output # 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] Two safety nets every change task should carry On ios_config set backup: yes — it writes a timestamped copy of the running-config to backup/ (filename defaults to  _config. @  ) 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. ① Declare playbook: vlan_id 30 / name guest , state merged ▼ ② Read device module runs show vlan → finds 10 sales, 20 voice, 30 old already there ▼ ③ Diff compare desired vs actual → only delta is vlan 30 name old→guest ▼ ④ Push sends vlan 30 / name guest only; 10 and 20 left alone; save_when fires Press Play to step through the healthy path. Then press Break it . ▶ Play Next ▶ ⚠ Break it ↺ Reset 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. Likely cause 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. Diagnosis 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) Fix 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. while the corrected play converges. Verify 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. Quick check · Q3 of 10 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? a) overridden — it's the only one that rewrites an interface b) replaced — it rewrites the listed interface to match your data but leaves unlisted ones alone c) merged — it can never remove an attribute you dropped d) deleted — it makes the interface match your data Correct: b. replaced rewrites each resource you listed to exactly match your data (so a dropped attribute is removed on the box) while leaving resources you didn't list untouched — precisely Karthik's need. overridden would also delete/normalise every OTHER interface; merged can add/update but won't remove an attribute you dropped; deleted removes config, it doesn't reconcile to a desired state. 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. Reveal answer Answer: The device does NOT change — --check (dry-run) never pushes config; it only reports what would change. Whether the backup file is written depends on the run: with backup: yes the module is designed to capture the running-config before changes, but in pure check mode no config is altered, so treat --check as 'show me the plan'. The real safety value of backup: yes is on the actual apply run, where it drops a timestamped running-config copy into backup/ before any line is pushed — your rollback if the change goes wrong. ## ④ 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. 👉 So far: the four-task shape of a real VLAN rollout. Next: the playbook, the run, the diff, and how to think about rollback. vlan30_rollout.yml — the whole change, safely, for 12 switches - 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 &#123;&#123; inventory_hostname &#125;&#125;" Expected output 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 ram@netauto:~/playbooks $ ansible-playbook -i inventory vlan30_rollout.yml --check --diff --limit SW-Mumbai-01 shows the exact lines and applies nothing. (Recreated for clarity — your terminal matches this.)"> 🖥️ Always dry-run first — ram@netauto:~/playbooks $ ansible-playbook -i inventory vlan30_rollout.yml --check --diff --limit SW-Mumbai-01 shows the exact lines and applies nothing. (Recreated for clarity — your terminal matches this.) ram@netauto:~/playbooks — dry run 1 Command ansible-playbook ... vlan30_rollout.yml --check --diff 2 Flag --check report-only, change nothing Flag --diff show running-config before/after 3 diff (VLAN 30) +vlan 30 / + name guest diff (trunk) + switchport trunk allowed vlan add 30 4 RECAP (check) changed=3 (nothing actually applied) ▶ dry-run Common mistake — the trunk loses VLANs you didn't list 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 thinking + the banner idempotency gotcha 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. Figure 3 — The safe-change loop: check → diff → limit → apply → verify → save A six-step circular workflow for safe Cisco IOS changes with Ansible. Step one, dry-run with dash dash check and dash dash diff to preview without changing. Step two, limit to one switch with dash dash limit. Step three, apply for real on that one switch. Step four, verify with ios_command and assert. Step five, widen to all twelve switches. Step six, save with save_when and rely on backup for rollback. Amber marks the gate and decision steps; green marks the safe-to-widen and applied steps; red marks what goes wrong if you skip the dry-run. The safe-change loop for IOS at scale 1 · Dry-run --check --diff see the plan, change nothing 2 · Limit to one --limit SW-Mumbai-01 small blast radius 3 · Apply (one box) ansible-playbook push for real, watch it 4 · Verify ios_command + assert prove VLAN 30 is there 5 · Widen to 12 drop --limit safe now — proven once 6 · Save + rollback save_when · backup: yes write-mem + a way back Skip step 1? overridden wipes a VLAN on all 12 at once loop next change what skipping costs you gate / decision step safe to proceed The six-step muscle memory for any IOS change at scale. Amber is the decision/gate steps; green is when it's safe to widen the blast radius. Skipping the dry-run is how Friday outages happen. 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. Figure 4 — Cisco IOS + Ansible — the cheat-sheet A six-tile cheat sheet. Tile one, connection variables: ansible_connection network_cli, ansible_network_os cisco.ios.ios, become enable. Tile two, the module picker: resource modules for structured state, ios_config for raw lines, ios_command for read-only. Tile three, the three states: merged adds, replaced rewrites listed, overridden wipes the rest. Tile four, the resource modules list: ios_vlans, ios_interfaces, ios_l2_interfaces, ios_l3_interfaces, ios_ospfv2. Tile five, safety flags: dash dash check, dash dash diff, backup yes, save_when. Tile six, verify commands: ios_command show vlan brief, show interfaces trunk, assert. Ansible × Cisco IOS — your one-glance card Connect + become connection: network_cli network_os: cisco.ios.ios become: true · method: enable secrets → Ansible Vault Pick the module structured state → resource module raw lines (banner/NTP) → ios_config read / verify → ios_command facts → ios_facts The 3 states merged → adds/updates only replaced → rewrites listed only overridden → wipes the rest ⚠ default is merged Resource modules ios_vlans · ios_interfaces ios_l2_interfaces (trunk/access) ios_l3_interfaces (SVI/IP) ios_ospfv2 Safety flags --check (dry-run) --diff (show config delta) backup: yes (rollback copy) save_when: modified Verify it landed ios_command: show vlan brief show interfaces trunk assert: that: 'guest' in stdout re-run → changed=0 = idempotent The golden rule Default to merged · dry-run with --check --diff · backup before you apply · verify with assert overridden = the device must mirror your list exactly — anything else gets deleted Your one-card map: the connection vars, the module-picker, the three states, the safety flags, and the verify commands. Keep it open in your first change window and before any automation interview. Daily-life analogy — Swiggy bulk order vs 12 phone calls 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. Quick check · Q4 of 10 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? a) Save the running-config with save_when: always so the change persists b) Increase the SSH timeout so the run doesn't drop c) Email the NOC that a change is coming d) Run it with --check --diff (and --limit to one switch) to see exactly which VLANs overridden would delete Correct: d. overridden deletes any VLAN not in her list, so the one thing that prevents an outage is previewing the exact change with --check --diff, ideally scoped to one switch first — if it shows 'no vlan 10/20', she stops. Saving config persists a change that might be wrong; a longer timeout and an NOC email are good hygiene but don't reveal the destructive delta. 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. Reveal answer Answer: It should report changed=0 (ok=5, failed=0) on every switch. That proves idempotency: the resource modules read the device, found VLAN 30, the SVI and the trunk member already in the desired state, computed an empty diff, and pushed nothing. A second run that still shows changed>0 is a red flag — usually a non-idempotent ios_config line (like a banner whose stored form differs from yours) re-pushing every time. changed=0 on re-run is how you trust a playbook in production. ### 🤖 Ask the AI Tutor Tap any question — instant, scoped to this lesson. No login, no waiting. ios_config vs the resource modules — when do I use which? Why can't Ansible just SSH and run my commands like it does on Linux? What's the real difference between replaced and overridden? They sound the same. How do I run a change without breaking anything the first time? My playbook shows 'changed' every single run even though nothing changed. Why? How does this map to RHCE EX294 and Cisco DevNet? 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. Q5 · Remember Which inventory value tells Ansible that a target device speaks Cisco IOS and should be driven over a persistent CLI session? a) ansible_connection: local b) ansible_become_method: sudo c) ansible_network_os: cisco.ios.ios (with ansible_connection: ansible.netcommon.network_cli) d) ansible_python_interpreter: /usr/bin/python3 Correct: c. ansible_network_os: cisco.ios.ios names the IOS dialect and pairs with ansible_connection: ansible.netcommon.network_cli for the persistent CLI session. local is the deprecated model; sudo is a Linux become method IOS doesn't use (it needs enable); the python_interpreter var is irrelevant because there's no Python on the switch. Q6 · Apply You must add VLAN 50 to 20 switches that already carry several production VLANs, without disturbing any of them. Which ios_vlans state do you use? a) state: merged b) state: overridden c) state: deleted d) state: gathered Correct: a. merged adds VLAN 50 and leaves every existing VLAN untouched — exactly the requirement. overridden would delete all VLANs not in your list; deleted removes VLANs; gathered is read-only and changes nothing. merged is the safe default for additive changes. Q7 · Apply Before applying a change to 12 live switches, which single ansible-playbook invocation shows the exact running-config lines that WOULD change while applying nothing? a) ansible-playbook site.yml --force-handlers b) ansible-playbook site.yml --syntax-check c) ansible-playbook site.yml --start-at-task d) ansible-playbook site.yml --check --diff Correct: d. --check runs in dry-run mode (no changes) and --diff prints the before/after config delta, so together they preview the exact lines. --syntax-check only validates YAML/playbook structure, not the device delta; --force-handlers and --start-at-task control execution flow, not previewing changes. Q8 · Analyze An engineer ran an ios_vlans task with only VLAN 30 declared and state: overridden on a switch holding VLANs 10, 20, 30. Users on 10 and 20 immediately lost connectivity. What happened and what was the correct state? a) The SVI was misconfigured; replaced was needed b) overridden deleted VLANs 10 and 20 because they weren't in the declared list; merged was the correct state to add 30 safely c) merged wiped the unlisted VLANs; overridden would have been safe d) The trunk dropped; ios_config was needed instead Correct: b. overridden forces the device's whole VLAN set to equal the declared list, so VLANs 10 and 20 (absent) were deleted, orphaning their ports. merged would have simply ensured 30 exists and left 10 and 20 alone. It's not an SVI or trunk issue, and merged never deletes unlisted resources — that's the whole point of choosing it. Q9 · Analyze A change playbook reports changed=3 on the first run and changed=2 on every run after, never settling to 0. The two recurring changes are a banner motd line pushed via ios_config. What's the most likely cause? a) The banner ios_config line isn't idempotent — IOS stores the delimiters/newlines differently than typed, so it re-pushes every run b) ios_vlans is broken and must be replaced with ios_config c) The switches are rebooting between runs and losing config d) save_when: always is forcing a config change each time Correct: a. A task that keeps reporting changed means non-idempotency; banners are the classic offender because the on-device stored form (delimiters, newlines) differs from what you typed, so the module sees a mismatch and re-pushes. ios_vlans isn't implicated; a reboot would reset far more than two lines; save_when controls write-mem, not whether a task reports changed. Q10 · Evaluate Two engineers describe their VLAN-rollout approach. (A): "I dump the needed vlan and switchport lines into ios_config and run it on all 12 switches." (B): "I use ios_vlans + ios_l2_interfaces with state: merged, dry-run with --check --diff, --limit one switch first, backup: yes, then verify with ios_command + assert." Which is the stronger production approach and why? a) A — fewer modules to learn and it's faster to write b) B — resource modules give a structural diff and idempotency, and the check/limit/backup/verify loop contains the blast radius and proves the result c) A — ios_config is more reliable than resource modules d) Identical — both push the same final config Correct: b. B is the senior approach: resource modules reconcile structured state (clean diff, idempotent re-runs) and the --check/--limit/backup/verify discipline previews the change, limits exposure, gives a rollback, and proves success. A is brittle (raw lines risk non-idempotency and offer no structural diff), and 'same final config' ignores that A has no safety net or proof — which is exactly what production changes require. Submit all answers Try again Lesson complete — saved to your profile. Almost! You need 70% (7 of 10) — re-read the path that tripped you up and tap "Try again". ### 🧠 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. Compare with expert answer Expert version: Because merged only adds or updates the resources you declared and leaves everything else untouched, while overridden forces the entire resource set to match your list — so any VLAN or interface setting you didn't include gets deleted, which on a live switch orphans whatever was using it. ### 🗣 Teach a friend Best way to lock it in — explain it in one line to a teammate. Tap to generate a paste-ready summary. Generate my one-liner 📩 Quiz me on this in 7 days. Opt in and we'll email 3 micro-questions on Cisco IOS Automation at Day 1, Day 7 and Day 30 — spaced repetition is how this sticks. Un-tick any time. ### 📖 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.

                 Next · Ansible for Firewall Automation: Palo Alto & Fortinet →
                 Practice on exam.techclick.in →

---
Cite this Techclick lesson with the source URL. Do not invent fees, batch dates, or job guarantees.
Browse all lessons: https://ai.techclick.in/blogs
AI index: https://ai.techclick.in/llms.txt
