Every sysadmin writes the same script eventually. setup.sh: install these packages, copy this config, create this user, start this service. It works the first time. Then you run it again on a machine that is half-configured and it fails, or worse, it does not fail and quietly does something wrong.
The problem is that a shell script describes steps. Ansible describes the end state — “nginx should be installed and running, this file should have these contents” — and works out what to change. Run it against a fresh machine and it does everything; run it against a configured one and it does nothing.
That property is called idempotence, and it is the entire reason to adopt a configuration management tool.
Is it worth your time?
Yes, past about three servers — or one server you would be in trouble if you lost. The moment “how was this configured?” is answered by memory rather than a file, you want this.
Not for one machine you set up once. A shell script and good notes are genuinely fine, and pretending otherwise wastes a weekend.
Ansible over the alternatives mostly because it needs nothing installed on the servers — just SSH and Python, which you already have.
What makes it different from a script
| Shell script | Ansible | |
|---|---|---|
| Describes | Steps to take | State to reach |
| Running it twice | Often breaks | Changes nothing |
| Partial failure | Leaves a mess | Stops, reports which task |
| Many servers | A loop you write | Built in, parallel |
| Preview changes | No | --check |
| Needs an agent installed | No | No — just SSH |
| Learning curve | You know it already | A weekend |
| Good for one-off tasks | Yes | Overkill |
The agentless row is why Ansible won. Puppet and Chef want software running on every managed machine; Ansible connects over SSH, does its work, and disconnects. If you can log in, you can manage it.
Installing
Ansible is installed only on your own machine — the control node. Nothing goes on the servers.
sudo apt install ansible
sudo dnf install ansible
sudo pacman -S ansible
# Or, for a newer version than your distro ships
pipx install --include-deps ansible
ansible --versionThe servers need SSH access with key authentication, a Python interpreter (every modern distribution has one), and sudo. That is the whole requirement, and you have already done all three if you followed securing a new server.
The inventory
A list of the machines you manage, grouped however is useful.
# inventory.ini
[web]
web-01 ansible_host=203.0.113.10
web-02 ansible_host=203.0.113.11
[db]
db-01 ansible_host=203.0.113.20
[all:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3# Check you can reach everything
ansible all -i inventory.ini -m ping
# Run one command everywhere
ansible web -i inventory.ini -a "uptime"
# With sudo
ansible all -i inventory.ini -b -a "systemctl status nginx"Those ad-hoc commands are useful before you write a single playbook. “Run uptime on all twelve servers” is a one-liner, and it is often how people start using Ansible without meaning to.
A first playbook
A playbook is a YAML file describing the desired state. This one sets up a web server:
# webserver.yml
---
- name: Configure web servers
hosts: web
become: true
tasks:
- name: Install packages
ansible.builtin.package:
name:
- nginx
- ufw
- tmux
state: present
- name: Deploy the site configuration
ansible.builtin.template:
src: templates/site.conf.j2
dest: /etc/nginx/sites-available/default
owner: root
mode: '0644'
notify: Reload nginx
- name: Allow HTTP and HTTPS
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop: [80, 443]
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded# See what WOULD change, without changing anything
ansible-playbook -i inventory.ini webserver.yml --check --diff
# Do it
ansible-playbook -i inventory.ini webserver.yml
# Just one server
ansible-playbook -i inventory.ini webserver.yml --limit web-01Three things in that playbook are worth understanding, because they are most of what makes Ansible worth using.
state: present and state: started are descriptions, not commands. Ansible checks whether nginx is installed and installs it only if not. Run the playbook ten times and nine of them report zero changes.
notify and handlers mean things restart only when they need to. The reload handler runs only if the config file actually changed, so a no-op playbook run does not bounce your web server for nothing.
--check --diff is the killer feature. It reports exactly what would change, including a diff of every file, without touching anything. There is no shell-script equivalent, and on production it is the difference between confidence and hoping.
The modules you will actually use
Ansible ships thousands. About eight cover most real work.
| Module | For |
|---|---|
package | Install software, distribution-agnostic |
service | Start, stop, enable |
copy | Put a file in place |
template | A file with variables filled in |
lineinfile | Change one line in an existing file |
user / group | Accounts |
file | Directories, permissions, symlinks |
cron | Scheduled jobs |
command / shell | Escape hatch — see below |
package deserves a note: it picks apt, dnf or pacman based on the target, so one playbook covers Debian and Red Hat machines. That only goes so far — package names still differ, and config file paths differ more, as Debian and Fedora both show — but it removes one layer of difference.
The command and shell modules are where good playbooks go wrong. They run arbitrary commands, which means they are not idempotent: Ansible cannot know whether the state already matched, so it reports “changed” every time and re-runs the command on every play. A playbook made mostly of shell tasks is a shell script wearing a costume, with all the original problems plus YAML.
When you genuinely need one, guard it:
- name: Build the search index
ansible.builtin.command: /usr/local/bin/build-index
args:
creates: /var/lib/myapp/index.db # skip if this already existscreates and removes restore idempotence by giving Ansible a way to check. Use one of them on every command task you write.
Secrets
Playbooks belong in version control, and version control is a poor place for database passwords. Ansible Vault encrypts values in place:
# Create an encrypted variables file
ansible-vault create group_vars/all/secrets.yml
# Edit it later
ansible-vault edit group_vars/all/secrets.yml
# Run a playbook that uses it
ansible-playbook -i inventory.ini site.yml --ask-vault-passThe encrypted file is safe to commit. Variables inside it are used exactly like any other, so nothing else in the playbook changes.
Things that catch people out
| Symptom | Cause |
|---|---|
YAML errors on a value like 0644 | Quote it — YAML reads it as a number and drops the zero |
| Every run reports “changed” | You are using shell or command without creates |
| Permission denied on the target | Missing become: true |
| Host unreachable | SSH key or ansible_user wrong — test with plain ssh first |
| Works on one distro, not another | Package or path names differ |
| Handler never runs | The task did not report a change, so nothing notified it |
| Indentation errors constantly | YAML. Use an editor with a YAML mode. |
The file-mode one bites everybody once. mode: 0644 is octal to you and decimal to YAML; always write mode: '0644' in quotes. File permissions covers what those digits mean.
When Ansible is the wrong answer
- One server, configured once. The honest answer is a shell script and a written record. Ansible pays off through repetition, and one machine does not repeat.
- Creating infrastructure. Ansible configures machines that exist. Creating the VMs, networks and DNS records is Terraform’s job, or your provider’s console.
- Containers. If the application ships as an image, the Dockerfile is the configuration — see getting started with Docker. Ansible is still useful for the host underneath.
- Anything urgent and interactive. During an incident, SSH in and fix it. Write the playbook afterwards so the fix survives.
- As a deployment tool, in most cases. It can deploy applications and it is rarely the best thing for it.
A reasonable adoption path: keep using shell scripts, and the first time you set up a second server that should match the first, convert that script into a playbook. The comparison makes the value obvious in a way that reading about idempotence does not.
Quick reference
| You want | Command |
|---|---|
| Can I reach everything? | ansible all -i inv -m ping |
| Run a command everywhere | ansible web -i inv -a "uptime" |
| Preview changes | ansible-playbook -i inv play.yml --check --diff |
| Apply | ansible-playbook -i inv play.yml |
| One host only | --limit web-01 |
| Skip ahead to a task | --start-at-task "Deploy config" |
| See what a host looks like | ansible web-01 -i inv -m setup |
| Check syntax | ansible-playbook play.yml --syntax-check |
| Encrypt secrets | ansible-vault create secrets.yml |
Related reading
- ssh — keys, which Ansible depends on entirely
- Securing a new server — the playbook you will eventually write first
- systemctl — what the
servicemodule drives - File permissions — and the quoted
modetrap - Getting started with Docker — the other answer to reproducible setup
- Migrating a server — much easier when configuration is in a file
