Using ansible’s templating capabilities to deliver a keepalived configuration file

I’ve become a huge fan of ansible’s templating capabilities over the past few months. If you haven’t used them they allow you to control the content of a file that is delivered to a system. The templates can contain variable names which get filled in with well known values, you can use math operations and various filters to derive values, and these can all be wrapped in logic statements to control when and where this occurs.

To illustrate this lets say we are looking to stand up a fault tolerant haproxy cluster and want to use keepalived to control the virtual IPs that float between servers. You could create one configuration file per server and then push these to the appropriate server through the copy module. This violates the duplication anti-pattern and adds more maintenance over the long term. A better approach would be to create one configuration file and fill it in with variables that are unique to each server. These unique variables could be the name of the host, its primary interface, the EC2 auto scaling group, etc. Ansible makes this crazy easy with templates.

Ansible templates are built on top of the amazing Jinja2 templating language. The language allows you to do things like format data, perform math and set operations, calculate random values, fill in variables if a logic operation succeeds etc. I won’t go into any additional detail on the language since the official ansible and Jinja2 documentation are solid!

Now back to the keepalived example. Lets say I want to create a unique keepalived.conf on each server. The ansible template module can take a template file we created, process it with Jinja2 and then spit out a unique configuration file on each server. A template module task takes the following basic form:

- name: Create keepalived configuration file
  template:
    src: keepalived.conf.j2
    dest: /etc/keepalived/keepalived.conf
    owner: root
    group: root
    mode: 0600

In this example the template file named keepalived.conf.j2 will be processed and a file named /etc/keepalived/keepalived.conf will be created on the server or servers this play was run against. The keepalived.conf.j2 file is a standard configuration file which contains variables (enclosed in mustaches) and logic (e.g., for foo in bar …). To fill in the keepalived.conf global_defs section we can create a couple of variables to define well known values (this gets powerful when you define variables in one place and use them throughout your roles and playbooks):

keepalived_email_to: root
keepalived_email_from: root
keepalived_smtp_server: localhost

These can be combined with the well know ansible_fqdn variable to give something similar to this:

global_defs {

   # Send an e-mail to each of the following
   # addresses when a failure occurs
   notification_email {
       {{ keepalived_email_to }}
   }
   # The address to use in the From: header
   notification_email_from {{ keepalived_email_from }}

   # The SMTP server to route mail through
   smtp_server {{ keepalived_smtp_server }}

   # How long to wait for the mail server to respond
   smtp_connect_timeout 30

   # A descriptive name describing the router
   router_id vrrp-director-{{ ansible_fqdn }}
}

If this play was run against a server named haproxy01 we would get the following global configuration:

global_defs {

   # Send an e-mail to each of the following
   # addresses when a failure occurs
   notification_email {
       root
   }
   # The address to use in the From: header
   notification_email_from root

   # The SMTP server to route mail through
   smtp_server localhost

   # How long to wait for the mail server to respond
   smtp_connect_timeout 30

   # A descriptive name describing the router
   router_id vrrp-director-haproxy01
}

That’s handy, and allows you to create a single configuration file with unique values for each system. To continue on with our HA keepalived setup lets say it needs to manage two virtual IP addresses and we want each server to master one IP address. Once again we could hard code the values in multiple configuration files or we can use a bit of logic to create unique vrrp_instances for each server. The following snippet shows an example of this:

{% for ip_address in vars['keepalived_virtual_ipaddresses'] %}
# Create a VRRP instance
vrrp_instance vrrp-director-{{ ansible_fqdn }} {
    # The initial state to transition to. This option isn't
    # really all that valuable, since an election will occur
    # and the host with the highest priority will become
    # the master. The priority is controlled with the priority
    # configuration directive.
    state MASTER

    # The interface keepalived will manage
    interface {{ ansible_default_ipv4.interface }}

{% set router_id = keepalived_initial_router_id + loop.index %}
    # The virtual router id number to assign the routers to
    virtual_router_id {{ router_id }}

{% set node1 = groups["haproxyservers"][0:1] | join(" ") %}
{% set node2 = groups["haproxyservers"][1:2] | join(" ") %}

{% if loop.index % 2 %}
  {% if inventory_hostname == node1 %}
    {% set priority = 1 %}
  {% else %}
    {% set priority = 2 %}
  {% endif %}
{% else %}
  {% if inventory_hostname == node2 %}
    {% set priority = 1 %}
  {% else %}
    {% set priority = 2 %}
  {% endif %}


    # The priority to assign to this device. This controls
    # who will become the MASTER and BACKUP for a given
    # VRRP instance.
    priority {{ priority }}

    # How many seconds to wait until a gratuitous arp is sent
    garp_master_delay 10

    # How often to send out VRRP advertisements
    advert_int 1

    # Execute a notification script when a host transitions to
    # MASTER or BACKUP, or when a fault occurs. The arguments
    # passed to the script are:
    #  $1 - "GROUP"|"INSTANCE"
    #  $2 = name of group or instance
    #  $3 = target state of transition
    # Sample: VRRP-notification.sh VRRP_ROUTER1 BACKUP 100
    # notify "/usr/local/bin/VRRP-notification.sh"

    # Send an SMTP alert during a state transition
    smtp_alert

    # Authenticate the remote endpoints via a simple 
    # username/password combination
    authentication {
        auth_type AH
        auth_pass {{ keepalived_auth_key }}
    }
    # The virtual IP addresses to float between nodes. The
    # label statement can be used to bring an interface 
    # online to represent the virtual IP.
    virtual_ipaddress {
        {{ ip_address }}/32 dev {{ ansible_default_ipv4.interface }}
    }
}
{% endfor %}

In the output above I am iterating over one of more IPs defined in the keepalived_virtual_ipaddresses variable and building a unique vrrp_instance stanza for each one. The physical interfaces are assigned based on the value of the well known ansible_default_ipv4.interface variable, the virtual_router_id is assigned dynamically for each stanza and the priority value (this controls who owns the IP initially) is generated on the fly based on a modulus operation. I’m still learning everything there is to know about Jinja2 and I’m sure I will refactor this in a couple of months once I come across a better way to do this. This blog post is more of a reference to myself than anything else.

1 thought on “Using ansible’s templating capabilities to deliver a keepalived configuration file”

Leave a Reply

Your email address will not be published. Required fields are marked *