Chapter 11: Configuration, Plugins & Performance
Topic 64

Connection Plugins

TransportPlugins

A connection plugin is how Ansible actually reaches a managed node before any module runs — the transport underneath every task. The default is ssh, which shells out to your system's OpenSSH binary and gets ControlPersist multiplexing for free. The alternatives exist for the cases OpenSSH can't cover: a Python-native SSH client, no network hop at all, Windows, and network gear that has no concept of running a Python module.

Connection plugins — pick one per host with ansible_connection
ssh
The default. Shells out to OpenSSH and gets ControlPersist multiplexing for free — the fast path for any Linux fleet.
local
Runs on the control node with no SSH hop — the default for localhost and delegate_to: localhost work.
winrm
Reaches Windows nodes over WinRM, with different auth and a Windows-specific module set.
network_cli
Drives routers and switches over an interactive CLI — gear that runs no Python and can't host a copied module.

You pick one per host with ansible_connection, and the right choice is ssh far more often than not. The point of knowing the others is recognizing the narrow situations that force them — a controller-side play, a Windows box, a switch with no Python — so you reach for the right transport instead of bending ssh onto a host it can't serve.

ssh — the Default Transport

For the Larkspur Linux fleet, Ansible invokes the local OpenSSH client. That means it inherits everything your interactive SSH already knows: ~/.ssh/config, agent forwarding, jump hosts, and crucially ControlPersist connection multiplexing, which keeps one SSH master open and reuses it across tasks. This is why ssh is both the default and the fast path — a 40-task play against a host opens the connection once, not forty times, because OpenSSH holds the master socket open between tasks.

paramiko — the Python SSH Fallback

paramiko is a pure-Python SSH implementation Ansible bundles for environments without a usable OpenSSH binary, or where its older SSH behavior is specifically needed. It speaks SSH like the default does, but it does not do OpenSSH-style ControlPersist, so every task pays a fresh handshake and the whole run is slower. You reach for it only when ssh genuinely can't run — a stripped-down control node with no OpenSSH client, for instance — and you accept the loss of multiplexing as the cost.

local — No Network at All

ansible_connection: local runs tasks directly on the control node with no SSH hop. This is how you target localhost for work that belongs on the controller: rendering a template, calling a cloud API, or any task behind delegate_to: localhost in the middle of a play. It is a real connection type, not a no-op — Ansible still runs the module, it just runs it in a local process instead of copying it over a wire. Skipping the SSH round-trip is both faster and free of any dependency on an SSH daemon being reachable on the controller.

winrm — Windows Targets

Windows nodes are reached over WinRM, not SSH, with ansible_connection: winrm and a Windows-specific module set. This is a brief mention for the Larkspur book, which is Linux-first — the fleet is Ubuntu, and Windows is a different transport, different authentication, and entirely different modules. The takeaway is that "agentless over SSH" is the Linux story; Windows is reached over its own management protocol, and the apt and systemd modules you have been writing don't apply there.

network_cli and the Network Family

Network devices — routers, switches — run no Python and can't host a copied module the way a server does. So ansible.netcommon.network_cli drives them over an interactive CLI session instead, sending commands and reading responses as if a person were typing at the device's prompt. This is the reason "agentless over SSH" stretches to gear that isn't a server at all: the connection plugin adapts to a device that can run a CLI but not a Python module, and a whole family of network connection plugins covers the variations.

Selecting the Connection

You set ansible_connection in inventory, in group_vars, or on a play, and it picks the plugin per host. The defaults are sensible: a normal host defaults to ssh, and localhost defaults to local, so controller-side work usually needs no override at all. You override only for the specific cases above — paramiko when OpenSSH is absent, winrm for Windows, network_cli for devices — leaving the bulk of the fleet on the ssh default it should be on.

Setting the connection per group in inventory
all:
  children:
    web:            # the Larkspur Linux fleet — ssh is the default, no override needed
      hosts:
        web0[1:6].prod.larkspur.io:
    controller:
      hosts:
        localhost:
          ansible_connection: local
    edge_switches:
      hosts:
        sw0[1:2].prod.larkspur.io:
      vars:
        ansible_connection: ansible.netcommon.network_cli

The web group carries no ansible_connection because ssh is already what it should be. localhost is pinned to local so controller-side tasks skip SSH, and the switches are pinned to network_cli because they can't run a copied module. The override appears exactly where the default is wrong, and nowhere else.

