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: files we want to deploy on target machines. Those files do not contain Ansible variables and other Ansible operators like those we can use inside templated files
  • group_vars: variables that will be available for specific machine's groups defined inside machines inventory files
  • handlers: contains tasks that can be run after specific changes... Ex: reload an HTTP server when a specific configuration file has changed
  • hosts: machines 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 machines defined inside the machines inventory files
  • templates: applications and middlewares configuration files containing Ansible variables that will be dynamically replaced during deployment on machines. Files inside that directory have 'j2' extension and the templating engine used is called Jinja2
  • playbook.yml: main file from where Ansible tasks are run

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's 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's 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, that you can use when creating tasks
  • 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 'playbook'. To change that behavior, have a look at controlling-when-handlers-run
  • 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 'plays'
  • That file should give us a clean overview of all the available 'plays'
  • For each 'play' inside that file, we define:
    • a name
    • the target machines
    • whether we want to use root privileges for perfoming the 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 (run only plays having those tags)
  • 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] 
  • For a list of all the keywords we can use inside the main playbook file, have a look at playbooks_keywords

Linting Ansible playbooks

  • Linting helps improve Ansible plays quality by giving recommendations on best practices for writing playbooks
  • High quality playbooks are easier to maintain and use with newer Ansible versions
  • For linting Ansible playbooks, we can use Ansible Lint
  • Here is an example using the 'ansible-lint' command line utility:
# 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 machines
  • --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