CCNA Ansible: Configuration Management with ios_command and ios_config
<p><strong>Ansible is an agentless configuration management tool that runs commands and pushes configuration to many network devices at once from a single control node over SSH.</strong> The CCNA v2.0 blueprint asks you to use configuration management mechanisms such as Ansible to execute commands, so you need to read and fix real inventories and playbooks: the connection variables for Cisco IOS, enable mode, the <code>ios_command</code> and <code>ios_config</code> modules, how a playbook verifies and saves a change, and how to interpret what a run reports. This lesson works through each piece with the YAML you will see in exam scenarios.</p>
On this page10 sections
- What is Ansible and how does it manage Cisco devices?
- Which inventory variables let Ansible connect to IOS devices?
- How does Ansible reach enable mode and keep passwords safe?
- How do inventory groups and variables work?
- How do you run show commands with ios_command?
- How do wait_for, match, retries and interval verify a change?
- How do you change configuration with ios_config?
- What do idempotency and ok versus changed mean?
- How do save_when and backup protect configuration?
- How do you run Ansible commands and read the results?
- Explain why Ansible is agentless and push-based, and what a Cisco device needs before Ansible can manage it
- Write the inventory variables that connect Ansible to IOS devices and reach enable mode, and place them in group_vars or host_vars
- Use cisco.ios.ios_command to run show commands, capture output with register and verify state with wait_for
- Use cisco.ios.ios_config lines, parents, before and match to apply configuration idempotently
- Choose save_when and backup settings, and run playbooks safely with --check, --diff and --limit
- Interpret a PLAY RECAP and protect credentials with Ansible Vault
What is Ansible and how does it manage Cisco devices?
Ansible is an open-source configuration management tool that pushes commands and configuration from one Linux control node to many devices over SSH, without installing any software on those devices. For the CCNA it is the standard example of automation-based management: instead of logging in to 40 switches one by one, an engineer describes the work once in a YAML file and Ansible carries it out on every device in the list.
Three properties define how Ansible works with routers and switches:
- Agentless — nothing is installed on the managed device. There is no Ansible service, no client and no Python interpreter on the switch. The device needs only what an engineer would need: SSH enabled and an account Ansible can log in with.
- Push-based — the control node starts every session. The device never polls a server or pulls a playbook, so any filter on the device's management access must admit the control node's address as an SSH source.
- Modules run on the control node — on a Linux server Ansible copies Python code to the target and runs it there, but network devices cannot run that code. For Cisco IOS and IOS XE the module logic runs on the control node, which opens an SSH session to the CLI, sends commands and parses the text that comes back.
The building blocks you will meet in every question:
| Term | What it is |
|---|---|
| Control node | The Linux host (or container) where Ansible is installed and from which it runs |
| Managed node | A device Ansible configures or queries: here, an IOS or IOS XE router or switch |
| Inventory | The list of managed nodes, organised into groups, plus their variables |
| Module | A unit of work, such as cisco.ios.ios_command or cisco.ios.ios_config |
| Task | One call to a module with its arguments |
| Play / playbook | A play maps a group of hosts to an ordered list of tasks; a playbook is a YAML file of one or more plays |
| Collection | A package of modules and plugins, such as cisco.ios and ansible.netcommon, installed with ansible-galaxy |
Ansible is one of several configuration management tools. Puppet and Chef traditionally use an agent on each managed node that pulls its configuration from a server; Ansible's agentless SSH push is why it fits network devices so easily. The wider comparison of management approaches (device, cloud, controller, automation and infrastructure as code) is covered in the network management approaches lesson.
Which inventory variables let Ansible connect to IOS devices?
Two inventory variables tell Ansible how to talk to a Cisco IOS or IOS XE device: ansible_connection: ansible.netcommon.network_cli and ansible_network_os: cisco.ios.ios. They answer two different questions, and both are needed.
ansible_connection: ansible.netcommon.network_cli— how to connect. network_cli opens a persistent SSH session to the device's command line and keeps it open for the tasks of the play. It replaces the defaultsshconnection used for Linux servers, which expects to copy and run Python code on the target.ansible_network_os: cisco.ios.ios— which platform to expect. network_cli uses it to load the right terminal and CLI handling (prompts, paging, error strings) for IOS.
A typical group variables file for IOS devices, following the Ansible IOS platform guide, looks like this:
# group_vars/ios.yml
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: netops
ansible_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
ansible_become: true
ansible_become_method: enable
ansible_become_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
Other variables in the same file answer other questions, and confusing them with the two connection settings is a common mistake:
| Variable | Question it answers |
|---|---|
ansible_connection | Which connection plugin to use (network_cli for IOS CLI) |
ansible_network_os | Which network platform the connection talks to |
ansible_host | Which address to connect to, when the inventory name is not resolvable or differs from it |
ansible_user / ansible_password | Which account to log in with |
ansible_become, ansible_become_method, ansible_become_password | How to reach privileged EXEC (enable mode) |
Variables that matter only to managed Linux servers, such as the location of a Python interpreter on the target, play no part for IOS, because the IOS modules execute on the control node.
network_cli is SSH only. The device must accept SSH on its VTY lines (transport input ssh), and any management filter on those lines — an access-class applied with line vty — must permit the control node's own source address. If the session cannot be opened, Ansible marks the host unreachable and runs none of its tasks, however well other administrators can log in from their own workstations.
How does Ansible reach enable mode and keep passwords safe?
Ansible reaches privileged EXEC on IOS through privilege escalation: ansible_become: true turns escalation on, ansible_become_method: enable makes it send the IOS enable command, and ansible_become_password supplies the enable secret. After logging in, the network_cli connection enters enable mode before running tasks, so commands such as show running-config and configuration changes work.
You need this when the login account lands at privilege level 1 (user EXEC, prompt R1>). If AAA or the local user definition already places the account at privilege 15 (prompt R1#), no escalation is needed.
Three details are tested:
- Both settings are required.
ansible_becomedefaults to false, so setting only the method does nothing. Setting become to true without a method falls back to Ansible's default method,sudo, which is for Linux hosts and means nothing to IOS. - The method for IOS is
enable, notsudo. - Scope follows the inventory. Put the become settings in
group_varswhen every device in the group needs them, or inhost_varsfor only the devices that do. A host_vars value applies to that one device and leaves the rest of the group as it was.
Become can also be set on a play or a task with the become: true and become_method: enable keywords; the inventory variables are the usual place for network devices because the need depends on the device, not on the task.
How do you protect device credentials with Ansible Vault?
Ansible Vault encrypts secrets such as device passwords and enable secrets so they can stay in the inventory or playbooks without being readable; the vault password is supplied when the playbook runs. The Ansible IOS platform guide stores ansible_password and ansible_become_password as vault-encrypted values and warns never to store passwords in plain text.
ansible-vault encrypt_stringencrypts a single value, which you paste into the variables file in place of the plain text (the!vault |blocks in the earlier example).ansible-vault encryptencrypts a whole file, such as a dedicatedvault.yml.- At run time Ansible needs the vault password to decrypt them:
--ask-vault-passprompts for it, and--vault-password-filereads it from a protected file (common in automation pipelines).
The playbooks themselves do not change — variables are decrypted in memory when used. Encryption applies to the file or value you encrypt, so a secret is protected only if the file or variable that holds it is the one encrypted.
How do inventory groups and variables work?
An inventory is the list of devices Ansible manages, organised into named groups that plays target and that carry shared variables. It can be written in INI or YAML. A play's hosts: line names a host, a group or a pattern, and Ansible runs the play on every matching device.
[wan_edge]
edge-r1
edge-r2
[distribution]
dist-a
dist-b
dist-c
[site_east:children]
wan_edge
distribution
A :children section creates a parent group made of other groups. Here site_east lists no hosts of its own, yet a play with hosts: site_east runs on all five devices of its child groups. Every host is also a member of the built-in all group.
Variables are usually kept in two directories beside the inventory:
group_vars/<group>.yml— applies to every host in that group (for example connection settings for all IOS devices).host_vars/<host>.yml— applies to one device (for example a different account or a management address).
Precedence: when the same variable is set in more than one place, the more specific value wins. Host variables beat group variables; a child group's variables beat its parent's; the all group is the weakest. Variables are merged one by one, so a host that sets only ansible_user in host_vars still inherits ansible_network_os and everything else from its groups. For example, if group_vars/routers.yml sets ansible_user: automation and ansible_network_os: cisco.ios.ios, and host_vars/edge-r2.yml sets only ansible_port: 2222, edge-r2 connects as automation, with the IOS platform, on port 2222. Shared settings live in group_vars; per-device exceptions live in host_vars.
The ansible-inventory command (for example ansible-inventory --graph or --list) shows how Ansible reads the inventory — useful for checking which hosts a group really contains. It displays the inventory; it does not run modules.
How do you run show commands with ios_command?
cisco.ios.ios_command runs EXEC-mode commands, such as show commands, on IOS devices and returns their output; it cannot enter configuration mode. It is the module for reading state: collecting show version, checking neighbours, or verifying a change.
- name: Collect state
hosts: routers
gather_facts: false
tasks:
- name: Run show commands
cisco.ios.ios_command:
commands:
- show version
- show ip interface brief
register: out
What it returns, per device:
stdout— a list with one string per command, in the order the commands were listed.stdout_lines— a list of lists: each command's output split into lines, in the same order.
What it cannot do: send configuration commands. The module documentation states that it does not support running commands in configuration mode and points to ios_config for changes. A commands list that tries to enter a configuration sub-mode fails; configuration changes go through ios_config.
Interactive prompts. Some EXEC commands ask a question before running; clear logging, for example, asks you to confirm clearing the log buffer. ios_command waits for the normal CLI prompt after each command, so a question nobody answers leaves the task waiting until it times out. The module handles this when the entry is written as a dictionary:
- cisco.ios.ios_command:
commands:
- command: clear logging
prompt: 'confirm'
answer: 'y'
command is what to send, prompt is a pattern matching the device's question, and answer is the reply.
Do not confuse it with ansible.builtin.command, which runs a program on a managed server's operating system. IOS offers no such operating system to Ansible; network devices use the platform's own modules.
How do register and stdout_lines pass output to later tasks?
The task keyword register saves a task's full result as a variable, so later tasks in the same play can read its output. Without register, the output of ios_command is shown in verbose logs but nothing else in the playbook can use it.
- name: Gather routing data
cisco.ios.ios_command:
commands:
- show ip route summary
- show ip protocols
- show ip ospf interface brief
register: rt
- name: Print the OSPF interface table
ansible.builtin.debug:
var: rt.stdout_lines[2]
Indexes start at 0 and follow the order of the commands list:
| Expression | Contains |
|---|---|
rt.stdout[0] | The show ip route summary output as one string |
rt.stdout_lines[1] | The show ip protocols output as a list of lines |
rt.stdout_lines[2] | The show ip ospf interface brief output as a list of lines |
rt.stdout_lines[2][0] | The first line of the show ip ospf interface brief output |
Other task keywords do different jobs and are easy to confuse with register: notify triggers a handler when the task reports changed, environment sets environment variables for the task, delegate_to runs the task on another host, and when makes a task conditional. Only register captures output. The debug module prints a variable (var:) or a message (msg:) and is the usual way to show registered output.
How do wait_for, match, retries and interval verify a change?
The wait_for option turns ios_command into a check: it re-runs the commands until the listed conditions are true, and fails the task if they are still false when the retries run out. It is how a playbook verifies that a change worked before moving on — here, that this router has become the HSRP active router.
- cisco.ios.ios_command:
commands:
- show standby brief
wait_for:
- result[0] contains Active
retries: 5
interval: 12
result[n]refers to the output of the nth command in the task's commands list, from 0. Conditions use operators such ascontains,eqandgt.retriesis how many times the commands are re-run after the first try before the conditions are considered failed (the module default is 9);intervalis the number of seconds to wait between tries (default 1).- Outcome: as soon as the conditions are met the task succeeds. If they are still not met after the last try, the task fails and returns
failed_conditions. In the example, a router that never becomes active is checked up to six times (the first try plus five retries), about a minute in total, and then the task fails — it never quietly succeeds.
match decides how several conditions combine:
| match | Task passes when | Use for |
|---|---|---|
all (default) | Every condition is true | "Every tracked link must be up" |
any | At least one condition is true | "Any one path is enough" |
match combines the conditions of one task. Each task is its own pass/fail check, so conditions spread across separate tasks can only ever behave like all. And because each condition names its command by index, a condition must point at the result[n] of the command whose output it is meant to test.
How do you change configuration with ios_config?
cisco.ios.ios_config is the module that changes IOS configuration: lines lists the commands to apply and parents names the configuration section they belong in. The module reads the running configuration, compares the requested lines with what is already under that parent, and sends only the lines that are missing.
- name: Relay DHCP for the voice VLAN
cisco.ios.ios_config:
parents: interface Vlan30
lines:
- ip helper-address 172.16.5.10
Think of parents as the command that enters a sub-mode, and lines as the commands typed inside it:
| Option | Meaning | Example |
|---|---|---|
parents | The section (sub-mode) the lines live under; a list for nested sections | interface Vlan30, router ospf 1, ip access-list extended GUEST-IN |
lines | The commands inside that section (or global commands when parents is unset) | ip helper-address 172.16.5.10 |
before | Commands sent first, only when a change is needed | no ip access-list extended GUEST-IN |
after | Commands sent after the lines, only when a change was made | A command that must follow the change |
match | How lines are compared: line (default), strict, exact, none | See below |
replace | What is sent on a difference: line (default, only missing lines) or block (the whole set of lines) | See below |
Getting parents and lines the right way round matters. Without parents, an interface-level line such as ip helper-address 172.16.5.10 is compared with the global configuration, where it never matches, and is sent in global configuration mode, where IOS rejects it as invalid input — so the task fails. Swapping parents and lines produces an invalid hierarchy in the same way.
Comparison modes. match: line checks each line on its own and pushes only those missing. match: strict also requires the lines to be in the same order. match: exact requires the section to be exactly the listed lines — extra lines on the device count as a difference. match: none skips the comparison and always pushes.
Removing unwanted lines. ios_config adds lines; it never deletes lines you did not list, whichever replace value is used. The usual pattern for a section that must contain exactly the listed lines, in order, is to pair a before command that removes the section with match: exact. For an ACL named GUEST-IN, before: no ip access-list extended GUEST-IN plus match: exact means: when the ACL on the device differs from the list in any way, delete it first, then rebuild it from the lines; when it already matches, send nothing.
Many configuration areas also have resource modules in the cisco.ios collection (for example cisco.ios.ios_interfaces, cisco.ios.ios_vlans, cisco.ios.ios_static_routes) that take structured data and a state such as merged, replaced, overridden or deleted. ios_config, which works with raw CLI lines, is the general-purpose module.
What do idempotency and ok versus changed mean?
Idempotency means running the same playbook again produces the same end state and makes no further changes: ios_config compares with the running configuration and sends nothing when the desired lines are already there. This is the core property of configuration management — a playbook describes the state you want, not a list of keystrokes to repeat.
Ansible reports each task's outcome per host:
- changed — the module altered the device (for ios_config, it sent configuration lines).
- ok — the task succeeded and nothing needed changing. A show command through ios_command reports ok.
So a task that adds a VLAN name reports changed the first time; run it again unaltered and it reports ok, because the name is now in the running configuration. The decision is made on every run by reading the device's current configuration and comparing it with the task.
Abbreviated commands break idempotency. The ios_config comparison is text matching against the running configuration. IOS accepts sw mode acc but stores and displays switchport mode access, so the abbreviated line never appears in the running config and the module pushes it on every run, reporting changed each time. The cisco.ios documentation notes that abbreviated commands are not idempotent. The fix is to write every line exactly as it appears in show running-config, full keywords included.
An accurate changed count matters: change teams use it to see which devices a run really touched, and handlers (tasks triggered by notify) run only when a task reports changed.
How do save_when and backup protect configuration?
ios_config changes only the running configuration unless save_when tells it to copy running-config to startup-config; backup: true separately stores a copy of the device's running configuration on the Ansible control node before the change. They solve different problems: save_when keeps changes across a reload, backup keeps a restore point off the device.
save_when values:
| Value | Copies running to startup when |
|---|---|
never (default) | Never — changes are lost at the next reload unless saved some other way |
always | On every run, whether or not anything changed |
modified | The running config differs from the startup config, for any reason (including unsaved changes someone made by hand) |
changed | Only when this task made a change |
Because the default is never, a device that reloads after an Ansible run boots from its old startup configuration unless something saved it. changed is the setting that ties saving to the task itself: NVRAM is written only on runs where the task altered the device.
backup: true saves the device's current running configuration to a backup folder beside the playbook (or a file name and directory set with backup_options) before changes are made, and returns the file's location as backup_path. That copy lives on the control node, so it survives even if the device fails.
A third option, diff_against, is unrelated to both: it chooses what the configuration is compared with when Ansible produces --diff output (running, startup or intended), has no default, and neither saves nor backs anything up.
How do you run Ansible commands and read the results?
Ansible runs work in two ways: an ad hoc command (ansible) runs one module once against a group of hosts, and ansible-playbook runs a YAML playbook of repeatable tasks.
ansible distribution -m cisco.ios.ios_command -a "commands='show ntp status'"
ansible-playbook -i inventory vlans.yml
In the ad hoc form, the first argument is the host pattern (a host or group from the inventory), -m names the module and -a passes its arguments. It suits one-off checks, such as confirming NTP sync across a group. Anything you will run again, review or keep in version control belongs in a playbook.
| Command | Job |
|---|---|
ansible | Run a single module ad hoc against a host pattern |
ansible-playbook | Run playbooks |
ansible-inventory | Display or check the inventory |
ansible-galaxy | Install collections and roles, for example ansible-galaxy collection install cisco.ios |
ansible-vault | Encrypt and decrypt secrets |
Flags for safe changes:
| Flag | Effect |
|---|---|
--check | Check mode, a dry run: modules that support it (ios_config does) report what they would change without changing the device |
--diff | Show the differences each task makes or would make, for modules that produce diff output |
--limit <pattern> | Restrict the run to inventory hosts matching the pattern, without editing the play |
-i <source> | Choose which inventory to load |
--list-hosts | List the hosts the play would target, then stop |
--step | Ask before each task; confirmed tasks really run |
-v | Verbose output, including each task's returned values |
What a dry run of ios_config shows. In check mode, ios_config still reads the running configuration and works out which lines are missing; it reports the task as changed and returns the lines it would send in its commands and updates values, which -v displays — but sends nothing. A unified --diff from ios_config is produced only when the task sets diff_against, and with diff_against: running in check mode the module just warns that it cannot diff against the running configuration. So a safe preview for one device is a run such as ansible-playbook vlans.yml --check -v --limit dist-b, reading the would-be commands from the task output.
How do you read a PLAY RECAP?
The PLAY RECAP at the end of a run gives one line per host with counters for each task outcome; reading them tells you whether a problem is a connection problem or a task problem.
PLAY RECAP
edge-r1 : ok=4 changed=2 unreachable=0 failed=0 skipped=1
edge-r2 : ok=2 changed=0 unreachable=0 failed=1 skipped=0
dist-a : ok=0 changed=0 unreachable=1 failed=0 skipped=0
| Counter | Meaning | Where to look next |
|---|---|---|
ok | Tasks that succeeded (including those that changed something) | Nothing |
changed | Tasks that altered the device | Confirm the change was intended |
unreachable | Ansible could not open a session to the host, so no tasks ran on it | Reachability, SSH on the VTY lines, VTY access-class, credentials, host key |
failed | Ansible connected but a task returned an error; later tasks for that host are skipped | The failed task's error message |
The recap can also show skipped (a task's when condition was false), rescued and ignored. Reading the example: edge-r1 completed, two of its tasks altered the device and one was skipped by a condition; edge-r2 connected and two tasks succeeded before one returned an error; dist-a never got a session. Anything a task changed before a failure stays on the device — Ansible has no automatic undo — so restoring a known state is the job of your backups and your playbook design.
Tip. Questions on this topic are mostly evidence-based: a YAML task, an inventory or group_vars file, a VTY configuration, a command line or a PLAY RECAP is shown, and you are asked what it does, why it fails, or which change meets a requirement. Expect to tell ios_command from ios_config, place parents and lines correctly, choose between match and save_when values, predict wait_for and register behaviour, pick the right become and connection variables and where to scope them, and separate unreachable from failed. Distractors are real Ansible options used for the wrong job, so learn what each option actually does.
- Ansible is agentless and push-based: the control node opens SSH to each device, and IOS modules run on the control node, so a switch needs only SSH and an account — and must permit the control node's address on its VTY lines.
- IOS inventories need ansible_connection: ansible.netcommon.network_cli (how to connect) and ansible_network_os: cisco.ios.ios (which platform).
- Enable mode is privilege escalation: ansible_become: true plus ansible_become_method: enable (not sudo) and an enable password from Vault; scope it with group_vars or host_vars.
- host_vars beat group_vars for the same variable; anything the host does not set is inherited. A :children group includes all hosts of its child groups.
- ios_command runs EXEC commands only; register saves the result, and stdout / stdout_lines are indexed by command order from 0. Answer confirmation prompts with command/prompt/answer.
- wait_for re-runs commands up to retries times after the first try, interval seconds apart, and fails the task if the conditions never pass; match: all is the default, match: any passes on one true condition.
- ios_config puts lines under parents and pushes only what is missing; before + match: exact rebuilds a section exactly. Write full keywords — abbreviated lines always look missing.
- save_when defaults to never; changed saves only when the task changed something. backup: true keeps a copy of the running config on the control node.
- Dry-run with --check (ios_config returns the would-be commands; its --diff needs diff_against) and scope with --limit; in the PLAY RECAP, unreachable means no session and failed means a task errored after connecting. Ansible has no automatic undo.
Frequently asked questions
Does Ansible need an agent on Cisco routers and switches?
No. Ansible is agentless. For Cisco IOS and IOS XE it uses the network_cli connection: the control node opens an SSH session to the device's CLI, sends commands and parses the output, and the module logic runs on the control node. The device needs only SSH enabled, an account Ansible can log in with, and management access that permits the control node's address.
What is the difference between ios_command and ios_config?
cisco.ios.ios_command runs EXEC-mode commands, such as show commands, and returns their output; it cannot enter configuration mode. cisco.ios.ios_config changes configuration: lines lists the commands, parents names the section they belong in, and the module compares them with the running configuration and pushes only what is missing.
How does Ansible get into enable mode on a Cisco device?
Through privilege escalation. Set ansible_become: true, ansible_become_method: enable and ansible_become_password (stored with Ansible Vault). The connection then sends the enable command and password after logging in. Both become and the enable method are required; the default method, sudo, is for Linux hosts.
Why does an Ansible ios_config task report changed on every run?
Usually because a line is written differently from how the device stores it, most often an abbreviated command such as 'desc' instead of 'description'. ios_config compares text with the running configuration, so an abbreviated line never matches and is pushed every time. Writing each line exactly as it appears in show running-config restores idempotency.
Does Ansible save the configuration to startup-config?
Not by default. ios_config's save_when option defaults to never, so changes stay only in the running configuration and are lost at reload. save_when: changed copies running to startup only when the task changed something; modified saves whenever running and startup differ; always saves every run.
How can I see what an Ansible playbook would change without changing anything?
Run ansible-playbook with --check, which is a dry run: ios_config reads the running configuration and reports the task as changed with the lines it would send in its commands and updates return values (shown with -v), but pushes nothing. ios_config produces --diff output only when the task sets diff_against. Add --limit with a host or group name to restrict the preview to particular devices.
What does unreachable mean in an Ansible PLAY RECAP?
unreachable means Ansible could not open a session to that host, so none of its tasks ran. Check IP reachability, that SSH is enabled on the VTY lines, that any VTY access-class permits the control node's address, and the credentials. failed is different: Ansible connected, then a task returned an error.
Source
This lesson covers the "AI, and Network Operations and Management" domain of the official CCNA exam guide. Vendors revise their guides — check the source for the current version.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.