How IT Pros use Ansible

This is for people already familiar with Ansible wanting to improve their playbooks quality for better scalability, ease of use, maintainability and security.

How IT Pros use Ansible

Organize Ansible files

  • There are many ways of organizing Ansible files
  • Below is an example strucutre I recommand for good maintainability and scalability
  • Feel free to only create directories and files you need
ansible-project/
├── README.md
├── files
├── group_vars
│   ├── all
│   │   └── main.yml
│   ├── prod
│   │   ├── main.yml
│   │   └── secret.yml
│   └── staging
│       ├── main.yml
│       └── secret.yml
├── handlers
│   └── main.yml
├── hosts
│   ├── prod
│   └── staging
├── playbook.yml
├── roles
├── tasks
└── templates
  • files: every files the project needs, that are not (Jinja2) templated
  • group_vars: variables that will be available for specific hosts groups defined inside the hosts inventories
  • handlers: contains tasks that can be run after specific changes... Ex: reload an HTTP server when a specific configuration file has changed
  • hosts: servers inventory (IP adresses, domain names, aliases...)
  • roles: another Ansible project that can be reused to easily accomplish specific tasks
  • tasks: organized tasks files containing actions we want to perform on the inventory machines
  • templates: Jinja2 templated files... apps, middlewares configuration files containing Ansible variables that will be dynamically replaced during deployment on servers
  • playbook.yml: main playbook for deployments

Hosts inventories and variables

  • Folders inside the group_vars directory except all, should correspond to a group of hosts declared inside the hosts inventory file
  • Variables defined inside yaml files created in the group_vars/hostsgroup folder will be available only for hosts that are part of the hostsgroup group.
  • Here is how we declare groups of hosts inside the Ansible hosts inventory file:
# Defining a group named webservers
# containing webservers hosts
[webservers]
wsrv1.web.local
wsrv2 ansible_ssh_host=wsrv2.web.local # using host alias

# Defining a group named dbservers
# containing databases hosts
[dbservers]
dsrv1.db.local
dsrv2.db.local

# Defining a group of groups
# named webapp containing the
# webservers and dbservers groups
[webapp:children]
webservers
dbservers
  • Using the previous example hosts inventory file, variables defined inside yaml files in the group_vars/webapp folder will be available only for hosts that are part of the webapp group, therefore hosts from the webservers and dbservers groups as those two groups are part of the webapp group.
  • It is also possible to declare variables directly inside the hosts inventory file that are available only for specific hosts or group of hosts. Here is an example:
# Defining variables that are 
# available only for the hosts that
# are part of the webservers group
[webservers:vars]
domain_name=myapp.example.local
database_vip=db.example.local

# Defining variables that are available only
# for the dsrv1 and dsrv2 database hosts
[dbservers]
dsrv1.db.local master=dsrv2.db.local
dsrv2.db.local master=dsrv1.db.local

Files and templates

  • The files directory is where we put raw files we want to deploy on target machines
  • We then use the copy module to deploy the files
  • The copy module src parameter is where we specify the path (absolute or relative to the files directory) to local files we want to deploy on target machines
  • The templates directory is where we put Ansible Jinja2 templated files we want to deploy on target machines. Those files can contain Ansible variables, conditions, flow controls... and have j2 extension
  • We then use the template module to deploy the files
  • The template module src parameter is where we specify the path (absolute or relative to the templates directory) to local template files we want to deploy on target machines

Tasks and handlers

  • The tasks directory is where we put the different tasks files that are called from plays present inside the main playbook file (playbook.yml)
  • Each tasks file serves a specific purpose and contains the declaration of the actions we want to perform or state we want the machines to be in
  • Here is the Ansible module index by category for easy tasks creation
  • The handlers directory is where we put yaml files containing handlers declaration. Handlers are used to perform actions on specific changes (for instance, reload an HTTP server on configuration file change)
  • To tell a task to use a handler, we use the notify keyword indicating the name of the handler to use when the task state is changed
  • The handlers should be declared in the plays importing the task (inside the main playbook file) in order to be recognized
  • Handlers are by default executed at the end of the whole play. Have a look at controlling-when-handlers-run for changing that behavior
  • Here is an example task using a handler:
# tasks/nginx_config.yml
---
- name: Deploy custom vhosts
  ansible.builtin.template:
    src: nginx/{{ item }}.j2
    dest: "{{ sites_available_dir }}/{{ item }}"
    mode: "0644"
  notify: Reload Nginx
  with_items:
    - "{{ custom_vhosts_list }}"
  when: enable_custom_vhost

