> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Ansible Check Mode and Diff on IOS XE: What a Dry Run Misses
- URL: https://www.pinglabz.com/ansible-check-mode-diff-ios/
- Published: 2026-11-01T03:38:33.000Z
- Updated: 2026-11-01T04:41:44.000Z
- Description: A dry run with --check --diff on three IOS XE routers, the real run, and a second check. Then two lines check mode called changed that the router rejected as invalid input.
- Author: Jaime
- Tags: Network Automation, Cisco IOS XE, Cisco

Running a playbook with `--check --diff` before the real thing is one of the cheapest habits in network automation. It is also easy to trust too much. On IOS XE, check mode never talks to the device's parser, so it can only tell you what Ansible would send, not whether the router will take it. This post runs one small change through check mode, a real run and a second check, and then shows two cases where check mode said "changed" and the real run failed.

It was built in my home lab on the six IOS XE lab routers from the earlier parts, using the three HQ routers: hq-core1, hq-core2 and hq-edge1\. The previous post was [part 18, IOS XE ACLs with the ios\_acls resource module](https://www.pinglabz.com/ansible-ios-acls-resource-module/).

Versions: ansible-core 2.21, cisco.ios 11.6, IOS XE 17.18.2.

[![Lab diagram: one change run three times on the HQ routers hq-core1, hq-core2 and hq-edge1. In the dry run with --check --diff -v, the logging resource module shows only commands (after equals before), ios_config shows updates for ip ssh time-out 60 and warns it cannot diff against running-config in check mode, and write memory is not executed but reports ok, with changed=2 per router. The real run with --diff shows -ip ssh time-out 30 and +ip ssh time-out 60, write memory runs, and each router reports changed=2. A second check run reports changed=0 on all three, so the play is idempotent. Below, check mode reports changed for ntp server 10.66.0.124 prefered and for no ip domain-lookup on hq-core1, but the real run rejects both with % Invalid input detected at the caret marker.](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/11/diagram-topology-8.png)](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/11/diagram-topology-8.png?ref=pinglabz.com)

Dry run, real run and a second check on the HQ routers, plus the two lines check mode passed and IOS rejected.

## The change

Two settings on the HQ routers: a 64000 byte informational logging buffer, and an SSH timeout of 60 seconds instead of 30\. One uses a resource module, one uses `ios_config`, and the last task saves the config. Mixing them is deliberate, because they behave differently in check mode.

```yaml
---
- name: Logging buffer and SSH timeout on HQ
  hosts: hq
  gather_facts: false
  tasks:
    - name: Buffered logging through the resource module
      cisco.ios.ios_logging_global:
        config:
          buffered:
            size: 64000
            severity: informational
        state: merged

    - name: SSH timeout through ios_config
      cisco.ios.ios_config:
        lines:
          - ip ssh time-out 60
        diff_against: running

    - name: Save the config
      cisco.ios.ios_command:
        commands:
          - write memory

```

Before the run, all three routers had `ip ssh time-out 30` and no `logging buffered` line.

## The dry run

`--check --diff -v` so the module results are printed. hq-core1 shown, the other two matched:

```
TASK [Buffered logging through the resource module] ****************************
changed: [hq-core1] => {"after": {"hosts": [{"host": "10.66.0.51"}], "trap": "informational"}, "before": {"hosts": [{"host": "10.66.0.51"}], "trap": "informational"}, "changed": true, "commands": ["logging buffered 64000 informational"]}

TASK [SSH timeout through ios_config] ******************************************
[WARNING]: unable to perform diff against running-config due to check mode
changed: [hq-core1] => {"banners": {}, "changed": true, "commands": ["ip ssh time-out 60"], "updates": ["ip ssh time-out 60"]}

TASK [Save the config] *********************************************************
[WARNING]: Only show commands are supported when using check mode, not executing write memory
ok: [hq-core1] => {"changed": false, "stdout": [], "stdout_lines": []}

PLAY RECAP *********************************************************************
hq-core1                   : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

```

Three things in there are worth reading closely.

**The resource module gives you commands, not a diff.** `commands` is the useful part: exactly the line it would push. The `after` key in check mode is a copy of `before`, so it does not show the predicted end state. `--diff` printed nothing extra for this task. If you want to review a resource module change, read `commands`.

**`diff_against: running` does nothing in check mode.** The module says so in a warning. You still get `updates`, which is the list of lines it would send, but the before and after config diff only appears on a real run.

**`ios_command` does not run a config command in check mode.** It does not execute `write memory`. It warns "Only show commands are supported when using check mode" and reports `ok`. That is the right behaviour, but it means a check run never proves your save step works, and a later task that reads the registered output of that command gets an empty `stdout`.

## The real run

Same playbook, `--diff` only. This is where the `ios_config` diff appears. hq-core1's hunk, with two management lines trimmed from the context:

```
TASK [SSH timeout through ios_config] ******************************************
--- before
+++ after
@@ -54,7 +54,7 @@
 ip http secure-server
 ip ssh bulk-mode 131072
-ip ssh time-out 30
+ip ssh time-out 60
 no logging btrace
 logging host 10.66.0.51

changed: [hq-core1]

PLAY RECAP *********************************************************************
hq-core1                   : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
hq-core2                   : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
hq-edge1                   : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

```

The resource module task printed `changed` with no diff, as in check mode. The changed counts matched the dry run exactly. On the real run, `write memory` ran, and the startup config on hq-core1 afterwards had both lines:

```
ip ssh time-out 60
logging buffered 64000 informational

```

## Check again after the change

Running the same `--check --diff` a second time is the cheapest idempotency test there is:

```
PLAY RECAP *********************************************************************
hq-core1                   : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
hq-core2                   : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
hq-edge1                   : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

```

Zero changed. If a task stays `changed` here, the line you wrote does not match how the device stores it, and that task will report a change on every run.

## Where check mode and the real run disagree

`ios_config` in check mode compares your lines against the running config as text. It never enters config mode, so the IOS parser never sees the command. Two lines that look plausible:

```yaml
---
- name: Where check mode and the real run disagree
  hosts: hq-core1
  gather_facts: false
  tasks:
    - name: A typo the device will reject
      cisco.ios.ios_config:
        lines:
          - ntp server 10.66.0.124 prefered

    - name: Old syntax this release rejects
      cisco.ios.ios_config:
        lines:
          - no ip domain-lookup

```

The first has `prefered` for `prefer`. The second is the hyphenated form that older IOS accepted and still turns up in a lot of copied baselines. This router already stores `no ip domain lookup`, without the hyphen.

Check mode says both are fine:

```
TASK [A typo the device will reject] *******************************************
changed: [hq-core1]

TASK [Old syntax this release rejects] *****************************************
changed: [hq-core1]

PLAY RECAP *********************************************************************
hq-core1                   : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

```

The real run:

```
TASK [A typo the device will reject] *******************************************
fatal: [hq-core1]: FAILED! => {"changed": false, "msg": "Task failed: b\"ntp server 10.66.0.124 prefered\\r\\nntp server 10.66.0.124 prefered\\r\\n                             ^\\r\\n% Invalid input detected at '^' marker.\\r\\n\\r\\nhq-core1(config)#\""}

```

And with `--start-at-task` to get past the first failure:

```
TASK [Old syntax this release rejects] *****************************************
fatal: [hq-core1]: FAILED! => {"changed": false, "msg": "Task failed: b\"no ip domain-lookup\\r\\nno ip domain-lookup\\r\\n            ^\\r\\n% Invalid input detected at '^' marker.\\r\\n\\r\\nhq-core1(config)#\""}

```

The second one is the more useful lesson. The router already had the setting the line was trying to apply. Check mode could not know that because the text did not match, so it reported a change. The real run found out the line is not valid syntax on 17.18 at all.

In both cases nothing was applied, because IOS rejects the line before it changes anything. On a multi-line `ios_config` task that is not guaranteed, because the lines are sent one at a time and any before the bad one are already in the running config when it fails. Check mode will not warn you about that either.

## In practice

- Read `commands` and `updates` with `-v`, not just the changed count. They are the actual lines that will be sent.
- Expect no config diff from resource modules, and no `diff_against` diff from `ios_config`, until the real run.
- Check mode does not validate syntax. Test new lines on one device first with `--limit`, then roll out.
- Run check mode again after the change. Anything still `changed` is a line written differently from how the device stores it.
- Tasks that depend on `ios_command` output get nothing in check mode. Guard them with `when: not ansible_check_mode` or give them a default.

## Where to take it

Check mode fits well as the first stage of a pipeline: run `--check --diff -v` on every change, save the output as the review artifact, and only allow the real run after someone has read the `commands` lists. The real run's diff then becomes the record of what actually changed.

[Part 20](https://www.pinglabz.com/ansible-services-hardening-ios/) The next part, \[standardizing and hardening services on the IOS routers\](https://www.pinglabz.com/ansible-services-hardening-ios/), pushes NTP, syslog, SNMPv3, banners and AAA to every device. The rest of the series is on the [series page](https://www.pinglabz.com/ansible-network-automation/).