CoursesInfrastructure as Code & automationBeginner · Configuration management

Ansible: playbooks & inventory

Agentless config over SSH.

Beginner14 min · lesson 7 of 23

Where Terraform provisions infrastructure, Ansible configures it — installing packages, editing config files, starting services, running commands across many machines. Its defining traits: it is agentless (it connects over plain SSH, so there is nothing to install on the targets) and it is written in readable YAML. You describe the desired configuration in a playbook, point Ansible at an inventory of hosts, and it makes each host match. It is the standard tool for "set up what runs on these servers."

playbook.yml
- name: Configure web servers
hosts: webservers # which group from the inventory
become: true # run with sudo
tasks:
- name: Install nginx
apt: { name: nginx, state: present } # a module: desired state
- name: Write the site config
copy: { src: site.conf, dest: /etc/nginx/conf.d/site.conf }
notify: reload nginx # trigger a handler if this changed
- name: Ensure nginx is running
service: { name: nginx, state: started, enabled: true }
handlers:
- name: reload nginx
service: { name: nginx, state: reloaded }

Inventory: which hosts

The inventory lists the machines Ansible manages, organized into groups (webservers, dbservers) so a playbook can target a whole role at once. It can be a simple static file or generated dynamically from your cloud (a "dynamic inventory" that asks AWS which instances exist). Grouping is the key idea: you write a playbook for "webservers" and Ansible runs it against every host in that group, which is how one playbook configures a fleet.

inventory.ini
[webservers]
web-01.acme.internal
web-02.acme.internal
[dbservers]
db-01.acme.internal
[all:vars]
ansible_user=deploy # connect as this user over SSH

Modules and idempotence

Each task uses a module (apt, copy, service, template, ...) that declares a desired state, and — crucially — modules are idempotent: apt: name=nginx state=present installs nginx only if it is not already there, and reports "changed" only when it actually did something. So running a playbook repeatedly is safe and converges the host to the desired state, the same declarative-in-effect behavior as Terraform. The "changed" vs "ok" count in Ansible’s output tells you exactly what it altered on this run.

terminal
$ ansible-playbook -i inventory.ini playbook.yml
PLAY [Configure web servers] ****
TASK [Install nginx] ****
changed: [web-01] # nginx was installed (a change was made)
ok: [web-02] # nginx already present (no change — idempotent)
PLAY RECAP ****
web-01 : ok=4 changed=2 web-02 : ok=4 changed=0
Prefer modules over the command/shell task
Ansible lets you run arbitrary commands (the command and shell modules), but these break idempotence — a raw shell command runs every time and Ansible cannot tell whether it "changed" anything, so re-running a playbook can repeat side effects. Always prefer a real module (apt, copy, service, lineinfile) that declares desired state and is idempotent; reserve command/shell for the rare case no module covers, and add a "creates"/"changed_when" guard so it does not run needlessly. Idempotence is Ansible’s core value — do not throw it away with unguarded shell tasks.