Skip to main content
RunBook Academy

AnsibleII · Ansible ArchitectureThe execution model

Connection plugins: SSH is a default, not a law

Intermediate⏱ ~18 minansiblessh

What you'll learn

  • Name the connection plugins that ship in ansible-core and distinguish them from collection-provided ones
  • Explain what a connection plugin is responsible for and what it is not
  • State why a container connection and an SSH connection to a VM prove different things
  • Choose a connection plugin deliberately rather than inheriting the default

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.

Stage 5 of a run opens a connection. Nothing about the rest of the architecture requires that connection to be SSH — the payload has to reach the target and the JSON has to come back, and any mechanism that can do both will serve.

Ansible makes this explicit: the transport is a plugin. SSH is the default because it is what nearly everyone has, not because the model depends on it.

This matters more than it sounds, and for a reason that has nothing to do with exotic transports. Your tests and your production runs may use different connection plugins, and if they do, they are exercising different code paths. A role that passes under one is not thereby proven under the other.

What ships in ansible-core

Fewer than most people expect. Verified on ansible-core 2.21.3:

Read-only / Safethe complete set in core
$ 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

Four. Two of them are for Windows.

Everything else — Docker, Podman, LXD, Kubernetes, network devices, serial consoles — arrives in a collection. That is not a second-class status; community.docker and ansible.netcommon are the normal way to get those transports. It does mean they are a dependency you declare and pin, which the collections part of this course treats as a discipline rather than an afterthought.

What a connection plugin is responsible for

Narrower than people assume, which is what makes the model work.

A connection plugin must be able to:

  • Establish and tear down a connection to the target.
  • Transfer a file to the target (put_file).
  • Retrieve a file from the target (fetch_file).
  • Execute a command on the target and return stdout, stderr and an exit status (exec_command).

That is the interface. Everything above it — building the module payload, evaluating changed_when, rendering templates, deciding when — is transport-independent controller-side logic, and it does not know or care how the bytes arrived.

The plugins you will actually meet

ssh

The default. Shells out to the system ssh binary, which means your ~/.ssh/config, your agent, your jump-host configuration and your known_hosts all apply exactly as they would interactively.

That is a significant property. Ansible is not reimplementing SSH; it is driving the client you already have, so anything that works from your shell works here, including ProxyJump, certificate authentication and Match blocks.

Its default arguments are worth knowing, because they explain a performance characteristic that otherwise looks like magic:

Read-only / Safethe default SSH arguments
$ ansible-doc -t connection ssh | grep -A 12 '^   ssh_args'
   ssh_args  Arguments to pass to all SSH CLI tools.
      set_via:
        env:
        - name: ANSIBLE_SSH_ARGS
        ini:
        - key: ssh_args
          section: ssh_connection
        vars:
        - name: ansible_ssh_args
      default: -C -o ControlMaster=auto -o ControlPersist=60s
      type: string

ControlMaster=auto with ControlPersist=60s means the first connection to a host opens a control socket and subsequent connections reuse it for sixty seconds. Given the previous lesson’s count of round trips per task, this is doing a great deal of work — without it, every task would pay a full SSH handshake.

The SSH part of this course covers tuning it. What matters here is that it exists and that it is a connection-plugin setting rather than something Ansible does internally.

local

Runs tasks on the controller itself. No network, no SSH — it forks a process and runs the payload with the controller’s own Python.

Note that it is still a connection: the same payload is built, written to a temporary directory and executed. The trace looks identical to a remote run minus the network.

This is what delegate_to: localhost and connection: local use, and it is how controller-side work — calling an API, updating a load balancer, writing a report — is expressed as a task.

winrm and psrp

Windows targets. Different transport, different execution model on the far side — PowerShell rather than Python — and a different set of modules. Out of scope for this course beyond knowing they exist and that the architecture accommodates them.

Container connections, from collections

community.docker.docker and containers.podman.podman execute inside a container by driving the container runtime, with no SSH and no sshd in the container.

This is how Molecule tests roles quickly, and it is the source of the most consequential point in this lesson.

Network device connections, from collections

ansible.netcommon.network_cli connects to a switch or router and drives its CLI. There is no Python on the target and no payload transfer — the modules run on the controller and send configuration commands over the connection.

That is a genuinely different execution model, not just a different transport, and the network part of this course treats it separately. The architectural point is that the plugin boundary is wide enough to accommodate it.

The consequence: your test transport decides what your test proves

This is why the lesson exists.

A Molecule scenario using the Docker connection and a production run using SSH differ in more than the network path:

Container scenarioProduction VM over SSH
TransportContainer runtime execSSH to sshd
PID 1Usually the entrypoint, often not an initsystemd
Service managementFrequently unavailable or stubbedReal systemctl
RebootNot possiblePossible, and sometimes required
Kernel parametersShared with the host; many are not settableOwn namespace-wide settings
StorageContainer filesystemReal block devices, mounts, quotas
Privilege escalationOften already root, so become is untestedReal sudo path
SSH itselfNot exercised at allThe commonest source of production failures

Read the last row carefully. A container scenario does not test SSH, authentication, host keys, or ProxyJump — and a large share of the problems that stop a production run are exactly those. A role with perfect container test coverage can still fail on every host in the fleet for a reason the tests were structurally unable to see.

Choosing and setting the connection

The connection is a variable like any other, which means it can be set per host, per group, per play or per task.

# inventory/hosts.yml
all:
  children:
    switches:
      hosts:
        sw01:
          ansible_host: 192.0.2.51
      vars:
        ansible_connection: ansible.netcommon.network_cli
        # Platform-specific, and fully qualified: the value names the
        # collection that owns the platform, not the connection plugin.
        ansible_network_os: vyos.vyos.vyos

    controller_side:
      hosts:
        localhost:
          ansible_connection: local

Set it in inventory rather than in the playbook wherever you can. A playbook that hard-codes a connection cannot be reused against a different environment, and — more importantly for this course — the inventory is where a reader looks to answer “what will this run actually do to which hosts”. Transport is part of that answer.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Which connection plugins ship in ansible-core 2.21?

  2. Q2. Check mode support is a property of the module, so switching a task from the SSH connection to a container connection cannot change whether that task supports check mode.

  3. Q3. A role passes its Molecule container scenario completely. Which production behaviours does that result leave unproven? Select all that apply.

  4. Q4. A role fails in production on a branch that its container tests never executed, because a conditional was added during development so the tests would pass. What is the correct reading?

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