Ansible/Roles
From charlesreid1
Contents
Playbook Roles
What are Ansible roles?
Roles allow you to split your playbook into different parts for different servers.
For example, a webapp with a database backend can define a webserver role and a database role, and it becomes much easier to modify the playbook to run these on the same host or on different hosts.
Feature: pre-tasks and post-tasks
In the playbook you can specify a pre-task and a post-task for a role.
For example, suppose you want to update aptitude before deploying the web server, and you want to send a notification to Slack when you are finished.
Then you could use the following playbook, which defines a pre_tasks list of things to do before the roles are defined, and a post_task list of things to do once the roles have been carried out.
- name: deploy mezzanine on vagrant
hosts: web
vars_files:
- secrets.yml
pre_tasks:
- name: update the apt cache
apt: update_cache=yes
roles:
- role: mezzanine
database_host: "{{ hostvars.db.ansible_eth1.ipv4.address }}"
live_hostname: 192.168.33.10.xip.io
domains:
- 192.168.33.10.xip.io
- www.192.168.33.10.xip.io
post_tasks:
- name: notify Slack that the servers have been updated
local_action: >
slack
domain=acme.slack.com
token={{ slack_token }}
msg="web server {{ inventory_hostname }} configured"
Feature: role files
If a role is particularly complicated or has details of its own to take care of, you can put all of the files specific to one particular role into a directory.
Suppose you have two roles, webserver and database. Then your directory structure would look like this:
playbooks/
roles/
database/
tasks/
main.yml
handlers/
main.yml
files/
pg_hba.conf
postgresql.conf
webserver/
...
Tasks, handlers, files, templates, etc. are all put into subfolders with the same name as the role.
Example
Let's look at an example of deploying an application with Ansible that a classic architecture: a web frontend with a database backend.
The web frontend can use an nginx web server playbook (see Ansible/Nginx Playbook for sample nginx playbook).
Example Playbook: Single Host
Here is an example playbook with two roles, web and database.
---
# site.yml
- name: deploy webapp on vagrant
hosts: web
vars_files:
- secrets.yml
roles:
- role: database
database_name: "{{ mezzanine_proj_name }}"
database_user: "{{ mezzanine_proj_name }}"
- role: webserver
live_hostname: 192.168.33.10.xip.io
domains:
- 192.168.33.10.xip.io
- www.192.168.33.10.xip.io
Example Playbook: Single Host with Pre and Post Tasks
Here is the same playbook but running a pre/post task before/after the roles are defined. These pre/post tasks will be run once per host.
---
# site.yml
- name: deploy mezzanine on vagrant
hosts: web
vars_files:
- secrets.yml
pre_tasks:
- name: update the apt cache
apt: update_cache=yes
roles:
- role: database
database_name: "{{ mezzanine_proj_name }}"
database_user: "{{ mezzanine_proj_name }}"
- role: webserver
live_hostname: 192.168.33.10.xip.io
domains:
- 192.168.33.10.xip.io
- www.192.168.33.10.xip.io
post_tasks:
- name: notify Slack that the servers have been updated
local_action: >
slack
domain=acme.slack.com
token={{ slack_token }}
msg="web server {{ inventory_hostname }} configured"
Example Playbook: Multiple Hosts
Here is the prior playbook adapted to multiple hosts:
---
# site.yml
- name: deploy postgres on vagrant
hosts: db
vars_files:
- secrets.yml
roles:
- role: database
database_name: "{{ mezzanine_proj_name }}"
database_user: "{{ mezzanine_proj_name }}"
- name: deploy web frontend on vagrant
hosts: web
vars_files:
- secrets.yml
roles:
- role: mezzanine
database_host: "{{ hostvars.db.ansible_eth1.ipv4.address }}"
live_hostname: 192.168.33.10.xip.io
domains:
- 192.168.33.10.xip.io
- www.192.168.33.10.xip.io
Roles
Roles are used to organize and divide up the various tasks, variables, and files for a given play.
Database role
The database role has multiple files associated with it, to define the tasks, default variable values, handlers, files, and templates used by Ansible.
These should go in playbooks/roles/database/.
Database Role Directory Layout
The directory defining how this role works for the playbook will have the following directory structure:
playbooks/
roles/
database/
defaults/
main.yml
vars/
main.yml
handlers/
main.yml
tasks/
main.yml
templates/
(As mentioned on the Ansible/Directory_Layout/Details#Creating_the_directory_structure page, you can also use the ansible-galaxy init -p <path-to-roles-directory> <name-of-role> command to create this directory structure for each role.)
The directories and the files they contain play the following roles:
- defaults - defines default variable values for this role
- vars - defines variables for this specific role
- handlers - sets up any handlers (conditional tasks) for this role
- tasks - defines any tasks run as part of this role (can be divided into multiple files/multiple tasks)
- templates - defines Jinja templates used for substituting variables into files
Tasks File
The main thing we want to define for our database role are the tasks required to set up the database server. These will be specific to the database role and are not executed by the webserver.
The task file is located in playbooks/roles/database/tasks/main.yml and are defined like normal tasks:
- install packages
- copy postgres config file into machine
- copy client authentication config file
- create project locale
- create postgresql user
- create the database as the postgresql user
playbooks/roles/database/tasks/main.yml:
- name: install apt packages
apt: pkg={{ item }} update_cache=yes cache_valid_time=3600
become: True
with_items:
- libpq-dev
- postgresql
- python-psycopg2
- name: copy configuration file
copy: >
src=postgresql.conf dest=/etc/postgresql/9.3/main/postgresql.conf
owner=postgres group=postgres mode=0644
become: True
notify: restart postgres
- name: copy client authentication configuration file
copy: >
src=pg_hba.conf dest=/etc/postgresql/9.3/main/pg_hba.conf
owner=postgres group=postgres mode=0640
become: True
notify: restart postgres
- name: create project locale
locale_gen: name={{ locale }}
become: True
- name: create a user
postgresql_user:
name: "{{ database_user }}"
password: "{{ db_pass }}"
become: True
become_user: postgres
- name: create the database
postgresql_db:
name: "{{ database_name }}"
owner: "{{ database_user }}"
encoding: UTF8
lc_ctype: "{{ locale }}"
lc_collate: "{{ locale }}"
template: template0
become: True
become_user: postgres
Note that copying the configuration file includes a notify: restart postgres. These are handler notifications (see next section).
Handlers File
Handlers can be defined in the handlers folder of the directory for this role.
Recall that handlers are simply tasks that are dependent on other tasks.
We define a task to restart the postgresql service. Whenever the configuration files are changed, this handler is notified/run.
playbooks/roles/database/handlers/main.yml:
- name: restart postgres service: name=postgresql state=restarted become: True
Defaults
To define defaults, we can use the defaults folder following the same patterns as above.
roles/database/defaults/main.yml:
database_port: 5432
Variables
Note that we defined a vars_files variable at the top of the playbook:
- name: deploy mezzanine on vagrant
hosts: web
vars_files:
- secrets.yml
The secrets.yml file will contain, we presume, secrets like the database password.
The variables we referred to were:
- database_name
- database_user
- db_pass
- locale
The rest of the variables can be defined as follows:
- If the variable will be the same across all roles, define the variable in
playbooks/group_vars/all
- If the variable will change from role to role, put it in the
playbooks/<role-name>/defaults/main.ymlfile
Webapp role
Here we will go through the webserver or webapp role, which runs the application that operates the web frontend for our application.
Here, "Mezzanine" refers to the name of a hypothetical webapp.
All files related to this role should go in playbooks/roles/mezzanine/.
Webapp Role Directory Layout
The directory defining how this role works for the playbook will have the following directory structure:
playbooks/
roles/
mezzanine/
defaults/
main.yml
vars/
main.yml
handlers/
main.yml
tasks/
main.yml
django.yml
nginx.yml
templates/
gunicorn.conf.py.j2
local_settings.py.filters.j2
local_settings.py.j2
nginx.conf.j2
supervisor.conf.j2
The directories and the files they contain play the following roles:
- defaults - defines default variable values for this role
- vars - defines variables for this specific role
- handlers - sets up any handlers (conditional tasks) for this role
- tasks - defines any tasks run as part of this role (can be divided into multiple files/multiple tasks)
- templates - defines Jinja templates used for substituting variables into files
Variables
There are two directories that should contain YAML files that define variables for each role:
- in the role-specific variables file
playbooks/<rolename>/vars/main.yml - in the variable defaults file
playbooks/<rolename>/defaults/main.yml
In the vars file, the variables for the mezzanine role are all prefixed with mezzanine, the role, which keeps different roles with the same variable names from stepping on each others' toes.
roles/mezzanine/vars/main.yml:
# vars file for mezzanine
mezzanine_user: "{{ ansible_user }}"
mezzanine_venv_home: "{{ ansible_env.HOME }}"
mezzanine_venv_path: "{{ mezzanine_venv_home }}/{{ mezzanine_proj_name }}"
mezzanine_repo_url: git@github.com:lorin/mezzanine-example.git
mezzanine_proj_dirname: project
mezzanine_proj_path: "{{ mezzanine_venv_path }}/{{ mezzanine_proj_dirname }}"
mezzanine_reqs_path: requirements.txt
mezzanine_conf_path: /etc/nginx/conf
mezzanine_python: "{{ mezzanine_venv_path }}/bin/python"
mezzanine_manage: "{{ mezzanine_python }} {{ mezzanine_proj_path }}/manage.py"
mezzanine_gunicorn_port: 8000
Many of these role-specific variables use other role-specific variables. A few use Ansible configuration variables (ansible_user or ansible_env).
Defaults
The default variable values just defines one variable, to turn TLS (HTTPS) on:
playbooks/roles/mezzanine/defaults/main.yml:
tls_enabled: True
Tasks
This task is broken up into three YML files.
The main (top-level) task list defines how to install all packages (using a for loop over packages), then transcludes django and nginx task files.
roles/mezzanine/tasks/main.yml
- name: install apt packages
apt: pkg={{ item }} update_cache=yes cache_valid_time=3600
become: True
with_items:
- git
- libjpeg-dev
- libpq-dev
- memcached
- nginx
- python-dev
- python-pip
- python-psycopg2
- python-setuptools
- python-virtualenv
- supervisor
- include: django.yml
- include: nginx.yml
The reason for splitting the tasks out is that they get long and fairly involved.
Here is the django task list:
roles/mezzanine/tasks/django.yml:
- name: create a logs directory
file: path="{{ ansible_env.HOME }}/logs" state=directory
- name: check out the repository on the host
git:
repo: "{{ mezzanine_repo_url }}"
dest: "{{ mezzanine_proj_path }}"
accept_hostkey: yes
- name: install Python requirements globally via pip
pip: name={{ item }} state=latest
with_items:
- pip
- virtualenv
- virtualenvwrapper
- name: install required python packages
pip: name={{ item }} virtualenv={{ mezzanine_venv_path }}
with_items:
- gunicorn
- setproctitle
- psycopg2
- django-compressor
- python-memcached
- name: install requirements.txt
pip: >
requirements={{ mezzanine_proj_path }}/{{ mezzanine_reqs_path }}
virtualenv={{ mezzanine_venv_path }}
- name: generate the settings file
template: src=local_settings.py.j2 dest={{ mezzanine_proj_path }}/local_settings.py
- name: apply migrations to create the database, collect static content
django_manage:
command: "{{ item }}"
app_path: "{{ mezzanine_proj_path }}"
virtualenv: "{{ mezzanine_venv_path }}"
with_items:
- migrate
- collectstatic
- name: set the site id
script: scripts/setsite.py
environment:
PATH: "{{ mezzanine_venv_path }}/bin"
PROJECT_DIR: "{{ mezzanine_proj_path }}"
PROJECT_APP: "{{ mezzanine_proj_app }}"
WEBSITE_DOMAIN: "{{ live_hostname }}"
- name: set the admin password
script: scripts/setadmin.py
environment:
PATH: "{{ mezzanine_venv_path }}/bin"
PROJECT_DIR: "{{ mezzanine_proj_path }}"
PROJECT_APP: "{{ mezzanine_proj_app }}"
ADMIN_PASSWORD: "{{ admin_pass }}"
- name: set the gunicorn config file
template: src=gunicorn.conf.py.j2 dest={{ mezzanine_proj_path }}/gunicorn.conf.py
- name: set the supervisor config file
template: src=supervisor.conf.j2 dest=/etc/supervisor/conf.d/mezzanine.conf
become: True
notify: restart supervisor
- name: ensure config path exists
file: path={{ mezzanine_conf_path }} state=directory
become: True
when: tls_enabled
- name: install poll twitter cron job
cron: >
name="poll twitter" minute="*/5" user={{ mezzanine_user }}
job="{{ mezzanine_manage }} poll_twitter"
The next set of tasks relate to the nginx server.
These install the nginx configuration files and certificate, and restart the nginx service.
roles/mezzanine/tasks/nginx.yml:
- name: set the nginx config file
template: src=nginx.conf.j2 dest=/etc/nginx/sites-available/mezzanine.conf
notify: restart nginx
become: True
- name: enable the nginx config file
file:
src: /etc/nginx/sites-available/mezzanine.conf
dest: /etc/nginx/sites-enabled/mezzanine.conf
state: link
notify: restart nginx
become: True
- name: remove the default nginx config file
file: path=/etc/nginx/sites-enabled/default state=absent
notify: restart nginx
become: True
- name: create tls certificates
command: >
openssl req -new -x509 -nodes -out {{ mezzanine_proj_name }}.crt
-keyout {{ mezzanine_proj_name }}.key -subj '/CN={{ domains[0] }}' -days 3650
chdir={{ mezzanine_conf_path }}
creates={{ mezzanine_conf_path }}/{{ mezzanine_proj_name }}.crt
become: True
when: tls_enabled
notify: restart nginx
Handlers
roles/mezzanine/handlers/main.yml:
- name: restart supervisor supervisorctl: name=gunicorn_mezzanine state=restarted become: True - name: restart nginx service: name=nginx state=restarted become: True
Dependent Roles
Suppose two particular roles need to perform a set of tasks that are the same for both machines (e.g., installing NTP on both a web server and a database server).
Hard-coding the tasks in each role's playbook would be unnecessary duplication.
Creating a separate ntp role would require us to separately apply this role each time (can forget).
What we want is to specify a "sub-role" (a dependent role) that will be executed before the main role.
The meta folder of each role can store information about dependencies.
Dependent Roles Example
Let's look at an example of dependent roles.
Web Server
We can specify a dependency for all machines playing a web server role (role "web") by editing roles/web/meta/main.yml:
roles/web/meta/main.yml:
dependencies:
- { role: ntp, ntp_server=ntp.ubuntu.com }
Django
Suppose we have a role for a Django server, and we want to specify two dependent roles. Then we can create the following dependent roles list:
roles/django/meta/main.yml:
dependencies:
- { role: web }
- { role: memcached }
Flags