Skip to content

Repository files navigation

Ansible Role: dnsdist

Build Status License Ansible Role GitHub tag

An Ansible role create by the folks behind PowerDNS to set up dnsdist.

Requirements

An ansible-core 2.16 or newer installation. Enterprise Linux 8 targets must be managed with ansible-core 2.16: their system Python is 3.6, which the modules of ansible-core 2.20 cannot run.

Dependencies

None.

Role Variables

Available variables are listed below, along with default values (see defaults/main.yml):

dnsdist_install_repo: ""

By default, dnsdist is installed from the software repositories configured on the target hosts.

# Install dnsdist from the master branch
- hosts: dnsdist
  roles:
  - { role: PowerDNS.dnsdist,
      dnsdist_install_repo: "{{ dnsdist_powerdns_repo_master }}"

# Install dnsdist 1.3.x
- hosts: dnsdist
  roles:
  - { role: PowerDNS.dnsdist,
      dnsdist_install_repo: "{{ dnsdist_powerdns_repo_13 }}"

The examples above, show how to install DNSdist from the official PowerDNS repositories (see the complete list of pre-defined repos in vars/main.yml).

- hosts: all
  vars:
    dnsdist_install_repo:
      name: "dnsdist" # the repository name
      apt_repo_origin: "example.com"  # used to pin dnsdist to the provided repository
      apt_version: "dnsdist-19"  # deb822 suites suffix (appended to release codename)
      gpg_key_url: "http://example.com/MYREPOGPGPUBKEY.asc" # repository public GPG key
      yum_repo_baseurl: "http://example.com/centos/$basearch/$releasever/dnsdist"
      yum_debug_symbols_repo_baseurl: "http://example.com/centos/$basearch/$releasever/dnsdist/debug"
  roles:
  - { role: PowerDNS.dnsdist }

It is also possible to install dnsdist from custom repositories as demonstrated in the example above.

dnsdist_install_epel: true

By default, install EPEL to satisfy some DNSdist dependencies like lidsodium. To skip the installation of EPEL set the dnsdist_install_epel variable to false.

dnsdist_package_name: "{{ default_dnsdist_package_name }}"

The name of the dnsdist package: "dnsdist" on both RHEL and Debian derivates distributions.

dnsdist_package_version: ""

Optionally, allow to set a specific version of the dnsdist package to be installed.

dnsdist_package_state: "present"

The desired state of the dnsdist packages. Use "present" (default) to install, "latest" to upgrade, or "absent" to uninstall.

dnsdist_install_debug_symbols_package: false

Install dnsdist debug symbols package.

dnsdist_debug_symbols_package_name: "{{ default_dnsdist_debug_symbols_package_name }}"

The name of the dnsdist debug symbols package to be installed when dnsdist_install_debug_symbols_package is true.

dnsdist_additional_packages: []

List of additional packages to install, list support version pining for each of the packages.

dnsdist_acls: []

Configures the dnsdist ACLS (netmasks).

dnsdist_locals: ['127.0.0.1:5300']

Configure dnsdist's listen addresses.

dnsdist_servers:
  - '127.0.0.1'
  - "{ address='127.0.0.1:5300', source='127.0.0.1@lo', order=1 }"

The list of IP addresses of the downstream DNS servers dnsdist should be send traffic to OR of Lua tables that the newServer function ( https://dnsdist.org/reference/config.html#newServer ) can parse.

dnsdist_carbonserver: ""

The IP address of the Carbon server that should receive dnsdist metrics.

dnsdist_setkey: ""

Encryption key for the dnsdist's TCP control socket. If it is empty, a random key will be generated. If a key is already present in the configuration file, it will be kept.

dnsdist_generatekey: "{{ (dnsdist_setkey | length == 0) | bool }}"

Whether to auto-generate a control socket encryption key. Defaults to true when dnsdist_setkey is empty.

dnsdist_controlsocket: "127.0.0.1"

The listen IP address of the dnsdist's TCP control socket.

dnsdist_webserver_address: ""

The listen IP address of the built-in webserver, empty thus disable by default.

dnsdist_webserver_password: ""

The authentication credentials for the built-in webserver. Must be set when dnsdist_webserver_address is set.

dnsdist_webserver_apikey: ""

The authentication credentials for the built-in API.

dnsdist_webserver_acl: ""

Since 1.5.0, only connections from 127.0.0.1 and ::1 are allowed by default. See https://dnsdist.org/guides/webserver.html for more information.

dnsdist_config: ""

Additional dnsdist configuration to be injected verbatim in the configuration file. Lua format only.

dnsdist_config_format: "lua"

Format of the generated configuration file, lua or yaml. On dnsdist 2.0 the yaml format also needs a unit override, because only 2.1 and newer look for the file. See Configuration format.

dnsdist_config_yaml: {}

The configuration in YAML format, used when dnsdist_config_format is yaml. The dict is passed to dnsdist as given, so every setting is reachable without the role knowing about it. See https://dnsdist.org/reference/yaml-settings.html and the dnsdist.yml-dist file the packages ship. See Configuration format.

dnsdist_config_yaml_string_keys:
  - api_key
  - key
  - name
  - password
  - pool
  - pools
  - secret

Settings whose value must stay a string even when it looks like a number or a boolean. See Configuration format.

dnsdist_config_remove_stale_yaml: true

Whether a lua run removes the YAML configuration file that an earlier yaml run of this role wrote. On dnsdist 2.1 and newer a daemon started without an explicit --config prefers the .yml, so without this a switch back to Lua would leave the old YAML file in charge and the Lua configuration ignored. On 2.0 the .conf is read either way, so the removal changes nothing there - it still runs, because a leftover that starts being read the day the host is upgraded to 2.1 is worse than removing a file the daemon currently ignores. Only the .yml is ever removed, and only while the format is lua.

The path removed is the .yml sibling of dnsdist_config_location, never the platform default, so an invocation that manages dnsdist-a.conf removes dnsdist-a.yml and cannot touch the configuration of another instance.

dnsdist_config_files: {}

Additional dnsdist configuration files to be placed in the configuration directory.

dnsdist_config_owner: ""
dnsdist_config_group: ""

User and Group that own the configuration file. When empty, version-specific defaults are used.

dnsdist_service_overrides: {}

Dict with overrides for the service (systemd only). This can be used to change any systemd settings in the [Service] category.

dnsdist_unit_overrides: {}

Dict with overrides for the service unit (systemd only). This can be used to change any systemd settings in the [Unit] category.

dnsdist_environment_overrides: {}

Dict with overrides for the service environments (systemd only). This can be used to change any environment variables in systemd settings in the [Service] category.

The three dicts are written to override.conf, override-unit.conf and override-environment.conf in /etc/systemd/system/<service name>.service.d/. Emptying one of them removes its own file again and restarts the service; the other two files and any drop-in an operator added next to them are left alone.

dnsdist_service_name: "dnsdist"

Name of the managed service. Set it to dnsdist@<instance> to manage an instance of the templated systemd unit shipped by the dnsdist packages, and set dnsdist_config_location to the matching /etc/dnsdist/dnsdist-<instance>.conf. See Handlers.

dnsdist_config_location: >-
  {{ (dnsdist_config_format == 'yaml')
     | ternary(default_dnsdist_config_location_yaml, default_dnsdist_config_location) }}

Location of the configuration file, /etc/dnsdist/dnsdist.conf on Linux and /usr/local/etc/dnsdist.conf on FreeBSD; with dnsdist_config_format set to yaml, the same path with a .yml extension. Setting it by hand is supported, but the extension has to match the format or the role fails the run. The additional files from dnsdist_config_files are written next to it.

Configuration format

The role writes a Lua configuration by default. Set dnsdist_config_format to yaml to write the YAML configuration that dnsdist 2.0 and newer accept. dnsdist 2.1 and newer then read that file on their own; a 2.0 has to be pointed at it with --config, see Which file the daemon reads.

- hosts: all
  roles:
    - role: PowerDNS.dnsdist
      dnsdist_config_format: yaml
      dnsdist_config_yaml:
        binds:
          - listen_address: "127.0.0.1:5300"
            protocol: Do53
        backends:
          - address: "192.0.2.1:53"
            protocol: Do53
        acl:
          - 127.0.0.0/8
        console:
          listen_address: "127.0.0.1:5900"
        webserver:
          listen_addresses:
            - "127.0.0.1:8083"
          password: "{{ vault_dnsdist_webserver_password }}"

dnsdist_config_yaml is submitted as given. Nothing in it is interpreted, reordered or renamed, so a setting the role has never heard of works as soon as the installed dnsdist supports it. Values that arrive as strings - from an inventory file, from --extra-vars, or from a vault-encrypted variable - are converted to a real number or boolean, because the YAML parser of dnsdist is strictly typed and rejects a quoted number where it wants a real one.

That conversion follows the shape of the value, since a template cannot know the type a setting declares. So a value that must stay a string even though it looks like a number - a password of 12345678, a backend named no - is protected by the key list:

dnsdist_config_yaml_string_keys:
  - api_key
  - key
  - name
  - password
  - pool
  - pools
  - secret

Any setting whose key appears here is emitted as a string untouched, at any depth and inside lists, so an entry under pools stays a string too. Add a key to the list if one of your values is being rewritten.

The two formats are separate inputs and nothing is translated between them. In yaml the configuration comes from dnsdist_config_yaml alone, and dnsdist_locals, dnsdist_servers, dnsdist_acls, dnsdist_controlsocket, dnsdist_webserver_address, dnsdist_webserver_password, dnsdist_webserver_apikey, dnsdist_webserver_acl, dnsdist_carbonserver, dnsdist_tlslocals and dnsdist_config are all ignored. Their YAML equivalents are binds, backends, acl, console, webserver and carbon.

The console encryption key is the one exception, because it is generated on the first converge and has to survive every later one. dnsdist_setkey and dnsdist_generatekey therefore apply in both formats: in yaml the key is read back from and written to console.key, and only when the configuration declares a console section without a key of its own. With no console section there is nowhere to put a key, so none is generated.

The format and the file name have to agree, and the role fails the run when they do not: dnsdist picks its parser from the extension, so a YAML document in a .conf file is read as Lua. The check exists because that mistake is otherwise silent in the worst way - the configuration check passes, and the daemon fails at start.

Which file the daemon reads

Because the parser follows the extension, yaml also moves the configuration file to dnsdist.yml, and dnsdist_config_location follows unless it is set explicitly. Whether the daemon then finds that file on its own depends on the release.

Auto-detection of dnsdist.yml starts with dnsdist 2.1. Given both dnsdist.conf and dnsdist.yml, a 2.1 started without an explicit --config reads the .yml one. The packaged dnsdist.service passes no --config, so on 2.1 and newer switching a single instance over needs no unit override.

On dnsdist 2.0 it does. A 2.0 reads /etc/dnsdist/dnsdist.conf and never looks at the .yml, so a 2.0 host left to the packaged unit keeps running the packaged Lua configuration and ignores the YAML this role wrote - without an error, because both files are valid and the role's configuration check passes against the name it chooses. The daemon has to be pointed at the file:

- hosts: all
  roles:
    - role: PowerDNS.dnsdist
      dnsdist_config_format: yaml
      dnsdist_service_overrides:
        ExecStartPre: /usr/bin/dnsdist --check-config --config /etc/dnsdist/dnsdist.yml
        ExecStart: /usr/bin/dnsdist --supervised --disable-syslog --config /etc/dnsdist/dnsdist.yml
      dnsdist_config_yaml:
        binds:
          - listen_address: "127.0.0.1:5300"
            protocol: Do53

Keep --supervised and --disable-syslog: the unit is Type=notify, and the packaged ExecStart carries both for that reason.

The templated dnsdist@.service needs the same overrides on every release, 2.1 included: its ExecStart and ExecStartPre name /etc/dnsdist/dnsdist-<instance>.conf outright, and an explicit path has no .yml fallback, so an instance in YAML mode needs both overridden:

- hosts: all
  roles:
    - role: PowerDNS.dnsdist
      dnsdist_service_name: dnsdist@a
      dnsdist_config_format: yaml
      dnsdist_config_location: /etc/dnsdist/dnsdist-a.yml
      dnsdist_service_overrides:
        ExecStartPre: /usr/bin/dnsdist --check-config --config /etc/dnsdist/dnsdist-a.yml
        ExecStart: /usr/bin/dnsdist --supervised --disable-syslog --config /etc/dnsdist/dnsdist-a.yml
      dnsdist_config_yaml:
        binds:
          - listen_address: "127.0.0.1:5300"
            protocol: Do53

The role clears the packaged ExecStart and ExecStartPre before setting its own, so the drop-in replaces them rather than appending to them.

dnsdist_service_state: "started"
dnsdist_service_enabled: true
dnsdist_service_masked: false

Allow to specify the desired state of the DNSdist service. E.g. This allows to install and configure DNSdist without automatically starting the service. Masking is a systemd concept and is ignored on hosts without systemd.

dnsdist_disable_handlers: false

Disable automated service restart on configuration changes.

dnsdist_flush_handlers: false

Run the notified handlers at the end of the role instead of at the end of the play. See Handlers.

dnsdist_tlslocals: []

Configures DNS over TLS listeners. The entries are copied verbatim entry-by-entry.

dnsdist_force_reinstall: false

Force reinstall of dnsdist packages by performing a removal prior to the package installation steps. Intended for usage where a downgrade of dnsdist needs to be performed.

Role Tags

Tags for --tags / --skip-tags:

  • repository: repo and GPG key setup, APT pinning, removal of stale versioned repo files.
  • install: package installation and removal.
  • config (alias configure): config files, systemd overrides, control socket key.
  • service: service state.
  • always: OS variable import.

Repository tasks are also tagged install. The control socket key tasks are tagged install, configure and config, so the key exists before the config is rendered.

Contributors: tags belong on the tasks inside install.yml, configure.yml and repo-*.yml, not only on the include_tasks in tasks/main.yml. A dynamic include_tasks does not pass its tags to included tasks, so a narrow --tags run would execute the include and skip its body, silently, with rc=0.

Check Mode

Supported only on a host where this role already ran successfully.

Converged host: --check reports real drift only. Read-only probes such as the key lookup carry check_mode: false so they still run and register results; they change nothing. Without that, every run reports the config as changed with the existing setKey(...) removed.

Fresh host: --check is expected to fail. It installs neither the repository, python3-debian nor the package, so deb822_repository fails and the config cannot be validated without the dnsdist binary.

Package and Service State

  • dnsdist_package_state: present, latest, absent, ...
  • dnsdist_force_reinstall: remove before install, for downgrades.
  • dnsdist_service_state (started, stopped, restarted, reloaded), dnsdist_service_enabled, dnsdist_service_masked (systemd hosts only).

dnsdist_package_state: absent removes the packages, but the config and service tasks still run, so a full run fails on the service task (Could not find the requested service dnsdist). Remove via the install path only:

ansible-playbook site.yml -e dnsdist_package_state=absent --tags install

Works with or without the config file present. That run also removes /etc/systemd/system/<service name>.service.d/ and reloads systemd, so a later reinstall does not inherit the drop-ins of the previous installation.

Handlers

Handlers run at the end of the play, and Ansible shares them between invocations of the same role. A role parameter read inside a handler resolves to the value of the last invocation, so with more than one invocation in a play the restart targets the wrong service or is collapsed into a single run. Set dnsdist_flush_handlers: true to run meta: flush_handlers as the last task of the role, which restarts the dnsdist_service_name of that invocation.

Every instance needs its own service name and configuration file; the templated systemd unit reads /etc/dnsdist/dnsdist-<instance>.conf:

- hosts: dnsdist
  tasks:
    - name: Instance a
      ansible.builtin.include_role:
        name: PowerDNS.dnsdist
      vars:
        dnsdist_service_name: dnsdist@a
        dnsdist_config_location: /etc/dnsdist/dnsdist-a.conf
        dnsdist_locals: ['127.0.0.1:5301']
        dnsdist_controlsocket: '127.0.0.1:5401'
        dnsdist_flush_handlers: true

    - name: Instance b
      ansible.builtin.include_role:
        name: PowerDNS.dnsdist
      vars:
        dnsdist_service_name: dnsdist@b
        dnsdist_config_location: /etc/dnsdist/dnsdist-b.conf
        dnsdist_locals: ['127.0.0.1:5302']
        dnsdist_controlsocket: '127.0.0.1:5402'
        dnsdist_flush_handlers: true

The dnsdist_locals and dnsdist_controlsocket of every instance have to differ, otherwise the second instance cannot bind its addresses.

The systemd overrides of an instance are written to /etc/systemd/system/<service name>.service.d/.

meta: flush_handlers is play-wide: it also runs handlers that earlier roles in the same play notified. dnsdist_disable_handlers: true skips the restart handlers entirely.

dnsdist_flush_handlers defaults to false, which is correct for a single invocation and wrong for more than one: without it the pending restarts of every instance run once, at the end of the play, against the service name of the last invocation.

On systemd hosts the restart handler reloads the units in the same task, so a restart never runs against a unit systemd has not read. The reload happens even when dnsdist_service_state: stopped keeps the service down, so the next manual start uses the drop-ins this run wrote.

Ansible does not filter handlers by tag, so the restart handlers read ansible_skip_tags themselves: under --skip-tags service the service task is skipped and the handler restarts nothing, while the systemd units of that run are still reloaded. --tags config is unaffected and still restarts. dnsdist_disable_handlers: true remains the way to apply configuration without restarting in a run that is not tag-filtered.

Example Playbook

Deploy dnsdist in front of Quad9 and enable the web monitoring interface

- hosts: dnsdist
  roles:
    - { role: PowerDNS.dnsdist,
        dnsdist_servers: ['9.9.9.9'],
        dnsdist_webserver_address: "{{ ansible_default_ipv4['address'] }}:8083",
        dnsdist_webserver_password: 'geheim' }

Changelog

A detailed changelog of all the changes applied to the role is available here.

Testing

Tests are performed by Molecule.

$ pip install tox
$ tox

See molecule/README.md for the test layout, how to run a single leg, and what to change when a new release or operating system has to be covered.

License

MIT

About

An ansible role for dnsdist

Topics

Resources

Stars

41 stars

Watchers

9 watching

Forks

Releases

Packages

Used by

Contributors

Languages