Organizing our files is a simple task currently because we are dealing with just deploying Apache. However, what if we want to deploy other types of servers? Do we just keep adding additional plays for databases, application servers, and so on to our current YAML file? Even if we apply modular design and separate our plays out into individual YAML files, what is the best way to organize the supporting files like configuration files, variable files, and so forth? Do we keep them in common directories, or do we create different subdirectories according to function?
Ansible’s authors considered these questions and implemented the roles functionality to address them. System administrators can group related files with a particular directory structure according to the function that they will fulfill. Figure 10-2 shows a tree structure based on a role for our Apache web servers.
Figure 10-2 Ansible role directory structure
The roles directory and the apache.yml file sit at the same level on the file system (for example, /src/apache.yml and /src/roles/), and the remainder of the directories and files reside in the roles directory (/src/roles/web/files/, /src/roles/web/handlers, and so on).
Alternatively, you could store your web role in /etc/ansible/roles, and that is a practice we use in the next chapter. However, to keep things simple, we will keep our web role located in the same directory as our apache.yml file.
Before we discuss the required code changes to accommodate the role structure, let’s review the purpose of each of the folders in the role:
defaults: If your role accepts parameters when you call it (you’ll see an example in the next chapter), you can place a YAML file that contains default values that aren’t specified with the role in the main playbook (for example, default passwords).
files: Any files that you would like to copy to the target hosts. For the Apache server, this would be the configuration files.
handlers: The instructions for executing actions in response to notifications from a playbook task (for example, restarting Apache services after the configuration file changes).
meta: References to other Ansible roles that your current role depends on.
tasks: The location for your playbook tasks.
templates: Jinja2 files that can be used to generate content dynamically according to system facts or other variables.
vars: Variables that will be used by your playbooks.
Note
It is not necessary to manually create the role directory structure. Ansible includes a utility that will generate it for you: ansible-galaxy init web.
Ansible Galaxy is introduced later in this chapter, but for now, it is sufficient to know that this utility helps you generate the correct role structure so that you can share it with the Ansible community if you want to.
By utilizing the role directory structure, you do not have to provide explicit paths when accessing content in the various directories. Ansible can interpret which folder to look into based on the module that you are using. For example, in Listing 10-3, the copy module’s src argument required the explicit path to the configuration file if we were not storing it in the same directory as our playbook file (copy: src=files/httpd.conf). When we adopt the role structure, we follow the correct Ansible conventions and place the
configuration file in the role’s files subdirectory. In our playbook, it would only be necessary to specify the name of the file, and Ansible will automatically interpret the correct path to the file.
You need to make a few changes to your playbook file for the role structure to function correctly. First, the apache.yml file now contains references to the web role instead of the actual play tasks and handlers (see Listing 10-8).
Listing 10-8 Updated apache.yml
–
- hosts: web roles:
- web
The name of the role listed (web) must match the name of the subdirectory residing in the roles folder. Then, your playbook’s tasks and handlers are divided into separate files that are each called main.yml in the tasks and handlers subdirectories, respectively (see Listing 10-9).
Listing 10-9 Apache Deployment Tasks Stored in the Web Role’s Tasks and Handlers Subdirectories
Click here to view code image
#web/tasks/main.yml
–
include_vars: “{{ ansible_os_family }}.yml”
- name: Deploy Apache for Red Hat Systems yum: pkg=httpd state=latest
when: ansible_os_family == ‘RedHat’
- name: Deploy Apache for Debian Systems apt: pkg=apache2 state=latest
when: ansible_os_family == ‘Debian’
- name: Apache Config File
copy: src={{ conffile }} dest={{ confpath }}
notify:
- restart httpd
- name: Apache Service Runs
service: name={{ webserver }} state=started
#web/handlers/main.yml –
include_vars: “{{ ansible_os_family }}.yml”
- name: restart httpd
service: name={{ webserver }} state=restarted
The filename main.yml indicates to Ansible that these tasks and handlers should be automatically loaded when the role is referenced from another file. The role structure requires referencing variables in a different way. Instead of using the vars_files:
keyword, we use the include_vars: keyword. Also, we no longer need to specify the vars subdirectory because Ansible will automatically know where to look for our variables files.
These changes might seem confusing at first, but they help tremendously with keeping the code organized according to the function it is supposed to fulfill. This comes in handy if you are working with a LAMP stack, for example, and you have separate web, database, and PHP roles. You can make changes to the database role without potentially affecting the web and application server roles.
Templates
Ansible supports Jinja2 templates for generating role content. Jinja2 is a technology used by Python application frameworks such as Django and Flask for dynamic web content that is generated at runtime. Jinja2 is similar to the Embedded RuBy (ERB) technology
introduced in Chapter 4 for Puppet except that it uses Python-like conventions for its code structure.
Jinja2 templates are useful for configuration files and for application content. We will use a Jinja2 template to dynamically generate our web server’s index.html file instead of discussing the various options and settings necessary to generate an Apache configuration file. Our index.html file’s content will change based on the operating system of the target host. Listing 10-10 shows a sample Jinja2 template for our web content.
Jinja2 utilizes many conventions from Python. So, a basic knowledge of Python can be useful if you would like to develop your own templates. For our example, we use Python’s string management to evaluate text and an if-then-else statement to determine what text is printed out
Listing 10-10 index.html.j2 Web Content Template
Click here to view code image
#jinja2: variable_start_string: ‘[%’ , variable_end_string: ‘%]’
<html><body><h1>Ansible Rocks!</h1>
<p>This is the default web page for
{# Print the correct Operating System name with the right article i.e. “a”
vs “an”#}
{% if ansible_distribution[0].lower() in “aeiou” %}
an
{% else %}
a
{% endif %}
<b> [% ansible_distribution %] </b> server.</p>
<p>The web server software is running and content has been added by your
<b>Ansible</b> code.</p>
</body></html>
The first line is a special directive for the Jinja2 template interpreter indicating that we are using a nonstandard method of interpolating system facts. We do this because the standard double bracket convention can sometimes cause issues with templates in Ansible.
Note that there are different prefixes for the blocks of Jinja2 statements:
{# #} encloses comments.
{% %} encloses conditional logic and other Python code. Our if-then-else statement is deciding which article (either a or an) is grammatically correct to place in front of the operating system’s name.
[% %] encloses system facts according to directive from the first line of the
template file. Again, this convention is customizable according to the first line of the template. In our example, we are using the ansible_distribution system fact to detect which operating system our target server is running.
Note
Experienced Python developers may notice that we are using an endif
statement. This is not a common practice with standard Python. However, it is helpful to Jinja2 to understand where your conditional statement ends.
When we have finished our Jinja2 file, we place it in the templates subdirectory of the web role and create a task in the role to utilize the template (see Listing 10-11).
Listing 10-11 Adding a Task to Create HTML Content
Click here to view code image tasks/main.yml –
include_vars: “{{ ansible_os_family }}.yml”
- name: Deploy Apache for Red Hat Systems yum: pkg=httpd state=latest
when: ansible_os_family == ‘RedHat’
- name: Deploy Apache for Debian Systems apt: pkg=apache2 state=latest
when: ansible_os_family == ‘Debian’
- name: Apache Config File
copy: src={{ conffile }} dest={{ confpath }}
notify:
- restart httpd
- name: Apache Content File
template: src=index.html.j2 dest={{ contentpath }} mode=0644 - name: Apache Service Runs
service: name={{ webserver }} state=started
We add the task with the name Apache Content File, and we use the template module, which will copy the template and place it in the destination path that we specify.