Ansible playbooks

Here the Video Transcript

An Ansible playbook is a collection of plays or instructions defined against a host or a group of hosts. Let us start by writing a simple example for the master node:

---
- name: Enable/start some services for master node
  hosts: master

  tasks:
    - name: enable/start httpd service
      service:
        name: httpd
        state: started
        enabled: yes

    - name: enable/start named service
      service:
        name: named
        state: started
        enabled: yes

    - name: enable/start cobblerd service
      service:
        name: cobblerd
        state: started
        enabled: yes
...

In this example, we have three different tasks (plays) to enable/start the system services httpd, named and cobblerd on the master node. The playbook is written in YAML syntax, and therefore needs to follow the correct indentation in order to avoid errors. Let us now go through the different YAML tags used here:

  1. name: This tag specifies the name of the Ansible playbook or a task. A logical description in this tag will help to the reader to understand the purpose of the playbook as well as to debug it.

  2. hosts: This tag specifies the lists of hosts or host group in which we want to run the tasks. The hosts tag is mandatory.

  3. tasks: This is the list of actions to be performed. Each task includes a piece of code called module (in this example, the module service). If the module requires arguments, they must be provided within indentation after the module invocation (in our example, the arguments name, state and enabled).

To run this playbook, the ansible-playbook command should be used instead of ansible. If the inventory hosts file to be used is not located in /etc/ansible/hosts, it must be specified with the -i option. Using the inventory created in Ansible Basics, the playbook is executed in the following manner:

[root@master ~]# ansible-playbook -i inventory  master.yml

PLAY [Enable/start some services for master node] **********************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]

TASK [enable/start httpd service] **************************************************************************************
ok: [localhost]

TASK [enable/start named service] **************************************************************************************
ok: [localhost]

TASK [enable/start cobblerd service] ***********************************************************************************
ok: [localhost]

PLAY RECAP *************************************************************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0

Working with variables

The vars tag lets you to define the variables which you can use in your playbooks. Usage is similar to variables in any programming language. For instance, in the following task

- name: ensure a list of packages installed
  yum:
    name: "{{ packages }}"
  vars:
    packages:
    - httpd
    - httpd-tools

the vars tag defines the variable packages as a list. In general, Ansible supports YAML variables, including dictionaries, lists, single-value attributes, etc. Once you have defined variables, you can use them in your playbooks using the Jinja2 templating system, look at the usage of the package variable in the previous example:

name: "{{ packages }}"

Variables are defined in the tag: value syntax. In a more advance use of ansible, variables can be stored in files within a vars folder inside a role (see Introduction to roles).

Another special kind of variables are the Ansible facts. These facts are variables discovered by Ansible as a result of the interaction with a system and not set by the user. For instance, the OS distribution, the IP address, among others. To see what facts are available on a particular system, you can add the following task in a playbook:

- debug: var=ansible_facts

or run the following command:

[root@master ~]# ansible <hostname> -m setup

Note that in the execution of a playbook, the first step is Gathering Facts unless it is explicitly suppressed. You can disable the fact gathering by adding

gather_facts: no

after the host declaration. Depending on your needs, facts can also be declared by the user using the module set_fact.

Iterative items (loops)

Loops can be used to gather some common tasks. For instance, the previous example can be simplified to one task with the tag with_items as follows:

---
- name: Configuration for the master node
  hosts: master

  tasks:
    - name: enable/start services
      service:
        name: "{{ item }}"
        state: started
        enabled: yes
      with_items:
       - httpd
       - named
       - cobblerd
...

Here, the variable "{{ item }}" will iterate over the list with_items. The execution of this playbook is now:

[root@master ~]# ansible-playbook -i inventory  master.yml

PLAY [Configuration for the master node] **********************************************************************

TASK [Gathering Facts] ****************************************************************************************
ok: [localhost]

TASK [enable/start services] **********************************************************************************
ok: [localhost] => (item=httpd)
ok: [localhost] => (item=named)
ok: [localhost] => (item=cobblerd)

PLAY RECAP ****************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0

Ansible modules

Besides the service module we have been using so far, Ansible contains a wide variety of modules. The most used Ansible modules for our purposes are:

Module

Description

Example

shell

Execute commands in nodes.

- name: Shell module example
  shell: ping
  args:
    creates: /path/file  # skip if this exists
    removes: /path/file  # skip if this is missing
    chdir: /path         # cd here before running

script

Runs a local script on a remote node after transferring it.

- name: Script module example
  script: /x/y/script.sh
  args:
    creates: /path/file  # skip if this exists
    removes: /path/file  # skip if this is missing
    chdir: /path

