Configuration file from template

Use case

When you need to maintain configuration files including dynamic content, depending on external data (node properties, group, external API, etc…​), and if you do not need the file to be editable manually by users, the usage of templates is recommended!

Policy design

Now we decided to use templating to achieve our goal, we need to choose a method. We have two available languages for templating:

  • Mustache: it is Rudder’s native templating engine, with a very simple syntax, and the best performance. It should be the default choice for most cases.

  • Jinja2: it provides more features to manipulate displayed data, and is compatible with a lot of other automation. It is a bit slower than mustache though (as it uses an external python library).

As jinja2 is not a native templating engine, you have to make sure the jinja2 python module is installed on the target nodes before using it (installing the python-jinja2 package using Rudder is usually enough).

For a quick templating syntax reference, use the Rudder cheatsheet. Here we have no specific reason to use Jinja2, so we will stick with Mustache!

Advanced example: Nginx load balancer configuration

We will here see step by step how to use file templating for an Nginx load-balancer configuration. We suppose that we need to serve various sites, relying on different backends, all of them being defined dynamically.

Source data

We define the following node property in our reverse proxy node details, named nginx:

{
  "port": 80,
  "upstreams": [
    {
      "name": "pool1",
      "location": "/path1",
      "servers": [
        { "host": "server1.rudder.local", "weight": 3 },
        { "host": "server2.rudder.local", "weight": 2 }
      ]
    },
    {
      "name": "pool2",
      "location": "/path2",
      "servers": [
        { "host": "server4.rudder.local", "weight": 1 },
        { "host": "server5.rudder.local", "weight": 1 }
      ]
    }
  ]
}

Configuration policy

The template that will be shared from the Rudder server:

nginx.conf.tpl

