0. Introduction to Ansible Playbooks
In our Getting Started with Ansible guide, we explained the basics of working with Ansible: how to set up an Ansible inventory, connect remotely to your other servers, and execute commands on them.
For example, you can run this command to check disk usage on specific servers.
ansible all -i inventory.ini -a "df -h"
While this works perfectly for individual commands, it is not really suitable for more complex tasks. Let's say you need to update the list of available packages, install Nginx, start it, and enable it to launch automatically.
sudo apt update
sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginx
Running these commands manually on multiple servers - over and over again - is inconvenient and simply time-consuming, especially if you have a large number of servers. Ansible Playbooks were created exactly for such cases.
A playbook is a script that outlines exactly what Ansible should do - such as installing packages, creating users, copying files, or restarting a service. It essentially functions as a straightforward list of tasks. As a result, a playbook is quick to read and easy to edit.
Here are some examples of tasks that you can run on multiple servers simultaneously with playbooks:
Install software;
Create files and directories;
Configure services;
Manage system users;
0.1 How Ansible Playbooks work
A playbook is a file written in the YAML format. YAML is a text-based format used for creating configuration files
A playbook usually consists of three main elements:
Hosts: The exact servers where the playbook will execute tasks.
Tasks: action to be applied to the managed hosts.
Modules: The code or binaries used by Ansible to perform specific actions on managed hosts, such as installing packages or copying files.
More information about Ansible concepts you can find here.
Ansible executes tasks in the order in which they are listed in a playbook. You can run the same playbook multiple times. Ansible automatically checks whether the requested changes have already been applied and does not re-run tasks that have previously been completed.
0.2 Requirements
Before you continue and start creating your first playbook, make sure you have completed our previous guide:
You should already have:
A control node with installed Ansible;
One or more managed VPS;
SSH access to the managed servers;
An inventory file created in previous guide;
A user with sudo privileges on the managed servers.
We will continue using the same example from the previous guide:
Server | IP address | SSH user |
Ubuntu | 192.0.2.10 | adminuser |
AlmaLinux | 192.0.2.20 | adminuser |
The inventory file inventory.ini in our example looks like this:
[ubuntu]
ubuntu1 ansible_host=192.0.2.10 ansible_user=adminuser
[alma]
alma1 ansible_host=192.0.2.20 ansible_user=adminuser
[linux:children]
ubuntu
alma
Replace the IP addresses and username (adminuser in our example) with your actual VPS details.
1. Prepare your Ansible directory
First, connect via SSH to your control node (server with Ansible).
Navigate to ansible-lab directory (created in the previous guide):
cd ~/ansible-lab
Check the files in the directory:
ls -l
You should see the inventory file:
inventory.ini
Also, check if Ansible can connect to your managed servers:
ansible all -i inventory.ini -m ping
Both servers should return:
"ping": "pong"
If everything is correct, you can now create your first playbook.
2. Create your first Ansible Playbook
We will create playbook that performs these tasks:
Installs the
nginxpackage on our managed servers.Starts the Nginx service and enables it to launch automatically after a server reboot.
Creates a text file containing a custom message.
Create a new playbook file:
nano first-playbook.yml
Paste this content:
---
- name: Install and configure Nginx
hosts: linux
become: true
tasks:
- name: Update APT cache and install Nginx (Ubuntu/Debian)
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
when: ansible_facts['os_family'] == "Debian"
- name: Update DNF cache and install Nginx (AlmaLinux/Rocky)
ansible.builtin.dnf:
name: nginx
state: present
update_cache: true
when: ansible_facts['os_family'] == "RedHat"
- name: Start and enable Nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
Save changes and exit.
Important: YAML is indentation-sensitive, so make sure all indentation in your playbook matches our example exactly.
Make sure to always use spaces instead of tab characters.
3. Understanding the Playbook
3.1 Playbook name
- name: Prepare Linux servers
name acts as a kind of heading for a set of actions. It helps in understanding exactly what Ansible is doing.
You can choose any name that you want.
3.2 Target servers
hosts: linux
hosts specifies the servers on which tasks should be executed. In our example, this is the linux group, which includes both Ubuntu and Alma servers.
So this means that the tasks specified below will be executed on the servers in this group.
You can also specify other groups, such as ubuntu:
hosts: ubuntu
In this case, tasks specified in the playbook will be executed only on Ubuntu servers.
If you want to execute tasks only on Alma servers:
hosts: alma
You can also specify all servers that you have in the inventory:
hosts: all
3.3 Root privileges
become: true
Sometimes you need root privileges to run specific operations.
The become: true allows you to run operations with sudo.
become in playbook equivalent to -b option in the commands from our previous guide.
3.4 Tasks
tasks:
tasks section contains the actions that Ansible should execute on managed hosts.
Every task has a name that describes it.
In our example, we have three tasks.
First task: Update APT cache and install Nginx (Ubuntu/Debian)
- name: Update APT cache and install Nginx (Ubuntu/Debian)
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
when: ansible_facts['os_family'] == "Debian"
This task updates APT packages and intalls Nginx on Ubuntu and Debian servers.
The ansible.builtin.package allows you to select the appropriate package manager based on the operating system. In our case we use ansible.builtin.apt, which is the module used to manage software on Debian and Ubuntu. You can use it to update packages, install or uninstall programs, or check their status.
name: nginx - Specifies the package name to work with. In our case, it is Nginx. In simple words, this means that we will work with Nginx.
state: present - checks Nginx state on the managed servers. If Nginx is already installed, the playbook will not attempt to install it again.
update_cache: true - updates the list of available apt packages.
cache_valid_time: 3600 - updates the list of available apt packages if it is older than 3600 seconds (1 hour).
when - condition, when the task should be executed. In this case, the condition is ansible_facts['os_family'] == "Debian". This means, task will be executed only on Debian-based systems (including Ubuntu).
Second task: Update DNF cache and install Nginx (AlmaLinux/Rocky)
- name: Update DNF cache and install Nginx (AlmaLinux/Rocky)
ansible.builtin.dnf:
name: nginx
state: present
update_cache: true
when: ansible_facts['os_family'] == "RedHat"
This task will install Nginx on AlmaLinux and Rocky Linux. The principle is similar to the previous task.
ansible.builtin.dnf specifies, that we will use DNF package manager.
All other parameters are the same. The difference is that we do not have cache_valid_time: 3600. We don’t need to check whether the package list is outdated, as DNF will check this itself.
Condition in when: is ansible_facts['os_family'] == "RedHat" because this task will be executed only on RedHat-based systems (AlmaLnux and Rocky Linux in our case).
Third task: Start and enable Nginx
- name: Start and enable Nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
This task will start Nginx and configure it to launch automatically after every reboot (enable).
ansible.builtin.service: module specifies that we will work with services.
name: is again nginx.
state: started checks, if Nginx service already started.
enabled: true configure Nginx to start automatically after server reboot.
If Nginx is already running and enabled to started automatically ar every boot, the task will leave it unchanged.
This task will run on both Debian and Redhat based systems, so we do not have condition when.
4. Check your Playbook before running it
It is always good idea to check syntax before you run the playbook.
Run the following command:
ansible-playbook -i inventory.ini first-playbook.yml --syntax-check
If syntax is correct, you will see:
playbook: first-playbook.yml
You can also run this command to check which servers will be affected by playbook:
ansible-playbook -i inventory.ini first-playbook.yml --list-hosts
You should see:
playbook: first-playbook.yml
play #1 (linux): Install and configure Nginx
pattern: ['linux']
hosts (2):
ubuntu1
alma1
This will not change on your servers yet.
5. Run your first Playbook
Now we can run our playbook.
Enter this command (-K is not needed if your user do not have password):
ansible-playbook -i inventory.ini first-playbook.yml -K
Ansible will execute the tasks on all managed servers.
You should see output similar to:
PLAY [Install and configure Nginx] *****************************
TASK [Gathering Facts] ******************************************
ok: [ubuntu1]
ok: [alma1]
TASK [Update APT cache and install Nginx (Ubuntu/Debian)] *******
changed: [ubuntu1]
skipping: [alma1]
TASK [Update DNF cache and install Nginx (AlmaLinux/Rocky)] *****
skipping: [ubuntu1]
changed: [alma1]
TASK [Start and enable Nginx] **********************************
changed: [ubuntu1]
changed: [alma1]
PLAY RECAP *****************************************************
ubuntu1 : ok=3 changed=2 unreachable=0 failed=0 skipped=1
alma1 : ok=3 changed=2 unreachable=0 failed=0 skipped=1
ok - means the task was executed successfully. The number 3 means all three tasks completed without any issues.
changed - task actually made changes on the managed server. In our case number 2 means two tasks made changes on each server.
unreachable - server was unreachable. In our cases all servers works and accessible, so the number is 0.
failed - a task encountered an error. In our case all tasks completed succesfully, so the number is 0 as well.
skipped - number of skipped tasks. Ansible will skip the task, if it is not suitable for particular server. In our case Ansible skips APT task on Redhat servers and DNF task on Debian and Ubuntu.
6. Verify that the Playbook worked
From the control node run this command:
ansible linux -i inventory.ini -a "systemctl is-active nginx"
You should see:
ubuntu1 | CHANGED | rc=0 >>
active
alma1 | CHANGED | rc=0 >>
active
Both managed servers should provide the same output: active
Check if Nginx enabled to start automatically on every boot:
ansible linux -i inventory.ini -a "systemctl is-enabled nginx"
Both servers should return:
enabled
7. Run the Playbook again
As mentioned previously, Ansible will not make any changes if the required settings are already applied. This principle is called idempotency.
Run the same playbook again:
ansible-playbook -i inventory.ini first-playbook.yml -K
This time, if nothing has changed on your managed servers, the output should look like this:
TASK [Update APT cache and install Nginx (Ubuntu/Debian)] *******
ok: [ubuntu1]
skipping: [alma1]
TASK [Update DNF cache and install Nginx (AlmaLinux/Rocky)] *****
skipping: [ubuntu1]
ok: [alma1]
TASK [Start and enable Nginx] **********************************
ok: [ubuntu1]
ok: [alma1]
PLAY RECAP *****************************************************
ubuntu1 : ok=3 changed=0 unreachable=0 failed=0 skipped=1
alma1 : ok=3 changed=0 unreachable=0 failed=0 skipped=1
This time changed value will be:
changed=0
This means that Ansible checked the server configuration and detected no need to reinstall, restart or enable Nginx again.
8. Modify your Playbook
Now we will make a small change to see how Ansible detects differences in configuration.
For this example, we will modify the enable option.
Open the playbook:
nano first-playbook.yml
Find this task:
- name: Start and enable Nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
Replace with this content:
- name: Start Nginx without enabling it at boot
ansible.builtin.service:
name: nginx
state: started
enabled: false
Save the file.
Execute the playbook again:
ansible-playbook -i inventory.ini first-playbook.yml -K
Now Ansible will catch the mismatch between the Nginx startup config on the machine and the one in the playbook.
You should see:
TASK [Start Nginx without enabling it at boot]
changed: [ubuntu1]
changed: [alma1]
So Ansible updated the service config on both servers. The enabled: false setting stops Nginx from starting automatically after a reboot.
However, we still have:
state: started
So the Nginx remains running.
To restore the original configuration, open your playbook again:
nano first-playbook.yml
Change the content back to:
- name: Start and enable Nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
Save the file and then execute the playbook one more time:
ansible-playbook -i inventory.ini first-playbook.yml -K
Ansible again will detect the difference and enable automatic Nginx startup after every reboot.
Verify the result:
ansible linux -i inventory.ini -a "systemctl is-enabled nginx"
You should see:
enabled
This is one of the main advantages of Ansible playbooks. You edit a single file, run it again, and every server gets updated.
9. Run a Playbook on specific servers
If you need to run playbook tasks only on specific servers, you can use the --limit option. This option will restrict tasks execution only to specific hosts that you select.
For example, you can run playbook only on Ubuntu servers:
ansible-playbook -i inventory.ini first-playbook.yml --limit ubuntu -K
Or only on AlmaLinux servers:
ansible-playbook -i inventory.ini first-playbook.yml --limit alma -K
You can also specify an individual server:
ansible-playbook -i inventory.ini first-playbook.yml --limit ubuntu1 -K
In this case, Ansible will execute the playbook only on the ubuntu1 server.
ubuntu1 corresponds to the name you specified for a particular managed server in your inventory file.
This is useful when you want to test configuration changes on one server before applying them to the rest of your infrastructure.
10. Troubleshooting
10.1 YAML syntax errors
If you are getting this error message:
ERROR! We were unable to read either as JSON nor YAML
This means Ansible could not read the playbook due to mistake in YAML format:
incorrect indentation, missing colons or other YAML formatting problems. Check your playbook file and make sure that everything is correct.
10.2 UNREACHABLE
If you see:
fatal: [ubuntu1]: UNREACHABLE!
It means Ansible cannot connect to the managed server.
In this case, make sure to test the SSH connection manually:
If the connection fails when you run this command, check SSH username, server IP address, SSH port, firewall settings, and authentication method.
You can also test connectivity by running this command:
ansible all -i inventory.ini -m ping
10.3 Missing sudo password
If you receive an error related to sudo password, make sure you have sudo permissions.
Try running the playbook with his command
ansible-playbook -i inventory.ini first-playbook.yml -K
Ansible will prompt you for the sudo password.
If it still doesn’t work, log in to the managed server and test it there:
sudo whoami
If everything is correct, the command should return:
root
Otherwise, your user doesn’t have sudo privileges.
10.4 No hosts matched
Next error:
[WARNING]: Could not match supplied host pattern
Usually means that particular server or group not found in the inventory file
For example, if your playbook contains:
hosts: linux
Make sure that the linux group is also exists in inventory.ini file.
You can list all hosts from inventory file by running this command:
ansible all -i inventory.ini --list-hosts
Make sure you are running the command inside the directory where the inventory file is located.
10.5 Package installation fails
If Ansible can connect to the managed server but fails to install services, you should check if the package exists on your managed server system.
Run these commands:
Ubuntu:
sudo apt update
sudo apt install nginx -y
AlmaLinux:
sudo dnf install nginx -y
11. Useful Ansible Playbook commands
When working with Ansible playbooks, you often will need these commands:
Command | Description |
| Execute a playbook |
| Execute a playbook and ask for the sudo password |
| Check playbook syntax |
| Simulate playbook tasks without actually applying changes |
| Show targeted servers |
| Show tasks in the playbook |
| Execute only on the specific group (Ubuntu group in this example) |
| Display more detailed execution information |
12. Conclusion
In this guide, we learned how to properly create Ansible playbooks, verify their syntax, execute tasks on multiple managed servers, troubleshoot issues and make modifications to existing playbooks. In this example, we installed Nginx; however in the same way you can automate Apache, MariaDB, Docker, PHP, firewall and many other services installation and configuration.
For more information, you can refer to the official Ansible Playbooks documentation.
