Ansible is a configuration and deployment automation tool: tasks are described
as YAML playbooks and executed over SSH without agents on the target machines.
The scenario of this article: an inventory with webservers/dbservers
groups, and we set up nginx on them.
Installation#
The Ubuntu repository ships a badly lagging version — it is livelier to take the official PPA:
sudo apt update
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible
ansible --version
An alternative that gives the freshest releases:
pipx install ansible(or pip in a virtual environment). Do not mix both methods on one machine.
Inventory#
The inventory describes hosts and their groups. A inventory.ini file next
to the playbooks:
[webservers]
web1.example.com
web2.example.com
[dbservers]
db1.example.com
db2.example.com
[all:vars]
ansible_user=admin
ansible_ssh_private_key_file=~/.ssh/id_rsa
By default Ansible takes the inventory from
ansible.cfgin the playbook directory:# ansible.cfg [defaults] inventory = ./inventory.iniotherwise every command call has to be decorated with
-i inventory.ini.
First playbook#
A playbook is YAML with a list of tasks. File update.yml:
---
- name: Update system packages
hosts: all
become: yes
tasks:
- name: Refresh the package cache
apt:
update_cache: yes
- name: Install the basic set
apt:
name:
- nginx
- git
- curl
state: present
Run it:
ansible-playbook update.yml
Ansible tasks are idempotent: rerunning a playbook does nothing redundant — modules compare the current state with the desired one and change only what diverges.
Modules#
A module is an atomic action. The most-used ones cover most tasks:
| Module | What it does |
|---|---|
apt |
packages on Debian/Ubuntu |
yum, dnf |
packages on the RHEL family |
copy |
a file to a remote host |
template |
a file from a Jinja2 template with variable substitution |
file |
permissions, owners, directories |
service |
start/stop/restart a service |
user, group |
users and groups |
Full module names carry the collection prefix:
ansible.builtin.copy,ansible.builtin.service. Help on any of them:ansible-doc copy; documentation at docs.ansible.com.
Variables and handlers#
Inside a playbook — vars; in templates — substitution via {{ … }}.
File nginx.conf.j2 next to the playbook:
server {
listen {{ http_port }};
root {{ document_root }};
}
File webserver.yml:
---
- name: Web server setup
hosts: webservers
become: yes
vars:
http_port: 80
document_root: /var/www/html
tasks:
- name: nginx configuration from a template
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx # fires only if the file actually changed
handlers:
- name: Restart nginx
service:
name: nginx
state: restarted
notifyis a handler: it runs once at the end of the run and only when a task actually changed something. A run without changes does not restart services.
Roles#
A role is a reusable component with its own directory structure:
ansible-galaxy init roles/webserver
roles/webserver/
├── tasks/main.yml # tasks of the role
├── handlers/main.yml
├── templates/ # nginx.conf.j2 — goes here
├── defaults/main.yml # default variables
└── vars/main.yml
Use it in a playbook instead of listing tasks:
---
- name: Web servers
hosts: webservers
become: yes
roles:
- webserver
Ready-made community roles — Ansible Galaxy (
ansible-galaxy installgeerlingguy.nginx); put your own intoroles/next to the playbook.
Everyday commands#
# Check host reachability (ping module)
ansible all -m ping
# Only one group
ansible webservers -m ping
# One-off command on hosts
ansible all -a "uptime"
# Playbook with superuser on the target machines
ansible-playbook webserver.yml --become
# Show the task list without executing anything
ansible-playbook webserver.yml --list-tasks
# Dry run: changes are computed but not applied
ansible-playbook webserver.yml --check --diff
# Verbose output (v, -vv, -vvv — more verbose)
ansible-playbook webserver.yml -v
# Restrict the run to a single host
ansible-playbook webserver.yml --limit web1.example.com
Keep secrets (passwords, API keys) in encrypted variables:
ansible-vault encrypt_string— and use them in a playbook as a regular variable; decryption asks for the vault password during the run.