> ## 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 Resource Module States on IOS XE: merged vs replaced vs overridden vs deleted
- URL: https://www.pinglabz.com/ansible-resource-module-states-merged-replaced-overridden-deleted/
- Published: 2026-10-31T18:46:07.000Z
- Updated: 2026-11-01T04:41:32.000Z
- Description: I ran the same ios_l3_interfaces data through merged, replaced, overridden and deleted on an IOS XE 17.18 router and captured the exact commands each state sent. One of them would have removed the management IP. Another quietly turned off ICMP redirects and unreachables.
- Author: Jaime
- Tags: Network Automation, Cisco IOS XE, Cisco

The `state` option on a Cisco resource module decides what Ansible removes, and the module docs describe it in one line each. I wanted to see the actual commands, so I took one router with some messy hand-built interface config, fed the same YAML to `cisco.ios.ios_l3_interfaces` four times, and reset the router between runs.

Two results surprised me. `overridden` was about to delete the management IP on Ethernet0/0, the address Ansible was connected through. And `replaced` added `no ip redirects` and `no ip unreachables` to every interface it touched, even though my YAML never mentioned ICMP.

This is part 3 of the [Ansible for Network Engineers](https://www.pinglabz.com/ansible-network-automation/) series. It reuses the inventory from [part 2](https://www.pinglabz.com/ansible-network-inventory-site-role/) and the control node from [part 1](https://www.pinglabz.com/ansible-network-lab-setup-first-playbook-ios-xe/). I built it in my home lab on Cisco IOS XE 17.18.2, with ansible-core 2.21.5 and cisco.ios 11.6.0\. All output is from real runs.

## The starting point

hq-core1 is the HQ core router from the earlier posts. Its management port, Ethernet0/0, is 10.66.10.11/24 in a management VRF. The three other ports look like a router that people have been editing by hand for a while:

[![Lab diagram: The same YAML input, vars/hq-core1-l3.yml, feeds four ios_l3_interfaces states, each with the commands it sends to IOS XE. merged sends interface Ethernet0/1, ipv6 address 2001:db8:66:101::1/64. replaced sends no ip redirects and no ip unreachables. overridden (focal) in check mode would send no ip address 10.66.10.11 255.255.255.0 on Ethernet0/0, the mgmt IP. deleted on Ethernet0/3 sends no ip address 10.66.250.1 255.255.255.0; deleted with no config list resets every interface.](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/10/ansible-resource-module-states.png)](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/10/ansible-resource-module-states.png?ref=pinglabz.com)

What each ios\_l3\_interfaces state sends for the same intent file

```text
interface Ethernet0/1
 description to hq-core2
 ip address 10.66.101.1 255.255.255.252
interface Ethernet0/2
 description spare - old WAN
 ip address 10.66.199.1 255.255.255.0 secondary
 ip address 10.66.102.1 255.255.255.252
interface Ethernet0/3
 description TEMP lab test - remove
 ip address 10.66.250.1 255.255.255.0
 shutdown

```

Ethernet0/2 still carries a secondary address from an old migration, and Ethernet0/3 has a test subnet someone forgot about. I kept a small playbook, `20-seed-brownfield.yml`, that pushes exactly this config with `ios_config`, so every test below starts from the same state.

## The intent file

This is what I want the two core-facing ports to look like. Ethernet0/1 gains an IPv6 address. Ethernet0/2 keeps only its /30.

```yaml
# vars/hq-core1-l3.yml
l3_intent:
  - name: Ethernet0/1
    ipv4:
      - address: 10.66.101.1/30
    ipv6:
      - address: 2001:db8:66:101::1/64
  - name: Ethernet0/2
    ipv4:
      - address: 10.66.102.1/30

```

The file doesn't list Ethernet0/0 or Ethernet0/3\. That omission is the whole difference between the four states.

One playbook runs it, with the state passed on the command line, and prints the `commands` list the module returns. That list is the most honest diff you'll get from a resource module: it's what went down the SSH session.

```yaml
---
- name: ios_l3_interfaces with a chosen state
  hosts: hq-core1
  gather_facts: false
  vars_files:
    - ../vars/{{ l3_file | default('hq-core1-l3.yml') }}
  tasks:
    - name: L3 interfaces, state={{ st }}
      cisco.ios.ios_l3_interfaces:
        config: "{{ l3_intent }}"
        state: "{{ st }}"
      register: result

    - name: Commands Ansible sent (or would send)
      ansible.builtin.debug:
        var: result.commands

```

I also tried `--diff`. With these modules it printed nothing extra for me, so `result.commands` plus a `show run` before and after is how I compared the runs.

## merged: add, never remove

```text
$ ansible-playbook -i site-inv playbooks/21-l3-state.yml -e st=merged --diff

TASK [L3 interfaces, state=merged] *********************************************
changed: [hq-core1]

TASK [Commands Ansible sent (or would send)] ***********************************
ok: [hq-core1] => {
    "result.commands": [
        "interface Ethernet0/1",
        "ipv6 address 2001:db8:66:101::1/64"
    ]
}

```

One line. The secondary address on Ethernet0/2 is still there, because `merged` only adds what's missing. It treats the YAML as "at least this", so leftovers survive. A second run sent nothing and reported `changed=0`.

Use `merged` when you're adding something to a brownfield device and you don't yet trust your data to describe everything on it.

## replaced: own the listed interfaces

```text
$ ansible-playbook -i site-inv playbooks/21-l3-state.yml -e st=replaced

TASK [L3 interfaces, state=replaced] *******************************************
changed: [hq-core1]

TASK [Commands Ansible sent (or would send)] ***********************************
ok: [hq-core1] => {
    "result.commands": [
        "interface Ethernet0/1",
        "no ip redirects",
        "no ip unreachables",
        "no ipv6 redirects",
        "no ipv6 unreachables",
        "ipv6 address 2001:db8:66:101::1/64",
        "interface Ethernet0/2",
        "no ip redirects",
        "no ip unreachables",
        "no ipv6 redirects",
        "no ipv6 unreachables",
        "no ip address 10.66.199.1 255.255.255.0 secondary"
    ]
}

```

The secondary address is gone, which is what `replaced` is for: for every interface in the YAML, the YAML is the complete L3 config. Ethernet0/0 and Ethernet0/3 weren't touched.

The ICMP lines were the part I didn't expect. My YAML didn't set `redirects` or `unreachables`, and the module read "not set" as "false". The running config afterwards:

```text
interface Ethernet0/1
 description to hq-core2
 ip address 10.66.101.1 255.255.255.252
 no ip redirects
 no ip unreachables
 ipv6 address 2001:DB8:66:101::1/64
 no ipv6 redirects
 no ipv6 unreachables

```

On a core link that may even be what you want, since plenty of hardening guides turn redirects off. But with unreachables off, the router stops answering with ICMP unreachable messages, and traceroute through it gets harder to read. [Ping Fails: ICMP Troubleshooting](https://www.pinglabz.com/ping-fails-icmp-troubleshooting-lab/) shows how much those messages tell you during an outage. It should be a decision you make on purpose, so I put the values in the data:

```yaml
  - name: Ethernet0/1
    redirects: true
    unreachables: true
    ipv4:
      - address: 10.66.101.1/30
    ipv6:
      - address: 2001:db8:66:101::1/64

```

```text
TASK [Commands Ansible sent (or would send)] ***********************************
ok: [hq-core1] => {
    "result.commands": [
        "interface Ethernet0/1",
        "ip redirects",
        "ip unreachables",
        "interface Ethernet0/2",
        "ip redirects",
        "ip unreachables"
    ]
}

```

That fixed IPv4 only. The IPv6 lines stayed `no ipv6 redirects` and `no ipv6 unreachables` until I also set `ipv6_redirects` and `ipv6_unreachables`, which are separate keys. The second run of the fixed file reported `changed=0`.

## overridden: own the whole device

Before running `overridden` for real I ran it with `--check`. I'm glad I did:

```text
$ ansible-playbook -i site-inv playbooks/21-l3-state.yml -e st=overridden --check

TASK [Commands Ansible sent (or would send)] ***********************************
ok: [hq-core1] => {
    "result.commands": [
        "interface Ethernet0/0",
        "no ip address 10.66.10.11 255.255.255.0",
        "interface Ethernet0/3",
        "no ip address 10.66.250.1 255.255.255.0",
        "interface Ethernet0/1",
        "ipv6 address 2001:db8:66:101::1/64",
        "interface Ethernet0/2",
        "no ip address 10.66.199.1 255.255.255.0 secondary"
    ]
}

```

The first two commands remove the address of the management interface. `overridden` means "the YAML is the complete L3 config for the device", and the YAML didn't list Ethernet0/0, so the module planned to clear it. The play would have cut its own SSH session partway through, and I'd have been on the console.

A management VRF doesn't protect you here. The module doesn't look at VRFs; it saw an interface with an address that wasn't in the data.

The fix is to put the management interface in the data. With Ethernet0/0 listed (`vars/hq-core1-l3-full.yml`), the real run removed only the forgotten test subnet and the old secondary:

```text
    "result.commands": [
        "interface Ethernet0/3",
        "no ip address 10.66.250.1 255.255.255.0",
        "interface Ethernet0/0",
        "no ip redirects",
        "no ip unreachables",
        "no ipv6 redirects",
        "no ipv6 unreachables",
        "interface Ethernet0/1",
        "ipv6 address 2001:db8:66:101::1/64",
        "interface Ethernet0/2",
        "no ip address 10.66.199.1 255.255.255.0 secondary"
    ]

```

Look at Ethernet0/0 again: the ICMP defaults from the `replaced` section hit the management port too. Ethernet0/3 kept its description and its `shutdown`, since `ios_l3_interfaces` only manages L3 attributes. Descriptions and admin state belong to `ios_interfaces`, which has the same four states.

## deleted: with and without a config list

With an interface named, `deleted` removes that interface's L3 config and nothing else:

```yaml
    - name: Remove whatever L3 config Ethernet0/3 has
      cisco.ios.ios_l3_interfaces:
        config:
          - name: Ethernet0/3
        state: deleted

```

```text
    "result.commands": [
        "interface Ethernet0/3",
        "no ip address 10.66.250.1 255.255.255.0"
    ]

```

Without `config`, `deleted` applies to every interface on the device. I only ran this one in check mode, as an ad-hoc command:

```text
$ ansible -i site-inv hq-core1 -m cisco.ios.ios_l3_interfaces -a "state=deleted" --check

```

```json
[
 "interface Ethernet0/0",
 "ip redirects",
 "ip unreachables",
 "ipv6 redirects",
 "ipv6 unreachables",
 "no ip address 10.66.10.11 255.255.255.0",
 "interface Ethernet0/1",
 "ipv6 redirects",
 "ipv6 unreachables",
 "no ip address 10.66.101.1 255.255.255.252",
 "interface Ethernet0/2",
 "ipv6 redirects",
 "ipv6 unreachables",
 "no ip address 10.66.199.1 255.255.255.0 secondary",
 "no ip address 10.66.102.1 255.255.255.252"
]

```

Every address on the router, management included. One forgotten `config:` key turns a cleanup task into an outage, so I never write `state: deleted` without a `config` list, and I'd catch it in review the way I'd catch `no router ospf` in a change ticket.

Note that `deleted` puts the ICMP settings back to their IOS defaults (`ip redirects`, `ip unreachables`), while `replaced` with no values set turned them off. The two states read a missing key differently.

## What each state sent, side by side

| State                   | Scope                           | Ethernet0/1          | Ethernet0/2 secondary | Ethernet0/3 test subnet | Ethernet0/0 mgmt IP   |
| ----------------------- | ------------------------------- | -------------------- | --------------------- | ----------------------- | --------------------- |
| merged                  | listed interfaces, add only     | IPv6 added           | kept                  | kept                    | kept                  |
| replaced                | listed interfaces, full replace | IPv6 added, ICMP off | removed               | kept                    | kept                  |
| overridden              | whole device                    | IPv6 added           | removed               | removed                 | removed unless listed |
| deleted (one interface) | listed interfaces               | untouched            | untouched             | removed                 | kept                  |
| deleted (no config)     | whole device                    | removed              | removed               | removed                 | removed               |

## How I use them now

- Brownfield, first contact: `merged`. It can't delete anything.
- Interfaces I fully own in the data: `replaced`, with `redirects` and `unreachables` (and the IPv6 versions) set explicitly so the module doesn't pick for me.
- `overridden` only after the data describes every interface on the box, management port included, and only after a `--check` run whose `commands` list I've read.
- `deleted` always with a `config` list.

Getting that complete data set is the next problem. Typing it by hand for six devices is how you leave out Ethernet0/0\. The `gathered` state reads the running config into the same YAML structure, and the next posts in the series use it as the starting point. For the playbook basics behind all of this, start with the [series overview](https://www.pinglabz.com/ansible-network-automation/).