Ansible in Depth: Playbooks, Roles, and Idempotent Automation
Ansible's biggest design decision is also its simplest: no agent runs permanently on the machines it manages. It connects over plain SSH, pushes over a small Python payload, runs it, and disconnects. That agentless model is a big part of why it became the default choice for configuration management and ad-hoc automation across so many teams.
Inventory: telling Ansible what machines exist
Before running anything, Ansible needs to know which hosts it's managing. The simplest form is a static inventory file:
[webservers]
web1.example.com
web2.example.com
[dbservers]
db1.example.com ansible_user=admin
Groups ([webservers], [dbservers]) let you target a subset of your infrastructure at once. For anything beyond a handful of static machines, most teams switch to a dynamic inventory, a script or plugin that queries a cloud provider's API (AWS, Azure, GCP) at runtime and builds the host list automatically, so newly launched instances are picked up without anyone having to edit a file by hand.
Playbooks: the actual automation
A playbook is a YAML file describing what state you want a set of hosts to end up in, expressed as a sequence of tasks, each using a module (a small, focused unit of Ansible functionality, one for installing packages, one for managing files, one for starting services, and so on).
- name: Configure web servers
hosts: webservers
become: true
tasks:
- name: Install nginx
apt:
name: nginx
state: present
- name: Copy nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: true
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
become: true tells Ansible to escalate privileges (sudo, typically) for these tasks, since installing packages and managing system services isn't something a regular user can do. The notify and handlers pairing is worth noting: the handler only runs if the task that notified it actually changed something, so nginx doesn't get needlessly restarted on every single run, only when its config actually changed.
Idempotency: the property that makes reruns safe
This is the single most important concept in Ansible, and in configuration management generally. Idempotent means running the same playbook twice produces the same end result as running it once, the second run simply confirms everything is already in the desired state and makes no changes.
The apt task above with state: present doesn't blindly run apt-get install nginx every time, it first checks whether nginx is already installed, and only takes action if it isn't. This is exactly why running Ansible against a server that already matches the desired config is safe, and why it's the standard way to describe infrastructure as code, you're describing a desired end state, not a fixed sequence of one-time commands.
Contrast this with a raw bash script that just runs apt-get install nginx unconditionally, running it repeatedly might not break anything, but the safety isn't structural, you'd need to write that safety logic yourself for every single action. Ansible's modules give you that safety by default.
Roles: reusable, shareable structure
Once a playbook grows past a handful of tasks, or you find yourself wanting to reuse the same configuration logic across multiple projects, roles are the standard way to organize it:
roles/
nginx/
tasks/main.yml
handlers/main.yml
templates/nginx.conf.j2
defaults/main.yml # default variable values
vars/main.yml # variables meant to be overridden less often
A playbook then just references the role by name:
- name: Configure web servers
hosts: webservers
roles:
- nginx
- monitoring-agent
This structure is what makes Ansible Galaxy (a public registry of community-shared roles) useful, a well-written role for "install and configure PostgreSQL" can be dropped into any project's roles/ directory and just works, rather than every team reinventing the same automation from scratch.
ansible-vault: keeping secrets out of plain YAML
Playbooks often need secrets, database passwords, API keys, that obviously shouldn't sit in plain text in a Git repository. ansible-vault encrypts specific files (or specific variables within a file) so they can be committed safely, and only decrypted at runtime with a vault password:
ansible-vault encrypt group_vars/production/secrets.yml
ansible-playbook site.yml --ask-vault-pass
This is the built-in answer to "how do I keep credentials in version control without leaking them," rather than reaching for an entirely separate secrets management tool for a small team's needs.
Ad-hoc commands: when you don't need a whole playbook
Not everything needs a playbook. For a one-off task, Ansible's ad-hoc mode runs a single module against your inventory directly:
ansible webservers -m shell -a "df -h"
ansible webservers -m service -a "name=nginx state=restarted" --become
This is genuinely useful for quick checks across a fleet ("is disk space fine everywhere?") without writing and saving a playbook for something you'll run exactly once.
Ansible's whole design bet is that agentless, idempotent, human-readable YAML beats a more powerful but more complex agent-based system for the vast majority of real infrastructure work, and for most teams, that bet has held up well. The Infrastructure as Code quiz on OpsQuiz covers Ansible alongside Terraform if you want to test how the two compare.