Dynamic Templating

Overview

When I switched companies a while ago, I came across some a pattern in several Salt formulas that the CTO at my new company had written. To this day, it’s still one of the more brilliant snippets I’ve seen for the use case of templating out several files in a state all at once.

Context

Some applications can require a great many configuration files. For example a webserver such as apache or nginx can have many configuration files to support various virtual hosts. In this case, the most straightforward way of writing a formula would dictate creating a declaration for each file or template. A time consuming, repetitive task that requires upkeep.

Oddly, though I’ve configured and worked with SaltStack across several organizations at this point, I had not come across this type of scenario before now. At most, I’ve only had a handful of config files for an application and so this problem wasn’t really a problem. At my new company though, the CTO had already come up against this problem, and then put the time in to figure out a very elegant solution.

Setup

Let’s say our application is nginx and we need to drop in a few configuration files for it. For arguments sake, some of these conf files need to be templated, and some do not.

The Conventional Method

Now, lets say that we need to manage a static nginx.conf, but also drop some templated config files, eggs.conf and bacon.conf, into the /etc/nginx/conf.d directory.

Conventionally, this would be accomplished with three separate declarations. On our Salt Master, we would need to create a state file along with the static and template files we will need to go with it. The directory structure in our file_roots may look something like:

nginx
├── files
│ ├── bacon.conf.jinja
│ ├── eggs.conf.jinja
│ └── nginx.conf
└── init.sls

And then our state file would look like:

{{ sls }} - manage the nginx config file:
file.managed:
- name: /etc/nginx/nginx.conf
- source: salt://nginx/files/nginx.conf

{{ sls }} - managed the eggs config file:
file.managed:
- name: /etc/nginx/conf.d/eggs.conf
- source: salt://nginx/files/eggs.conf.jinja

{{ sls }} - managed the bacon config file:
file.managed:
- name: /etc/nginx/conf.d/bacon.conf
- source: salt://nginx/files/bacon.conf.jinja

Add Jinja Magic

Now, instead of being conventional, lets use jinja to make this dynamic. First, we’ll need to revise the layout of our states’s files directory to:

nginx
├── files
│ └── etc
│ └── nginx
│ ├── conf.d
│ │ ├── bacon.conf.jinja
│ │ └── eggs.conf.jinja
│ └── nginx.conf
└── init.sls

As can be seen, in our state’s files directory, we’ve added the directory path where the nginx config files will be when rendered to the minion. This is important because it provides the hint our jinja code will need to know where on the minion filesystem to put the files.

Now, we revise the state:

Given the files and state above, the output of a render is:

nginx/init.sls - manage the config file /etc/nginx/nginx.conf: 
file.managed:
- source: salt://nginx/files/etc/nginx/nginx.conf
- name: /etc/nginx/nginx.conf
- makedirs: True

nginx/init.sls - manage the template file /etc/nginx/conf.d/bacon.conf:
file.managed:
- source: salt://nginx/files/etc/nginx/conf.d/bacon.conf.jinja
- name: /etc/nginx/conf.d/bacon.conf
- makedirs: True
- template: jinja

nginx/init.sls - manage the template file /etc/nginx/conf.d/eggs.conf:
file.managed:
- source: salt://nginx/files/etc/nginx/conf.d/eggs.conf.jinja
- name: /etc/nginx/conf.d/eggs.conf
- makedirs: True
- template: jinja

How It Works

The single declaration in our state uses the salt cp.list_master function to retrieve a list of full paths to all files on the master that match the prefix we specify. In our declaration, we specify the prefix as being the files directory of the running formula ( nginx/files). So now that we have a list of full paths to retrieve source files from the master with, we only need to know where to put the file on the minion. Since the directory layout once inside the SLS files directory fully matches the path for each config file on the minion, we are able to derive the target location by using a jinja replace function to substitute the prefix with nothing for each file.

As such, this code is completely portable between different states with no modifications. The only caveat is having the files directory contain the full directory paths of where the config files and templates need to be placed.

A Note On Security

Working with the salt cp.list_master function gives us a good reminder that ALL minions have access to everything in the salt master's file roots. Keep this in mind with static config files and make sure you never put secrets in anything in the file roots.

Originally published at https://smartaleksolutions.com on October 20, 2020.

--

--

Climber, surfer, yogi, dad who does some IT on the side to get by.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store