Terraform and NREC: Part V - DNS Management

Last changed: 2024-04-17

This document is an introduction on how to create a DNS zone and DNS records in Openstack using Terraform.

The files used in this document can be downloaded:

Creating a DNS zone

It is quite easy to create a DNS zone using Terraform. Consider zone-tf below. It is a single resource declaration needed to create a zone.

Important

The DNS service expects the zone name to be a fully qualified domain name, which means that the name of the zone provided in the resource declaration must end with a dot “.”. Omitting the trailing dot will result in an error.

This is correct:

name = "test.com."

This is incorrect and will not work:

name = "test.com"

In this example we create a zone “test.com”:

zone.tf
1resource "openstack_dns_zone_v2" "test_com" {
2  name        = "test.com."
3  email       = "trondham@uio.no"
4  description = "An example zone"
5}

This is all that is needed. You may add additional parameters, most commonly TTL, if you need to set a TTL value other than the default (3600).

Creating DNS records

In this example we create 3 records in the “test.com” zone:

  1. An A record which poinst to a single IPv4 address for “test01.test.com”

  2. An AAAA record which points to a single IPv6 address for “test01.test.com”

  3. A CNAME record (alias) “www” which points to “test01.test.com”

The record resources are specified in the recordset-tf file below:

recordset.tf
 1resource "openstack_dns_recordset_v2" "A_test01_test_com" {
 2  zone_id     = openstack_dns_zone_v2.test_com.id
 3  name        = "test01.test.com."
 4  description = "An example record set"
 5  type        = "A"
 6  records     = ["10.0.0.1"]
 7}
 8
 9resource "openstack_dns_recordset_v2" "AAAA_test01_test_com" {
10  zone_id     = openstack_dns_zone_v2.test_com.id
11  name        = "test01.test.com."
12  description = "An example record set"
13  type        = "AAAA"
14  records     = ["2001:700:2:8200::226c"]
15}
16
17resource "openstack_dns_recordset_v2" "CNAME_www_test_com" {
18  zone_id     = openstack_dns_zone_v2.test_com.id
19  name        = "www.test.com."
20  description = "An example record set"
21  type        = "CNAME"
22  records     = ["test01.test.com."]
23}

Important

The DNS service expects the record name to be a fully qualified domain name, which means that the name of the record provided in the resource declaration must end with a dot “.”. Omitting the trailing dot will result in an error. This is correct:

name = "app-01.test.com."

This is incorrect and will not work:

name = "app-01.test.com"

This also applies to the records list in case of a CNAME, as show in the example above.

Apply and check

Running terraform apply creates the zone, as well as the three records we specified:

$ terraform apply
...
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

We can check that the authoritative name servers have our zone and records by querying one of them them directly:

$ host www.test.com. ns1.nrec.no
Using domain server:
Name: ns1.nrec.no
Address: 158.37.63.251#53
Aliases:

www.test.com is an alias for test01.test.com.
test01.test.com has address 10.0.0.1
test01.test.com has IPv6 address 2001:700:2:8200::226c

As always, you can use terraform destroy to remove the created resources:

$ terraform destroy
...
Destroy complete! Resources: 4 destroyed.

Dynamically add DNS records

The previous examples show how to add a zone and create records within that zone. What if the zone already exists, and how do we automatically add a DNS record for an instance when the instance is created? We’ll answer those questions here.

First, let’s consider how to add records to an already existing zone. The problem here is that we need to know the ID of the zone. We can manually fetch the ID from the output of openstack zone list and hard code the ID into our Terraform config, but there is a more dynamic and flexible way to do this. In order to fetch the needed metadata for our zone we use a data directive in Terraform:

dynamic.tf
1# DNS zone
2variable "zone_name" {
3   default = "mytestzone.com"
4}
5# Find zone info
6data "openstack_dns_zone_v2" "myzone" {
7  name = "${var.zone_name}."
8}

In this example, we have a resource declaration for instances that creates an arbitrary number of instances. In our example, we create 2 instances:

dynamic.tf
 1# How many instances to create
 2variable "node_count" {
 3  default = "2"
 4}
 5
 6# Create instances
 7resource "openstack_compute_instance_v2" "testserver" {
 8  region      = var.region
 9  count       = var.node_count
10  name        = "${var.region}-test-${count.index}"
11  image_name  = "GOLD CentOS 8"
12  flavor_name = "m1.small"
13
14  key_pair = "mykey"
15  security_groups = [ "default" ]
16
17  network {
18    name = "dualStack"
19  }
20
21  lifecycle {
22    ignore_changes = [image_name,image_id]
23  }
24}

Finally, in order to create DNS records for our instances we need to reference the name and IP of the instances. Notice the usage of the data variable to reference the zone ID (highlighted):

dynamic.tf
 1# Create records for A (IPv4)
 2resource "openstack_dns_recordset_v2" "a_record" {
 3  zone_id     = data.openstack_dns_zone_v2.myzone.id
 4  count       = var.node_count
 5  name        = "${openstack_compute_instance_v2.testserver[count.index].name}.${var.zone_name}."
 6  type        = "A"
 7  records     = [ "${openstack_compute_instance_v2.testserver[count.index].access_ip_v4}" ] 
 8}
 9
10# Create records for AAAA (IPv6)
11resource "openstack_dns_recordset_v2" "aaaa_record" {
12  zone_id     = data.openstack_dns_zone_v2.myzone.id
13  count       = var.node_count
14  name        = "${openstack_compute_instance_v2.testserver[count.index].name}.${var.zone_name}."
15  type        = "AAAA"
16  records     = [ "${openstack_compute_instance_v2.testserver[count.index].access_ip_v6}" ] 
17}

