Easily manage Apache VirtualHosts with Ansible and Jinja2

Server Check.in's entire infrastructure is managed via Ansible, which has helped tremendously as the service has grown from one to many servers.

Ansible Borg Cow
cowsay and Ansible were made for each other.

One pain point with running Apache servers that host more than one website (using name-based virtual hosts) is that the virtual host configuration files quickly get unwieldy, as you have to define the entire <VirtualHost /> for every domain you have on the server, and besides Apache's mod_macro, there's no easy way to define a simple structured array of information and have the vhost definitions built from that.

In my case, for ease of generic local development, I have one Vagrant/Ansible profile that installs MySQL, Apache, PHP, Node.js, and a few other tools for local development, and I originally included a barebones vhosts.conf file that was included by the main httpd.conf file Apache used.

The file looked something like this:

NameVirtualHost *:80

<VirtualHost *:80>
  ServerName www.domain.tld
  DocumentRoot /www/domain
  [more rules...]
</VirtualHost>

<VirtualHost *:80>
  ServerName www.otherdomain.tld
  DocumentRoot /www/otherdomain
  [more rules...]
</VirtualHost>

[more definitions here...]

However, this file grows over time into an unmanageable mess, as I've had more than 20 vhosts at times. (Aside: I could avoid this mess by working on sites individually with different Vagrant/Ansible profiles per instance, but when I'm doing generic work on a normal LAMP stack, I like having one main VM that has all the tools installed (mostly so I don't have to keep booting and halting VMs and loading up my small SSD with a bunch of VMs)).

Ansible and it's built-in integration with the Jinja2 templating system makes managing virtual hosts (and other files where you might have a lot of repeated boilerplate with different variables) simple.

As a simple example (without many options for the virtual hosts), here's all I had to do to make Ansible and Jinja do the heavy lifting for me:

1. Add .j2 to the end of my vhosts.conf file to indicate that it's a Jinja2 template.

2. Modify the vhosts.conf.j2 template so instead of listing all the virtual hosts, it just has one definition inside a for loop, like so:

NameVirtualHost *:80

{% for vhost in apache_vhosts %}
<VirtualHost *:80>
  ServerName {{ vhost.servername }}
  DocumentRoot {{ vhost.documentroot }}
{% if vhost.serveradmin is defined %}
  ServerAdmin {{ vhost.serveradmin }}
{% endif %}
  <Directory "{{ vhost.documentroot }}">
    AllowOverride All
    Options -Indexes FollowSymLinks
    Order allow,deny
    Allow from all
  </Directory>
</VirtualHost>

{% endfor %}

(The extra space before the endfor is to allow for a space between each vhost entry, and the if clause is indented as such to prevent the following line from being indented an extra space).

3. Define the apache_vhosts variable inside your playbook, or via an included playbook, or via any other supported Ansible variable definition method, for example:

vars:
  apache_vhosts:
    - {servername: "www.domain.tld", documentroot: "/www/domain"}
    - {servername: "www.otherdomain.tld", documentroot: "/www/otherdomain", serveradmin: "webmaster@otherdomain.tld"}

I've taken to using Ansible for even the simplest of server configurations, even for one-off servers (like a master monitoring server), as it not only codifies every bit of configuration, it also makes changes, additions, and redeployments so much easier and faster!

I tried my hand at Puppet and Chef, and thought about using SaltStack for a while, but Ansible has been so much easier to grok (not to mention deploy—there's no extra software to install on my servers!), and can do everything I've thrown at it so far. Expect more on how Server Check.in is using Ansible in the future!

Comments

Hi Jeff, I think placing multiple vhosts in one file is not very compatible with the certbot role https://github.com/geerlingguy/ansible-role-certbot, because I think that the certbot program expects each subdomain in a separate file in sites-available folder (correct me if I am wrong).