Ansible: conf.d compilation pattern
Managing complex application configuration
It’s nice to take a complex application config, say for example the routing rules used in logstash and split them up in to stand-alone files. Use descriptive filenames and you’ve got an easy to maintain configuration!
How do you install these configuration files on a remote server? Simply copying the files over will work the first time but in the future you might remove a file from your configuration control. Do you script the removal of the remote files? Perhaps you change your copy script to first delete all files in the folder then copy over?
Neither of these is a good option. Removing all remote files breaks callback systems. You want to restart processes when files are changed and by deleting you would force this restart each time. And keeping track of which files are removed is more error-prone book-keeping. Realistically, people just want to add/remove/edit config files in the folder and expect them to show up on the remote server.
To get the intended behavior you should compile the smaller config files in to a larger config file. This makes diffing/callbacks easy and frees you from tracking files.
Get it done with Ansible
Ansible has a useful module called assemble
which concatenates static files in to one larger file. Which might get you pretty far but I need template evaluation.
Getting an ansible run to evaluate templates and concatenate them will require some gymnastics. Here’s what you’ll need:
1. A folder full of your broken down configuration templates
2. A special template in that same folder which can evaluate the templates
Your ansible role will be laid out as such:
roles/logstash/
├── tasks
│ └── main.yml
├── templates
│ ├── 16-foo.conf.j2
│ ├── 35-bar.conf.j2
│ ├── 40-quuz.conf.j2
│ └── logstash.conf.j2.noglob
└── vars
└── main.yml
It’s important to put the number at the beginning of the filename. The glob tool lists the files in lexographical order and this way you’ll always have a consistent rendering. And for some applications it matters how things are ordered.
You install the template as always, from roles/logstash/tasks/main.yml
:
- name: assemble logstash es config
template: >
src=logstash.conf.j2.noglob
dest=/home/core/logstash-config/logstash.conf
The file roles/logstash/templates/logstash.conf.j2.noglob
is the root template which iterates on the variable:
{% for template_name in lookup('fileglob', '../roles/logstash/templates/*.j2', wantlist=True) %}
### START PARTIAL {{ template_name }} ###
{{ lookup('template', template_name) }}
### END PARTIAL ###
{% endfor %}
The tricks here are lookup('fileglob', $GLOB, wantlist=True)
. Getting the parameters correct took some exploration.
lookup('fileglob)
: much like a shell script glob pattern, this lets you expand a path with wildcards. You must add wantlist=True
so you can a python list back. My biggets concern with this templating code: how to keep it generalized and independent of location in the project’s folder structure.
It’s not impossible but there are some got’chas. When lookup('fileglob')
is run from a roles varfile it’s relative to the roles files directory. When run inline from a roles template file it’s relative to the playbook’s folder. I put my playbooks in $ROOT/playbooks
so that’s why I have to do “../roles”. If you’re using a standard layout you’ll just need roles/
.
lookup('template)
: evaluate a template at that point. Luckily the fileglob
module provided a full path so there’s no confusion about where to find the template. I use the | basename
helper to strip off my workstation’s local path but still know what file generated the content.
Done
So there ya go, an idempotent, composable config compiler. Neat.