AnsibleII · Ansible ArchitectureThe execution model
Connection plugins: SSH is a default, not a law
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
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:
$ ansible-doc -t connection -lansible.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 WinRMFour. 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:
$ 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: stringControlMaster=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 scenario | Production VM over SSH | |
|---|---|---|
| Transport | Container runtime exec | SSH to sshd |
| PID 1 | Usually the entrypoint, often not an init | systemd |
| Service management | Frequently unavailable or stubbed | Real systemctl |
| Reboot | Not possible | Possible, and sometimes required |
| Kernel parameters | Shared with the host; many are not settable | Own namespace-wide settings |
| Storage | Container filesystem | Real block devices, mounts, quotas |
| Privilege escalation | Often already root, so become is untested | Real sudo path |
| SSH itself | Not exercised at all | The 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
Q1. Which connection plugins ship in ansible-core 2.21?
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.
Q3. A role passes its Molecule container scenario completely. Which production behaviours does that result leave unproven? Select all that apply.
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.