# handlers/main.yml
---
- name: Reload Nginx
  ansible.builtin.systemd:
    name: nginx
    state: reloaded

# playbooks.yml
---
- name: Configure Nginx
  hosts: webservers
  become: true
  tasks:
    - ansible.builtin.import_tasks: tasks/nginx_config.yml
  handlers:
    - ansible.builtin.import_tasks: handlers/main.yml
  tags: [nginx_config, nginx]

The main playbooks file

  • The playbook.yml file is the entrypoint for running our playbooks
  • That file should give us a clean overview of all the available plays
  • For each play inside that file, we define:
    • a name for the play
    • the target hosts
    • whether we want to use root privileges for perfoming actions
    • a list of tasks imported from the tasks folder that we want to perform
    • when using Ansible roles, a list of roles to use
    • a list of handlers files when required
    • a list of tags that can be used to filter plays during runs
  • Here is an example playbook.yml content:
- name: Install Nginx
  hosts: webservers
  become: true
  tasks:
    - ansible.builtin.import_tasks: tasks/nginx_install.yml
  tags: [nginx_install, nginx]
  
- name: Configure Nginx
  hosts: webservers
  become: true
  tasks:
    - ansible.builtin.import_tasks: tasks/nginx_config.yml
  handlers:
    - ansible.builtin.import_tasks: handlers/main.yml
  tags: [nginx_config, nginx]
  
- name: Install MySQL
  hosts: dbservers
  become: true
  tasks:
    - ansible.builtin.import_tasks: tasks/mysql_install.yml
  tags: [mysql_install, mysql]
  
- name: Configure MySQL
  hosts: webservers
  become: true
  tasks:
    - ansible.builtin.import_tasks: tasks/mysql_config.yml
  handlers:
    - ansible.builtin.import_tasks: handlers/main.yml
  tags: [mysql_config, mysql]
  
- name: Servers postconf
  hosts: webservers:dbservers
  become: true
  tasks:
    - ansible.builtin.import_tasks: tasks/servers_postconf.yml
  tags: [servers_postconf] 
  • Have a look at playbooks_keywords for a list of all the keywords we can use inside the main playbook file

Linting Ansible playbooks

  • Linting helps improve Ansible plays quality by giving recommendations on best practices of writing playbooks
  • High quality playbooks are easier to maintain and use with newer Ansible versions
  • We use Ansible Lint command line utility for linting Ansible playbooks
  • Here is an example:
# Install ansible-lint
pip install ansible-lint

# Run
ansible-lint <ansible_files_path>

# Automatically fix some of the errors
ansible-lint --fix

Dealing with secret variables

  • Variables containing sensitive information should be encrypted
  • The ansible-vault utility can be used for that purpose
  • A vault password will be defined in order to encrypt secret.yml. Make sure to put that password in a safe place
  • The defined vault password will be asked to view, edit and decrypt the encrypted secret.yml variable file
  • The --ask-vault-pass option has to be used when running the playbooks in order to provide the vault password required to decrypt secret.yml
  • Use --vault-password-file /path/to/vault_password_file> instead when running the playbooks non interactively (from a CI/CD pipeline for instance), after securely injecting the password file inside the deployment environment
  • Below the commands to use in order to encrypt, decrypt, view and edit secret.yml
# Encrypt staging secret variables file
ansible-vault encrypt group_vars/staging/secret.yml

# Decrypt staging secret variables file
ansible-vault decrypt group_vars/staging/secret.yml

# View the content of staging secret variables file
ansible-vault view group_vars/staging/secret.yml

# Edit the content of staging secret variables file
ansible-vault edit group_vars/staging/secret.yml

Running the playbooks

# Staging
$ ansible-playbook -i hosts/staging playbook.yml --ask-vault-pass

# Prod
$ ansible-playbook -i hosts/prod playbook.yml --ask-vault-pass

Here are additional options that may be useful in some cases:

  • --syntax-check: only check the playbooks syntax
  • --check: run the playbooks in dry run mode (predict changes whithout executing them)
  • --tags <tag_name(s)>: only run plays from the main playbook.yml file, whose tags match the specified tag(s)
  • -u <remote_user>: use the specified user for SSH connection on server(s)
  • --ask-pass, -k: ask for the <remote_user> password for the SSH connection
  • --ask-become-pass, -K: ask for the <remote_user> privilege escalation password. Useful for plays using become: true in order to be run with root privileges
  • --vault-password-file: path to the file containing the vault password. Replaces --ask-vault-pass for non interactive runs