# Ansible

## What it is

Ansible is open-source automation software that connects to machines over SSH
and brings each one to the configuration described in a set of YAML files: which
packages are installed, which users and files exist, which services run and how
they are set up. The machine Ansible runs from is the control node; the machines
it configures are managed nodes, and nothing has to be installed on them as an
agent, because Ansible logs in with the operating system's own credentials and
does its work over that connection. The files list the outcome, and Ansible
works out on each host whether that outcome is already true.

The pieces have plain names. An inventory lists the managed nodes and sorts them
into groups such as `web` or `db`, with variables that apply to a whole group or
to one host. A playbook holds one or more plays, and each play maps a group of
hosts to a list of tasks. Each task calls a module, a small program that knows
how to manage one kind of thing: a package, a file, a service, a user, a
firewall rule. The project's own documentation describes playbooks as a
repeatable, reusable, simple configuration management and multi-machine
deployment system. A role bundles tasks, templates, default variables and
handlers under one name, so “a PostgreSQL server” or “a hardened SSH daemon” is
written once and applied to any host that needs it.

A short play shows the shape:

```text
- name: Web servers serve the site
  hosts: web
  become: true
  tasks:
    - name: nginx is installed
      ansible.builtin.package:
        name: nginx
        state: present

    - name: nginx runs now and after a reboot
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true
```

Each task names a state, not an action. Most modules check first whether that
state already holds and exit without doing anything if it does, so the second
run of this play on a configured host changes nothing and says so. That
property, idempotence, is what makes it safe to run the same playbook against
the whole fleet every time, instead of tracking which host has had which script
applied.

People asking about Ansible often weigh it against three neighbours. Against a
shell script, the difference is that a script lists steps and runs them again
whether they are needed or not, while a module checks before it acts. Against
Terraform or Bicep, the difference is the layer: those tools create cloud
resources such as networks, databases and virtual machines, while Ansible
configures what runs inside a machine once it exists. Many teams use one of
each. Against Puppet and Chef, the difference is the model: those usually run an
agent on every machine that pulls its configuration from a central server, while
Ansible pushes from wherever it is started and needs only SSH on the far side.

## Why it matters

A server set up by hand is only as reproducible as the memory of whoever set it
up. Six months later nobody can say with confidence why a kernel parameter has
its value, which package was pinned and why, or what has to happen first when
the machine is rebuilt. Writing the setup down as playbooks turns that memory
into files that can be read, reviewed in a pull request and run again on a fresh
machine with the same result.

The written form also changes what drift means. When the playbook is the
description of the host, running it in check mode against a live machine reports
every place where the two disagree. With the `--diff` flag the report shows the
lines in each file that would change. A fleet that is converged regularly stays
close to its description, and a host that has been edited by hand shows up as a
change the next time the playbook runs.

The model has costs. Ansible acts only when it is run, so drift between runs
stays until the next one. Tasks run in order, one at a time across all matched
hosts, and a large playbook against many machines is slower than a script that
does one thing. Logic written in YAML and Jinja2 templates gets hard to follow
once a role grows many conditions. And a run that stops partway leaves its hosts
partly changed, because Ansible has no transaction to roll back.

## What it looks like in practice

Several projects on this site show what a written-down estate looks like over
time, and what it takes to keep it honest.

The company's own hosts are described entirely in Ansible.
[The estate grew into 19 playbooks and 34 roles](/portfolio/built-the-companys-infrastructure-as-code-108/)
across 12,065 lines of YAML. A host sits in a tier group whose variables declare
which roles it runs, so provisioning with no arguments converges every host to
its declared state. Idempotence is treated as a contract: a converged host
reports zero changes, and a play that cannot say that is not finished. Four
command namespaces keep the promises apart, from a gate that touches no host to
verification that changes a host on purpose and returns a verdict. The page is
candid about the cost: every change is slower to make than editing a file on the
server, and a converge that half-applies is worse than one that refuses.

Access control is another place where a written description pays off.
[More than 1,000 RBAC rules were configured through Ansible](/portfolio/enhanced-data-security-by-implementing-1-000-rbac-14/)
across developer environments, application instances, PostgreSQL, MS SQL and
Linux servers, each rule tied to one role and one system. The page reports
unauthorized-access risk reduced by 85% and setup time cut by 60% compared to
manual methods, and a framework that teams could audit or change quickly because
it existed as code.

Keeping the dry run truthful took work of its own.
[Check mode was made to tell the truth across the whole platform](/portfolio/made-ansible-check-mode-tell-the-truth-116/)
after a dry run in September 2026 stopped on an assertion that was wrong about
the host and advised setting a flag that would have relaxed a security sandbox.
A sweep found six probes deciding on values the host had never given. Probes
that read state the run has not touched now run even under check mode; the rest
are skipped with a message naming what went unverified, and the rule went into
the development guide.

## Where it goes wrong

Most Ansible trouble comes from trusting a report more than the mechanism behind
it.

**A dry run reports a clean result for commands it never ran.** Check mode is a
simulation. Modules that support it predict their change; the documentation says
modules that lack support report nothing and do nothing. The `command` and
`shell` modules support it only partly: unless `creates` or `removes` is given,
they skip the command and register a result with empty output. Any later `when:`
that reads that result decides on a value nobody measured. A probe that only
reads the host can be told to run anyway, and one that cannot run should say so:

```text
- name: Read the firewall state, even in check mode
  ansible.builtin.command: ufw status
  register: fw
  check_mode: false
  changed_when: false

- name: Say what this dry run did not verify
  ansible.builtin.debug:
    msg: "check mode: the service restart was not exercised"
  when: ansible_check_mode
```

**A run stops on its ninth task with the first eight already applied.** When a
task fails on a host, Ansible drops that host from the rest of the run and
leaves whatever it already changed in place. Checks that can run before anything
touches a host belong in front of the converge. That is what happened after a
secrets change on this site's own estate:
[a preflight play now validates the vault locally first](/portfolio/added-a-preflight-play-for-secrets-117/),
running the same task files as the real converge in about a second, so a secret
that does not resolve costs a two-second refusal on a laptop instead of a partly
applied change to a live server. The same page notes a trap: facts set during
one play outlive it, so a check and a converge chained into one invocation can
share a value the check should have re-read.

**A host nobody touched reports changes on every run.** The `command` and
`shell` modules report a change every time they run, because they have no way to
know what the command did. A playbook full of them never converges to zero, and
real changes hide in the noise. Prefer a module that manages the thing directly,
add `creates` or `removes`, or tell Ansible when output means a change with
`changed_when`.

**A password turns up in a log or in the repository.** Variables are plain text
unless something encrypts them. Ansible Vault encrypts variable files or single
values so they can live in the repository, and `no_log: true` on a task keeps
its arguments and results out of the output. Run the playbook once with high
verbosity in a test environment and read what it prints before trusting either.

## Further reading

- [Ansible Community Documentation](https://docs.ansible.com/ansible/latest/index.html)
  — the project's own entry point to installation, concepts and every guide.
- [Ansible playbooks](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html)
  — plays, tasks, modules, execution order and what idempotence means in
  practice.
- [Validating tasks: check mode and diff mode](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_checkmode.html)
  — what a dry run can and cannot see, and the per-task switches.
- [How to build your inventory](https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html)
  — hosts, groups and the variables attached to them.
- [Protecting sensitive data with Ansible Vault](https://docs.ansible.com/ansible/latest/vault_guide/index.html)
  — encrypting files and single values that live beside the playbooks.
- [ansible.builtin.command](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/command_module.html)
  — the module reference, including its partial check-mode support.

<https://engineer.company/glossary/ansible/>
