Imagine you’re working on a routine network task. You run an automation job to create VLAN 100 on a switch and name it users. But halfway through the run, the connection drops.
The job might have failed before sending the configuration. Or the switch might have applied the change successfully but the automation tool never got confirmation before the connection dropped. You can’t tell from the failed job alone.
You could log into the switch and investigate. But if every interrupted job requires manual intervention before you can safely retry it, automation quickly becomes difficult to operate at scale.
This is where idempotency becomes important.
An operation is idempotent when running it again produces the same resulting state as running it once. With idempotent operations, you can rerun automations without worrying that each execution will introduce another change.
Idempotency doesn’t guarantee a job will succeed, that its input is correct, or that a partially completed workflow will roll back, but it does make retries and repeated execution much more predictable.
What idempotency looks like in practice
Idempotent automation starts with a clear definition of the desired state. In our opening example, the desired state is VLAN 100 must exist on the switch, and its name must be users.
Depending on the switch’s current configuration, the automation should then do one of three things:
- Create VLAN 100 and name it
users, if it doesn’t exist. - Rename VLAN 100 to
users, if it exists with another name. - Leave VLAN 100 alone if it already matches the desired state.
The third case is particularly important. Once the desired state has been reached, running the same automation again should make no further configuration changes. An operation that makes no change is called a no-op.
See idempotency at work in an Ansible playbook
Let’s use an Ansible playbook to further explore how idempotency works in practice.
Ansible’s network resource modules let you describe the configuration you want, then compare it with what’s already configured on a device to determine what needs to change.
Here’s a playbook using the Arista EOS VLAN resource module to configure VLAN 100 with the name users:
- name: Configure the users VLAN hosts: leaf_switches gather_facts: false tasks: - name: Ensure VLAN 100 has the requested name arista.eos.eos_vlans: config: - vlan_id: 100 name: users state: merged
The config section specifies VLAN 100 and its name. The module compares that configuration with what’s already on the switch and determines whether changes are needed.
The state: merged setting tells Ansible to apply the supplied configuration while preserving other VLANs.
(To run this example, you’d need the arista.eos collection and an inventory group named leaf_switches, with appropriate connection settings and credentials. Ansible’s EOS platform guide covers that setup.)
Using this playbook, let’s look at two consecutive runs:
- First run: If VLAN 100 doesn’t exist, Ansible creates it. The play recap reports
changed=1for that switch. - Second run: VLAN 100 already exists with the correct name. Ansible makes no change and reports
changed=0.
The recap counts changed tasks, rather than individual commands or VLANs. If the VLAN was already configured correctly before the first run, that run should also report changed=0.
Now suppose an engineer logs into the switch and manually renames VLAN 100 to staff. The next time the playbook runs, Ansible detects the different name and restores users. Run the playbook again and no changes are made.
This is drift correction. It brings configuration that’s diverged from the intended state back into alignment.
Decide which configuration your automation owns
Idempotency applies only to the configuration an automation task is responsible for managing. In Ansible, you can manage specific settings or treat the supplied configuration as authoritative for an entire resource.
Suppose our switch has two VLANs:
- VLAN 100, named
users - VLAN 300, named
servers
Our playbook specifies only VLAN 100. What happens to VLAN 300 depends on the state we’ve selected.
With state: merged, Ansible ensures VLAN 100 matches the requested configuration and leaves VLAN 300 alone.
With state: overridden, Ansible treats the supplied VLAN configuration as authoritative. Because VLAN 300 isn’t included, Ansible will attempt to remove it from the switch.
Both the merged and overridden approaches can be idempotent. Once the switch matches the intended configuration for the selected state, subsequent playbook runs should make no further changes.
The important difference is what each approach considers the intended configuration.
With merged, you’re managing only the VLAN settings you’ve specified. With overridden, you’re taking responsibility for the wider VLAN configuration, including VLANs you’ve omitted.
Use overridden only when your automation is intended to manage the complete VLAN configuration within the module’s scope. Otherwise, an incomplete list could remove VLANs that other teams or systems still need.
Ansible’s change reports don’t guarantee idempotency
Ansible reports whether tasks changed a device’s configuration, but that status isn’t conclusive proof of idempotency. A task can report a change when the configuration remains unchanged, or modify configuration while reporting changed=0.
Ansible lets you control a task’s reported status using changed_when. For example, changed_when: false is useful when a command retrieves information without changing configuration, such as checking an interface’s status.
But changed_when: false only controls the reported status. It doesn’t prevent a command from executing or modifying configuration. A task could report changed=0 even though it sends configuration commands on every run. It can also prevent handlers from running, since Ansible uses the changed status to determine whether to trigger certain follow-up tasks.
Another consideration is how lower-level command tools handle repeated execution. Ansible’s ansible.netcommon.cli_command and Netmiko’s send_config_set, for example, don’t automatically compare the requested VLAN configuration with what’s already present.
That doesn’t mean the commands they execute are necessarily non-idempotent. Setting an interface description to its existing value, for example, may leave the configuration unchanged. When using these tools, you need to understand how the device handles repeated commands and implement any necessary checks yourself.
Ansible provides two modes that can help you inspect a playbook’s behavior:
- Check mode (
--check) simulates supported tasks without applying their changes. - Diff mode (
--diff) shows configuration differences where supported.
You can combine them to preview a playbook:
ansible-playbook vlan.yml --check --diff
Be aware that --diff on its own doesn’t prevent changes. Without --check, the playbook can still apply configuration to the device.
These modes also have limitations, as explained in the Ansible documentation. Some modules don’t fully support them, and tasks can be explicitly configured to execute even during a check. A preview showing proposed changes isn’t necessarily a problem either. The device may genuinely need correcting to match the intended configuration.
The stronger test of idempotency is what happens after a successful run. Apply the configuration, then run the same playbook again without changing the input. Verify that the switch’s configuration remains unchanged and that Ansible reports no further changes.
Make the data behind your automation idempotent too
So far, we’ve typed VLAN 100 directly into an Ansible playbook. But when you’re managing hundreds of VLANs across dozens of sites, those details will most likely come from a source of truth.
The source of truth introduces another place where idempotency matters. If automation creates or updates records in the source of truth, repeated runs shouldn’t create duplicates or make unnecessary changes. Otherwise, your configuration automation could be working with inconsistent data before it even connects to a switch.
Infrahub provides a way to model and manage the infrastructure data behind these workflows. Infrahub calls each record a node. The Infrahub schema defines the types of nodes you can store, their attributes, and the relationships between them.
For example, you could model VLAN 100 as a node belonging to a particular site, with 100 as its VLAN ID and users as its name.
In Infrahub, you can manage infrastructure data directly or generate records automatically from a higher-level design. An Infrahub Generator can take a site design and create or update the VLAN nodes derived from it.
Here’s how an idempotent Generator handles repeated runs:
- First run: Creates the VLAN 100 node.
- Second run: Recognizes the existing node instead of creating a duplicate.
- After an intentional design change: If the site design changes the VLAN name from
userstoemployees, the Generator updates the existing node with the new name.
If a Generator isn’t written to check for an existing node and update it, it might try to create a new node every time it runs. If the schema enforces uniqueness for that VLAN, the operation will fail. If duplicate nodes are permitted, the Generator could create multiple records representing the same VLAN.
To avoid this, Infrahub’s Python SDK supports an operation called upsert, which creates a node if it doesn’t exist or updates an existing match. When saving a node, you can request this behavior with allow_upsert=True.
For an upsert to work, Infrahub needs a reliable way to identify the existing node. In our example, the schema could identify a VLAN by its site and VLAN ID. Changing the VLAN’s name would then update the existing node rather than create a new one.
Once Infrahub contains the intended VLAN data, a Transformation can turn it into device configuration that feeds an Ansible deployment workflow. The Generator maintains the intended data, the Transformation produces the configuration, and Ansible applies it to the switch.
Each operation needs to behave predictably when repeated. An idempotent Ansible playbook can’t compensate for a Generator that creates duplicate VLAN records every time it runs.
Running and reviewing Generators
Infrahub Generators can run manually, as part of a Proposed Change workflow, or through configured triggers. Regardless of how they’re started, repeated runs with unchanged input should leave the generated nodes unchanged.
When a Generator runs on a branch, you can review its changes before merging them. After a successful first run, the branch diff may still show the nodes created or updated by that run. This doesn’t mean a second run made those changes again. The diff shows how the branch differs from its base, including changes made during the first run.
To verify idempotency, compare the generated data before and after the second run. With unchanged input, the Generator shouldn’t create duplicate nodes or make additional changes.
Handle removal as carefully as creation
We’ve seen how a Generator can create or update VLAN 100 without producing duplicate records. Now suppose the network design changes and VLAN 100 is no longer needed.
Infrahub’s Python SDK provides tracking mode, which lets a Generator keep track of the records it manages. With delete_unused_nodes=True, a subsequent run can delete previously tracked records that the Generator no longer produces.
In our example, that could remove the VLAN 100 record from Infrahub after it’s removed from the site design. Cleanup should be limited to records the Generator is responsible for managing so it doesn’t delete data that another workflow still needs.
Removing VLAN 100 from Infrahub doesn’t remove it from the switch. Our Ansible playbook uses state: merged, which leaves VLANs alone when they’re omitted from the supplied configuration. Unless the deployment workflow explicitly removes VLAN 100, the switch could retain a VLAN that no longer exists in the intended design.
To keep the network aligned with the source of truth, the deployment workflow must handle removals as well as additions and updates.
How to verify idempotency
Before relying on automation to be idempotent in production, test how it behaves when repeated, when configuration drifts, and when the intended state changes.
Start with a lab switch and work through these checks:
- First run and rerun: Apply the VLAN 100 configuration, verify the resulting state, then run the playbook again. Confirm that the configuration remains unchanged and Ansible reports
changed=0. - Drift correction: Manually rename VLAN 100, rerun the playbook, and confirm that it restores
users. A subsequent run should make no further changes. - Configuration ownership: Confirm that the playbook preserves unrelated VLANs when using
merged, and that your chosen removal method deletes only the intended VLAN. - Source of truth: If you’re using Infrahub Generators, verify that reruns don’t create duplicate records, that changes to non-identity fields update existing records, and that cleanup affects only records the Generator owns.
Also test interrupted jobs. Ansible might successfully configure one switch before losing its connection to another. Idempotency doesn’t make that job an all-or-nothing transaction. You still need to identify which switches reached the intended state and retry those that didn’t.
Idempotency also doesn’t protect against incorrect input. Automation can consistently apply the wrong configuration if the desired state is wrong.
A successful first run tells you that your automation worked once. A successful second run, with no unnecessary changes, gives you greater confidence that you can run it again.