The Ansible cases we tested so far from Part I and Part II are what we call ad-hoc mode. If you are pretty comfortable on combining these ad-hoc commands and bash scripts, you can do a lot of work for a small amount of time. But Ansible can offer a lot more features. We’ll explore creating Ansible playbooks on this part.
The YAML file
Ansible uses easy-to-read configuration file for making “playbooks”, which are files full of separate Ansible “tasks”. A task is basically an ad-hoc command written out in a configuration file that makes it more organized and easy to expand. The configuration files use YAML (Yet Another Markup Language). It’s an easy-to-read markup language, but it does rely on whitespace so be careful with your spaces and tabs. A simple playbook looks something like this:
---
- hosts: webservers
become: yes
tasks:
- name: this installs a package
apt: name=apache2 update_cache=yes state=latest
- name: this restarts the apache service
service: name=apache2 enabled=yes state=restarted
You can basically summarize this down to two ad-hoc commands broken up into a YAML configuration file. For YAML files, it’s important to note that every filename ends with .yaml, and every YAML file must begin with three hyphen characters. And since whitespace matters, you should be careful when a hypen should precede a section or just be indented appropriately.
The above playbook would be executed by typing:
# ansible-playbook filename.yaml
And that is the equivalent of these two commands:
# ansible webservers -b -m apt -a "name=apache2 update_cache=yes state=latest"
# ansible webservers -b -m service -a "name=apache2 enabled=yes state=restarted"
Using Handlers
Aside from organization of tasks on the YAML files, there are also called “Handlers”. These are tasks that are executed only when “notified” that a task has made a change. As an example, we can rewrite the above YAML to make the second task a handler:
---
- hosts: webservers
become: yes
tasks:
- name: this installs a package
apt: name=apache2 update_cache=yes state=latest
notify: enable apache
handlers:
- name: enable apache
service: name=apache2 enabled=yes state=started
It may still look similar but the execution of the trigger to execute the second task is different. When the first task (installing Apache) executes and if a change is made, it notifies the “enable apache” handler, which makes sure Apache is enabled on boot and currently running. The significance is that if Apache is already installed, and no changes are made, the handler never is called. That makes the code much more efficient, but it also means no unnecessary interruption of the already running Apache process.
Note: Multiple tasks can call a handler and handlers are executed (notified) only when an Ansible task makes a change on the remote system.
Using Variables
Variable substitution can work inside a playbook. Here’s a simple example and syntax:
---
- hosts: webservers
become: yes
vars:
package_name: apache2
tasks:
- name: this installs a package
apt: "name={{ package_name }} update_cache=yes state=latest"
notify: enable apache
handlers:
- name: enable apache
service: "name={{ package_name }} enabled=yes state=started"
Note: sometimes Ansible can cause errors on unquoted variable substitutions, so always try to put things in quotes when variables are involved.
Getting Verbose
When moving from Ansible ad-hoc commands to playbooks, you’ll notice that Ansible tends a lesser output. With ad-hoc mode, you often can see what is going on, but with a playbook, you know only if it finished properly and if a change was made. There are two easy ways to change that. The first is just to add the -v flag when executing ansible-playbook. That adds verbosity and provides lots of feedback when things are executed.
If you’re creating a playbook and want to be notified of things along the way, the debug module comes in handy. For example:
---
- hosts: webservers
tasks:
- name: describe hosts
debug: msg="Computer {{ ansible_hostname }} is running {{ ansible_os_family }} or equivalent"
The will show you something like the below output, which is incredibly useful when you’re trying to figure out the sort of systems you’re using. The good thing about the debug module is that it can display anything you want, so if a value changes, you can have it displayed on the screen so you can troubleshoot a playbook that isn’t working like you expect it to work.
It is important to note that the debug module doesn’t do anything other than display information on the screen for you. It’s not a logging system – it’s just a way to have information (customized information, unlike the verbose flag) displayed during execution.
#ansible-playbook os.yaml
PLAY ****************************************************************
TASK [setup] *********************************************************
ok: [ansible3]
ok: [ansible2]
TASK [describe hosts] ********************************************
ok: [ansible2] => {
"msg": "Computer 8a04f124f46a is running Debian or equivalent"
}
ok: [ansible3] => {
"msg": "Computer 8a851373ac3d is running Debian or equivalent"
}
PLAY RECAP *************************************************
ansible3 : ok=2 changed=0 unreachable=0 failed=0
ansible2 : ok=2 changed=0 unreachable=0 failed=0
Conditional statements
Conditionals are a part of pretty much every programming language. Ansible YAML files also can take advantage of conditional execution, but the format is a little different. Normally the condition comes first, and then if it evaluates as true, the following code executes. With Ansible, it’s a little backward. The task is completely spelled out, then a when statement is added at the end. This makes the YAML file readable but if you are used to using the usual “if, true, then, do this” conditional, if feels different. Here’s a slightly more complicated playbook.
---
- hosts: webservers
become: yes
tasks:
- name: install apache for Ubuntu
apt: name=apache2 update_cache=yes state=latest
notify: start apache2
when: ansible_os_family == "Debian"
- name: install apache for Red Hat
yum: name=httpd state=latest
notify: start httpd
when: ansible_os_family == "RedHat"
handlers:
- name: start apache2
service: name=apache2 enabled=yes state=started
- name: start httpd
service: name=httpd enabled=yes state=starte
This playbook that will install Apache on hosts using either yum or apt based on which type of distro they have installed. Then handlers make sure the newly installed packages are enabled and running.
Using Loops
As with other configuration management systems, ansible includes most features of programming and scripting languages. For example, there are loops. Here’s an example YAML config that uses a simple list and loops it.
---
- hosts: webservers
become: yes
tasks:
- name: install multiple packages
apt: "name={{ item }} state=latest update_cache=yes"
with_items:
- apache2
- vim
- htop
- dnsutils
- apt-utils
This playbook will install multiple packages using the apt module. Note the special variable named item, which is replaced with the items one at a time in the with_items section. Other loops work in similar ways, but they’re formatted differently. For a wide variety of ways Ansible can repeat similar tasks, you can check the Ansible docs here: https://docs.ansible.com/
Templates
Another module you may use fairly often is the template module. For template, you essentially create a text file and then use variable substitution to create a custom version on the fly. Ansible uses the Jinja2 templating language, which is similar to standard variable substitution in playbooks themselves. The example uses a custom HTML template file that will be copied on a remote batch of web servers. Here’s the playbook:
---
- hosts: webservers
become: yes
tasks:
- name: install apache2
apt: name=apache2 state=latest update_cache=yes
when: ansible_os_family == "Debian"
- name: install httpd
yum: name=httpd state=latest
when: ansible_os_family == "RedHat"
- name: start apache2
service: name=apache2 state=started enable=yes
when: ansible_os_family == "Debian"
- name: start httpd
service: name=httpd state=started enable=yes
when: ansible_os_family == "RedHat
- name: install index
template:
src: index.html.j2
dest: /var/www/html/index.html
Here’s the template file, which must end in .j2 (it’s the file referenced in the last task above):
<html><center>
<h1>This computer is running {{ ansible_os_family }},
and its hostname is:</h1>
<h3>{{ ansible_hostname }}</h3>
{# this is a comment, which won't be copied to the index.html file #}
</center></html
The playbook takes a few different things it learned and installs Apache on the remote systems, regardless of whether they are Red Hat- or Debian-based. Then, it starts the web servers and makes sure the web server starts on system boot.
Finally, the playbook takes the template file, index.html.j2, and substitutes the variables while copying the file to the remote system. Note the {# #} format for making comments. Those comments are completely erased on the remote system and are visible only in the .j2 file on the Ansible machine.
More Possibilities!
Ansible is a very powerful tool that is surprisingly simple to understand and use. If you’ve been experimenting with ad-hoc commands, try to create playbooks that will allow you to do multiple tasks on a multitude of computers with minimal effort. You should at least play around with the “Facts” gathered by the ansible-playbook app, because those are things unavailable to the ad-hoc mode of Ansible.
On the next part, we’ll use playbooks with roles and take advantage of the Ansible community contributions available online.
See you on Part IV: Roles Overview
Ansible Part I: Installation and Setup
Ansible Part II: Using Modules
Ansible Part III: Using Playbooks
Ansible Part IV: Roles Overview
———————————
– masterkenneth