NetAuto Academy

Ansible for Network Engineers

Idempotent Modules, Loops, Conditionals, and Check Mode

Intermediate15 min read

What you'll learn

  • What cisco.ios.ios_config and cisco.ios.ios_command actually do, and what installing cisco.ios requires
  • How loop: replaces manually repeating a task once per VLAN
  • How when: conditionally runs a task based on data
  • How --check and --diff map directly onto the dry_run=True pattern from Reliable Automation
🎓 Teach This Lesson

Listen to a guided explanation of this lesson.

Spoken narration isn't supported in this browser.

Real Cisco modules, and what they need

Two cisco.ios modules matter most for this module's capstone:

  • cisco.ios.ios_command — runs read-only show commands, the same role send_command() played with Netmiko.
  • cisco.ios.ios_config — pushes configuration-mode lines, the same role send_config_set() played — but idempotently: it checks the device's current configuration first, and only sends lines that would actually change something.

Requires the cisco.ios collection, and a real device to run for real

Both modules live in the cisco.ios collection (ansible-galaxy collection install cisco.ios — Lesson 2), not ansible-core itself. Running either one for real requires a real or virtual Cisco device (Option A). Neither module's examples in this lesson were run against a live device in this authoring environment — see the callout below.

check_vlans.yml — per Ansible's documented cisco.ios.ios_command behavior, not independently executed against a device in this authoring environment
- name: Check current VLANs before changing anything
hosts: switches
gather_facts: false

tasks:
  - name: Gather current VLAN brief
    cisco.ios.ios_command:
      commands:
        - show vlan brief
    register: vlan_output

Read-only, same as every 'check first' example since Netmiko Fundamentals Lesson 3. register: saves the command's output into a variable for a later task to use.

Quick checkConcept check
What is cisco.ios.ios_command used for?

ios_config: idempotent by design

ensure_vlan.yml — per Ansible's documented cisco.ios.ios_config behavior, not independently executed against a device in this authoring environment
- name: Ensure VLAN 10 exists
hosts: switches
gather_facts: false

tasks:
  - name: Create VLAN 10
    cisco.ios.ios_config:
      lines:
        - "vlan 10"
        - "name USERS"
      parents: "vlan 10"

lines are the configuration-mode commands to ensure are present; parents names the configuration context they belong under. This single task is documented to check the device's current config first and only send lines that would actually change something — the exact check-before-change apply_ntp_config() implemented by hand in Reliable Automation, now a framework guarantee.

This mirrors apply_ntp_config() directly

Reliable Automation's apply_ntp_config() read the current config, compared it against what's required, and only pushed a change if something was actually missing. ios_config is documented to do exactly that internally — the hand-written pre-check/decide/apply pattern from Phase 2.8, now built into the module itself.

Quick checkScenario
ios_config's task above is run twice in a row against the same device, with nothing else changing in between. Based on how Ansible documents idempotent modules, what happens the second time?

loop: one task, many VLANs

Configuration & Template Automation's capstone needed a {% for %} loop inside a template to render multiple VLANs. A playbook needs the same repetition at the task level — loop: runs one task once per item in a list:

ensure_required_vlans.yml — per Ansible's documented behavior, not independently executed against a device in this authoring environment
- name: Ensure required VLANs exist
hosts: switches
gather_facts: false

vars:
  required_vlans:
    - { id: 10, name: USERS }
    - { id: 20, name: VOICE }

tasks:
  - name: Create each required VLAN
    cisco.ios.ios_config:
      lines:
        - "vlan {{ item.id }}"
        - "name {{ item.name }}"
      parents: "vlan {{ item.id }}"
    loop: "{{ required_vlans }}"

loop: runs this one task once per item in required_vlans, with item bound to the current VLAN dict each time — the exact same mechanic as Configuration & Template Automation's {% for vlan in vlans %}, now looping a task instead of a block of rendered text.

Quick checkIdentify the correct code
Inside a task using loop: "{{ required_vlans }}", which expression correctly refers to the current item's id on each pass?

when: conditional tasks

when: skips a task unless a condition is true — the playbook-level equivalent of the {% if %} conditional already used inside a template:

conditional_task.yml — per Ansible's documented behavior, not independently executed in this authoring environment
- name: Only touch devices that actually need this VLAN
cisco.ios.ios_config:
  lines:
    - "vlan {{ item.id }}"
    - "name {{ item.name }}"
  parents: "vlan {{ item.id }}"
loop: "{{ required_vlans }}"
when: item.id is defined

when: item.id is defined skips the task entirely for any loop item missing an id — the same 'don't trust the data blindly' discipline validate_rendered_config() applied to rendered text, now applied before a task even runs.

Quick checkPredict output
A when: condition on a looped task evaluates to false for one specific item in the loop. What happens to that one item?

Check mode and diff: Ansible's own dry-run

Reliable Automation's entire safety model rested on one flag: dry_run=True, on by default, guaranteeing nothing real happens unless explicitly turned off. Ansible has a direct equivalent, built into the framework itself:

Reliable Automation
  1. dry_run=Truethe function's own default parameter
  2. Preview onlyno send_config_set() call happens
  3. dry-run-preview result state
Ansible
  1. ansible-playbook --checka command-line flag, not a code default
  2. Preview onlymodules report what WOULD change, without changing it
  3. --diff shows the exact before/after lines
terminal — per Ansible's documented behavior, not independently executed in this authoring environment
$ ansible-playbook ensure_required_vlans.yml --check --diff

TASK [Create each required VLAN] ****************************************
--- before
+++ after
@@ -1,3 +1,5 @@
!
+vlan 20
+ name VOICE
changed: [SW2] => (item={'id': 20, 'name': 'VOICE'})

PLAY RECAP ****************************************************************
SW2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0

--check runs the entire playbook without applying any real change; --diff additionally shows the exact lines that would be added. This describes Ansible's own documented check-mode output format — no device was actually touched to produce this.

Syntax success is not the same as a successful device change

ansible-playbook --syntax-check only confirms the playbook's YAML and schema are valid — it says nothing about whether a real device would accept the commands. --check goes further, simulating what modules report they would do — but for network modules, that simulation is still based on what the module believes the device's state is, not a guarantee. Only a real run against a real device, with a real post-check, confirms a change actually succeeded — exactly the distinction Reliable Automation drew between "the script ran without error" and "the change was verified."

Quick checkMyth check
If ansible-playbook --check completes with no errors, does that guarantee a real (non-check-mode) run against the same device would also succeed?

Key takeaways

  • cisco.ios.ios_command (read-only) and cisco.ios.ios_config (idempotent config changes) require the cisco.ios collection and a real device to run for real — neither was executed against a device in this authoring environment.
  • ios_config is documented to check-before-change internally — the same pattern apply_ntp_config() implemented by hand in Reliable Automation, now a framework guarantee.
  • loop: repeats one task per item in a list — the task-level equivalent of a template's {% for %} loop.
  • when: conditionally skips a task per loop item — the task-level equivalent of a template's {% if %}.
  • ansible-playbook --check (with --diff) is Ansible's own dry-run — the direct framework equivalent of Reliable Automation's dry_run=True default. Syntax success, check-mode success, and a real verified device change are three distinct claims, not one.