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.

 

livre blanc From Zero to Hero 1 Infra as code