Network Configuration Templates with Ansible and Jinja2

In a previous blog post I showed how to utilize Python to create a script that will generate network configurations from templates utilizing Jinja2. While the solution did work, creating scripts from scratch using a programming language can be a daunting task for network engineers. Luckily for us, there are already tools in place that we can leverage to do the heavy lifting for us. Today we will look at Ansible to generate network device configurations from templates.

How Ansible Works

Ansible is an automation framework that was first developed as a way for server administrators to automate tasks such as deploying software and making configuration changes. It uses the SSH protocol to do this. The aspect of Ansible that has allowed network engineers to embrace it is the fact that it is agent-less. That means it does not require us to install software on our devices in order to utilize it. Ansible also happens to be written in Python, so we can leverage Jinja2 for templating.

Having a framework for automation means that we don’t have to create Python scripts from scratch for different types on automation tasks. The way that Ansible normally works is by connecting to the devices within an inventory file, and then copying Python code onto that device and running it.

Ansible is run a little differently for network devices, since most of them don’t have Python installed and thus dumping Python code onto it will not execute. Instead, Ansible will run locally, and the Python code will be executed by the host running Ansible. This code is what is used to connect to network devices via SSH or an API. These pieces of Python code are known as Ansible modules.

Think of Ansible Modules as plugins that assist you in your automation tasks. Examples of modules for networking gear are the ios_command and ios_config modules. These modules contain all of the code for us to automate gathering data (ios_command) and configuring (ios_config) Cisco devices that run the IOS software. Using modules like this keeps your Ansible playbook clean and easy to create/read, and also hides the complex coding that can be off-putting for us network engineers that are new to programming.

Ansible uses a YAML file known as a playbook. A playbook can easily be described as a list of tasks that need to be accomplished for that particular automation routine. YAML allows playbooks to be complex in what is accomplished, but simple to read and understand.

The only other file required to run an Ansible playbook is the inventory file. The inventory file contains a list of devices that the playbook will be run on. Although Ansible will be executing the Python code locally when interacting with network devices, the inventory will still list the devices that will be interacted with remotely. When you run a playbook you can explicitly name the inventory file. If you do not, Ansible will utilize the default inventory file found at /etc/ansible/hosts. You can also break up your inventory file into different parts. This allows you to use a single inventory file, but break up the devices within it into different groups.

Getting Ansible

Ansible is available for Linux, macOS, and Windows (beta). To install it on macOS, you can use the pip Python installer (Instructions Here). For Windows you can use the Linux Subsystem for Windows, but it is only in beta and not officially supported by Ansible or Microsoft (Instructions Here). For Linux, you can easily install Ansible via your package manager. For my Fedora system, a simple “dnf install” is all that is required.

[[email protected] ~]$ sudo dnf install ansible


Once complete you are ready to start automating with Ansible!

Creating Jinja2 Templates

The first thing we are going to do is create our Jinja2 templates. You can view my Jinja2 and YAML blog post to go over the basics of Jinja2 templates. We will be using two templates in this example to show how multiple templates can be linked together to create a single configuration. This can allow you to make your configurations modular so that you can update only a particular module and version control that rather than the configuration as a whole. This comes in handy when certain parts of the configuration (such as local emergency account credentials) change more frequently than others (disabling unused services for security purposes). We will be using the following templates: baseline.j2 and acl.j2.

Baseline.j2 is going to look very similar to the previous Jinja2 post in that it contains some very basic security configurations, and acl.j2 will contain a simple ACL and the configuration to apply it to the vty line. Below is the configuration each:


service password-encryption
hostname {{ global.hostname }}
enable secret {{ global.enable }}
username {{ global.username }} privilege 15 secret {{ global.adminpass }}

logging trap informational
{% for server in global.syslog %}
logging host {{ server }}
{% endfor %}

ntp authentication-key 1 md5 {{ global.ntpkey }}
ntp trusted-key 1
{% for server in global.ntpserver %}
ntp server {{ server }} key 1
{% endfor %}


ip access-list standard SSH-MGMT
permit log
deny any log

line vty 0 4
transport input ssh
transport output none
ip access-class SSH-MGMT
exec-timeout 5 0

When I previously used a python script with the Jinja2 template, I had a separate .yaml file that was used to contain the variables for the Jinja2 template. Ansible playbooks are also able to use variables, so we can combine both variables into a single location. Within the working directory for your playbook, you can create a folder named “group_vars”. Ansible will look for this folder when executing a playbook and variables are used. Use the “mkdir” command in order to create a directory for our YAML variable file and change to that directory.

[[email protected] playbook1]$ mkdir group_vars

[[email protected] playbook1]$ cd group_vars

[[email protected] group_vars]$