yum

Manages packages with the yum package manager.

- name: Install the latest version of Apache
  yum:
    name: httpd
    state: latest

file

Sets attributes of files.

- name: File module example
  file:
    path: /etc/dir
    state: directory # file | link | hard | touch | absent
    # Optional:
    owner: bin
    group: wheel
    mode: 0644
    recurse: yes  # mkdir -p
    force: yes    # ln -nfs

copy

Copies files to remote locations.

- name: Copy example
  copy:
    src: /x/y/inventory
    dest: /etc/ansible/hosts
    # Optional:
    owner: user
    group: user
    mode: 0644
    backup: yes

debug

Print statements during execution.

- name: Debug module example
  debug:
    msg: "Hello {{ var }}"

For a list of all available modules, see Module Index, or run the following at a command prompt:

[root@master ~]# ansible-doc -l

Working with conditionals

conditionals can be used inside playbooks to regulate the execution of certain tasks depending on the outcome of previous tasks. For instance,

- name: Check if CentOS 7 Distro has been imported
  command: cobbler profile report --name CentOS-7-2009-x86_64
  register: distro_exists
  ignore_errors: True

- name: Download CentOS 7 ISO
  get_url:
    url: http://mirror.cs.pitt.edu/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso
    dest: /provisioning/iso/CentOS-7-x86_64-DVD-2009.iso
    checksum: sha256:e33d7b1ea7a9e2f38c8f693215dd85254c3a4fe446f93f563279715b68d07987
  when: distro_exists is failed

In the previous example, the task get_url will be executed only if command fails. The variable containing the information of the success of the command task is distro_exists` in the form of register. Note that the when statement must contain a Boolean expression. This can use Ansible facts, variables, or results that were registered. For instance:

tasks:
  - name: "shut down CentOS 6 systems"
    command: /sbin/shutdown -t now
    when:
      - ansible_facts['distribution'] == "CentOS"
      - ansible_facts['distribution_major_version'] == "6"

Example: set up NTP server and clients

Let’s configure a Network Time Protocol (NTP) server and clients using ansible playbooks. Please see the NTP configuration in Synchronize time across the cluster for a detailed explanation on the manual configuration.

NTP server installation in a playbook

This playbook has to target the master node since it will serve as NTP server for the rest of the cluster. The necessary steps to configure the NTP server are,

  • Disable and stop the chronyd service.

  • Install of ntpd daemon, using yum.

  • Allow the cluster’s subnets (IPMI and network) to query the NTP server.

  • Enable and start the ntpd service.

These steps can be translated as ansible tasks in the following manner:

---
- name: NTP server configuration
  hosts: master

  tasks:
    - name: Disable and stop chronyd daemon
      service:
        name: chronyd
        enabled: no
        state: stopped
      ignore_errors: true

    - name: Remove chronyd package
      yum:
        name: chrony
        state: absent

    - name: Install ntpd daemon
      yum:
        name: ntp
        state: latest

    - name: Allow IPMI and node network to query ntp server
      blockinfile:
        dest: /etc/ntp.conf
        insertafter: '^#restrict '
        block: |
          restrict 192.168.0.0  mask 255.255.240.0 nomodify notrap
          restrict 192.168.16.0 mask 255.255.240.0 nomodify notrap

    - name: Enable and Restart ntpd daemon
      service:
        name: ntpd
        enabled: yes
        state: restarted
...

Note

Within the task blockinfile the symbol | in block: |, is used to allow multi-line entries.

To run the playbook, save the playbook as ntp_server.yml and execute the command:

[root@master ~]# ansible-playbook -i inventory ntp_server.yml

NTP client installation in a playbook

Once again, following the steps described for the NTP client configuration explained in Synchronize time across the cluster, our playbook looks like,

---
- name: NTP client configuration
  hosts: compute

  tasks:
    - name: Disable and stop chronyd daemon
      service:
        name: chronyd
        enabled: no
        state: stopped
      ignore_errors: true

    - name: Remove chronyd package
      yum:
        name: chrony
        state: absent

    - name: Install ntpd daemon
      yum:
        name: ntp
        state: latest

    - name: Set up default default ntp server
      lineinfile:
        dest: /etc/ntp.conf
        regexp: '^server'
        insertafter: '^#server '
        line: 'server ntp.hpc prefer'

    - name: Enable and Restart ntpd daemon
      service:
        name: ntpd
        enabled: yes
        state: restarted
...

Note the use of the module lineinfile and regular expressions. Again, using the inventory file created previously, the playbook can be executed with the command:

[root@master ~]# ansible-playbook -i inventory ntp_client.yml