Skip to main content
RunBook Academy

AnsibleXLII · Ansible Beyond Linux ServersBeyond Linux servers

Why network modules run on the controller

Advanced⏱ ~26 minansible-coreansible-docansible-galaxy

What you'll learn

  • Explain why network modules execute on the control node rather than the managed node
  • Choose between the network_cli, netconf and httpapi connection types and state what each requires
  • Set the inventory variables a network group needs, including ansible_network_os
  • Predict where a configuration backup file lands and why that is not the device
  • Recognise the controller-side resource cost that a fleet of network devices imposes

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Everything you know about how a task executes is wrong on a switch.

The model this course has taught so far is specific and mechanical: Ansible assembles the module into a self-contained Python payload, copies it to the managed node, runs it there with the node’s own Python interpreter, and reads back a JSON document. The module does its thinking on the target. The controller is a dispatcher.

A network device usually has no shell you can drop a file into, no general-purpose Python interpreter, and no filesystem you are invited to write to. It has a command-line interface, or an XML-over-SSH interface, or an HTTP API. None of those will run your module.

So Ansible inverts the model. For network platforms, the module runs on the control node. What crosses the wire is not a Python payload; it is the commands or API calls the module decided to send, and the raw text or structured data the device sent back.

That single inversion explains almost every difference in this part. Where the backup file lands. Why escalation is enable and not sudo. Why gather_facts in its usual form does nothing useful. Why a hundred switches cost the controller far more than a hundred Linux servers. Get the inversion, and the rest is consequence.

The documentation states it plainly

From the upstream page on how network automation differs:

Network modules do not run on the managed nodes.

and

[they] are executed on the Ansible control node, where ansible or ansible-playbook runs.

There is no subtlety to recover here. The device is a peer you talk to, not a machine you run code on.

The connection plugin is the whole interface

Because nothing executes on the device, the connection plugin stops being a transport detail and becomes the entire integration surface. It is what knows how to log in, how to recognise a prompt, how to page through output, how to enter configuration mode, and how to tell a successful command from an error message that the device politely printed instead of exiting non-zero.

Three persistent connection types cover the field:

ConnectionWhat it speaksTypical use
ansible.netcommon.network_cliThe device CLI, over SSHThe majority of switches, routers and firewalls
ansible.netcommon.netconfNETCONF: XML over SSHPlatforms with a modelled configuration store
ansible.netcommon.httpapiThe device HTTP or HTTPS APIControllers, appliances and API-first platforms

All three are persistent connections, and all three require you to say which platform you are talking to. That is what ansible_network_os is for.

Read-only / Safea bare ansible-core controller
$ ansible-doc -t connection -l
ansible.builtin.local execute on controller
ansible.builtin.psrp  Run tasks over Microsoft PowerShell Remoting Protocol
ansible.builtin.ssh   connect via SSH client binary
ansible.builtin.winrm Run tasks over Microsoft's WinRM

network_cli, netconf and httpapi live in the ansible.netcommon collection, and the modules for a given vendor live in that vendor’s own collection. Both are dependencies you install and pin deliberately, the same way this course has treated every other collection:

Read-only / Safewhat is actually installed
ansible-galaxy collection list

That matters operationally more than it sounds. A network playbook that works on one engineer’s laptop and fails on the controller with a “couldn’t resolve module/action” error is nearly always a missing collection, not a broken play.

ansible_network_os is mandatory, and it is not documentation

Set it wrong and the run does not politely correct you. ansible_network_os selects the platform-specific machinery the connection plugin loads: the terminal plugin that knows what this device’s prompts and error strings look like, the cliconf plugin that knows how to enter and leave configuration mode, and the code that knows whether this platform commits or saves or does neither.

Point that at the wrong platform and the connection plugin will confidently misread a real prompt as a failure, or a real error as success. The resulting symptom — a task that hangs until timeout, or one that reports ok having done nothing — has no obvious connection to the variable that caused it.

A network group in inventory therefore carries more connection variables than a Linux group ever does:

# inventory/network.yml
all:
  children:
    core_switches:
      hosts:
        core-sw-01:
          ansible_host: 192.0.2.21
        core-sw-02:
          ansible_host: 192.0.2.22
      vars:
        ansible_connection: ansible.netcommon.network_cli
        ansible_network_os: vendor_namespace.platform.os_name
        ansible_user: automation
        ansible_password: REPLACE_ME
        ansible_become: true
        ansible_become_method: enable
        ansible_become_password: REPLACE_ME

Two notes on that block before anything else.

