Hello!
Today I will show what ansible-playbook is and how to create it.
Playbook
Ansible Playbooks is an iterative, reusable, configuration management and deployment framework that is well-suited for deploying complex applications. If you need to run a task with Ansible more than once, you can do it with a playbook.
Let’s create the simplest playbook. In the playbook, you need to specify its name, the group of servers on which it will be executed, and the tasks themselves.
---
- name: Update web servers
hosts: webapp
tasks:
- name: Ensure apache is at the latest version
ansible.builtin.yum:
name: httpd
state: latest
become: true
In this example, the playbook is executed on the webapp
server group. On all servers from this group, ansible installs the latest version of the httpd
package using the yum module. Before executing the task, ansible checks whether the package is installed, if it is installed, nothing will happen.
The playbook is started using the ansible-playbook
command. Let’s save our playbook under the name main.yml
and run it
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Ensure apache is at the latest version] **********************************************************************************
changed: [3.92.61.18]
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Ansible host variables
Before the playbook starts executing, ansible collects information about the server. This option can be disabled by specifying gather_facts: false
in the playbook. Ansible collects a lot of information about the operating system, network settings, and more in this way
{
"ansible_all_ipv4_addresses": [
"REDACTED IP ADDRESS"
],
"ansible_all_ipv6_addresses": [
"REDACTED IPV6 ADDRESS"
],
"ansible_apparmor": {
"status": "disabled"
},
"ansible_architecture": "x86_64",
"ansible_bios_date": "10/02/2022",
"ansible_bios_version": "4.1.5",
To see all information, you can call the run command ansible command setup, and all parameters will be printed.
ansible -i inventory all -m ansible.builtin.setup
Debug
Sometimes you need to debug the execution of the playbook or look at the values of some parameters, all this can be done using the debug module.
For example, I need to see the value of the ansible_bios_version
parameter, obtained using gather_facts in the playbook startup process. This can be done like this:
- name: Print bios version
ansible.builtin.debug:
msg: Bios version is {{ ansible_bios_version }}
$ ansible-playbook -i inventory main.yml ✘ INT ansible-210 3.9.13 17:29:55
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Print bios version] ******************************************************************************************************
ok: [3.92.61.18] => {
"msg": "Bios version is 4.11.amazon"
}
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Register and Set Facts
Also, in Ansible, you can create parameters based on the result of the task. For example, I want to get how long the system has been running. This can be done by calling the Linux uptime
command. But if I do it like this:
- name: Get uptime information
ansible.builtin.shell: /usr/bin/uptime
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Get uptime information] **************************************************************************************************
changed: [3.92.61.18]
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
As a result, I get nothing. To get a response from the uptime
command, I need to save the result in a variable. This is done using register: VAR_NAME
and then the value of this variable can be output to the terminal.
- name: Get uptime information
ansible.builtin.shell: /usr/bin/uptime
register: sys_uptime
- name: Print system uptime
ansible.builtin.debug:
msg: "{{ sys_uptime }}"
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Get uptime information] **************************************************************************************************
changed: [3.92.61.18]
TASK [Print system uptime] *****************************************************************************************************
ok: [3.92.61.18] => {
"msg": {
"changed": true,
"cmd": "/usr/bin/uptime",
"delta": "0:00:00.005060",
"end": "2022-10-07 14:31:50.846284",
"failed": false,
"msg": "",
"rc": 0,
"start": "2022-10-07 14:31:50.841224",
"stderr": "",
"stderr_lines": [],
"stdout": " 14:31:50 up 15 min, 1 user, load average: 0,01, 0,05, 0,02",
"stdout_lines": [
" 14:31:50 up 15 min, 1 user, load average: 0,01, 0,05, 0,02"
]
}
}
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
If during execution you need to create a parameter, this can be done using the [set_fact] (https://docs.ansible.com/ansible/latest/collections/ansible/builtin/set_fact_module.html) module
- name: Setting host facts using complex arguments
ansible.builtin.set_fact:
important_var: important_value
- name: Print important value
ansible.builtin.debug:
msg: "{{ important_var }}"
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Setting host facts using complex arguments] ******************************************************************************
ok: [3.92.61.18]
TASK [Print important value] ***************************************************************************************************
ok: [3.92.61.18] => {
"msg": "important_value"
}
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Condition
It is good when the playbook is universal and can work on different operating systems. But what if we have specific tasks that should be performed in Ubuntu but not in RedHat. This can be done using conditions. Let’s try to create a universal playbook that will install apache http regardless of the OS version. For RedHat we need to use the yum module and for Ubuntu apt. Let’s use the condition when
- name: Install apache on Redhat
ansible.builtin.yum:
name: httpd
state: latest
when: ansible_facts['os_family'] == "RedHat"
- name: Install apache on Debian
ansible.builtin.apt:
name: apache2
state: latest
when: ansible_facts['os_family'] == "Debian"
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Install apache on Redhat] ************************************************************************************************
ok: [3.92.61.18]
TASK [Install apache on Debian] ************************************************************************************************
skipping: [3.92.61.18]
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
Thus, the task is executed only if the condition returns `True’, otherwise not.
Block
Block is used to logically combine tasks in Ansible. For example, you can organize all tasks for Ubuntu and apply a condition at the block level. For example, sometimes not.
- name: Debian block
block:
- name: Install apache on Ubuntu
ansible.builtin.apt:
name: apache2
state: latest
- name: Start service apache2, if not started
ansible.builtin.service:
name: apache2
state: started
when: ansible_facts['os_family'] == "Debian"
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Install apache on Ubuntu] ************************************************************************************************
skipping: [3.92.61.18]
TASK [Start service apache2, if not started] ***********************************************************************************
skipping: [3.92.61.18]
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=1 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
You can also handle errors using rescue and always. The rescue block is executed only if an error occurs in the main block. Always is always launched, regardless of what happened in the previous block.
- name: Error block
block:
- name: Print a message
ansible.builtin.debug:
msg: 'I execute normally'
- name: Force a failure
ansible.builtin.command: /bin/false
- name: Never print this
ansible.builtin.debug:
msg: 'I never execute, due to the above task failing, :-('
rescue:
- name: Print when errors
ansible.builtin.debug:
msg: 'I caught an error'
- name: Force a failure in middle of recovery! >:-)
ansible.builtin.command: /bin/false
- name: Never print this
ansible.builtin.debug:
msg: 'I also never execute :-('
always:
- name: Always do this
ansible.builtin.debug:
msg: "This always executes"
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Print a message] *********************************************************************************************************
ok: [3.92.61.18] => {
"msg": "I execute normally"
}
TASK [Force a failure] *********************************************************************************************************
fatal: [3.92.61.18]: FAILED! => {"changed": true, "cmd": ["/bin/false"], "delta": "0:00:00.003974", "end": "2022-10-07 14:37:27.522485", "msg": "non-zero return code", "rc": 1, "start": "2022-10-07 14:37:27.518511", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
TASK [Print when errors] *******************************************************************************************************
ok: [3.92.61.18] => {
"msg": "I caught an error"
}
TASK [Force a failure in middle of recovery! >:-)] *****************************************************************************
fatal: [3.92.61.18]: FAILED! => {"changed": true, "cmd": ["/bin/false"], "delta": "0:00:00.002841", "end": "2022-10-07 14:37:30.324774", "msg": "non-zero return code", "rc": 1, "start": "2022-10-07 14:37:30.321933", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
TASK [Always do this] **********************************************************************************************************
ok: [3.92.61.18] => {
"msg": "This always executes"
}
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=4 changed=0 unreachable=0 failed=1 skipped=0 rescued=1 ignored=0
Loop
Ansible has several types of loops. Let’s start with the simplest loop through the list. In this programming, we will check only the simplest one
Simple list
In this case, several users (testuser1 and testuser2) are created, which are specified in the loop block, and {{ item }}
is specified instead of the user name, which will be replaced by the user name when executed
- name: Add several users
ansible.builtin.user:
name: "{{ item }}"
state: present
groups: "wheel"
loop:
- testuser1
- testuser2
become: true
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Add several users] *******************************************************************************************************
changed: [3.92.61.18] => (item=testuser1)
changed: [3.92.61.18] => (item=testuser2)
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
List of hashes
This is how the cycle looks like through the list of hash maps. The cycle looks the same as the previous one but with one difference in the task block the name of the key from the map is added to {{ item }}
and then the value of the name field will look like "{{ item.name }}"
and the group "{ { item.groups }}"
- name: Add several users
ansible.builtin.user:
name: "{{ item.name }}"
state: present
groups: "{{ item.groups }}"
loop:
- { name: 'testuser3', groups: 'wheel' }
- { name: 'testuser4', groups: 'root' }
become: true
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Add several users] *******************************************************************************************************
changed: [3.92.61.18] => (item={'name': 'testuser3', 'groups': 'wheel'})
changed: [3.92.61.18] => (item={'name': 'testuser4', 'groups': 'root'})
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Register var in loop
If we use a loop and want to use register
to save the output of the task, then the result in this case will be saved in a list.
- name: Register loop output as a variable
ansible.builtin.shell: "echo {{ item }}"2022-10-02
loop:
- "one"
- "two"
register: echo
- name: Print variable
ansible.builtin.debug:
msg: "{{ echo }}"
{
"changed": true,
"msg": "All items completed",
"results": [
{
"changed": true,
"cmd": "echo \"one\" ",
"delta": "0:00:00.003110",
"end": "2022-10-02 12:00:05.187153",
"invocation": {
"module_args": "echo \"one\"",
"module_name": "shell"
},
"item": "one",
"rc": 0,
"start": "2022-10-02 12:00:05.184043",
"stderr": "",
"stdout": "one"
},
{
"changed": true,
"cmd": "echo \"two\" ",
"delta": "0:00:00.002920",
"end": "2022-10-02 12:00:05.245502",
"invocation": {
"module_args": "echo \"two\"",
"module_name": "shell"
},
"item": "two",
"rc": 0,
"start": "2022-10-02 12:00:05.242582",
"stderr": "",
"stdout": "two"
}
]
}
Delegate To
delegate_to
is used if you need to transfer the execution of tasks to another server or locally. For example, the server is configured, and when finished, it must be added to the load balancer. But the server itself may not have such rights, so the execution of this task can be transferred to a local server that has such rights.
- name: Add server to load balancer
ansible.builtin.command: echo "Adding to loadbalancer"
delegate_to: 127.0.0.1
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Add server to load balancer] *********************************************************************************************
changed: [3.92.61.18 -> 127.0.0.1]
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Failed when
If ansible executes a shell script, it cannot always correctly determine whether the task was completed successfully or not, in such cases, you can use the parameter failed_when
, which will describe in which cases the task is considered not completed successfully
- name: Making sure the Physical Memory more than 2gb
ansible.builtin.shell: "cat /proc/meminfo|grep -i memtotal|awk '{print $2/1024/1024}'"
register: memory
failed_when: "memory.stdout|float < 2"
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Making sure the Physical Memory more than 2gb] ***************************************************************************
fatal: [3.92.61.18]: FAILED! => {"changed": true, "cmd": "cat /proc/meminfo|grep -i memtotal|awk '{print $2/1024/1024}'", "delta": "0:00:00.006063", "end": "2022-10-07 14:46:37.761174", "failed_when_result": true, "msg": "", "rc": 0, "start": "2022-10-07 14:46:37.755111", "stderr": "", "stderr_lines": [], "stdout": "0.943119", "stdout_lines": ["0.943119"]}
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Ignore Errors
If we have tasks in which we expect that there may be an error, but we still want to continue the execution, then we can add the parameter ignore_errors: yes
and the execution of the playbook will continue.
- name: Do not count this as a failure
ansible.builtin.command: /bin/false
ignore_errors: yes
$ ansible-playbook -i inventory main.yml
PLAY [Update web servers] ******************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [3.92.61.18]
TASK [Do not count this as a failure] ******************************************************************************************
fatal: [3.92.61.18]: FAILED! => {"changed": true, "cmd": ["/bin/false"], "delta": "0:00:00.002920", "end": "2022-10-07 14:43:48.380284", "msg": "non-zero return code", "rc": 1, "start": "2022-10-07 14:43:48.377364", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring
PLAY RECAP *********************************************************************************************************************
3.92.61.18 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1