Update: Proving that you never stop learning, I actually made a mistake in the first example of the article. Used a loop instead of Ansible facts which made the playbook quite moronic!

Welcome to the first “real” post of the Ansible for Dummies series - in this episode, we’re going to look into the most important thing about Ansible - playbooks. If you haven’t read it yet, check out the previous A4D article here: Ansible for Dummies #0 - The Basics

In short, a playbook is a list of tasks we are pushing to our hosts described in the Inventory. Each playbook consists of plays (which essentially is a sub-set of related tasks) which can be pretty much anything from the simple stuff like issuing a ping command to very complex such as creating new users based on a list of usernames and an encrypted table of passwords (Ansible Vault).

Our first playbook

We’ll begin by taking a look at a simple playbook which sole purpose is to change the hostname on our hosts:

---
- name: "Simple Playbook - Play 1"
  hosts: all
  become: yes
  tasks:
    - name: "set hostname on all 5 test servers"
      hostname: 
        name: "test{{ play_hosts.index(inventory_hostname) }}"

As you can see, the playbook is written in YAML - a human-friendly data serialization standard. You don’t have to really learn YAML to write playbooks; all you need to remember is that whitespaces matter very much and that we begin new major lines with two additional spaces.

Let’s break down our first playbook and go through it step-by-step:

1) Play’s header

Note: the leading dashes are optional - it’s just a common way to begin a YAML file.

---
- name: "Simple Playbook - Play 1"
  hosts: all
  become: yes

Each play in a playbook must begin with the two main items: hosts and tasks; any additional tags (in this case, name and become) are optional:

  • hosts - defines the host(s) which should be affected by the play; we can use either built-in Ansible groups such as all (run against all hosts in the current inventory file), individual IPs and hostnames or group names defined in the inventory;

  • tasks - is a list of individual tasks to be carried out as part of the play; each play has its own list of tasks to run. For instance, a play configuring a network could include a task for setting a hostname and a second task configuring the IP settings.

The two other tags used in this playbook - name and become - are common enough for me to explain them at this time as well:

  • name - while not mandatory, the name tag is very useful as it allows us to add a label to each play (and task) which we’re going to run. A name we assign to a play or a task will be displayed in the terminal during the playbook’s execution;

-become - this simple tag is used to select whether the play should be run as a regular user or a superuser (root/administrator).

2) Tasks

tasks:
    - name: "set hostname on all 5 test servers"
      hostname: 
        name: "test{{ play_hosts.index(inventory_hostname) }}"

In the case of tasks, there is not a single rule on how they should be written down - it all depends on the modules you’d like to use & what’s the intended use-case.

At the very basic level, all you need to add to the tasks list are the module names and their arguments. In this particular play, we’re using a hostname module which requires just a single parameter name.

The string for the tag is built of two parts - first, we’ve got a static “test” which will be the same for every host, while the second bit {{ play_hosts.index(inventory_hostname) }}, is an Ansible fact returning an index number of the host currently being processed. By combining the two, the playbook will end up using test1 for the first host in the inventory, test2 for the seconds one and so on.

3) Summary

Let’s step away from Ansible for a second. Think about the playbook on our hands as an email you’ve sent to a junior engineer with a task to carry out:

Right, so we need to change hostnames on all 5 of our test servers. Remember you will need to log in as root to do that. Use the hostname command on each box - it uses the desired hostname for the box as the only argument. I want you to set those hostnames to “test” followed by a sequence number.

Mr Ansible - our junior engineer - has then translated our email into his native language, YAML:

"Right, we need to set the hostnames on all 5 of our test boxes"
hosts: all

"Remember you will need to log in as root to do that."
become: yes


"Use the hostname command on each box. It uses a desired hostname for the box as the only argument"
tasks:
  - name: "set hostname on all 5 test servers"
    hostname: 
      name: "test{{ play_hosts.index(inventory_hostname) }}"


"I want you to set those hostnames to "test" followed by a sequence number."
name: "test{{ play_hosts.index(inventory_hostname) }}"

