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

Video