What are Ansible Playbooks
- Ansible playbooks are text files or configuration files that are written in particular format called YAML.
- These are expressed in the YAML format and have a minimum of syntax, which is not to be a programming language or script, but rather a model of a configuration.
- Each playbook includes one or more "plays" in a list.
- The goal of a play is to map a group of hosts to some well-defined roles and tasks.
- A
task
is nothing but a call or operation, which applies on group ofhosts
.
Example-1: Your first playbook to install single package
Let us write a simple playbook to install wget
rpm on all our managed nodes.
So we will install wget
on these 2 hosts. Below is our ansible playbook. I have added comments for better understanding:
[ansible@controller ~]$ cat install_wget.yml # --- represents this file as a playbook --- # Use [space]play[space] where play is combination of hosts+tasks - hosts: all # Become root user become: yes # List of tasks to be executed tasks: - yum: name=wget state=absent
Our playbook contains one play. Each play consists of the following two important parts:
- What to configure: We need to configure a host or group of hosts to run the play against. Also, we need to include useful connection information, such as which user to connect as, whether to use sudo command, and so on.
- What to run: This includes the specification of tasks to be run, including which system components to modify and which state they should be in, for example, installed, started, or latest. This could be represented with tasks and later on, by roles.
Creating a host inventory
Before we even start writing our playbook with Ansible, we need to define an inventory of all hosts that need to be configured, and make it available for Ansible to use. You can also choose to use dynamic inventory which we have already learned in earlier chapter. Here I will keep it simple and stick to static inventory.
We will use the default inventory file /etc/ansible/hosts
to define our managed nodes under with following content
server1 server2 server3
Earlier I had deployed server3
without passphrase to understand how we can work with managed nodes with password. Now I will re-deploy the public key to server3
to have a password less communication between controller
and server3
[ansible@controller ~]$ ssh server3
Activate the web console with: systemctl enable --now cockpit.socket
Last login: Mon Sep 21 09:49:33 2020 from 172.31.7.253
[ansible@server3 ~]$ logout
Connection to server3 closed.
Our password less communication is working with server3
Using hosts pattern
We have used - hosts: all
in our playbook which means that the playbook will be executed for all the nodes found in the inventory. We can also define patterns instead of "all
" to match a selective set of hosts or groups from the inventory file.
Pattern Types | Examples |
---|---|
Group Name | app |
Match all | all |
Range | server[000:999] |
Hostname/Hostname globs | *.example.com, host01.example.com |
Exclusions | app:!server3 |
Regular Expressions | ~(nn|zk).*\.example\.org |
We have already discussed about these individual patterns in "Working with Inventory Files"
Tasks
Plays map hosts to tasks
. Tasks are a sequence of actions performed against a group of hosts that match the pattern specified in a play. Each play typically contains multiple tasks that are run serially on each machine that matches the pattern.
For example, following snippet is from our playbook
tasks: - yum: name=wget state=present
Under tasks
we have defined the module which should be used to execute the task i.e. "yum
". This module expects certain arguments to complete the task such as name of the package, state
which defines the action present/latest/absent
. We have defined these values under new line for better readability.
Additionally since installing new rpm requires root privilege hence we have also used become: yes
in the playbook
Running the playbook
Ansible comes with the ansible-playbook
command to launch a playbook with. Let us execute this playbook
[ansible@controller ~]$ ansible-playbook install_wget.yml
Here is what happens when you run the preceding command:
- The
ansible-playbook
parameter is the command that takes the playbook as an argument (install_wget.yml
) and runs the plays against the hosts - The
install_wget
parameter contains the single play that we created i.e. to installwget
rpm - The
hosts: all
parameter is our host's inventory, which lets Ansible know which hosts, or groups of hosts, to call plays against
Launching the preceding command will start calling plays, orchestrating in the sequence that we described in the playbook. Here is the output of the preceding command:
- Ansible reads the playbooks specified as an argument to the ansible-playbook command and starts executing plays in the serial order.
- Since we have declared single play, it runs against the "
all
" hosts. Theall
keyword is a special pattern that will match all hosts. So, thetasks
in the this play will be executed on all hosts in the inventory we passed as an argument. - Before running any of the tasks, Ansible will gather information about the systems that it is going to configure. This information is collected in the form of facts. If you recall I had mentioned this in "Ansible Facts" chapter. In the playbook we have not mentioned anything about gathering facts and yet "
setup
" module was called to collect the facts before executing the tasks - The next section is the output from tasks where
yum
module is used to install the packages. The output showsOK
forserver1
andserver2
but forserver3
it says changed. This would mean that onserver1
andserver2
the execution was successful but nothing was changed i.e. nothing was installed. It is possible thatwget
was already in installed state on these servers hence ansible skipped any changes. While onserver3
we have the output as "changed
" which means thatwget
was successfully installed - Finally, Ansible prints the summary of the playbook run in the "
PLAY RECAP
" section. It indicates how many modifications were made, if any of the hosts were unreachable, or execution failed on any of the systems.
Let us use "state=absent" instead of "present" with yum
module inside the playbook to remove the wget
rpm from all our managed nodes:
Now we re-ran the playbook by use "state=absent
" and we see that "changed=1
" so this means the execution has successfully changed one data where wget
was removed from the managed nodes.
Example-2: Install multiple packages on different managed nodes
Now let us take things forward and in this example we will install multiple packages using single playbook file. I have modified my inventory file
[ansible@controller ~]$ cat /etc/ansible/hosts [web] server1 server2 [app] server3
So as you see now I have divided my managed nodes into two groups where server1
and server2
are part of web
group while server3
is part of app
group. We will install httpd
rpm on the web
group while on app
group we will install wget
and vim
rpm.
Following is our second_playbook.yml
--- - hosts: app become: yes tasks: - yum: name=wget state=latest - yum: name=vim state=latest - hosts: web become: yes tasks: - yum: name=httpd state=latest
I have removed the comments from this YAML file as now you should be familiar with the flow. We have created two play in a single playbook. Here in the first play we will install wget
and vim
on hosts part of "app
" group. In the second play we will install httpd
on the hosts part of "web
" group.
Let us execute the playbook:
[ansible@controller ~]$ ansible-playbook second_playbook.yml PLAY [app] ************************************************************************************************** TASK [Gathering Facts] ************************************************************************************** ok: [server3] TASK [yum] ************************************************************************************************** changed: [server3] TASK [yum] ************************************************************************************************** changed: [server3] PLAY [web] ************************************************************************************************** TASK [Gathering Facts] ************************************************************************************** ok: [server2] ok: [server1] TASK [yum] ************************************************************************************************** changed: [server2] changed: [server1] PLAY RECAP ************************************************************************************************** server1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 server2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 server3 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The output should be pretty clear now:
- The first play i.e "
PLAY [app]
" was executed as part of which the first step was to gather facts on all the servers part of "app
" group. Then the tasks were expected onapp
group i.e. installing two rpms - In the second play i.e. "
PLAY [web]
" againsetup
module was called to gather all the facts from all the hosts part of "web" group. Next the tasks are executed i.e. installingwget
andvim
rpm on the respective managed nodes PLAY_RECAP
refers to the final result. Onserver3
we have changed=2 which means 2 rpms were installed while onserver1
andserver2
we have changed=1 because singlehttpd
package was installed using this playbook.
Example-3: Disable gathering facts module
We know that by default ansible-playbook will execute setup module to gather facts from the respective managed nodes. We also have an option to disable this feature if we feel this is not required using "gather_facts: false
"
Let us use this in our playbook, remove_gather_facts.yml with following content:
--- - hosts: server1 become: yes gather_facts: false tasks: - yum: name=wget state=absent
In this playbook we will remove wget rpm as root user and additionally we have also disabled "ansible facts".
Following the the output from the execution of this playbook. This time the output is must shorter because ansible didn't collected any facts from server3
Example-4: Assign custom name to the play and tasks
In all the examples above if you observe the output of ansible-playbook, the name of the play is based on the value of "hosts
", similarly the name of the TASK
is based on the module
used. Now this may seem confusing when we have a single playbook with multiple plays so we would like to give a custom name to individual play and tasks by using name=<NAME>
.
For example here we have a playbook where I have a single PLAY which will perform two tasks.
- Copy
/tmp/demo.txt
from the ansible engine to the hosts part ofweb
group in my inventory - Create an empty file again on the hosts part of
web
group in my inventory
Sample assign_custom_name.yml
--- - name: Hello World hosts: web gather_facts: false tasks: - name: Copy file to web group copy: src=/tmp/demo.txt dest=~/ - name: Create an empty file on web group file: path=/tmp/src_file.txt state=touch
Here since we have two tasks we have used hyphen
twice. A "name=" is defined for the PLAY and both the tasks. So now let us execute this playbook:
You can check the output and now we have proper name visible for PLAY
and both the TASK
.
Example-5: Execute playbook as shell scripts
We also have an option to call the playbook as we would call a shell script. To achieve this we must replace the first line with three dash "---
" to the path of ansible-playbook
binary "/usr/bin/ansible-playbook
" with she-bang character as we would do for shell script.
You can get the path of ansible-playbook
binary using which
command:
~]$ which ansible-playbook /usr/bin/ansible-playbook
We will use our last example script assign_custom_name.yml
to replace the first line with this binary path in the playbook as shown below
#!/usr/bin/ansible-playbook - name: Hello World hosts: web gather_facts: false tasks: - name: Copy file to web group copy: src=/tmp/demo.txt dest=~/ - name: Create an empty file on web group file: path=/tmp/src_file.txt state=touch
Provide executable permission to the playbook:
[ansible@controller ~]$ chmod u+x assign_custom_name.yml
Let's run our playbook as we would execute a shell script:
[ansible@controller ~]$ ./assign_custom_name.yml PLAY [Hello World] ************************************************************************************************************************ TASK [Copy file to web group] ************************************************************************************************************* ok: [server1] ok: [server2] TASK [Create an empty file on web group] ************************************************************************************************** changed: [server1] changed: [server2] PLAY RECAP ******************************************************************************************************************************** server1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 server2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Example-6: Print debug message with playbooks
In all the examples till now we are performing tasks but printing nothing on the console. We did changed the name of the play and tasks but those are different use case. In all other scripts and code we have an option to print text on the console while performing any tasks such as by using echo
, print
, printf
etc. Similarly in ansible we have debug
module which is nothing but Ansible’s version of a print
statement.
You can use debug
module within tasks section to print messages which can help you debug
the playbook.
I have written a small playbook print-message.yml
which just prints a message "Hello World" at the tasks
section:
--- - hosts: server2 tasks: - debug: msg="Hello World"
Let us execute this playbook:
So we have an output on the console as expected. If you have to print multi-line text then you have to place the msg
in the next line:
--- - hosts: server2 tasks: - debug: msg: - "This is first line" - "This is second line" - "This is third line"
Observe the indentation. I have placed msg
in the next line and added some extra whitespace. All the text messages are again added on individual lines starting with -
(dash) with some additional whitespace.
Let us check the output from the playbook when executed:
Example-7: Increase verbosity level of playbook
We can add debug message which is easy to debug individual tasks but to debug the entire playbook we also have an option to print verbose message and increase verbosity level to get more detailed output of activity performed in the backend.
In this sample yml "increase-verbosity.yml
" file I have created two tasks where in the first task I will print a message without any verbosity while in the second task I am printing a message with increased verbosity.
--- - hosts: server2 tasks: - name: default verbose debug: msg: - This is a test message without verbosity - name: verbosity level 2 debug: msg: - This is a test message with verbosity level 2 verbosity: 2
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook increase-verbosity.yml PLAY [server2] ******************************************************************************************************* TASK [Gathering Facts] *********************************************************************************************** ok: [server2] TASK [default verbose] *********************************************************************************************** ok: [server2] => { "msg": [ "This is a test message without verbosity" ] } TASK [verbosity level 2] ********************************************************************************************* skipping: [server2] PLAY RECAP *********************************************************************************************************** server2 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
From the output we see that our message with no verbosity was printed but the one with verbose level 2 was skipped.
The message with verbosity was skipped because in such case ansible expects the playbook to be executed with verbose input, so we will re-run the playbook with "-vv
" argument:
[ansible@controller ~]$ ansible-playbook increase-verbosity.yml -vv ansible-playbook 2.9.13 config file = /etc/ansible/ansible.cfg configured module search path = ['/home/ansible/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python3.6/site-packages/ansible executable location = /usr/bin/ansible-playbook python version = 3.6.8 (default, Apr 16 2020, 01:36:27) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)] Using /etc/ansible/ansible.cfg as config file PLAYBOOK: increase-verbosity.yml ************************************************************************************* 1 plays in increase-verbosity.yml PLAY [server2] ******************************************************************************************************* TASK [Gathering Facts] *********************************************************************************************** task path: /home/ansible/increase-verbosity.yml:2 ok: [server2] META: ran handlers TASK [default verbose] *********************************************************************************************** task path: /home/ansible/increase-verbosity.yml:4 ok: [server2] => { "msg": [ "This is a test message without verbosity" ] } TASK [verbosity level 2] ********************************************************************************************* task path: /home/ansible/increase-verbosity.yml:8 ok: [server2] => { "msg": [ "This is a test message with verbosity level 2" ] } META: ran handlers META: ran handlers PLAY RECAP *********************************************************************************************************** server2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now we see a more detailed output on the console. You may increase the count of "-v
" to get much more detail which can fill up your console.
Example-8: Perform syntax check
Defining whether a file has the right syntax or not is fairly easy for a machine, but might be more complex for humans. This does not mean that machines are able to fix the code for you, but they can quickly identify whether a problem is present or not.
When we execute a playbook, ansible will any how perform a syntax check but then they may break the functionality by partially performing the tasks. So before actually executing the playbook we can perform a syntax check
I have created a new yaml file syntax_check.yml
and I have intentionally added extra whitespace at "- debug
". Due to which both msg
and - debug
are starting at the same line. I should have also add some extra whitespace for line with "msg:
"
--- - hosts: all tasks: - debug: msg: "Hello World"
Let us execute the script using --syntax-check
to check the syntax of the playbook:
[ansible@controller ~]$ ansible-playbook syntax_check.yml --syntax-check
ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each:
JSON: Expecting value: line 1 column 1 (char 0)
Syntax Error while loading YAML.
did not find expected '-' indicator
The error appears to be in '/home/ansible/syntax_check.yml': line 5, column 8, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- debug:
msg: "Hello World"
^ here
Now I will fix the indentation and re-execute the --syntax-check
:
[ansible@controller ~]$ ansible-playbook syntax_check.yml --syntax-check
playbook: syntax_check.yml
When the syntax check doesn't find any errors, the output will resemble the previous one, where it listed the files that were analysed without listing any errors.
Since Ansible knows all the supported options in all the supported modules, it can quickly read your code and validate whether the YAML you provided contains all the required fields and that it does not contain any unsupported fields.
Example-9: Perform dry run of playbooks
Although you might be confident in the code you have written, it still pays to test it before running it for real in a production environment. In such cases, it is a good idea to be able to run your code, but with a safety net in place. This is what check mode is for:
Let's create a playbook called check-mode.yml
that contains the following content:
--- - hosts: server2 tasks: - name: Install nano yum: name=nano state=latest
This playbook is expected to install nano
rpm on server2
. You may have already realised what is wrong here? Installing an rpm requires root level privilege which is missing in this playbook.
But let us execute this playbook in dry mode using --check
and verify the output:
[ansible@controller ~]$ ansible-playbook --check check-mode.yml PLAY [server2] **************************************************************************************************************************** TASK [Gathering Facts] ******************************************************************************************************************** ok: [server2] TASK [Install nano] *********************************************************************************************************************** fatal: [server2]: FAILED! => {"changed": false, "msg": "This command has to be run under the root user.", "results": []} PLAY RECAP ******************************************************************************************************************************** server2 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
As expected the dry run has told us the problem i.e. missing root level privilege. The idea is that the run won't change the state of the machine and will only highlight the differences between the current status and the status declared in the playbook.
Not all modules support check mode, but all major modules do, and more and more modules are being added at every release. In particular, note that the command
and shell
modules do not support it because it is impossible for the module to tell what commands will result in a change, and what won't. Therefore, these modules will always return changed when they're run outside of check mode because they assume a change has been made
I personally also didn't had success with "file
" module with dry mode but it is possible in future we will have better results but dry mode can help you with a bunch of other ansible modules so you should definitely use it before executing a playbook in production environment.
What's Next
Next in our Ansible Tutorial we will use Microsoft Visual Studio Code Editor to write playbooks using GUI