2026
Ansible: Configuration Management Without the Agent
At some point, managing servers by hand stops being manageable. Ansible lets you describe the state a machine should be in and apply that description to as many hosts as you need, all at once, over plain SSH.
At some point, managing servers by hand stops being manageable. You SSH into each machine, run a few commands, edit a config file, restart a service. That works fine for three servers. At thirty it starts taking real time. At three hundred it is simply not possible to do consistently and consistency is the thing that matters most.
Ansible is a configuration management tool that lets you describe the state a machine should be in and then apply that description to as many hosts as you need, all at once. The same set of tasks that configures one server configures a thousand. If a machine drifts from the intended state, running the playbook again brings it back. No agents to install, no daemons to babysit on the targets. Ansible connects over SSH, does its work and leaves.
What agentless actually means
Most configuration management tools require you to install an agent on every machine they manage. That agent runs continuously, checks in with a central server and applies changes when instructed. Ansible takes a different approach: nothing runs on the target hosts between executions. When you want to apply a configuration, Ansible pushes the work over SSH.
The practical benefit is that you can point Ansible at a freshly provisioned machine and use it immediately. There is no bootstrapping phase where you install and configure an agent before the tool becomes useful. SSH access is enough. For teams managing cloud infrastructure where machines are created and destroyed frequently, this makes a real difference to how quickly automation can start doing useful work.
Worth noting
Agentless does not mean stateless. Ansible does not keep a record of what it has applied to which hosts. Each run evaluates the current state of the target from scratch. This is different from tools like Puppet that maintain a catalogue of applied configuration. It is a trade-off: simpler to operate, but without the continuous enforcement that agent-based tools provide between runs.
Core concepts
Ansible has a handful of concepts that everything else builds on. The vocabulary is small. Once these are clear, reading a playbook written by someone else becomes straightforward.
Inventory
A list of the machines Ansible should manage. The inventory can be a static file listing hostnames and IP addresses, a dynamic script that queries a cloud provider for live hosts, or a combination of both. Hosts can be organised into groups so that a single playbook can target all web servers without naming each one explicitly.
Playbook
The main unit of automation in Ansible. A playbook is a YAML file that describes what should happen on which hosts. It contains one or more plays. Each play targets a set of hosts from the inventory and runs a list of tasks against them in order. Playbooks are the thing you version-control, review and run.
Task
A single step inside a play. Each task calls a module with specific arguments. Tasks run sequentially by default. If a task fails on a host, Ansible stops processing that host for the remainder of the play unless you explicitly tell it to continue. The name field on each task shows up in the output, so well-named tasks make logs readable.
Module
The unit of work that Ansible actually executes. There are modules for managing packages, files, services, users, cloud resources, database state and hundreds of other things. Each module takes arguments and returns structured data. Modules are idempotent by design: running them multiple times on a machine in the desired state produces no changes.
Role
A structured way to package related tasks, variables, handlers and files together. A role for setting up a web server might contain tasks to install nginx, a handler to reload it when the config changes, template files for the configuration itself and default values for port numbers. Roles make playbooks shorter by moving the detail into a reusable unit.
Handler
A task that runs only when notified by another task. The classic use is restarting a service when its configuration file changes. A task that writes the config file notifies the handler. If the file was unchanged, the task reports no change and the handler never runs. If multiple tasks notify the same handler during a play, the handler runs once at the end.
Variable
Ansible has many places to define variables: inventory files, playbooks, roles, group variables files, host variables files and the command line. When the same variable is defined in multiple places, a precedence order determines which value wins. Understanding that precedence order matters when a variable is not taking the value you expect.
What a playbook looks like
Playbooks are YAML files. The syntax is designed to be readable by people who have not written Ansible before, which is part of why the tool spread quickly in teams where not everyone thinks of themselves as a programmer. A playbook that installs and configures nginx gives a reasonable sense of how the pieces fit.
Example: install and start nginx
---
- name: Configure web servers
hosts: webservers
become: true
vars:
nginx_port: 80
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
The become: true line tells Ansible to use privilege escalation on the remote host. By default this means sudo. Thetemplate module renders a Jinja2 template on the control node and copies the result to the target. The notify on that task means the Reload nginx handler runs at the end of the play only if the file actually changed.
How Ansible executes a playbook
Understanding what actually happens when you run a playbook helps explain both its behaviour and its limitations.
Control node connects
Ansible runs on a single machine you designate as the control node. When you run a playbook, Ansible opens SSH connections to the target hosts listed in your inventory. No agent is installed on the targets. The only requirement is that SSH is accessible and Python is present on the remote host.
Module is transferred
For each task, Ansible copies the relevant module code to a temporary directory on the remote host. The module is a small Python script that knows how to perform one specific operation. Ansible executes it over SSH, collects the JSON output and cleans up the temporary files.
Result is evaluated
The module returns whether the state changed, whether it failed and any values needed by later tasks. If the state was already correct, the module returns unchanged. Ansible records this and moves to the next task. Changed results can trigger handlers. Failed results stop the host unless failure is explicitly tolerated.
Handlers run at the end
After all tasks in a play complete, Ansible runs any handlers that were notified. Handlers execute once regardless of how many tasks notified them during the play. This is where service restarts typically happen, ensuring that a service is only bounced once even if five configuration files were updated.
Idempotency
The most important property of well-written Ansible is idempotency. Running a playbook once should produce the same result as running it ten times. If the desired state is already present, nothing changes. If a package is already installed, the package task reports unchanged. If a file already contains the correct content, the file task does nothing.
This matters because it means you can run a playbook against a fleet to bring everything into compliance without worrying about what each machine's current state happens to be. New machines get fully configured. Machines that are already correct get left alone. Machines that have drifted get corrected. The same playbook handles all three cases.
Not every task is idempotent by default. The command andshell modules run whatever you tell them to and report changed every time. Using creates or whenconditions to guard those tasks is one way to restore idempotency. The better approach is to reach for a purpose-built module that already handles the check before the change. Ansible's module library is large enough that a shell command is rarely the only option.
Roles
A flat playbook with fifty tasks is hard to reuse and hard to review. Roles solve this by giving tasks a home with a predictable directory structure. Ansible knows to look in specific subdirectories for tasks, handlers, templates, files, variables and defaults.
Role directory layout
roles/
nginx/
tasks/
main.yml
handlers/
main.yml
templates/
nginx.conf.j2
defaults/
main.yml
vars/
main.yml
meta/
main.yml
Once a role exists, using it in a playbook is a single line. The same nginx role can be applied to staging hosts with one set of variable values and to production hosts with another. Ansible Galaxy hosts community roles for common software. They are a useful starting point, though teams almost always end up writing their own once internal conventions get specific enough that a generic role no longer fits.
Variables and where they live
Ansible has more places to put variables than most tools. Understanding the precedence order between them prevents the kind of confusion where a value refuses to take effect because something else in the hierarchy is winning.
Command-line vars
Passed with -e or --extra-vars at runtime. These have the highest precedence and override everything else. Useful for one-off overrides during a manual run but should not be relied on in automated pipelines where reproducibility matters.
Role defaults
Defined in a role's defaults/main.yml. These have the lowest precedence and are intended to be the values that everything else overrides. A role that installs nginx might default to port 80 here, expecting callers to set a different value when they need one.
Group vars
Defined in files under a group_vars directory, named after the inventory group. All hosts in the web group pick up the variables in group_vars/web.yml. A special all group applies to every host in the inventory.
Host vars
Defined in files under a host_vars directory, named after the individual host. These override group variables for that specific host. Useful for values that genuinely differ between machines within the same group, such as a particular server needing a different memory allocation.
Worth noting
Sensitive values like passwords and API keys should not live in playbooks or variable files committed to a repository. Ansible Vault encrypts files so they can be stored safely in version control. The vault password unlocks them at runtime. Storing the vault password in a CI secret and the encrypted file in the repository is the standard approach.
Inventory
The inventory tells Ansible what machines exist. A static inventory is a plain text file. Hosts can be listed individually or in ranges. Groups let you target a subset of hosts in a playbook without listing them by name.
Example static inventory (INI format)
[webservers]
web01.example.com
web02.example.com
web03.example.com
[databases]
db01.example.com ansible_user=postgres
[production:children]
webservers
databases
Dynamic inventory scripts are more common in cloud environments where machines come and go. An AWS dynamic inventory plugin queries the EC2 API and returns the current list of instances with their tags as groups. A playbook targeting the tag_env_production group automatically includes every running instance with that tag, without anyone updating a file.
Where it fits
Ansible sits in the configuration management layer. Terraform provisions the infrastructure: the servers, the networks, the load balancers. Ansible configures what runs on them: installs packages, writes configuration files, manages users, starts services. The two tools are complementary. Terraform gets you a machine. Ansible makes that machine useful.
For containerised workloads, Ansible plays a smaller role in day-to-day operations because the configuration that used to live on the machine now lives in the container image. Where it remains relevant is in managing the infrastructure surrounding those containers: the Kubernetes nodes themselves, the jump hosts, the build servers, the monitoring agents that run outside the cluster. Anything that is still a long-lived machine with state is a reasonable target.
The argument for Ansible over writing shell scripts is reproducibility and readability. A shell script that provisions a server accumulates complexity over time and becomes hard to modify safely. A playbook structured with roles stays readable because the organisation is imposed by the tool rather than left to the author. The idempotency guarantee means you can run it again without understanding every step it takes. That confidence becomes valuable precisely when something has gone wrong and you need to bring a machine back to a known state quickly.