How To design a better Ansible Role for MySQL Environment ?

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 :

main.yml
#######################################################

# 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

 

percona-repo-Redhat.yml
---

#############################################################################################

# 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.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s