The DNS charms project

The goal of the suite of DNS charms is to provide an easy-to-use and somewhat opinionated way to deploy DNS for your infrastructure using Juju. Since DNS can be used in many different ways and bind (the DNS server that we use as a workload for these charms) can be configured in many different ways, it is difficult to choose “one true way”. Since we intend to use these charms for our own infrastructure, we had some constraints that guided our decisions along the way. We still hope that the result is usable by a large majority of people who want a quick, production-ready solution that also scales to quite large deployments.

dns_record interface

The first and simplest way to use these charms is by only deploying an authoritative DNS server (here bind-operator) and giving it a list of DNS records to be published. We want the operator to be able to give a list of DNS records as they usually would in a zone file. The format should therefore ideally be host_label, TTL, record_class, record_type and record_data. We can see that in this situation, someone is asking for those records to be published and someone is publishing them. The operator or charm asking for those records will be the requirer, and the authoritative name server is the provider.

DNS record

This realization led us to design the dns_record interface, where the requirer basically sends a list of DNS records with a UUID for each (creating a DNS entry/request that way), and the provider can respond to those requests by stating the status of each request (using the UUID given by the requirer as an identification mechanism).

And this is exactly what we do when integrating bind-operator to dns-integrator and configuring the latter with a list of DNS record requests: we are abstracting away the zone file to just a list of DNS records, and bind-operator is doing the hard work of stitching together all the requests from all the requirers into a cohesive set of DNS zone files that can be consumed and published by bind.

Handling DNS record request merges

We just explained that all DNS record requests received by bind-operator through all its dns_record relations are merged into one cohesive set of zone files for it to publish. This is very different from what traditional DNS servers do, and it is also opinionated in the way those files are created.

The first thing that is done by bind-operator is to organize the record requests into zones, based on the declared domain of each record. This enables bind-operator to produce the corresponding zone file for each zone. It also detects any conflicts between records: if they have the same domain, host label, record class and type but not the same TTL and/or data. Any errors during these processes stop the charm from updating the configuration files of bind and are reported through the relations so that operators can take action accordingly (by fixing the conflict, for example).

Requests merging

This conflict handling mechanism is one expression of our opinionated way to handle this DNS deployment since it doesn’t really exist in the DNS world. We wanted to make sure that operators of different teams would not step on each other’s shoes while deploying applications and integrating them to bind-operator. If you want the usual round-robin response that a DNS server like bind should give when multiple records with different data are published, we are working to allow conflicts on a record-by-record basis in the dns_record interface.

Adding a policy layer

Now that we understand that bind-operator is made to publish record requests from applications deployed by various teams, it raises the question of security: which application, or more specifically which records, will we allow to go through the publishing process? We wanted this approval operation to be doable by humans and software alike. We therefore designed the dns-policy charm. This operator is meant to sit between bind and the requirer application, working as a provider for the requirer application and a requirer for bind. All the record requests are accumulated in a database on the workload, and an overlay Django application exposes a GUI and an API to approve and/or deny those requests. All requests are uniquely identified by their UUID (generated by the requirer) so that if a record was previously approved, its data may change without having to approve it again. We made that decision because we wanted to reduce toil when an application needs to regularly change its data. The idea is that if an application has the right to publish some data on a host label in a domain, then it will retain that right until it is revoked.

Approve/Denial process

A charm for each DNS server

With bind-operator and dns-integrator, we have the core functionality necessary for our DNS setup. But we still want to be able to mimic the classic “hidden primary” setup where the primary DNS server is not visible in the zones and these are served by a set of secondaries instead. This is where the dns-secondary charm comes into play. When bind-operator gets integrated to dns-secondary, it rewrites the configuration for the zone, removing references to its own units in favor of those of dns-secondary and then transfers its zones to dns-secondary. Now dns-secondary can serve the zones without leaking any IP address of the primary deployment.

We also want to be able to deploy DNS resolvers, and that’s the role of the dns-resolver charm. Once integrated to bind-operator or dns-secondary, it will serve their zones without being involved in their definition.

Example deployment of DNS charms

With all these charms, we cover the most basic but also the most-used DNS deployment patterns in a simple but production-ready package.