http {
    server {
        listen {{{vars.node.properties.nginx.port}}};
        listen {{{vars.node.properties.nginx.port}}};

        {{#vars.node.properties.nginx.upstreams}}
        location {{{location}}} {
            proxy_pass http://{{{name}}};
        }
        {{/vars.node.properties.nginx.upstreams}}
    }

    {{#vars.node.properties.nginx.upstreams}}
    upstream {{{name}}} {
        {{#servers}} (1)
        server {{{host}}} weight={{{weight}}};
        {{/servers}}
    }
    {{/vars.node.properties.nginx.upstreams}}
}
1 Note that for the second level of iteration, we use a name relative to the first iterator’s value.

Result

The generated /etc/nginx/nginx.conf:

http {
    server {
        listen 80;

        location /path1 {
            proxy_pass http://pool1;
        }
        location /path2 {
            proxy_pass http://pool2;
        }
    }

    upstream pool1 {
        server server1.rudder.local weight=3;
        server server2.rudder.local weight=2;
    }
    upstream pool2 {
        server server5.rudder.local weight=1;
        server server6.rudder.local weight=1;
    }
}

Advanced example: sshd_config templating

This example ensures the content of a bastillion host’s sshd_config, configurable via either variables shared via group membership or private node properties.

Source data

We can either define the following node property in our Rudder server for any secured system, or use a Variables (any) directive to passing this to multiple systems in a group (like systems requiring extra security due being exposed to the internet).

Value of either Node property named ssh_access or a Variable (any) directive named bastillion.ssh_access:

{
  "fromip" : [ "10.111.20.1", "10.111.20.2" ],
  "user"   : [ "joe", "jack" ],
  "group"  : [ "admins" ]
}

Configuration policy

The configuration will consist of a shared template downloaded from shared-files of the Rudder server and a method to provide the input data for the template. We will show two approaches on that.

Example template with a Node property holding the data

Protocol 2
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
DenyUsers *

Match Address {{#vars.node.properties.ssh_access.fromip}}{{{.}}},{{/vars.node.properties.ssh_access.fromip}} User {{#vars.node.properties.ssh_access.user}}{{{.}}},{{/vars.node.properties.ssh_access.user}}
  DenyUsers !*

Match Address {{#vars.node.properties.ssh_access.fromip}}{{{.}}},{{/vars.node.properties.ssh_access.fromip}} Group {{#vars.node.properties.ssh_access.group}}{{{.}}},{{/vars.node.properties.ssh_access.group}}
  DenyUsers !*

The iterator of the arrays uses the {{{.}}} to reference the value if the currently iterated item, which is the value of the array item.

Example template with a Variables (any) directive holding the data.

You can use a dedicated rule to assign the directive to a group of nodes, with this approach you can have different directives allowing different access but reuse the same template and directive with data provided by different rules.

Protocol 2
PasswordAuthentication no
PubkeyAuthentication yes

DenyUsers *

Match Address {{#vars.bastillion.ssh_access.fromip}}{{{.}}},{{/vars.bastillion.ssh_access.fromip}} User {{#vars.bastillion.ssh_access.user}}{{{.}}},{{/vars.bastillion.ssh_access.user}}
  DenyUsers !*

Match Address {{#vars.bastillion.ssh_access.fromip}}{{{.}}},{{/vars.bastillion.ssh_access.fromip}} Group {{#vars.bastillion.ssh_access.group}}{{{.}}},{{/vars.bastillion.ssh_access.group}}
  DenyUsers !*

Technique to deploy the template

This template be handled by a dedicated technique that more or less consists of:

  • File from remote source: Download the file from /var/rudder/configuration-repository/shared-files/sshd_config.mustache for example to /etc/ssh/sshd_config.template

  • File from a mustache template: Create a populated file from the template /etc/ssh/sshd_config.template to /etc/ssh/sshd_config.final

  • File from local source with check: Copy /etc/ssh/sshd_config.final to `/etc/ssh/sshd_config if command /usr/sbin/sshd -t /etc/ssh/sshd_config.final returns 0 (verify configuration before trashing your sshd config)

  • Service restart: Restart sshd if previous method has condition _repaired

(Methods names taken from Rudder 4.3)

Resulting config file

The result is an output like this for the final config file

Protocol 2
PasswordAuthentication no
PubkeyAuthentication yes

DenyUsers *

Match Address 10.111.20.1,10.111.20.2, User joe,jack,
  DenyUsers !*

Match Address 10.111.20.1,10.111.20.2, Group admins,
  DenyUsers !*

Notes

  • Apparently the sshd_config is still valid if the Match-Group has commas on the end of a list, and if you don’t want to have any Groups or Users, just keep the json array for them as an empty array ([ ]), and it will still be a valid sshd_config (but it will definitely look strange).

  • Please check the configuration options that are available for the version of your sshd, most importantly of what is supported in the Match-Block. Earlier versions of sshd do not support all config options, this is also why validation the generated file is always a good option so an update of sshd can not break your access easily, just make sure you check the compliance after updates.

  • This approaches can be mixed with both node-property and generic-variable based input data, and is only an example on the

Advanced example: select IP by interface name priority

Jinja2 templates can contains more advanced logic. In this example, we will show how to get an IP by interface priority. It allows providing an algorithm to compute the "main IP address" based on the interface names that can exist on the machine.

In our case, we want the resulting IP to come from:

  • The bond0 interface if it exists

  • If not, then try to use the eth0 fallback

  • Finally, if none are found, just use the first IP provided by the agent

{%- if   'bond0' in vars.sys.interfaces %}
    # 'bond0' in vars.sys.interfaces
    {%- set my_ip = vars.sys['ipv4[bond0]'] %}
{%- elif 'eth0' in vars.sys.interfaces %}
    # Use eth0 as fallback
    {%- set my_ip = vars.sys['ipv4[eth0]'] %}
{%- else %}
    # Not found bond0 or eth0, using default sys.ipv4
    {%- set my_ip = vars.sys.ipv4 %}
{%- endif %}

← Ensure the presence of Windows software Enforce a line is present in a file only once →