Comment utiliser Hashicorp Terraform pour gérer vos DNS bind9 dans Azure

Je vous propose au travers de cet article, de voir comment configurer un serveur Bind9 sur une Ubuntu avec Terraform, puis de créer des enregistrements DNS (Domain Name System) toujours avec Terraform. Vous comprendrez l’intérêt de cette solution facilement je suppose. Sinon, pour résumer, l’idée est de créer des Vms dans Azure et de les enregistrer dans un DNS local.
La méthode que j’expose est volontairement simplifiée. Il faudrait déployer une machine dans Azure afin de lancer Terraform depuis le LAN et non pas exposer le serveur Bind sur Internet.
Pour nos amis anglophones, vous pourrez retrouver la version en anglais de cette article ici.
Terraform DNS Provider
Terraform propose un provider “DNS” afin de manipuler un serveur DNS configuré conformément aux RFC 2136 et 2845. Ces RFC normalisent les mises à jours des enregistrements DNS.
Cloud Init
Afin de lancer une machine rapidement configurée pour ce POC, j’ai décidé d’utiliser Cloud Init. Cloud Init permet de donner des instructions à la VM avant son démarrage.
Construction de la structure du POC
Ouvrez Visual Studio Code et créez un dossier de base $workdir
et générez la structure suivante :
- bind - main.tf - files/cloudconfig.tpl - data.tf - variables.tf - provider.tf - dns - main.tf - provider.tf
Construction de la VM Bind9
Terraform
Dans le fichier bind/provider.tf
, ajoutez ceci :
provider "azurerm" { version = "1.6.0" } provider "template" { version = "1.0.0" }
Dans le fichier bind/data.tf
:
data "template_cloudinit_config" "config" { gzip = true base64_encode = true part { content_type = "text/cloud-config" content = "${data.template_file.cloudconfig.rendered}" } } data "template_file" "cloudconfig" { template = "${file("cloudconfig.tpl")}" }
Dans bind/main.tf
en remplaçant le texte <VotreMotDePasse>
par un mot de passe :
resource "azurerm_resource_group" "tf-bind" { name = "RG-bind" location = "West Europe" tags = { Project = "bind" } } resource "azurerm_virtual_network" "tf-vnet" { name = "vnet-bind" location = "${azurerm_resource_group.tf-bind.location}" resource_group_name = "${azurerm_resource_group.tf-bind.name}" address_space = ["10.0.0.0/16"] tags = { Project = "bind" } } resource "azurerm_subnet" "tf-snet" { name = "main" resource_group_name = "${azurerm_resource_group.tf-bind.name}" virtual_network_name = "${azurerm_virtual_network.tf-vnet.name}" address_prefix = "10.0.0.0/24" } resource "azurerm_virtual_machine" "tf-vm-bind" { count = 1 name = "bind-vm0${count.index}" location = "${azurerm_resource_group.tf-bind.location}" resource_group_name = "${azurerm_resource_group.tf-bind.name}" network_interface_ids = ["${element(azurerm_network_interface.tf-nic.*.id, count.index)}"] vm_size = "Standard_B1ms" delete_os_disk_on_termination = true storage_image_reference { publisher = "Canonical" offer = "UbuntuServer" sku = "16.04-LTS" version = "latest" } storage_os_disk { name = "dsk-vm0${count.index}" caching = "ReadWrite" create_option = "FromImage" managed_disk_type = "Standard_LRS" } os_profile { computer_name = "bind-vm0${count.index}" admin_username = "edeneuve" admin_password = "<VotreMotDePasse>" custom_data = "${data.template_cloudinit_config.config.rendered}" } os_profile_linux_config { disable_password_authentication = false } tags = { Project = "bind" } provisioner "file" { source = "conf/myapp.conf" destination = "/etc/myapp.conf" connection { type = "ssh" user = "root" password = "${var.root_password}" } } } resource "azurerm_network_interface" "tf-nic" { count = "1" name = "nic-vm${count.index}" resource_group_name = "${azurerm_resource_group.tf-bind.name}" location = "${azurerm_resource_group.tf-bind.location}" ip_configuration { name = "ipconfig" private_ip_address_allocation = "dynamic" subnet_id = "${azurerm_subnet.tf-snet.id}" public_ip_address_id = "${element(azurerm_public_ip.MyResource.*.id, count.index)}" } tags = { Project = "bind" } } resource "azurerm_public_ip" "MyResource" { count = "1" name = "pip-vm${count.index}" resource_group_name = "${azurerm_resource_group.tf-bind.name}" location = "${azurerm_resource_group.tf-bind.location}" public_ip_address_allocation = "dynamic" tags = { Project = "bind" } }
Dans bind/files/cloudconfig.tpl
:
#cloud-config
package_upgrade: true
packages:
- bind9
- dnsutils
Lancer az login
puis un terraform init
depuis le dossier bind, puis un terraform plan
enfin un terraform apply
. D’ici quelques minutes, votre serveur sera prêt et nous pourrons passer à la petite configuration manuelle via SSH.
Configuration de Bind
Connectez-vous à la VM en SSH puis accédez au dossier /etc/bind/
, générez la clé pour les updates DNS via la commande sudo rndc-confgen
.
Vous devriez récupérer un fichier rdnc.key avec un contenu tel que :
$ sudo cat /etc/bind/rndc.key key "rndc-key" { algorithm hmac-md5; secret "REDACTED"; };
Nous allons maintenant éditer le fichier named.conf
et ajouter la ligne :
include "/etc/bind/rndc.key";
Ensuite, créer une nouvelle zone dans le fichier named.conf.local
:
zone "toto.int.local." { type master; file "/etc/bind/zones/db.toto.int.local"; update-policy { grant rndc-key zonesub any; }; };
Maintenant on crée notre fichier de zone dans un nouveau dossier zones
avec le contenu suivant dans un fichier /etc/bind/zones/db.toto.int.local
:
$ORIGIN . $TTL 3600 ; 1 hour toto.int.local IN SOA ns1.toto.int.local. root.toto.int.local. ( 2012033116 ; serial 3600 ; refresh (1 hour) 1800 ; retry (30 minutes) 604800 ; expire (1 week) 43200 ; minimum (12 hours) ) NS ns1.toto.int.local. NS ns2.toto.int.local. $ORIGIN toto.int.local. $TTL 3600 ; 1 hour ns1 A 137.117.141.166 ns2 A 137.117.141.166
Bind doit pouvoir accéder à cette zone et créer un fichier de journal :
sudo chown -R root:bind ./zones sudo chmod -R 640 ./zones
Puis on redémarre le service :
sudo systemctl restart bind9
Puis on vérifie les logs :
tail /var/log/syslog | grep named Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: 255.255.255.255.IN-ADDR.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.IP6.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.IP6.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: D.F.IP6.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: 8.E.F.IP6.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: 9.E.F.IP6.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: A.E.F.IP6.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: B.E.F.IP6.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: 8.B.D.0.1.0.0.2.IP6.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: automatic empty zone: EMPTY.AS112.ARPA Jun 15 07:44:25 bind-vm00 named[4233]: configuring command channel from '/etc/bind/rndc.key' Jun 15 07:44:25 bind-vm00 named[4233]: command channel listening on 127.0.0.1#953 Jun 15 07:44:25 bind-vm00 named[4233]: configuring command channel from '/etc/bind/rndc.key' Jun 15 07:44:25 bind-vm00 named[4233]: command channel listening on ::1#953 Jun 15 07:44:25 bind-vm00 named[4233]: managed-keys-zone: loaded serial 13 Jun 15 07:44:25 bind-vm00 named[4233]: zone 0.in-addr.arpa/IN: loaded serial 1 Jun 15 07:44:25 bind-vm00 named[4233]: zone 127.in-addr.arpa/IN: loaded serial 1 Jun 15 07:44:25 bind-vm00 named[4233]: zone 255.in-addr.arpa/IN: loaded serial 1 Jun 15 07:44:25 bind-vm00 named[4233]: zone localhost/IN: loaded serial 2 Jun 15 07:44:25 bind-vm00 named[4233]: zone toto.int.local/IN: loaded serial 2012033116 Jun 15 07:44:25 bind-vm00 named[4233]: all zones loaded Jun 15 07:44:25 bind-vm00 named[4233]: running Jun 15 07:44:25 bind-vm00 named[4233]: zone toto.int.local/IN: sending notifies (serial 2012033116)
Il est possible qu’AppArmor empêche les mises a jours des zones, il faut donc le configurer correctement pour bind. Il faut donc modifier le fichier /etc/apparmor.d/usr.sbin.named
et ajouter /etc/bind/zones/** rw,
dans cette partie :
# /etc/bind should be read-only for bind # /var/lib/bind is for dynamically updated zone (and journal) files. # /var/cache/bind is for slave/stub data, since we're not the origin of it. # See /usr/share/doc/bind9/README.Debian.gz /etc/bind/** r, /etc/bind/zones/** rw, /var/lib/bind/** rw, /var/lib/bind/ rw, /var/cache/bind/** lrw, /var/cache/bind/ rw,
Puis, on recharge AppArmor via sudo systemctl restart apparmor
et on vérifie via aa-status
.
aa-status apparmor module is loaded. 14 profiles are loaded. 14 profiles are in enforce mode. /sbin/dhclient /usr/bin/lxc-start /usr/lib/NetworkManager/nm-dhcp-client.action /usr/lib/NetworkManager/nm-dhcp-helper /usr/lib/connman/scripts/dhclient-script /usr/lib/lxd/lxd-bridge-proxy /usr/lib/snapd/snap-confine /usr/lib/snapd/snap-confine//mount-namespace-capture-helper /usr/sbin/named /usr/sbin/tcpdump lxc-container-default lxc-container-default-cgns lxc-container-default-with-mounting lxc-container-default-with-nesting 0 profiles are in complain mode. 2 processes have profiles defined. 0 processes are in enforce mode. 0 processes are in complain mode. 2 processes are unconfined but have a profile defined. /sbin/dhclient (921) /usr/sbin/named (6523)
Dynamic update avec Terraform
Nous allons maintenant travailler sur la partie DNS :
- dns - main.tf - provider.tf
Dans le fichier provider.tf
nous allons ajouter le provider DNS :
provider "dns" { update { server = "<ipduserverbind>" key_name = "rndc-key." key_algorithm = "hmac-md5" key_secret = "<laclé>" } }
Puis depuis le dossier DNS, exécutez la commande terraform init
pour récupérer le plugins DNS.
Dans le fichier main.tf
ajoutez des ressources Terraform :
resource "dns_a_record_set" "www" { zone = "toto.int.local." name = "www" addresses = [ "192.168.0.1", "192.168.0.2", "192.168.0.3", ] ttl = 300 } resource "dns_cname_record" "foo" { zone = "toto.int.local." name = "foo" cname = "tata.toto.int.local." ttl = 300 } resource "dns_a_record_set" "xxx" { zone = "toto.int.local." name = "tata" addresses = ["192.168.0.1"] ttl = 300 }
Puis on fait un terraform plan
:
Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. ------------------------------------------------------------------------ An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: + dns_a_record_set.www id: <computed> addresses.#: "3" addresses.1737095236: "192.168.0.3" addresses.2307365224: "192.168.0.1" addresses.277792978: "192.168.0.2" name: "www" ttl: "300" zone: "toto.int.local." + dns_a_record_set.xxx id: <computed> addresses.#: "1" addresses.2307365224: "192.168.0.1" name: "tata" ttl: "300" zone: "toto.int.local." + dns_cname_record.foo id: <computed> cname: "tata.toto.int.local." name: "foo" ttl: "300" zone: "toto.int.local." Plan: 3 to add, 0 to change, 0 to destroy. ------------------------------------------------------------------------ Note: You didn't specify an "-out" parameter to save this plan, so Terraform can't guarantee that exactly these actions will be performed if "terraform apply" is subsequently run.
Puis un terraform apply
. Vérifiez les logs du serveur bind avec un cat /var/log/syslog
:
Jun 15 09:02:24 bind-vm00 named[6523]: client XX.xx.XX.XX#61068/key rndc: updating zone 'toto.int.local/IN': deleting rrset at 'www.toto.int.local' A Jun 15 09:02:24 bind-vm00 named[6523]: zone toto.int.local/IN: sending notifies (serial 2012033123) Jun 15 09:02:24 bind-vm00 named[6523]: client XX.xx.XX.XX#61066/key rndc: updating zone 'toto.int.local/IN': deleting rrset at 'foo.toto.int.local' CNAME Jun 15 09:02:24 bind-vm00 named[6523]: client XX.xx.XX.XX#61067/key rndc: updating zone 'toto.int.local/IN': deleting rrset at 'tata.toto.int.local' A Jun 15 09:02:29 bind-vm00 named[6523]: zone toto.int.local/IN: sending notifies (serial 2012033125)
Test du DNS
Afin de valider que le DNS a bien récupéré les demandes d’enregistrement depuis votre poste de travail, vous pouvez utiliser la commande suivante :
nslookup - <ipduserver> > www.toto.int.local > www.toto.int.local. Serveur : UnKnown Address: 51.144.181.124 Nom : www.toto.int.local Addresses: 192.168.0.2 192.168.0.1 192.168.0.3
Grace à Terraform vous êtes maintenant capable de tenir à jour vos enregistrements DNS lors de vos deploiements dans Azure.