Makes sense, right? That’s the beauty of YAML - you probably guessed most of what I wrote so far by just looking at the playbook. Fortunately, reading YAML is as simple as writing it and you’ll soon be spitting out new playbooks like a production line.

4) Running a Playbook

Let’s quickly run the playbook we wrote today on out Ansible client. For the purpose of this exercise, let’s assume we stuck with the default ansible.cfg and our inventory is:

[test]
10.0.0.1
10.0.0.2
10.0.0.3
10.0.0.4
10.0.0.5

To run a playbook, we will be using the ansible-playbook command. In the case of today’s playbook, we won’t need any other argument aside from its filename:

vlku@client.ansible.lab : cat simplePlaybookHostname.yml
---
- name: "Simple Playbook - Play 1"
  hosts: all
  become: yes
  tasks:
    - name: "set hostname on all 5 test servers"
      hostname: "{{ item }}"
      loop:
        - test1
        - test2
        - test3
        - test4
        - test5

vlku@client.ansible.lab : ansible-playbook simplePlaybookHostname.yml
PLAY [Simple Playbook - Play 1] ************************************************

TASK [Gathering Facts] *********************************************************
ok: [10.0.0.1]
ok: [10.0.0.2]
ok: [10.0.0.3]
ok: [10.0.0.4]
ok: [10.0.0.5]

TASK [set hostname on all 5 test servers] **************************************
ok: [10.0.0.1]
ok: [10.0.0.2]
ok: [10.0.0.3]
ok: [10.0.0.4]
ok: [10.0.0.5] 

PLAY RECAP *********************************************************************
10.0.0.1     : ok=1    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
10.0.0.2     : ok=1    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
10.0.0.3     : ok=1    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
10.0.0.4     : ok=1    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
10.0.0.5     : ok=1    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

Ansible provides us with loads of info when running a playbook:

First, we’ve got the default “pre-play” task of gathering facts. This is an important stage of running a playbook that involves Ansible collecting info (“facts”) about the target hosts. Hosts, from which Ansible failed to collect info, will not be processed.

Next, Ansible starts going through the tasks included in the play. In this instance, all hosts had their hostnames changed successfully but, in event of a failure/problem, Ansible would report the module’s error message along.

Finally, we’ve got the recap which is pretty self-explanatory - it summarizes what went down, which hosts were processed successfully (“ok”), which were affected by the playbook (“changed”), etc.

More simple playbooks!

As a closing note, I’d like to share some easy playbooks with you. You can try running them against the hosts in your lab to get used to how Ansible works. Make sure you have a properly configured ansible.cfg and an inventory file handy with at least one host in it. Have fun!

Playbook 1: Set basic network settings

---
- name: "Set basic network settings"
  hosts: all
  become: yes
  tasks:
    - name: "Set hostname to AnsibleFTW"
      hostname:
        name: ansibleftw
    - name: "Set DNS servers"
      net_system:
        name_servers:
          - 8.8.8.8
          - 8.8.4.4

Playbook 2: File operations

---
- name: "File Operations"
  hosts: all
  tasks:
    - name: "Make sure the file exist with touch command"
      file:
        path: /tmp/testAnsibleFile
        state: present
    - name: "Use echo command to add data to the file"
      shell: "echo 'DATA DATA DATA DATA' >> /tmp/testAnsibleFile"
    - name: "Create a directory to host the file in"
      file:
        path: /tmp/testAnsibleDir
        state: directory
    - name: "Copy the file into our new directory"
      copy:
        remote_src: yes
        src: /tmp/testAnsibleFile
        dest: /tmp/testAnsibleDir/copyOfAnsibleTestFile

Playbook 3: Repos and Packages

---
- name: "Repos and Packages"
  hosts: all
  tasks:
    - name: "Create new repo"
      yum_repository:
        name: epel
        description: EPEL YUM repo
        baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/
    - name: "Install googler package from EPEL repo"
      yum:
        name: googler
        enablerepo: epel
        state: present