Ansible Deep Dive Part 1: What Ansible Is, and Why Agentless Still Wins

Every automation series has to start somewhere, and this one starts with the question I get asked most: why Ansible, specifically, and not one of the half-dozen other tools that promise the same thing?

I’ve been writing Python automation against network gear for years — this site has an entire series on Netmiko, TextFSM, and Nornir. Ansible sits one layer up from all of that: instead of writing Python that opens an SSH session and issues commands, you describe the state you want a fleet of machines to be in, and Ansible works out how to get there. That distinction — imperative scripting versus declarative state — is the whole reason this tool has survived over a decade of infrastructure fashion cycles.

This series goes deep. Thirteen parts, starting here with fundamentals and building through inventory, playbooks, templating, roles, vault, performance, testing, two full labs (a three-tier web app and a mixed Cisco/FortiGate network fleet), error handling, and a closing piece on whether tools like this still matter now that an LLM can just SSH in and improvise. If you want the whole map, the series index is at the bottom of the Python guides page.

Configuration management, briefly

Before Ansible there was Puppet (2005) and Chef (2009), both of which model infrastructure as code but require an agent daemon running on every managed node, phoning home to a central server on a schedule. That agent needs installing, updating, and trusting — it’s another piece of software with its own failure modes, running with elevated privileges, on every single box you own.

Ansible (2012, acquired by Red Hat in 2015) made a different bet: no agent. It connects over SSH (or WinRM for Windows), pushes a small Python payload, executes it, and cleans up after itself. Nothing persists on the target between runs. Salt started agent-based but grew an agentless “salt-ssh” mode later, which tells you something about which direction the market moved.

For network engineers this bet matters even more than it does for server fleets. You cannot install a Python agent on a Cisco IOS device or a FortiGate. Agentless was never optional for network automation — it was the only way in. That’s a large part of why Ansible ended up the default choice in NetDevOps circles, well ahead of its general popularity in server configuration management.

The mental model: control node and managed nodes

There are exactly two roles in Ansible’s world:

The control node is the machine you run ansible or ansible-playbook from. It needs Python and the Ansible package installed. This is your laptop, a CI runner, or a jump box — never the machines being managed.

Managed nodes are everything you’re configuring. For Linux/Unix targets they need Python present (nearly always true already) and an SSH server. For network devices, Ansible instead talks to the device’s own API or CLI over SSH using a network module — no Python execution happens on the device itself, because there’s nowhere for it to run.

There’s no database, no message queue, no daemon. When you’re not running a playbook, Ansible isn’t doing anything, anywhere. That’s a deliberate simplicity trade-off: you give up the “continuous enforcement” model (Puppet/Chef agents reconcile state every 30 minutes whether you ask them to or not) in exchange for a tool with almost nothing to operate.

Installing it

On the control node:

python3 -m venv ~/.venvs/ansible
source ~/.venvs/ansible/bin/activate
pip install ansible
ansible --version

I install into a venv rather than system-wide — Ansible ships a lot of transitive dependencies via collections, and pinning a version per project avoids the “it worked on my laptop” problem when a colleague runs the same playbook against a different Ansible version months later.

ansible --version should show core and ansible package versions, plus the config file and Python interpreter it resolved to — worth checking, because a stray ansible.cfg in a parent directory can silently redirect all of this.

Your first inventory and ad-hoc command

Ansible needs to know what “managed nodes” means for your environment. The simplest possible inventory is a text file:

# inventory.ini
web1.lab.local
web2.lab.local

[dbservers]
db1.lab.local

And the simplest possible thing to do with it — no playbook, no YAML, just a one-off command against every host:

ansible all -i inventory.ini -m ping

-m ping here is the ping module — not ICMP, but “can Ansible connect, authenticate, and execute Python on this host.” Every managed Linux host should come back green with "pong". This is the single most useful command for proving connectivity before you write a single line of playbook, and I run it after every inventory change out of habit.

A slightly more useful ad-hoc example — check disk space across the fleet without writing anything:

ansible all -i inventory.ini -m shell -a "df -h /"

Ad-hoc commands are Ansible’s equivalent of a one-liner in a shell for-loop over hosts. They’re not meant to be repeatable or version-controlled — that’s what playbooks are for, and that’s where Part 3 picks up. Part 2, next, covers inventory properly: groups, host variables, group variables, patterns, and the dynamic inventory plugins that replace hand-maintained INI files once your fleet stops being five machines you know by name.

What’s ahead

The full arc: Part 2 inventory, Part 3 playbooks and idempotency, Part 4 variables/facts/Jinja2, Part 5 roles and Galaxy, Part 6 vault and variable precedence, Part 7 performance and scaling, Part 8 testing (Molecule, ansible-lint, CI), Part 9 the first lab (three-tier web app from bare metal), Part 10 the second lab (a mixed Cisco/FortiGate fleet), Part 11 error handling in anger, Part 12 the best-practice style guide I actually follow, and Part 13 — the one I expect to get the most pushback on — whether Ansible and its peers still have a job to do once an AI agent can just log in and fix things itself.