TTechclick ⚡ XP 0% All lessons
Ansible · Playbooks · Cisco IOS AutomationInteractive · L1 / L2 / L3

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

You have 12 access switches and a change window of 30 minutes to push one VLAN, one SVI and a trunk tweak. Logging into each box by hand is how typos and 2 a.m. callbacks happen. This lesson shows how the cisco.ios collection turns that change into one playbook you can dry-run, diff, and roll out to every switch at once.

📅 2026-06-11 · ⏱ 13 min · 3 live demos · 4 infographics · 🏷 10-Q assessment + AI Tutor inline

⚡ Quick Answer

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.

🎯 By the end you will be able to

Read as:

Pick where you want to start

1

The cisco.ios modules

ios_config, ios_command, ios_facts and the resource modules.

2

Connect & become

network_cli, ansible_network_os, enable mode, credentials.

3

Structured state

merged vs replaced vs overridden, backup, save_when.

4

A real VLAN push

12 switches, verify with assert, --check first, rollback.

🧠 Warm-up — 3 questions, no score

Just notice which ones make you pause. We answer all three inside the lesson.

1. You need to push five lines of raw IOS config that has no dedicated module. Which task do you reach for?

Answered in The cisco.ios modules.

2. Your playbook lists only VLAN 30, but you set state: overridden on a switch that already has VLANs 10, 20, 30. What happens to 10 and 20?

Answered in Structured state.

3. Before pushing a change to 12 live switches, what do you run first to see what WOULD change without changing anything?

Answered in Connect & become.

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
The cisco.ios collection fans one playbook out to many switches; resource modules manage structured state, ios_config pushes raw lines 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 nodeansible-playbooksite.yml cisco.ios collection genericios_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 configgeneric / read pathraw 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?

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.

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: "{{ vault_ssh_password }}"
        ansible_become: true
        ansible_become_method: enable
        ansible_become_password: "{{ vault_enable_secret }}"
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"] }
🖥️ 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 {{ 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: 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?

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.

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
With only VLAN 30 declared, merged keeps 10 and 20, replaced rewrites just 30, but overridden silently deletes 10 and 20 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 devicedecision 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 <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.

① Declareplaybook: vlan_id 30 / name guest, state merged
② Read devicemodule runs show vlan → finds 10 sales, 20 voice, 30 old already there
③ Diffcompare desired vs actual → only delta is vlan 30 name old→guest
④ Pushsends 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.

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?

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.

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 {{ inventory_hostname }}"
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
🖥️ 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
Run every IOS change as a loop: dry-run with check and diff, limit to one switch, apply, verify with assert, then 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 --diffsee the plan, change nothing 2 · Limit to one--limit SW-Mumbai-01small blast radius 3 · Apply (one box)ansible-playbookpush for real, watch it 4 · Verifyios_command + assertprove VLAN 30 is there 5 · Widen to 12drop --limitsafe now — proven once 6 · Save + rollbacksave_when · backup: yeswrite-mem + a way back Skip step 1?overridden wipes a VLAN on all 12 at once loop next change what skipping costs yougate / decision stepsafe 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
Ansible for Cisco IOS on one card: connection variables, the module picker, the three states, safety flags and verify commands 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 + becomeconnection: network_clinetwork_os: cisco.ios.iosbecome: true · method: enablesecrets → Ansible Vault Pick the modulestructured state → resource moduleraw lines (banner/NTP) → ios_configread / verify → ios_commandfacts → ios_facts The 3 statesmerged → adds/updates onlyreplaced → rewrites listed onlyoverridden → wipes the rest ⚠default is merged Resource modulesios_vlans · ios_interfacesios_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 landedios_command: show vlan briefshow interfaces trunkassert: that: 'guest' in stdoutre-run → changed=0 = idempotent The golden ruleDefault to merged · dry-run with --check --diff · backup before you apply · verify with assertoverridden = 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?

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.

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.

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?

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?

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?

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?

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?

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?

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.
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.

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.

📖 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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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.