Control Structures

In this chapter, we will learn about the aspects of ansible that can change the execution flow. This includes,

  • Iterators/loops
  • Conditionals
  • Tags
  • Conditional inclusions

Refactoring systems playbook

Iterating over a list of items to install packages

  • Create a list of packages
  • Let us create the following list of packages in base role.
  • Edit group_vars/prod.yml and put
  systems:
    packages:
      - ntp
      - tree
      - vim

  • Also edit roles/systems/tasks/main.yml to iterate over this list of items and install packages
- name: install common systems packages
  package:  
    name:  "{{ item }}"
    state: installed
  with_items:
    - "{{ systems.packages }}"

apply

  ansible-playbook app.yml

Iterating over a hash table/dictionary to create users

  • This iteration can be done with using with_dict statement, let us see how.
  • Edit group_vars/all file from the parent directory and define a dictionary of systems users to be managed
users:
   admin:
     uid: 5001
     shell: /bin/bash
     home: /home/admin
     state: present
   dojo:
     state: absent
  • Update the systems task to iterate over this dictionary

file: roles/systems/tasks/main.yml

- name: create systems users
  user:
    name: "{{ item.key }}"
    uid:  "{{ item.value.uid  }}"
    shell: "{{ item.value.shell  }}"
    home: "{{ item.value.home    }}"
    state: "{{ item.value.state   }}"
  with_dict: "{{ users }}"
  • Execute the app playbook to verify the output
ansible-playbook app.yml

Troubleshooting : Setting defaults

The above playbook run fails as you have not defined all the fields for user dojo as part of the dictionary. You could either define it as part of vars, or better set defaults while invoking the vars, so that ansible automatically falls back to it.

file: roles/systems/tasks/main.yml

- name: create systems users
   user:
     name: "{{ item.key }}"
     uid:  "{{ item.value.uid | default('none') }}"
     shell: "{{ item.value.shell | default('none') }}"
     home: "{{ item.value.home  | default('none')  }}"
     state: "{{ item.value.state  | default('none') }}"
   with_dict: "{{ users }}"

Apply and validate

ansible-playbook app.yml

Refactoring apache playbook to add support for Ubuntu

Conditionals structures allow Ansible to choose an alternate path. Ansible does this by using when statements

When statement becomes helpful, when you will want to skip a particular step on a particular host

Adding app3

Now lets update the inventory to add app3

file: environments/prod

[app]
app1
app2
app3 ansible_user=devops ansible_ssh_pass=codespaces

update package cache on app3

ansible app3 -m ping
ansible app3 -b -a "apt-get update"

Selectively calling install tasks based on platform

Current configuration tasks that we have written is compatible with RedHat platform. It may fail on others. Lets selectively call it based on the platform family.

  • Edit roles/apache/tasks/main.yml,
- import_tasks: config.yml
  when: ansible_os_family == 'RedHat'

  • This will include config.yml only if the OS family is Redhat, otherwise it will skip the installation playbook

Apache role that we have developed supports only RedHat based systems at the moment. To add support for ubuntu (app2), we must handle platform specific differences.

e.g.

RedHat Debian
Package Name httpd apache2
Service Name httpd apache2

OS specific configurations can be defined by creating role vars and by including those in tasks.

file: roles/apache/vars/RedHat.yml

---
  apache:
    package: httpd
    service:
      name: httpd
      state: started

file: roles/apache/vars/Debian.yml

---
  apache:
    package: apache2
    service:
      name: apache2
      state: started

Lets now selectively include those var files from tasks/main.yml . Also selectively call configurations.

file: role/apache/tasks/main.yml

---
# tasks file for apache

  - include_vars: "{{ ansible_os_family }}.yml"

  - import_tasks: install.yml

  - import_tasks: service.yml

  - import_tasks: config.yml
    when: ansible_os_family == 'RedHat'

Update tasks and handlers to install and start the correct service

tasks/install.yml

---
- name: Install Apache...
  package:
    name: "{{ apache.package }}"
    state: latest

tasks/service.yml

---
- name: Starting Apache...
  service:
    name: "{{ apache.service.name }}"  
    state: "{{ apache.service.state }}"

handlers/main.yml

---
# handlers file for apache
- name: Restart apache service
  service:
    name: "{{ apache.service.name }}"  
    state: restarted

You also need to make sure systems role to use package instead of yum module.

apply playbook

ansible-playbook app.yml

Selective execution by using tags

What all can be tagged,
* tasks * roles * plays * playbooks

Options to ansible-playbook related to tags

--list-tags
--list-tasks
--tags=
--skip-tags=

Tag patterns

app
'all:!web'
'lb:db'

Lets tag the tasks

file: roles/apache/tasks/install.yml

---
- name: Install Apache...
  package:
    name: "{{ apache.package }}"
    state: latest

  tags:
    - apache
    - install

file: roles/apache/tasks/service.yml

---
- name: Starting Apache...
  service:
    name: "{{ apache.service.name }}"  
    state: "{{ apache.service.state }}"

  tags:
    - apache
    - service  

file: roles/apache/tasks/config.yml

---
  - name: copy apache config
    copy:
      src: httpd.conf
      dest: /etc/httpd.conf
      owner: root
      group: root
      mode: 0644
    notify: Restart apache service
    tags:
      - apache
      - config

Lets tag the roles and plays too,

file: app.yml

---
  - hosts: app
    become: true
    vars:
      fav:
        fruit: mango
    roles:
      - { role: apache, tags: www }
      - { role: php, tags: [ 'www', 'php' ] }
      - { role: frontend, tags: devopsdemo }
    tags:
      - frontend

and finally the playbooks,

file: site.yml

---
  # This is a sitewide playbook
  # filename: site.yml
  - import_playbook: lb.yml
    tags: lb

  - import_playbook: app.yml
    tags: app

  - import_playbook: db.yml
    tags: db

Now lets influence the tasks execution using tags,

ansible-playbook site.yml --list-tags
ansible-playbook site.yml --list-tasks
ansible-playbook app.yml  --tags=php
ansible-playbook app.yml  --tags='install:config'
ansible-playbook site.yml  --tags=frontend
ansible-playbook app.yml  --skip-tags=devopsdemo
ansible-playbook site.yml --tags='all:!db:!lb'


Adding conditionals in Jinja2 templates

  • Put the following content in roles/frontend/templates/config.ini.j2
[prefs]
{% if fav.color is defined %}
color  = {{ fav['color'] }}
{% endif %}

{% if fav.fruit is defined %}
fruit  = {{ fav['fruit'] }}
{% endif %}

{% if fav.car is defined %}
car    = {{ fav['car'] }}
{% endif %}

{% if fav.laptop is defined %}
laptop = {{ fav['laptop'] }}
{% endif %}

  • In case if the var is not defined, it will not add the config to this file.
ansible-playbook site.yml

Exercises

  • Define dictionary of properties for a new user in group_vars/prod. Observe if it gets created automatically.
  • Define a hash/dictionary of apache virtual hosts to be created, and create a template which would iterate over that dictionary and create vhost configurations. You would additionally have to create a task to create the vhost configs.
  • Learn about what else you could loop over, as well as how to do so by reading this document http://docs.ansible.com/ansible/playbooks_loops.html#id12