Big Bubbles (no troubles)

What sucks, who sucks and you suck

An Ansible Pattern for Parameter Configuration

Many of the Ansible roles we write are concerned with installing and managing the configuration of a particular application or package. For example, we have a role that installs PHP and configures particular settings in the php.ini file according to a Jinja2 template.

My preference for specifying these parameters is to gather them under a single dictionary. This seems like a logical organisation and avoids polluting the Ansible variable namespace with a large number of verbose names. For example, for PHP we might have the following in the role defaults:

1
2
3
4
5
6
7
8
php_param:
  display_errors: 'Off'
  expose_php: 'Off'
  memory_limit: '128M'
  max_execution_time: 30
  post_max_size: '8M'
  upload_max_filesize: '100M'
  short_open_tag: 'Off'

Each of these settings relates to a parameter in php.ini, which is configured in the template php.ini.j2, e.g.:

1
display_errors = {{ php_param.display_errors }}

The problem with this method comes when the user wants to change one of those settings. Doing this:

1
2
php_param:
  expose_php: 'On'

…will change the setting for expose_php, yes. But it will also remove all the other defaults, as we’ve now redefined the entire php_param dictionary. (Ansible doesn’t offer a way for variables to inherit or collate values from multiple definitions at different levels transparently, so you can’t append or override the values of individual keys in a dictionary simply by defining them higher up.)

The solution is to allow the role user to specify their own settings in a separate dictionary and then use the combine filter to merge this with the defaults:

1
2
3
4
5
6
7
8
9
php_custom_param:
  expose_php: 'On'
# ...
- set_fact:
    php_param: "{{ php_param | combine(php_custom_param) }}"
  when: php_custom_param is defined
- template:
    src: php.ini.j2
    dest: /etc/php.ini

Here, values for matching keys in php_custom_param will overwrite those in php_param and any additional keys will be appended to the original dictionary. Just remember to keep referring to php_param in the template and not the custom parameters. (You might want to define php_param in the role vars directory instead, so the user can’t redefine it in a possibly incompatible way.)

This is a useful pattern but in the case of PHP, we could extend it further in the template. In its current form, adding support for changing other PHP parameters or adding new ones would involve adding a new default value to php_param and editing the template to reference that value for the appropriate setting. But if the keys in php_param are assumed to always be valid PHP settings (and you’re happy to let users alter arbitrary settings), we might as well use them as-is for the parameter name too (and loop through all of them in one pass):

1
2
3
{% for k in php_param.keys() %}
{{ k }} = {{ php_param[k] }}
{% endfor %}

Typically, PHP reads its configuration settings first from /etc/php.ini and then from all the files in a defined configuration directory such as /etc/php.d/ (this is how the standard PHP package in Red Hat/CentOS behaves). Settings can be duplicated within all these files; the last matching value is used, so settings in the php.d/ files can override those in the main configuration. This means we can write our settings out to an override file and leave the installed configuration file untouched:

1
2
3
- template:
    src: php.override.ini.j2
    dest: /etc/php.d/zzz_override.ini

(The example here gives the destination configuration file a name that should ensure it is parsed last by PHP so that none of its settings are overridden. You might want to make this a configurable variable, so that users can change its precedence in the parsing order.)

Note that this method works for Red Hat/CentOS platforms but will need some adaptation for Debian-based distributions as those use different configuration directory paths depending on the context (SAPI) in which PHP is called.