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.yml
file
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