In the previous article in this series, 8 steps to developing an Ansible role in Linux, I created an Ansible role to install Vim and configure it with some plugins and a static vimrc
configuration file.
This article improves this role by replacing this static configuration file with a more flexible one, dynamically generated using an Ansible template.
Ansible templates allow you to create files dynamically by interpolating variables or using logical expressions such as conditionals and loops. It's useful to define configuration files that adapt to different contexts without having to manage additional files. For example, you can create an /etc/hosts
file containing the current node's IP address. You can then execute the playbook in different hosts, resulting in different files—one for each unique host.
The Ansible template engine uses Jinja2 template language, a popular template language for the Python ecosystem. Jinja allows you to interpolate variables and expressions with regular text by using special characters such as {
and {%
. By doing this, you can keep most of the configuration file as regular text and inject logic only when necessary, making it easier to create, understand, and maintain template files.
You can use this template language throughout Ansible playbooks but, to instantiate configuration files based on templates, you use the template module. This module works similar to the copy module but, instead of copying a predefined file, it first renders the template file based on its embedded logic, then copies it to the target host. This module is idempotent, and it only replaces the file if the target's content does not match the rendered template's content.
This example builds upon the role developed in the previous article. If you don't have it, you can find the original files in this repository.
Define a template
Get started by creating a template directory. Switch to the vim
role directory and create a new subdirectory, templates
. This is not strictly necessary, but Ansible looks for template files in this subdirectory by default, making it easier to use them without specifying a full path:
$ cd roles/vim
$ mkdir templates
$ ls
defaults files handlers meta molecule README.md tasks templates tests vars
Then switch into the newly created directory and edit a new template file. By convention, name the template file with the target file name and the .j2
extension. In this example, since you're templating the Vim configuration file .vimrc
, the template name is vimrc.j2
:
$ cd templates
$ vim vimrc.j2
Add the basic static configuration lines at the top of the file:
execute pathogen#infect()
syntax on
filetype plugin indent on
These initial lines are still static and will always be the same for every deployment. So next, let's interpolate some variables to generate a dynamic configuration.
[ Download now: A system administrator's guide to IT automation. ]
Using variables
The primary way to customize a template is by interpolating variables. Add a placeholder for a variable by providing the variable name between pairs of curly braces {{ }}
. When rendering the template, Ansible replaces the entire expression with the variable's value. For example, define a dynamic Vim configuration, allowing the user to specify the desired color scheme by using variable color_scheme
, like this:
colo {{ color_scheme }}
Use another variable, fzf_preview
, to specify the preview window type for the FZF plugin:
" Configuration Vim.FZF
let g:fzf_preview_window = '{{ fzf_preview }}'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" Configuration NERDTree
map <F5> :NERDTreeToggle<CR>
Using variables provides the flexibility to address several requirements, but the template engine can do even more. Next, let's create a conditional block.
Conditional configuration
In some cases, variables are not enough to do what you need. For example, imagine that you want to include a block of text in the configuration file only in certain conditions. The template engine provides more complex expressions, such as if
conditions. Add these conditions delimited by {% %}
. For example, you can include the Airline configuration lines to your .vimrc
file, but only if the vim-airline plugin is in the list of plugins to install:
{% if 'vim-airline' in plugins | map(attribute='name') | list %}
" Configuration vim Airline
set laststatus=2
let g:airline#extensions#tabline#enabled=1
let g:airline_powerline_fonts=1
{% endif %}
In this case, the if
condition looks for the name vim-airline
in the list of plugin names that are extracted from the variable plugins
using the map filter
. For more information about Ansible filters, check the documentation.
Use Ansible facts
In addition to variables that you define in the role or playbook directly, you can also customize your templates by using data collected from the hosts using Ansible facts. The data is available in the ansible_facts
dictionary where each key represents a piece of data collected from the target system.
This example configures the floaterm plugin shell as the user-defined shell in the target machine:
" Configuration floaterm
let g:floaterm_shell = '{{ ansible_facts["user_shell"] }} --login'
This is a powerful resource that allows you to generate a configuration tailored for every host automatically.
[ Need more on Ansible? Take a free technical overview course from Red Hat. Ansible Essentials: Simplicity in Automation Technical Overview. ]
Looping
The last resource you'll use in this example is for
loops. Loops allow you to iterate over a list or dictionary and execute something for each item found. This case allows users to provide a dictionary of extra options they want to use to configure floaterm and define a configuration line for each, like this:
{% for opt in floaterm_options %}
let g:{{ opt }} = {{ floaterm_options[opt] }}
{% endfor %}
In this case, floaterm_options is a dictionary of options defined with a key and value. For each iteration of the loop, the template engine assigns the key to variable opt
. You can use it directly within the for
block or use it to extract the value using the dictionary notation floaterm_options[opt]
.
Define default values for variables
You completed the template definition. The entire template looks like this:
execute pathogen#infect()
syntax on
filetype plugin indent on
colo {{ color_scheme }}
" Configuration Vim.FZF
let g:fzf_preview_window = '{{ fzf_preview }}'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" Configuration NERDTree
map <F5> :NERDTreeToggle<CR>
{% if 'vim-airline' in plugins | map(attribute='name') | list %}
" Configuration vim Airline
set laststatus=2
let g:airline#extensions#tabline#enabled=1
let g:airline_powerline_fonts=1
{% endif %}
" Configuration floaterm
let g:floaterm_shell = '{{ ansible_facts["user_shell"] }} --login'
{% for opt in floaterm_options %}
let g:{{ opt }} = {{ floaterm_options[opt] }}
{% endfor %}
Save and close this file, and then edit the default variable file defaults/main.yml
to define default values for these variables in case the user does not specify them:
---
# defaults file for vim
color_scheme: darkblue
floaterm_options:
floaterm_width: 0.9
floaterm_height: 0.9
floaterm_keymap_toggle: "'<F12>'"
fzf_preview: "right:50%"
plugins:
- name: vim-airline
url: https://github.com/vim-airline/vim-airline
- name: nerdtree
url: https://github.com/preservim/nerdtree
- name: fzf-vim
url: https://github.com/junegunn/fzf.vim
- name: vim-gitgutter
url: https://github.com/airblade/vim-gitgutter
- name: vim-fugitive
url: https://github.com/tpope/vim-fugitive
- name: vim-floaterm
url: https://github.com/voldikss/vim-floaterm
In this case, if the user does not specify the color scheme, the role uses darkblue by default. This is a good way to provide a ready-to-use configuration, yet allow the user to make changes when needed.
Save and close the default variables file. Next, ensure the role uses the template file.
Edit the task file to use the template
Now that your template file and default variables are ready, use them in your role by updating the task Ensure .vimrc config in place
in the file tasks/main.yml
to use the template module instead of the copy module. Make sure the src template name matches the template file with the .j2
extension:
---
- name: Ensure .vimrc config in place
template:
src: vimrc.j2
dest: "{{ vimrc }}"
backup: true
mode: 0640
This is the only required change to the tasks. The role executes all the other tasks in the same way and, in the end, renders the template and uses the resulting file as Vim's configuration file.
[ Looking for more on system automation? Get started with The Automated Enterprise, a free book from Red Hat. ]
Execute the playbook
Now, test your role by executing the playbook using the ansible-playbook
command and the playbook name, the same way you ran it in the previous article. If you've executed this playbook with the vim role before, it will change the configuration file only if needed. If this is the first time you've run it, it will execute all the tasks.
Note: Back up an existing .vimrc
configuration file prior to running this playbook:
$ cd ../..
$ ansible-playbook -K vim-config.yaml
Check the resulting configuration file to see the rendered template:
$ cat $HOME/.vimrc
execute pathogen#infect()
syntax on
filetype plugin indent on
colo darkblue
" Configuration Vim.FZF
let g:fzf_preview_window = 'right:50%'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" Configuration NERDTree
map <F5> :NERDTreeToggle<CR>
" Configuration vim Airline
set laststatus=2
let g:airline#extensions#tabline#enabled=1
let g:airline_powerline_fonts=1
" Configuration floaterm
let g:floaterm_shell = '/bin/bash --login'
let g:floaterm_width = 0.9
let g:floaterm_height = 0.9
let g:floaterm_keymap_toggle = '<F12>'
You can execute the playbook, vim-config.yaml
, again, changing some variables to see the effect on the configuration file. For example, remove the plugin vim-airline from the list, and change the color scheme to industry:
---
- name: Config Vim with plugins
hosts: localhost
gather_facts: true
become: false
tasks:
- name: Configure Vim using role
import_role:
name: vim
vars:
color_scheme: industry
plugins:
- name: nerdtree
url: https://github.com/preservim/nerdtree
- name: fzf-vim
url: https://github.com/junegunn/fzf.vim
- name: vim-gitgutter
url: https://github.com/airblade/vim-gitgutter
- name: vim-fugitive
url: https://github.com/tpope/vim-fugitive
- name: vim-floaterm
url: https://github.com/voldikss/vim-floaterm
Save the playbook, and execute it again:
$ ansible-playbook -K vim-config.yaml
The new configuration file no longer has the Airline configuration:
$ cat $HOME/.vimrc
execute pathogen#infect()
syntax on
filetype plugin indent on
colo industry
" Configuration Vim.FZF
let g:fzf_preview_window = 'right:50%'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
" Configuration NERDTree
map <F5> :NERDTreeToggle<CR>
" Configuration floaterm
let g:floaterm_shell = '/bin/bash --login'
let g:floaterm_width = 0.9
let g:floaterm_height = 0.9
let g:floaterm_keymap_toggle = '<F12>'
What's next?
Templating is one of Ansible's most powerful features. It allows you to define a customizable, dynamic configuration that adapts to each target machine during the playbook execution. You can manage configuration files for hundreds of target machines within a single file or a small group of files by using templates. It makes your roles more flexible and easier to maintain.
This article covered the template engine's basic features, and you can go a long way using only these features. The Jinja2 template language provides many other features that can help you create extensible and adaptable templates for a variety of needs. Make sure to consult its documentation for additional details.
For more information about Ansible, consult its official documentation.
About the author
Ricardo Gerardi is Technical Community Advocate for Enable Sysadmin and Enable Architect. He was previously a senior consultant at Red Hat Canada, where he specialized in IT automation with Ansible and OpenShift.
He has been a Linux and open source enthusiast and contributor for over 20 years. He is currently interested in hacking stuff using the Go programming language, and he's the author of Powerful Command-Line Applications in Go: Build Fast and Maintainable Tools. Ricardo also writes regularly about Linux, Vim, and command line tools for Opensource.com and Enable Sysadmin community publications.
Ricardo enjoys spending time with his daughters, reading science fiction books, and playing video games.
Browse by channel
Automation
The latest on IT automation for tech, teams, and environments
Artificial intelligence
Updates on the platforms that free customers to run AI workloads anywhere
Open hybrid cloud
Explore how we build a more flexible future with hybrid cloud
Security
The latest on how we reduce risks across environments and technologies
Edge computing
Updates on the platforms that simplify operations at the edge
Infrastructure
The latest on the world’s leading enterprise Linux platform
Applications
Inside our solutions to the toughest application challenges
Original shows
Entertaining stories from the makers and leaders in enterprise tech
Products
- Red Hat Enterprise Linux
- Red Hat OpenShift
- Red Hat Ansible Automation Platform
- Cloud services
- See all products
Tools
- Training and certification
- My account
- Customer support
- Developer resources
- Find a partner
- Red Hat Ecosystem Catalog
- Red Hat value calculator
- Documentation
Try, buy, & sell
Communicate
About Red Hat
We’re the world’s leading provider of enterprise open source solutions—including Linux, cloud, container, and Kubernetes. We deliver hardened solutions that make it easier for enterprises to work across platforms and environments, from the core datacenter to the network edge.
Select a language
Red Hat legal and privacy links
- About Red Hat
- Jobs
- Events
- Locations
- Contact Red Hat
- Red Hat Blog
- Diversity, equity, and inclusion
- Cool Stuff Store
- Red Hat Summit