We’ve been planning a redesign of a key software platform at my work for about a year now. The majority of the work has fallen on the developers as they had to start from scratch, but I had to be ready to deploy whatever they came up with in a scalable manner. The service is used by a few million people with a few hundred thousand hits a day - so while it is relatively large we don’t need a super-duper-exa-strato-cloud-scale deployment. What we do need is a way to configure multiple servers so that we deliver as few 404/500 errors as possible, and for this I used a combination of Ansible and Cloud Init. Both of these technologies are great and I highly recommend them, but for this post I’ll be looking at how the Jinja templating language brought them together seamlessly for my deployment.
Why does Ansible need Jinja?
I suspect veteran Ansible users would laugh at this question, because even as a new user who has only created three projects I can’t imagine not using templates. But for the sake of argument: Ansible is great at orchestrating events on remote machines and Cloud Init is a fantastic tool for having a server bootstrap itself into a desired state. The trouble, if you want to call it that, is that the simplest Cloud Init configurations still require everything a server normally needs - a hostname, IP address, and a set of users. If you want to configure multiple hosts you’ll need unique configuration files for each of them pre-defined, updated when changes are required, etc. The Jinja integration with Ansible allows this complexity to be managed as variables in your inventories and playbook files, and the Ansible documentation is excellent and can let you quickly find ways to do most things.
Variable substitution, what?
Jinja templates are usually identified by the .j2 suffix, which I have read as being chosen because Jinja is at version 2. Jinja denotes individual variable substitutions with a double curly brace - so in the case of something like this:
number: {{ some_variable_name }}
Ansible will check the available variables for some_variable_name
and replace that value. So if some_variable_name
was 10 then the result at runtime would be:
number: 10
Another common and powerful templating tool is the loop, denoted by a curly brace and a percent sign. This works just like any other loop you may be familiar with from languages like C, Java, Python, etc.
{% for user in users %}
name: {{ user.name }}
rating: {{ user.rating }}
{% endfor %}
So if we had a list of users Kevin and Bob with rating values of 5 and 3 respectively then our output would be:
name: kevin
rating: 5
name: Bob
rating: 3
Object oriented configuration files
Here’s the first part of the cloud-config.yml.j2
I used for a project where multiple hosts in the Ansible inventory file had these variables set either at the host or group level:
#cloud-config
hostname: "{{ server_hostname }}"
fqdn: "{{ server_fqdn }}"
manage_etc_hosts: true
users:
{% for user in users %}
- name: "{{ user.name }}"
gecos: "{{ user.gecos }}"
sudo: {{ user.sudo }}
groups: {{ user.groups }}
shell: /bin/bash
passwd: {{ user.passwd }}
lock_passwd: false
ssh_authorized_keys:
{% for key in user.pub_key %}
- "{{ key }}"
{% endfor %}
{% endfor %}
What’s I love about this is that Jinja is able to operate simple loops and insert the data as needed. In my case it was able to add 7 users with their SSH keys for instant access to the server. The way that Ansible implements Jinja these templates are filled in before the remote execution begins - so your variables are all ready to go before it even connects to the remote server. You can also manipulate files and then insert them into the VM at creation, which I had previously considered limited to the realm of containers through volume mounts, prebuilt VM images, or fancy paid virtualization packages. An example of what I mean (from further down the same file above):
write_files:
- path: /etc/cron.d/my_cron_job
encoding: b64
owner: root:root
permissions: '0644'
content: {{ lookup('template', 'templates/server_cron.j2') | string | b64encode }}
This looks for a template file called server_cron.j2 in the templates directory, processes it by filling in any necessary values, then encoding it as base64 content to be later decoded by Cloud Init and copied to the file /etc/cron.d/my_cron_job
in the provisioned system.
Infrastructure IS code now
Template files in Ansible were the last piece of the puzzle for me on cloud automation. While a lot of orgs have opted for AWS/Azure/GCP for an out of the box solution, with the combination of Cloud Init and Ansible with Jinja templates I can now easily manage a private cloud on our own hardware. Combining templates with well defined host and group variables, it is very easy to get “off the ground” with an automated Ansible deployment without needing to pay for an expensive cloud provider.