Playbooks are the entry points for any Ansible project.
In the simplest case, they contain an ordered set of individual steps (tasks) that describe how to achieve the desired configuration of the target hosts.
Although Ansible is not a programming language in the strictest sense, we want to follow the good-old tradition and create a “Hello World” playbook as our first official act. Playbooks are written in YAML, and they conventionally have the.yml or .yaml file extension. The playbook below is pretty much the simplest one possible:
---
- hosts: localhost
tasks:
- debug: msg="Hello Ansible!"
To ensure that your project folder isn’t cluttered over time, I suggest that you create a playbooks/ folder for playbooks as follows:
$ cd ~/ansible/projects/start
$ mkdir playbooks
Then, you should store the playbook file in this new folder.
Before we start the whole thing, we’ll explain the content:
You can also provide a list of patterns, like this one:
- hosts:
- debian
- rocky
Or you can provide a list of patterns in the compact YAML list form:
- hosts: [debian, rocky]
Or you can even provide a list of patterns as one Ansible pattern string:
- hosts: debian, rocky
At first glance, it might not be entirely clear to you why the playbook begins with a list. (Technically speaking in YAML, the content of our playbook is a list with a single element!) The explanation is that a playbook can consist of several so-called plays, each of which consists of a set of hosts to which certain tasks are assigned. However, you will not need playbooks with more than one play until much later.
To start the whole thing (at last), you use the ansible-playbook command and pass the path to the playbook as a parameter. If your current working directory is the root folder of your project, then it looks like this:
$ ansible-playbook playbooks/hello-ansible.yml
PLAY [localhost] ************************************************************
TASK [Gathering Facts] ******************************************************
ok: [localhost]
TASK [debug] ****************************************************************
ok: [localhost] => {
"msg": "Hello Ansible!"
}
PLAY RECAP ******************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0
skipped=0 rescued=0 ignored=0
The output requires some clarification:
- hosts: localhost
gather_facts: no
In our chosen directory structure, you must live with the minor inconvenience of having to make all calls from the root folder of the project, as follows:
$ ansible-playbook playbooks/hello-ansible.yml
You must do this unless you use the direnv software or a similar clever method that gives you more flexibility. From now on, I will omit the playbooks/ path component in the examples to spare you this redundancy:
$ ansible-playbook hello-ansible.yml
The fact is that localhost is not a common target for administrative tasks, so let’s address all the actual targets in the test lab:
---
- hosts: all
tasks:
- debug: msg="Hello Ansible!"
And to make things a bit more exciting, you can shut down a machine (e.g., suse) beforehand (by using vagrant halt suse in the vagrant/ folder). The result will be something like this:
$ ansible-playbook hello-all.yml
PLAY [all] ******************************************************************
TASK [Gathering Facts] ******************************************************
ok: [rocky]
ok: [debian]
ok: [ubuntu]
fatal: [suse]: UNREACHABLE! => {"changed": false, "msg":
"Failed to connect to the host via ssh: ssh: connect to host suse port 22:
Connection timed out”: "unreachable": true}
TASK [debug] ****************************************************************
ok: [debian] => {
"msg": "Hello Ansible!"
}
ok: [rocky] => {
"msg": "Hello Ansible!"
}
ok: [ubuntu] => {
"msg": "Hello Ansible!"
}
PLAY RECAP ******************************************************************
debian : ok=2 changed=0 unreachable=0 failed=0
rocky : ok=2 changed=0 unreachable=0 failed=0
suse : ok=0 changed=0 unreachable=1 failed=0
ubuntu : ok=2 changed=0 unreachable=0 failed=0
First, you’ll notice that there seems to be no fixed order of target systems per task. Due to parallel processing, the order is indeed quite random.
You should also note that suse was completely out of the game after its inaccessibility was determined, as the second task did not even attempt to contact this host.
Finally, the concluding statistics should not pose any puzzles here either. (I have abbreviated the output slightly to save space.) If you have followed the example in the lab, then remember to let the powered-off machine participate again (by using vagrant up suse).
Editor’s note: This post has been adapted from a section of the book Ansible: The Practical Guide for Administrators and DevOps Teams by Axel Miesen. Axel is an Ansible coach. His interest in Linux systems began with his studies at the University of Kaiserslautern, where he studied mathematics and computer science. After graduating in 1998, he began working as a consultant and trainer and has passed on his Linux knowledge and experience to numerous professionals.
This post was originally published 10/2025.