Configuration Management

Manage Exoscale DNS with Ansible

Posted on August 19, 2017

Introduction

In the following post, I will show you how to use Ansible to configure your DNS records on Exoscale, a Swiss Public Cloud provider. Exoscale offers a DNS service with an affordable pricing with features like geo replication, anycast support and an API.

The Ansible modules we are going to use later and I created, exo_dns_domain and exo_dns_record are included in Ansible since version 2.2.0 (see the offical docs).

Authentication

Exoscale’s computing API (powered by CloudStack) and the DNS API share the same API keys and secret. That is why the modules support the same authentication methods as the Ansible CloudStack modules.

If you have an existing cloudstack.ini file, the same configuration will also be shared with the DNS modules. If not, please head over to Credential part of the Ansible CloudStack Guide for more information how to set it up.

Define the Inventory

Okay now let’s start with a good old static example inventory:

1 # file: hosts
2 [cloud]
3 search-01.example.com
4 search-02.example.com

The hosts in the inventory are defined with a full qualified domain name. This domain and host records is what we are going to create.

Create the Playbook

Before we create the DNS host records, we must have a domain. That is why the very first action is to ensure this domain exists. Our domain of choice is example.com, which we can create in one task like the following:

1 ---
2 - name: cloud base setup
3   hosts: localhost
4   gather_facts: false
5   tasks:
6     - name: ensure domain exists
7       local_action:
8         module: exo_dns_domain
9         name: example.com

The host target is set to localhost, because only need this to run once. As modules communicate with a HTTP API we set a local_action to execute the modules locally.

Setting up multiple domains would as easy as using a list of domains and a loop with_items, just to give you an example:

 1 ---
 2 - name: cloud base setup
 3   hosts: localhost
 4   gather_facts: false
 5   vars:
 6     domains:
 7       - example.com
 8       - example2.com
 9       - example3.com
10   tasks:
11     - name: ensure domains exist
12       local_action:
13         module: exo_dns_domain
14         name: "{{ item }}"
15         with_items: "{{ domains }}"

A Records for all the Hosts

Let’s step back a bit from DNS. We need some machines first.

Since we already own an account on Exoscale, it takes no effort to use the CloudStack module cs_instance to deploy the VMs on Exoscale Compute Cloud.

Note: We assume in our example that the security groups and the ssh key used exist already, There are Ansible modules to manage these as well.

 1 ---
 2 - name: install VMs in the cloud
 3   hosts: cloud
 4   gather_facts: false
 5   tasks:
 6     - name: create cloud VMs
 7       local_action:
 8         module: cs_instance
 9         display_name: "{{ inventory_hostname }}"
10         template: Linux Debian 8 64-bit 10G Disk (2016-06-07-7afacb)
11         service_offering: Tiny
12         security_groups:
13           - default
14           - search
15         ssh_key: defaultkey
16       register: vm

When we run this playbook, the VMs specified of the group cloud in our inventory get created. And they also got an IP, but what IP do they get?

The API is returning a bunch of data which we get access to, among other the IP assigned. These data are called returns and are documented along the module docs. To see all available so called return values, consult the online docs or simple run ansible-doc cs_instance and scroll down to the RETURN VALUES section.

At the bottom of the task, we added the register clause to save the returns in a variable we named vm. This allows us to use the return data later in the following tasks.

To just print the IP we received from the API, it would be as simple as add a debug task after the VM creation:

1 - name: print VM infos
2   debug:
3     msg: "VM {{ inventory_hostname }} has IP {{ vm.default_ip }}"

At this point we have all what we need to register an A record with the corresponding IP. The following task creates the record, in our domain.

1 - name: create record with VM host's IP
2   local_action:
3     module: exo_dns_record
4     domain: "example.com"
5     name: "{{ inventory_hostname_short }}"
6     content: "{{ vm.default_ip }}"
7   when: "vm.default_ip is defined"

The name is equal to the DNS host part eg. search-01, the global variable inventory_hostname_short contains exactly the part of the inventory item to the fist dot.

Per default exo_dns_record creates an A record if no record_type is specified with a TTL of 3600 as default.

Domain record for our search service

Our application is a distributed search engine and we would like to create a service DNS record. The domain of choice for our next generation search engine is search.example.com and we would like to use DNS round robin for load distribution.

This means we want to have several A records with the same name but different IP (or content).

If we would change the task we used before with a static name search, it would end up in a race condition in which the last handled inventory host will get the domain search.example.com pointing to its IP:

1 - name: create search.example.com
2   local_action:
3     module: exo_dns_record
4     domain: "example.com"
5     name: "search"
6     content: "{{ vm.default_ip }}"
7   when: "vm.default_ip is defined"

But luckily, the module has an argument for setting multiple A records: multiple: yes

1 - name: create search.example.com
2   local_action:
3     module: exo_dns_record
4     domain: "example.com"
5     name: "search"
6     content: "{{ vm.default_ip }}"
7     multiple: true
8   when: "vm.default_ip is defined"

Note: This works by design of DNS for A records only and so does multiple.

Run the Playbook

The complete playbook as shown below is ready to lift off.

 1 - name: ensure domain exists
 2   hosts: localhost
 3   gather_facts: false
 4   tasks:
 5     - name: ensure domain exists
 6       local_action:
 7         module: exo_dns_domain
 8         name: example.com
 9 
10 - name: install VMs in the cloud
11   hosts: cloud
12   gather_facts: false
13   tasks:
14     - name: create cloud VMs
15       local_action:
16         module: cs_instance
17         display_name: "{{ inventory_hostname }}"
18         template: Linux Debian 8 64-bit 10G Disk (2016-06-07-7afacb)
19         service_offering: Tiny
20         security_groups:
21           - default
22           - search
23         ssh_key: defaultkey
24       register: vm
25 
26     - name: create search.example.com
27       local_action:
28         module: exo_dns_record
29         domain: "example.com"
30         name: "search"
31         content: "{{ vm.default_ip }}"
32         multiple: true
33       when: "vm.default_ip is defined"

We run the playbook in diff mode to see what is going to be changed.

$ ansible-playbook cloud.yml --diff -i hosts

Summary

In the above we showed how to use Ansible automated configuration of DNS. As we only created A records we also want to point out that exo_dns_record does support all well known record types.

Further the module have diff and check mode support included, which provides full transparency of what is going to be made.