ssh vs paramiko

ssh — shells out to the system OpenSSH binary and gets ControlPersist multiplexing, jump hosts, and ~/.ssh/config for free. One master connection is reused across every task, which makes it the faster default for any Linux fleet.

paramiko — a bundled pure-Python SSH client with no ControlPersist, so every task pays a full handshake and the run is slower. Use it only where a usable OpenSSH binary genuinely isn't available. Stay on ssh unless something forces paramiko.

Common Mistakes
  • Leaving ansible_connection defaulted to ssh for a localhost-only play that calls a cloud API, so Ansible SSHes into the loopback unnecessarily instead of using local — slower and dependent on an SSH daemon being reachable on the controller.
  • Forcing paramiko (or running on a host where OpenSSH isn't found) and losing ControlPersist, so every task pays a full SSH handshake and a 40-task play against 6 hosts crawls.
  • Pointing the default ssh connection at a network switch that has no Python, getting cryptic module-copy failures instead of using network_cli built for CLI-driven devices.
  • Assuming a Windows box is reachable over ssh like the Linux fleet — it needs winrm, a different module set, and different auth; the ssh connection and the apt/systemd modules don't apply.
  • Disabling ControlPersist in ansible_ssh_common_args to "fix" a connection quirk and silently halving deploy speed, because every task now reopens the master connection.
Best Practices
  • Keep ssh as the connection for the Linux fleet and let it inherit ~/.ssh/config (jump host, key, user), so Ansible's transport matches your interactive SSH and ControlPersist stays on.
  • Set ansible_connection: local on localhost for controller-side work (templating, API calls, delegate_to: localhost) instead of round-tripping through SSH.
  • Reach for paramiko only when a usable OpenSSH binary is genuinely absent, and accept the loss of multiplexing as the cost.
  • Use network_cli (or the relevant network connection plugin) for routers and switches rather than bending the server-oriented ssh connection onto gear that can't run a copied module.
  • Override ansible_connection only where the default is actually wrong, and leave the bulk of the fleet on the ssh default it should be on.
Comparable tools Plain ssh in a loop the unstructured version of the ssh plugin SaltStack transport is its own ZeroMQ bus rather than SSH Terraform SSH/WinRM provisioner connections, the same transport split at provision time

Knowledge Check

Why is the default ssh connection faster than paramiko for a multi-task play?

  • ssh shells out to OpenSSH and reuses one ControlPersist master connection across tasks, while paramiko has no multiplexing and re-handshakes every task
  • paramiko fully encrypts its traffic while ssh sends in the clear, so the ssh plugin skips all of the per-task crypto overhead
  • ssh copies all of the task modules across to the managed host fully in parallel, whereas paramiko is forced to copy them strictly one at a time in sequence
  • paramiko re-gathers the full host facts on every single task while ssh caches them once and reuses them

A play runs only on localhost to render templates and call a cloud API. Which connection fits, and what does it avoid?

  • local — it runs tasks directly on the control node and skips the SSH round-trip and any dependency on a reachable SSH daemon
  • ssh pointed at the 127.0.0.1 loopback address, which is documented as the only officially supported way to target and run tasks on the controller node itself
  • paramiko, because controller-side templating and API work requires the pure-Python SSH client to function
  • winrm, since local cloud API calls are routed through the Windows remote management protocol

A network switch with no Python keeps failing with module-copy errors under the default connection. What is the right transport?

  • network_cli — it drives the device over an interactive CLI session instead of copying a Python module the device can't run
  • paramiko, because its pure-Python SSH stack works on constrained devices that OpenSSH cannot reach at all
  • local, running the switch commands on the controller and then forwarding the results down to the device
  • The default ssh transport with ControlPersist explicitly disabled, which is what finally lets the Python module copy step succeed on the switch

Which connection plugin reaches a Windows node, and what else changes with it?

  • winrm — Windows uses its own management protocol, with different authentication and a Windows-specific module set rather than apt and systemd
  • ssh with a Windows-specific key, since the transport and module set are identical to the Linux fleet's
  • local, because the Windows-specific modules are always executed directly on the Ansible controller node
  • paramiko, which is the only connection client that can natively speak the Windows remote management protocol and ship the matching module set

You got correct