Now we are ready to create variable files. The most basic variable file is named all.yaml, and this can be used for global variables. We can also create .yaml files that correspond to the groups we created in our inventory file. For example, we could make an access.yaml file that would only have variables that would be applied to the access group. Also note that the file extension can be whatever you like, because the ‘- – -‘ at the beginning will signify that the file is a YAML file. For now just create an all.yaml file.

[[email protected] group_vars]$ nano all.yaml


  baseline: baseline.j2
  acl: acl.j2

  hostname: "{{ inventory_hostname }}"
  enable: otaku
  username: admin
  adminpass: 'abc123$%^'
  ntpkey: '123$%^abc'

If you look at the above YAML variables file, you will see that there are two main groups of variables: baselines and global. Notice that the “global” variables match up with the baseline.j2 Jinja2 template (such as global.enable). Also notice how we used a variable inside the variable YAML file! Here {{ inventory_hostname }} will insert the value of the hostname from the inventory file as the hostname for our device. This means you only have to update the hostname within the inventory file, and not in the variables file as well. The “baseline” variables will be used within the playbook when creating tasks for generating configuration modules for each Jinja2 template.

Creating The Inventory

I will create a simple inventory file for this tutorial. The default inventory file located at /etc/ansible/hosts contains a sample inventory that you can view to get a good idea on different ways to annotate hosts in an inventory file.

Now you may be wondering how the inventory file is going to work when generating configuration files and not actually connecting to devices. We can still use the inventory file, but rather than using it to name a remote device we will be connecting to, it will be used to generate the hostname used in the configuration files. This will also provide us a way to give the configuration file a filename that matches the hostname.

To show how an inventory file can contain grouped devices, as well as how a playbook or specific tasks within a playbook can be run only on certain devices, I will create three groups for our inventory file (groups are created with a named header within brackets []). Use your text editor of choice to create the inventory (I have also created a folder dedicated to this particular playbook).

[[email protected] playbook1]$ nano inventory


The inventory file can contain either hostnames or IP addresses. We have grouped our inventory into three groups: access, distro, and core. Each group has two devices within it. We will only be generating configurations for access_sw1 and access_sw2. Once complete, save and quit your text editor (ctrl+x for nano).

Creating The Playbook

Now that we have all of the pieces required for generating configuration templates, we need a way to put them all together. That is where the Ansible playbook comes into play. The playbook is what actually does the automating – it is the commands/scripts being run using the inventory, variable file(s), and Jinja2 template(s) as input data.

Below is the playbook that I have created for this tutorial. Make sure you back out (cd ..) of the group_vars directory and back into the main directory for your playbook. Then use your favorite text editor to create the playbook. (In this example I have created the Ansible playbook with a .yml extension which differs from the group_vars/all.yaml file extension to show how the file extension can be different. This was for demonstration purposes only, and you should really try to stick to a common naming convention for all of your YAML files.)

[[email protected] playbook1]$ nano baseline-playbook.yml

  - name: Generate Baseline Config(s)
  hosts: access
  connection: local
  gather_facts: no


    - name: Create Template storage directory within the staging directory
      file: path=./staging/{{ inventory_hostname }}/ state=directory

    - name: Generate Access Switch j2 template
      template: src=./{{ baselines.baseline }} dest=./staging/{{ inventory_hostname }}/{{ inventory_hostname }}_Access.cfg

    - name: Generate ACL j2 template
      template: src=./{{ baselines.acl }} dest=./staging/{{ inventory_hostname }}/{{ inventory_hostname }}_ACL.cfg

    - name: Append templates together to create final baseline configuration file
      assemble: src=./staging/{{ inventory_hostname }}/ dest=./complete/{{ inventory_hostname }}_FINAL.cfg

    - name: Delete Staging Directories
      file: path=./staging/{{ inventory_hostname }}/ state=absent

All playbooks are YAML files, so they must start with three hyphens at the top (—). After that you will give generic information about the playbook. Use name: to give the playbook a name, and make sure it is something descriptive. Hosts: is used to tell Ansible what hosts this will be run against. Here I have used “access” so that only configurations for the “access” group will be created. Connection: local tells Ansible to use the local machine to run the Python code. As I mentioned earlier, most network devices cannot run the Python code themselves, so connection: local will be used frequently, and definitely used when generating configuration files since there is no device on the other end for Ansible to connect to. Gather_facts: can instruct Ansible to gather information about the remote devices (such as what packages are installed if this was a remote server), but since we are running this locally we can set this to none.

Next comes tasks:, and this is where we start listing the actual parts of our automation script. Under tasks there will be – name: for each step and also what commands/modules/scripts will be run. Let’s run down each one.

Creating A Directory To Store Output

– name: Create Template storage directory within the staging directory