In this example, we create both A (IPv4) and AAAA (IPv6) records for our instances, since we specified the “dualStack” network for the instance resources.

After running terraform apply we can use the CLI command openstack recordset list to verify that the DNS records have been created:

$ openstack recordset list mytestzone.com. -c name -c type -c records
+----------------------------+-------+-------------------------------------------------------------+
| name                       | type  | records                                                     |
+----------------------------+-------+-------------------------------------------------------------+
| mytestzone.com.            | SOA   | ns2.nrec.no. foo.bar.com. 1575885141 3519 600 86400 3600    |
| mytestzone.com.            | NS    | ns1.nrec.no.                                                |
|                            |       | ns2.nrec.no.                                                |
| bgo-test-1.mytestzone.com. | A     | 158.39.74.137                                               |
| bgo-test-0.mytestzone.com. | AAAA  | 2001:700:2:8300::21d3                                       |
| bgo-test-1.mytestzone.com. | AAAA  | 2001:700:2:8300::207e                                       |
| bgo-test-0.mytestzone.com. | A     | 158.39.77.244                                               |
+----------------------------+-------+-------------------------------------------------------------+

And we can check that they exist in DNS by querying the authoritative name servers:

$ host bgo-test-1.mytestzone.com. ns1.nrec.no
Using domain server:
Name: ns1.nrec.no
Address: 158.37.63.251#53
Aliases:

bgo-test-1.mytestzone.com has address 158.39.74.137
bgo-test-1.mytestzone.com has IPv6 address 2001:700:2:8300::207e

Complete example

A complete listing of the example files used in this document is provided below.

zone.tf
 1# Define required providers
 2terraform {
 3  required_version = ">= 1.0"
 4  required_providers {
 5    openstack = {
 6      source  = "terraform-provider-openstack/openstack"
 7    }
 8  }
 9}
10
11# Configure the OpenStack Provider
12# Empty means using environment variables "OS_*". More info:
13# https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs
14provider "openstack" {}
15
16resource "openstack_dns_zone_v2" "test_com" {
17  name        = "test.com."
18  email       = "trondham@uio.no"
19  description = "An example zone"
20}
recordset.tf
 1resource "openstack_dns_recordset_v2" "A_test01_test_com" {
 2  zone_id     = openstack_dns_zone_v2.test_com.id
 3  name        = "test01.test.com."
 4  description = "An example record set"
 5  type        = "A"
 6  records     = ["10.0.0.1"]
 7}
 8
 9resource "openstack_dns_recordset_v2" "AAAA_test01_test_com" {
10  zone_id     = openstack_dns_zone_v2.test_com.id
11  name        = "test01.test.com."
12  description = "An example record set"
13  type        = "AAAA"
14  records     = ["2001:700:2:8200::226c"]
15}
16
17resource "openstack_dns_recordset_v2" "CNAME_www_test_com" {
18  zone_id     = openstack_dns_zone_v2.test_com.id
19  name        = "www.test.com."
20  description = "An example record set"
21  type        = "CNAME"
22  records     = ["test01.test.com."]
23}
dynamic.tf
 1# Define required providers
 2terraform {
 3  required_version = ">= 1.0"
 4  required_providers {
 5    openstack = {
 6      source  = "terraform-provider-openstack/openstack"
 7    }
 8  }
 9}
10
11# Configure the OpenStack Provider
12# Empty means using environment variables "OS_*". More info:
13# https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs
14provider "openstack" {}
15
16# DNS zone
17variable "zone_name" {
18   default = "mytestzone.com"
19}
20
21# Region in which to create instances
22variable "region" {
23  default = "bgo"
24}
25
26# How many instances to create
27variable "node_count" {
28  default = "2"
29}
30
31# Create instances
32resource "openstack_compute_instance_v2" "testserver" {
33  region      = var.region
34  count       = var.node_count
35  name        = "${var.region}-test-${count.index}"
36  image_name  = "GOLD CentOS 8"
37  flavor_name = "m1.small"
38
39  key_pair = "mykey"
40  security_groups = [ "default" ]
41
42  network {
43    name = "dualStack"
44  }
45
46  lifecycle {
47    ignore_changes = [image_name,image_id]
48  }
49}
50
51# Find zone info
52data "openstack_dns_zone_v2" "myzone" {
53  name = "${var.zone_name}."
54}
55
56# Create records for A (IPv4)
57resource "openstack_dns_recordset_v2" "a_record" {
58  zone_id     = data.openstack_dns_zone_v2.myzone.id
59  count       = var.node_count
60  name        = "${openstack_compute_instance_v2.testserver[count.index].name}.${var.zone_name}."
61  type        = "A"
62  records     = [ "${openstack_compute_instance_v2.testserver[count.index].access_ip_v4}" ] 
63}
64
65# Create records for AAAA (IPv6)
66resource "openstack_dns_recordset_v2" "aaaa_record" {
67  zone_id     = data.openstack_dns_zone_v2.myzone.id
68  count       = var.node_count
69  name        = "${openstack_compute_instance_v2.testserver[count.index].name}.${var.zone_name}."
70  type        = "AAAA"
71  records     = [ "${openstack_compute_instance_v2.testserver[count.index].access_ip_v6}" ] 
72}