The credentials belong in a vault, not in the file. They are written inline here so you can see the shape; every one of those REPLACE_ME values is a vault-encrypted variable in real life, and the vault lessons in Part XXI are the reference.

ansible_network_os takes a fully-qualified platform name from the collection that supports your device. Look it up on the platform options page rather than guessing from the product name — the string is a collection identifier, not a brand.

Escalation is enable, not sudo

A Linux managed node escalates by running a program: sudo, su, doas. The privilege boundary is a process boundary.

A network device escalates by changing the mode of the session you are already in. There is no second process. You are the same login; the device has decided to accept a different class of command from you.

Ansible models this with the same keywords and a different method:

ansible_become: true
ansible_become_method: enable
ansible_become_password: REPLACE_ME

The practical differences are worth stating because they trip up people who know become well:

  • It is a session property, not a task property. Once the session is privileged it stays privileged; there is no per-task descent back to unprivileged mode the way become: false gives you on Linux.
  • A wrong enable secret usually fails as a timeout, not as an authentication error, because the device prints a prompt the connection plugin was not expecting and then waits.
  • Not every platform has an enable mode at all. Some grant privilege at login based on the account. On those, setting ansible_become is at best a no-op and at worst a hang.

Facts do not come from setup

ansible.builtin.setup — the module gather_facts: true runs — is a Python program that inspects a Unix-like system from the inside. It cannot run on a device that will not execute Python, and there is nothing for it to inspect even if it could.

Network platforms supply their own facts module in the vendor collection, and it returns a different shape: interfaces, VLANs, neighbours, the running configuration, the software image. Not ansible_distribution and ansible_mounts.

The operational consequence is a small one that costs an afternoon the first time:

- name: Switch configuration audit
  hosts: core_switches
  gather_facts: false        # this is not optional politeness
  tasks:
    - name: Collect platform facts the platform way
      vendor_namespace.platform.os_facts:
        gather_subset:
          - config
      register: device_facts

Leaving gather_facts: true on a network play does not merely waste time. Depending on the platform and the connection plugin it produces a confusing failure at the very first step of the play, before any of your logic runs, and the error text talks about interpreters and modules rather than about the switch.

Backups land on the controller

The configuration modules in ansible.netcommon and the vendor collections generally accept a backup option. It is the single most useful safety feature in network automation and it is routinely misunderstood.

backup: true does not ask the device to save a copy of its configuration somewhere on the device. It retrieves the current configuration and writes it to a directory on the control node, next to your playbook, before applying the change.

That is the correct design, and it follows directly from the execution inversion: the module is running on the controller, so the controller is where it can write a file.

Read-only / Safewhere the backups actually are
ls -l ./backup/
find . -name '*.cfg' -newermt '-1 hour'

Three consequences to design around:

  1. The controller now holds device configurations. Those files contain ACLs, community strings, tunnel endpoints and sometimes hashed local credentials. They are sensitive, and Part XLVII treats the controller as the security boundary it has become.
  2. Nothing rotates them for you. A daily backup play against 200 devices produces 73,000 files a year in a directory nobody chose a retention policy for.
  3. They are your rollback. If the change goes wrong, the file written moments before is the only copy of the pre-change state you are guaranteed to have — the device may not keep one.

The controller becomes the bottleneck

On a Linux fleet, work is distributed by construction. Each managed node runs its own module, burning its own CPU. The controller marshals.

On a network fleet, every module for every device runs on one machine. Fifty devices under the default fork limit of 5 means five concurrent persistent connections, each with a helper process, each parsing device output on the controller’s CPU. Raising forks to 50 to speed it up raises the controller’s memory and CPU load fifty-fold, and there is no second machine to absorb it.

Read-only / Safethe number that now matters more
ansible-config dump --only-changed | grep -i forks

The sizing conversation for a network estate is therefore a conversation about the controller, not about the devices. Part XXXIV’s runtime-budget lesson applies with the multiplier moved: on Linux you are usually buying parallelism cheaply, and here you are buying it out of one machine’s RAM.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A colleague argues that network automation would be simpler if Ansible just copied the module to the switch like it does for Linux. What is the accurate response?

  2. Q2. A network play fails with an error about resolving the module or action, on the shared controller only. It works on the author machine. What is the first thing to check?

  3. Q3. Which of these are direct consequences of network modules executing on the control node? Select all that apply.

  4. Q4. Setting ansible_become: true with ansible_become_method: enable starts a privileged child process on the network device, in the same way sudo does on a Linux host.

Passing score: 75%. Answers are checked in this browser.