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:
mergedis additive. It touches only the resources you named and leaves everything else alone. Adding VLAN 50 withmergedadds VLAN 50 — nothing on the device disappears.replacedworks 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.overriddenapplies 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. Useoverriddenonly 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:
renderedtakes your structured data and returns the device-native CLI it would send, under arenderedkey — without touching any device. Review that output, or assert on it in a pipeline's build stage, before a real run ever connects.parseddoes the reverse: give it raw configuration text viarunning_config(matching the output of the relevantshowcommand) 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
statekey decides the blast radius.mergedadds and updates only what you list;replacedrewrites listed resources;overriddenmakes the whole feature match and deletes anything you left out. - Reach for
gatheredto read live state back as data for drift detection and inventory, and forrendered/parsedto convert between data and CLI entirely offline. - Use
ios_configonly when no resource module exists — raw CLI means you own the idempotency. - Verify with
changed=0on a re-run, and preview with--check --diffbefore touching production, most of all beforeoverridden.
Sources: cisco.ios.ios_vlans module,
Network resource modules (user guide),
cisco.ios collection index,
Ansible playbook check mode (--check/--diff).