Skip to main content

How to create dynamic configuration files using Ansible templates

Ansible templates extend your ability to configure applications quickly and easily. This example uses a template to configure Vim.
Image
Golden gears

Image by Pavlofox from Pixabay

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.

Topics:   Ansible   Automation  
Author’s photo

Ricardo Gerardi

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.  More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.