file: path=./staging/{{ inventory_hostname }}/ state=directory

This step creates a directory that will be used to store the configuration modules when they are generated for use in creating a final configuration later. File: is a module that lets us modify file attributes, and in this case it is creating a directory. Path= sets the path to the directory, with ./ being the current directory our playbook is run from, staging/ is a folder within that directory, and {{ inventory_hostname }} refers to a folder that matches the hostname within the inventory file. State=directory tells the file module that this path is a directory. Since the /{{ inventory_hostname }} part of the directory is not actually created, this task will create it. Since we know we will always need a staging directory, we will create it now:

[[email protected] playbook1]$ mkdir staging 

Generate Templated Configs

The next two tasks deal with creating .cfg files (you can use any file extension) that will hold configuration snippets relating to the Jinja2 templates used to create them.

– name: Generate Access Switch j2 template

template: src=./{{ baselines.baseline }} dest=./staging/{{ inventory_hostname }}/{{ inventory_hostname }}_Access.cfg

The above code will use the template Ansible module which deals with utilizing Jinja2 templates. Our template module has two parts: src and dest. Src tells the template module where the source Jinja2 template is located. Here it starts with ./ again to state the current directory, and baselines.baseline leverages a variable from our group_vars/all.yaml file. Dest states where the generated file will be stored. We will store it in the created staging directory for the current hostname with the filename of hostname_Access.cfg.

– name: Generate ACL j2 template

template: src=./{{ baselines.acl }} dest=./staging/{{ inventory_hostname }}/{{ inventory_hostname }}_ACL.cfg

The next task is essentially the same as the last one, except it is using the baselines.acl variable and uses a different filename.

Combining Config Files

Once both configuration files have been generated, we will combine the two files into one that can be used to configure a network device. To do this we will use the assemble module.

– name: Append templates together to create final baseline configuration file

assemble: src=./staging/{{ inventory_hostname }}/ dest=./complete/{{ inventory_hostname }}_FINAL.cfg

The assemble module takes the src and dest options. Src will tell the module what folder to use to find the files that we want to append together. The dest option will state where to put the final configuration file. Here we will put it into the complete directory, utilizing the hostname_FINAL.cfg filename. Like the staging directory, we will create the completion directory since we know we will always need it.

[[email protected] playbook1]$ mkdir complete 

Deleting The Staging Directories

The last thing we will do with this playbook is to do a little cleanup. Once the playbook is complete and we have our _FINAL.cfg file, we no longer need the separate config files we put into staging. We will again use the file module, but this time we will set the state to absent, which will delete the individual hostname folders for each device within the inventory file.

– name: Delete Staging Directories

file: path=./staging/{{ inventory_hostname }}/ state=absent

Running The Playbook

Once we have everything created, we can test our playbook. To run an Ansible playbook you use the ansible-playbook command. Since we have created our own inventory file, we can specify it with the option -i inventory_filename. Use the below command to run the Ansible playbook we have created:

[[email protected] playbook1]$ ansible-playbook -i inventory baseline-playbook.yml

When running the playbook, Ansible will display the status of every task within our playbook, and for each task it will show each device that was used within the inventory.


Running the Ansible playbook really shows why using a good, descriptive name for each task can really payoff. If any of these tasks fail, it will be much easier to know exactly where the playbook had a problem.


To verify everything work correctly, we will view two directories and the two files. First lets take a look at the staging directory. We should see a blank directory since the last task of the playbook was to delete any staging directories. Use the “ls staging” command for this:


You should see nothing. Now lets check the complete folder. This should contain our two _FINAL.cfg files. Use the “ls complete” command for this:


We have successfully generated two config files. Our last verification step is to ensure both files actually contain both configuration templates, and that each hostname is different. Use the “cat /complete/FILENAME” command for this, replacing FILENAME with the actual filename of the config file.



Both files contain all configurations, and each has a different hostname that corresponds to each hostname within our inventory file. Success!


This tutorial walked through utilizing Ansible to automate the task of generating network device configurations from Jinja2 templates. The two basic requirements for running Ansible playbooks, the inventory and the playbook, were explained. The group_vars folder and the variable files were explained to show different ways variables can be used with both Ansible and the Jinja2 templating engine. Two Jinja2 templates were created for two configuration “modules” to show how a full configuration can be broken up into smaller pieces. This can help with version controlling certain aspects of configurations that under change more frequently (such as passwords and ACLs). Finally an example playbook was created to tie all of these pieces together.

As explained in the original blog post about generating templates with Jinja2 and Python, generating network configurations are one of the best and safest ways to begin your journey into network automation. They are not very hard to set up (especially since you already have these baselines/templates in either notepad files or excel docs, right?) and do not involve making changes to any actual network device configurations.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s