In our earlier stage of Ansible, we just wrote simple playbook and ad-hoc command with very long ansible hosts file. When we plan to use Ansible extensively in our daily production use case, we understand that simple playbooks don’t help to scale up to our expectation.
Even though we had options for separate variables, handlers and template files according to our requirements, this un-organized way didn’t help. It looked very messy and made me unhappy when I saw the code too. That’s the place we decided to use Ansible Role.
My understanding of Ansible Roles?
The role is the primary mechanism for breaking a playbook into multiple files, we can simply refer to the Python Package. Roles help to group multiple tasks, Jinja2 template file, variable file and handlers into a clean directory structure. This will help us to reduce the syntax error while developing and also easily help to scale for future requirements.
Thumb rule for developing an Ansible role is, don’t develop a single role to do everything, it might break. Try to focus on a specific goal, for example installing MySQL another installing App Server, etc.
How to create an Ansible Role?
ansible-galaxy is the command to manage Ansible role in the shared repo. This command has a lot of sub-commands, but we are only going to use ansible-galaxy init. ansible-galaxy init <role name> command helps to create the skeleton framework of a role. By default, role creates under the current working directory.
[ec2-user@ip-172-31-28-102 ~]$ ansible-galaxy init mysql
- mysql was created successfully
Discussing The Ansible Role Directory Structure
Our MySQL roles directory consists of defaults, files, handlers, meta, tasks, templates, tests, and vars folders. We will discuss every individual directory characteristic little detail below.
[ec2-user@ip-172-31-28-102 ~]$ tree mysql
mysql
|-- defaults
| `-- main.yml
|-- files
|-- handlers
| `-- main.yml
|-- meta
| `-- main.yml
|-- README.md
|-- tasks
| `-- main.yml
|-- templates
|-- tests
| |-- inventory
| `-- test.yml
`-- vars
`-- main.yml
8 directories, 8 files
defaults/main.yml
Default folder name refers to the preexisting value of a user-configurable setting.
This directory contains default variable for the role. In our all role development, we have defined all mutable variable for the role here only because it has the lowest priority and it can be easily overridden through other variables from group _vars or hosts_vars or playbook vars.
Eg:
###########################################################
############Percona-utils Role Variable####################
###########################################################
###########################################################
# Percona Repo Variable #
###########################################################
percona_redhat_repo_url: "https://www.percona.com/redir/downloads/percona-release/redhat/percona-release-0.1-4.noarch.rpm"
percona_debian_repo_url: "https://repo.percona.com/apt/percona-release_0.1-4.{{ ansible_distribution_release }}_all.deb"
#########################################################################################################
# Percona installation state installed/latest, PMM Client version and PMM Client Re-Install "yes" or "".#
#########################################################################################################
common_percona_util_package_state: installed
percona_package_state: installed
pmm_client_version: "1.8.0"
pmm_client_reinstall: "no" #"yes" or "no"
files
Most of the time copy module uses this folder.
handlers/main.yml
This is the one place, we will write all our handlers that we are going to use in a role. In our task, we can just specify the name of the handler, it will be automatically called and executed at the end of the play.
basically, I don’t prefer to write handlers. Because everyone knows handler is similar to a task, but it only executes when the particular task changed the state of the machine. And it usually runs after all of the tasks are run at the end of the play.
In some situation following task failed, next time we re-run the play the handler calling task state will be ok. So that case handler fails to run.
I know using change_when we can fix the above issue, but I don’t like to complex my code. Using register, I will store the output and evaluate a certain condition. Based on the evaluation we will execute the task what handler will do. I feel it simple cool for me.
Eg:
- name: Check Percona repo is already configured
stat:
path:"{{ red_percona_repofile_path }}"
register: percona_repofile_status
- name: Installing PMM Client
package:
name: "{{ item }}"
state: present
with_items:
"{{ red_pmm_client_packages }}"
when: percona_repofile_status.stat.exists == True
meta/main.yml
Using this we can define meta information about the roles. eg: author, company, description, license, and dependencies, etc.
Here dependencies are very important, we can’t ignore just like that and pass away. Why because using dependencies we can specify the list of the role that needs to run before the executing the rest of the role included in the playbook. So when playbook runs automatically all depend on role execute first and continue the other roles. it helps to avoid lot human error in the care other role dependencies.
tasks/main.yml
the task is the place we put all our play’s to install, configure, manage services, and etc..
Eg :
#######################################################
# Percona Repo for Redhat and Debian #
#######################################################
- import_tasks: percona-repo-RedHat.yml
when: ansible_os_family == "RedHat"
static: no
- import_tasks: percona-repo-Debian.yml
when: ansible_os_family == "Debian"
static: no
#####################################################
# Install Common Utils Packages #
#####################################################
- import_tasks: utils-setup.yml
static: no
######################################################
# PMM Client Installation for RedHat and Debian #
######################################################
- import_tasks: pmm-client-setup-RedHat.yml
when: ansible_os_family == "RedHat"
static: no
- import_tasks: pmm-client-setup-Debian.yml
when: ansible_os_family == "Debian"
static: no
---
#############################################################################################
# Installing Percona Repo For RedHat #
#############################################################################################
- name: Check Percona repo is already configured.
stat: path="{{ red_percona_repofile_path }}"
register: percona_repofile_status
- name: Enable RedHat Optional repo.
command: yum-config-manager --enable rhui-REGION-rhel-server-optional
when: ansible_distribution == "RedHat"
- name: Install Percona repo.
yum:
name: "{{ percona_redhat_repo_url }}"
state: present
register: percona_install_result
when: percona_repofile_status.stat.exists == False
- name: Amazon Linux changing releaserver to 7 default.
command: sed -i 's/$releasever/7/g' "{{ red_percona_repofile_path }}"
when: ansible_distribution == "Amazon"
templates
It’s just text file that has special syntax for specifying variables that should be replaced by values. Ansible uses the jinja2 templating engine to implement templates.
In our case, we use the template for building configuration file dynamically.
Eg:
- name: Copy my.cnf global MySQL configuration.
template:
src: mysql_conf.j2
dest: "{{ mysql_config_file }}"
owner: root
group: root
force: "{{ overwrite_global_mycnf }}"
mode: 0644
# {{ ansible_managed }}
[client]
port = {{ mysql_port }}
socket = {{ mysql_socket }}
[mysqld]
port = {{ mysql_port }}
bind-address = {{ mysql_bind_address }}
datadir = {{ mysql_data_dir }}
socket = {{ mysql_socket }}
pid-file = {{ mysql_pid_file }}
{% if mysql_skip_name_resolve %}
skip-host-cache
skip-name-resolve
{% endif %}
{% if mysql_sql_mode %}
sql_mode = {{ mysql_sql_mode }}
{% endif %}
# Logging configuration.
{% if mysql_log_error == 'syslog' or mysql_log == 'syslog' %}
syslog
syslog-tag = {{ mysql_syslog_tag }}
{% else %}
{% if mysql_log %}
log = {{ mysql_log }}
{% endif %}
log-error = {{ mysql_log_error }}
{% endif %}
# Slow query log configuration.
{% if mysql_slow_query_log_enabled %}
slow_query_log = 1
slow_query_log_file = {{ mysql_slow_query_log_file }}
long_query_time = {{ mysql_slow_query_time }}
{% endif %}
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links = 0
# User is ignored when systemd is used (fedora >= 15).
user = mysql
# http://dev.mysql.com/doc/refman/5.5/en/performance-schema.html
#performance_schema
{% if mysql_version|string == "5.7" %}
performance_schema
{% endif %}
# Memory settings.
key_buffer_size = {{ mysql_key_buffer_size }}
max_allowed_packet = {{ mysql_max_allowed_packet }}
table_open_cache = {{ mysql_table_open_cache }}
sort_buffer_size = {{ mysql_sort_buffer_size }}
read_buffer_size = {{ mysql_read_buffer_size }}
read_rnd_buffer_size = {{ mysql_read_rnd_buffer_size }}
myisam_sort_buffer_size = {{ mysql_myisam_sort_buffer_size }}
query_cache_type = {{ mysql_query_cache_type }}
query_cache_size = {{ mysql_query_cache_size }}
query_cache_limit = {{ mysql_query_cache_limit }}
{% if mysql_max_connections | int > 3000 %}
max_connections = 3000
thread_cache_size = {{ (3000 * 0.15) | int }}
{% elif mysql_max_connections | int < 150 %}
max_connections = 150
thread_cache_size = {{ (150 * 0.15) | int }}
{% else %}
max_connections = {{ mysql_max_connections }}
thread_cache_size = {{ (mysql_max_connections | int * 0.15) | int }}
{% endif %}
max_connect_errors = {{ mysql_max_connect_errors }}
tmp_table_size = {{ mysql_tmp_table_size }}
max_heap_table_size = {{ mysql_max_heap_table_size }}
group_concat_max_len = {{ mysql_group_concat_max_len }}
join_buffer_size = {{ mysql_join_buffer_size }}
vars/main.yml
vars also hold variable for our roles same as defaults. The variables which reside under vars are more difficult to overwrite due to its high priority. so if we need to make the variable immutable, we can declare under vars.
# vars file for common
red_percona_utils_packages:
- innotop
- percona-toolkit
- perl-Sys-Statistics-Linux
- nagios-plugins-perl
red_pmm_client_packages:
- pmm-client-{{ pmm_client_version }}
- percona-nagios-plugins.noarch
- perl-DBI.x86_64
- perl-Nagios-Plugin.noarch
red_percona_repofile_path: "/etc/yum.repos.d/percona-release.repo"
I think I covered all related topics to roles. but there are a lot of things to discuss and share.
Finally, the Playbook Order of Execution
- Any pre_tasks defined in the play.
- Any handlers triggered so far will be run.
- Each role listed in roles will execute in turn. Any role dependencies defined in the roles meta/main.yml will be run first, subject to tag filtering and conditionals.
- Any tasks defined in the play.
- Any handlers triggered so far will be run.
- Any post_tasks defined in the play.
- Any handlers triggered so far will be run.