6 minutes
Ansible - Variables and Templates - Part 3
Introduction to Variables
Variables are fundamental to making your Ansible playbooks flexible, reusable, and maintainable. Instead of hardcoding values directly into your tasks, you can define them as variables and reference them. This allows you to easily change configurations without modifying the core playbook logic.
Ansible variables can come from various sources, including:
- Inventory variables: Defined directly in your
inventory.inifile. - Playbook variables: Defined within the playbook itself.
- Host/Group variables (preferred): Stored in separate files (
host_vars/ and group_vars/). - Command-line variables: Passed using
-eor--extra-varswhen runningansible-playbook. - Facts: Information gathered by Ansible about the remote hosts.
- Vault variables: Encrypted variables for sensitive data.
Referencing Variables
Variables are referenced in playbooks using Jinja2 templating syntax: {{ variable_name }}.
Types of Variables and Their Use Cases
- Inventory Variables
You’ve already seen ansible_user and ansible_private_key_file in inventory.ini. You can define custom variables too.
Example inventory.ini:
# inventory.ini
[webservers]
web1.example.com
web2.example.com nginx_port=8080
[databases]
db1.example.com
[all:vars]
ansible_user=your_ssh_user
ansible_private_key_file=~/.ssh/id_rsa
default_apache_port=80
In this example:
- nginx_port is a host-specific variable for
web2.example.com. - default_apache_port is a group variable that applies to all hosts under [all:vars].
- Playbook Variables
Defined directly within a playbook using the vars: keyword.
Example playbook_with_vars.yml:
---
- name: Deploy a simple web application
hosts: webservers
become: true
vars:
app_directory: /var/www/my_app
app_user: webappuser
app_group: webappgroup
nginx_config_file: /etc/nginx/sites-available/my_app.conf
tasks:
- name: Create application directory
ansible.builtin.file:
path: "{{ app_directory }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: '0755'
- name: Ensure app user exists
ansible.builtin.user:
name: "{{ app_user }}"
state: present
comment: "Application User"
- name: Ensure app group exists
ansible.builtin.group:
name: "{{ app_group }}"
state: present
- Host and Group Variables (Recommended for Structure)
For larger projects, organizing variables in dedicated host_vars/ and group_vars/ directories is highly recommended. Ansible automatically loads variables from these directories if they match hostnames or group names in your inventory.
Directory Structure:
my_ansible_project/
├── inventory.ini
├── group_vars/
│ ├── webservers.yml
│ └── all.yml
├── host_vars/
│ ├── web1.example.com.yml
│ └── db1.example.com.yml
├── playbooks/
│ └── deploy_app.yml
└── templates/
└── nginx.conf.j2
group_vars/webservers.yml:
# group_vars/webservers.yml
nginx_port: 80
apache_document_root: /var/www/html
host_vars/web1.example.com.yml:
# host_vars/web1.example.com.yml
nginx_port: 8080 # This will override nginx_port from group_vars for web1
group_vars/all.yml:
# group_vars/all.yml
default_timezone: "America/Los_Angeles"
ssh_port: 22
Variable Precedence
It’s important to understand variable precedence in Ansible. When the same variable is defined in multiple places, Ansible follows a specific order of precedence (higher number overrides lower):
- Role defaults
- Inventory variables (from inventory.ini)
- Facts (gathered during Gathering Facts task)
- Playbook variables (vars: in a playbook)
- Variables from group_vars/all
- Variables from group_vars/specific_group
- Variables from host_vars/specific_host
- set_fact (variables created by tasks during runtime)
- Extra variables (-e or –extra-vars on the command line)
This means host_vars will override group_vars, and command-line variables will override almost everything else.
Introduction to Templates (Jinja2)
While the copy module is great for static files, configuration files often need dynamic content (e.g., server names, port numbers, paths) based on variables. This is where the template module and Jinja2 templating come in.
Jinja2 is a modern and designer-friendly templating language for Python. Ansible uses it extensively.
Creating a Template File
Template files typically have a .j2 extension. Let’s create an Nginx configuration template.
- Create a templates directory:
mkdir templates
- Create templates/nginx.conf.j2:
# templates/nginx.conf.j2
server {
listen {{ nginx_port }};
server_name {{ ansible_fqdn }}; # ansible_fqdn is a fact
root {{ apache_document_root }};
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# Example of a conditional block in Jinja2
{% if enable_ssl is defined and enable_ssl %}
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
{% endif %}
}
Explanation of Jinja2 syntax:
- {{ variable_name }}: Renders the value of the variable.
- {% if condition %} … {% endif %}: Control structures for conditional logic.
- {% for item in list %} … {% endfor %}: Loops for iterating over lists (will cover later).
- ansible_fqdn: An example of an Ansible fact (fully qualified domain name).
Using the template Module in a Playbook
Now, let’s use this template in a playbook and define the necessary variables.
- Create playbooks/deploy_nginx_with_template.yml:
---
# playbooks/deploy_nginx_with_template.yml
- name: Deploy Nginx with a templated configuration
hosts: webservers
become: true
vars:
# Playbook-level variables for this specific playbook
nginx_conf_dest_path: /etc/nginx/conf.d/default.conf
# This will override any nginx_port from inventory or group_vars if defined
# unless a host_var is set higher in precedence
nginx_port: 80
tasks:
- name: Install Nginx
ansible.builtin.yum: # or apt for Debian/Ubuntu
name: nginx
state: present
- name: Deploy Nginx configuration from template
ansible.builtin.template:
src: ../templates/nginx.conf.j2 # Path relative to the playbook location
dest: "{{ nginx_conf_dest_path }}"
owner: root
group: root
mode: '0644'
notify: Restart Nginx
- name: Start and enable Nginx service
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers: # Handlers are special tasks that only run when notified
- name: Restart Nginx
ansible.builtin.service:
name: nginx
state: restarted
Important Notes:
- src and dest in template module:
- src: The path to your template file on the control node.
- dest: The path where the rendered file will be placed on the managed node.
- notify: Restart Nginx: This is how
handlersare triggered. If the template task makes a change (i.e., the rendered file is different from the existing one), it will notify the “Restart Nginx” handler. - handlers: section:
Handlersare special tasks that are only run once at the end of a play, even if notified multiple times. This is perfect for service restarts after configuration changes.
Setting Variables for the Playbook
To make this playbook work, we need to ensure the variables nginx_port (if you want to override the playbook default), apache_document_root, and potentially enable_ssl are defined.
Let’s use group_vars for apache_document_root and command-line variables for nginx_port and enable_ssl to demonstrate precedence.
- Create group_vars/webservers.yml:
# group_vars/webservers.yml
apache_document_root: /usr/share/nginx/html # Common default for Nginx
- Run the Playbook:
ansible-playbook -i inventory.ini playbooks/deploy_nginx_with_template.yml \
-e "nginx_port=8080 enable_ssl=true"
- -e “nginx_port=8080 enable_ssl=true”: This passes
nginx_portas8080andenable_sslas true directly to the playbook. These variables will override thenginx_port: 80defined in the playbook’s vars section due to higher precedence.
After running, check the default.conf file on your web server (cat /etc/nginx/conf.d/default.conf) to see the rendered values.
Debugging Variables
The debug module is incredibly useful for inspecting variable values during playbook execution.
---
- name: Debugging variables
hosts: webservers
tasks:
- name: Show Nginx port
ansible.builtin.debug:
msg: "The Nginx port is: {{ nginx_port }}"
- name: Show OS family (a fact)
ansible.builtin.debug:
msg: "OS Family: {{ ansible_os_family }}"
- name: Show the value of a variable that might not be defined
ansible.builtin.debug:
msg: "My custom var: {{ my_custom_var | default('Not Defined') }}"
# Using a Jinja2 filter 'default' to provide a fallback value
Run this playbook (perhaps after setting my_custom_var in your group_vars or with -e) and observe the output.
Next Steps
In Part 4, we will delve into Control Flow with loops and conditionals, which are essential for handling lists of items and making tasks run conditionally.