Skip to content

Ansible resource modules and the state key

The Ansible article on this site introduces resource modules and the state key in passing; the project-structure article calls them "the layer that changes a device" and then builds the inventory beneath them. This article is that layer in full: what a resource module actually manages, every value the state key can take, and the one distinction — merged versus overridden — that quietly deletes configuration if you get it wrong.

A resource module manages one feature as data

A resource module manages a single device feature — VLANs, interfaces, OSPF, ACLs — as structured data rather than as lines of CLI. You describe the end state you want in YAML, and the module works out the commands needed to reconcile the device to it. Because it compares your declared data against what is already on the box, it is idempotent: running it twice leaves the device in the same state, with no duplicate lines piling up.

For Cisco IOS XE the modules live in the cisco.ios collection, and the names follow the feature: ios_vlans, ios_interfaces, ios_l2_interfaces, ios_l3_interfaces, ios_ospfv2, ios_acls, and many more. Each takes a config: block of structured data and a state: key. Here is a complete, runnable playbook that ensures three access VLANs exist:

---
- name: Configure access VLANs
  hosts: ios_xe
  gather_facts: false
  tasks:
    - name: Ensure VLANs are present
      cisco.ios.ios_vlans:
        config:
          - name: SALES
            vlan_id: 10
          - name: VOICE
            vlan_id: 20
          - name: MGMT
            vlan_id: 99
        state: merged

You never write vlan 10 / name SALES; you declare the data and let the module render and push the CLI. That is the whole appeal — you own the intent, the module owns the diffing, ordering, and "is it already there?" logic.

The state key decides how your data meets the device

Every resource module takes a state key, and it is the single most consequential choice in the task. It decides how your declared data is reconciled against the running configuration:

state What it does
merged Add or update only what you list — the safe default
replaced Make each listed resource match your data exactly, item by item
overridden Make the whole feature match — removes anything you didn't list
deleted Remove the listed configuration (restoring defaults)
gathered Read current state back as structured data (no change)
rendered Convert your data to device CLI, offline (no change, no connection)
parsed Convert raw CLI text to structured data, offline (no connection)

Current cisco.ios modules also add purged, which removes the containing objects themselves (for example, deleting whole VLAN definitions rather than just resetting their attributes) — a sharper tool than deleted, and one to reach for deliberately.

merged vs. replaced vs. overridden — the distinction that bites

These three all change config, but their blast radius differs enormously:

  • merged is additive. It touches only the resources you named and leaves everything else alone. Adding VLAN 50 with merged adds VLAN 50 — nothing on the device disappears.
  • replaced works item by item: for each resource you list, it makes that resource match your data exactly, rewriting its attributes, but it does not touch resources you didn't mention.
  • overridden applies to the entire feature. It makes the whole thing match your input and silently removes any VLAN, ACL entry, or OSPF network you left out.

The trap is overridden. Supply a config: block with three VLANs under state: overridden, and every other VLAN on the switch is deleted — because you didn't list them, the module reads your data as the complete, authoritative definition of the feature. The official docs warn this can even cut your own access to the device, for instance by overriding the management interface.

Rule of thumb: when the goal is "add or change X without disturbing anything else," the answer is merged. Use overridden only when your data genuinely is the complete source of truth for that feature and pruning drift is the point — and preview it first (below).

Reading state back with gathered

gathered does the opposite of a change: it reads the current configuration off the device and returns it as structured data, in the same schema you'd use to declare it. That makes it the foundation of drift detection and compliance reporting — gather the live state, compare it to your intended data, and you have a diff without parsing a single line of show output.

A related, blueprint-friendly use is inventory collection. The ios_facts module returns device model, serial number, and software version as structured facts you can store or report on:

---
- name: Collect device inventory
  hosts: ios_xe
  gather_facts: false
  tasks:
    - name: Gather IOS facts
      cisco.ios.ios_facts:
        gather_subset: all

    - name: Save one CSV row per host
      ansible.builtin.copy:
        content: "{{ ansible_net_serialnum }},{{ ansible_net_version }},{{ ansible_net_model }}"
        dest: "inventory/{{ inventory_hostname }}.csv"
      delegate_to: localhost

Offline conversion: rendered and parsed

Two states need no device connection at all, which makes them ideal for testing and CI:

  • rendered takes your structured data and returns the device-native CLI it would send, under a rendered key — without touching any device. Review that output, or assert on it in a pipeline's build stage, before a real run ever connects.
  • parsed does the reverse: give it raw configuration text via running_config (matching the output of the relevant show command) and it returns structured data. Useful for turning a captured config into the data model, or for diffing offline.

Because rendered and parsed make no connection — and gathered makes no change — all three are safe to run anywhere, including against devices you can't reach from the CI runner.

When no resource module fits: ios_config

Resource modules cover most common features, but not every one. For the gaps there is the generic ios_config module, which pushes raw CLI. It works, but you give up what the resource modules hand you for free: idempotency and structured output. With ios_config you own the matching logic, so a carelessly written task re-applies lines on every run. Keep it for the handful of features no resource module covers yet, and prefer a resource module everywhere else.

Prove it: changed=0 and a dry run

A correctly written resource-module playbook reports changed=0 on its second run — the device already matches your data, so nothing is done. If repeated runs keep reporting changes, the task isn't truly idempotent (often a sign of raw ios_config without proper matching). This is the same idempotency property that makes declarative tools safe to run on a schedule.

Before any change reaches production, preview it: run the playbook with --check (simulate) and --diff (show the exact line-by-line delta). Together they validate intent without altering a single device — and they are especially worth doing before an overridden run, where the diff is your last chance to catch an unintended deletion. Don't confuse --check with --syntax-check, which only parses the YAML.

Key takeaways

  • A resource module manages one feature as structured data and is idempotent by design — declare the end state, and the module renders and reconciles the CLI for you.
  • The state key decides the blast radius. merged adds and updates only what you list; replaced rewrites listed resources; overridden makes the whole feature match and deletes anything you left out.
  • Reach for gathered to read live state back as data for drift detection and inventory, and for rendered/parsed to convert between data and CLI entirely offline.
  • Use ios_config only when no resource module exists — raw CLI means you own the idempotency.
  • Verify with changed=0 on a re-run, and preview with --check --diff before touching production, most of all before overridden.

Sources: cisco.ios.ios_vlans module, Network resource modules (user guide), cisco.ios collection index, Ansible playbook check mode (--check/--diff).