Connection Plugins
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.
ansible_connectionsshControlPersist multiplexing for free — the fast path for any Linux fleet.locallocalhost and delegate_to: localhost work.winrmnetwork_cliYou 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.
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 — 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.
- Leaving
ansible_connectiondefaulted tosshfor alocalhost-only play that calls a cloud API, so Ansible SSHes into the loopback unnecessarily instead of usinglocal— 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 losingControlPersist, so every task pays a full SSH handshake and a 40-task play against 6 hosts crawls. - Pointing the default
sshconnection at a network switch that has no Python, getting cryptic module-copy failures instead of usingnetwork_clibuilt for CLI-driven devices. - Assuming a Windows box is reachable over
sshlike the Linux fleet — it needswinrm, a different module set, and different auth; thesshconnection and theapt/systemdmodules don't apply. - Disabling
ControlPersistinansible_ssh_common_argsto "fix" a connection quirk and silently halving deploy speed, because every task now reopens the master connection.
- Keep
sshas the connection for the Linux fleet and let it inherit~/.ssh/config(jump host, key, user), so Ansible's transport matches your interactive SSH andControlPersiststays on. - Set
ansible_connection: localonlocalhostfor controller-side work (templating, API calls,delegate_to: localhost) instead of round-tripping through SSH. - Reach for
paramikoonly 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-orientedsshconnection onto gear that can't run a copied module. - Override
ansible_connectiononly where the default is actually wrong, and leave the bulk of the fleet on thesshdefault it should be on.
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?
sshshells out to OpenSSH and reuses oneControlPersistmaster connection across tasks, whileparamikohas no multiplexing and re-handshakes every taskparamikofully encrypts its traffic whilesshsends in the clear, so thesshplugin skips all of the per-task crypto overheadsshcopies all of the task modules across to the managed host fully in parallel, whereasparamikois forced to copy them strictly one at a time in sequenceparamikore-gathers the full host facts on every single task whilesshcaches 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 daemonsshpointed at the127.0.0.1loopback address, which is documented as the only officially supported way to target and run tasks on the controller node itselfparamiko, because controller-side templating and API work requires the pure-Python SSH client to functionwinrm, 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 runparamiko, because its pure-Python SSH stack works on constrained devices that OpenSSH cannot reach at alllocal, running the switch commands on the controller and then forwarding the results down to the device- The default
sshtransport withControlPersistexplicitly 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 thanaptandsystemdsshwith a Windows-specific key, since the transport and module set are identical to the Linux fleet'slocal, because the Windows-specific modules are always executed directly on the Ansible controller nodeparamiko, which is the only connection client that can natively speak the Windows remote management protocol and ship the matching module set
You got correct