Aller au contenu principal

Installation de PowerDNS et PowerDNS-Admin

· 10 minutes de lecture
Stéphane ROBERT
Consultant DevOps

Je continue le déploiement des applications sur mon Home Lab Devops, et cette fois, il s'agit du serveur DNS powerDNS. Jusqu'à présent il tournait sur un de mes raspberry pi, mais j'ai fait le choix de le déplacer sur une des machines du Home Lab. Encore un bon exercice, car cela m'a permis d'améliorer l'installation de libvirt, en automatisant la création du pool d'images et du bridge.

Pourquoi installer PowerDNS dans le homelab ? Tout simplement pour permettre l'enregistrement et la suppression des IP des machines instanciées via son API.

Démarche d'automatisation de l'installation de PowerDNS

Je reprends le même principe que celui utilisé pour nexus. J'ai donc commencé par développer le playbook Ansible sur une machine virtuelle créé avec Vagrant. Une fois le playbook fonctionnel, je l'ai transposé dans une VM instanciée avec du Terraform sur une de mes machines de mon Homelab.

Les objectifs que je me suis fixés et les contraintes imposées :

  • Pouvoir faire les upgrades facilement
  • Utiliser le backend sqlite3
  • Ajouter de la persistance sur les données via un simple partage Nfs.
  • Pas d'installation via Docker.

Pour écrire le playbook Ansible, j'ai tout simplement transposé la procédure d'installation disponible sur le site de la documentation de Nexus.

Ecriture du Vagrantfile

Cette fois, je vais utiliser une VM avec Ubuntu comme système d'exploitation.

# -*- mode: ruby -*-
# vi: set ft=ruby
Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu2110"
  config.vm.synced_folder '.', '/vagrant', disabled: true
  config.vm.provider "libvirt" do |hv|
    hv.cpus = "1"
    hv.memory = "1024"
  end
  config.vm.define "pdns" do |pdns|
    pdns.vm.network "forwarded_port", guest: 443, host: 8443
    pdns.vm.network :private_network, ip: "192.168.3.10"
    pdns.vm.hostname = "pdns"
    pdns.vm.provision "ansible" do |a|
      a.verbose = "v"
      a.playbook = "deploy_powerdns.yml"
      a.raw_arguments  = "--ask-vault-pass"
    end
  end
end

On reprend le même principe, mis à part que j'ai créé dans le playbook une variable de type vault. J'ai donc ajouté aux paramètres d'appel d'ansible-playbook l'option --ask-vault-pass.

Le mot de passe est passwd. La variable en question est la clé d'api. Pour la créer il suffit de taper la commande suivante :

ansible-vault encrypt_string mon-api-key
New Vault password:
Confirm New Vault password:
!vault |
          $ANSIBLE_VAULT;1.1;AES256
          35303935373534333166656461623331643565383937373037326335333761383636633066343736
          3261303836393263353930396135326432386533616533620a626539343931326232376463616165
          35376164636363613434386438383634313337393465656664323338346639326339633766323262
          3963376334613361640a356364353236353738323035333635626632306235633536666335613335
          3966
Encryption successful

Je ne partage que le port 443, car c'est nginx qui portera la partie SSL.

Ecriture du playbook

Pour créer la variable api_key il suffit de copier/coller la sortie de la commande ansible-vault.

---
- hosts: all
  gather_facts: true
  become: true
  vars:
    api_key: !vault |
            $ANSIBLE_VAULT;1.1;AES256
            64373932386466646561336463633030386532306533373665643765326339653538343361653730
            3030666264323564623061363835343263363535616331390a383664376331333530663164393632
            61313963646238353436656434366437646438363766306164643465396361346438326331363932
            3936663761323961330a643532663936366234323265303838613233363533373035643936373062
            3036
    NODEJS_VERSION: 14
    pdns_sqlite_schema_file: /usr/share/doc/pdns-backend-sqlite3/schema.sqlite3.sql
    ## Nginx
    nginx_version: 1.18
    nginx_fqdn: pdns.robert.local
    cert_file: "{{ nginx_fqdn }}+3.pem"
    cert_key: "{{ nginx_fqdn }}+3-key.pem"
    nfs_path : 192.168.1.101:/data
  tasks:
    - name: disable systemd-resolv
      ansible.builtin.service:
        name: systemd-resolved
        state: stopped
        enabled: false
    - name: Copy resolv.config
      ansible.builtin.copy:
        src: files/resolv.conf
        dest: /etc/resolv.conf
        owner: root
        group: root
        mode: 0644
    - name: Install the gpg key for GPG
      ansible.builtin.apt_key:
        url: https://dl.yarnpkg.com/debian/pubkey.gpg
        state: present
    - name: Create Yarn repo file
      ansible.builtin.file:
        path: /etc/apt/sources.list.d/yarn.list
        owner: root
        mode: 0644
        state: touch
    - name: "Add yarn repo"
      ansible.builtin.lineinfile:
        dest: /etc/apt/sources.list.d/yarn.list
        regexp: 'deb http://dl.yarnpkg.com/debian/ stable main'
        line: 'deb http://dl.yarnpkg.com/debian/ stable main'
        state: present
    - name: Install the gpg key for nodejs LTS
      ansible.builtin.apt_key:
        url: "https://deb.nodesource.com/gpgkey/nodesource.gpg.key"
        state: present
    - name: Install the nodejs LTS repos
      ansible.builtin.apt_repository:
        repo: "deb https://deb.nodesource.com/node_{{ NODEJS_VERSION }}.x {{ ansible_distribution_release }} main"
        state: present
        update_cache: yes
    - name: install packages
      ansible.builtin.package:
        state: present
        name:
          - pdns-server
          - pdns-backend-sqlite3
          - sqlite3
          - nfs-common
          - net-tools
          - htop
          - nginx
          - python3-dev
          - libsasl2-dev
          - libldap2-dev
          - libssl-dev
          - libxml2-dev
          - libxslt1-dev
          - libxmlsec1-dev
          - libffi-dev
          - pkg-config
          - apt-transport-https
          - virtualenv
          - build-essential
          - git
          - python3-flask
          - nodejs
          - yarn
    - name: create /data mount
      ansible.builtin.file:
        path: /data
        state: directory
        mode: 0755
    - name: mount nfs /data
      ansible.posix.mount:
        src: "{{ nfs_path }}"
        path: /data
        # opts: vers=4,udp
        state: mounted
        fstype: nfs
    - name: Create pdns directory
      ansible.builtin.file:
        path: "{{ item }}"
        state: "directory"
        owner: "pdns"
        group: "pdns"
        mode: 0755
      with_items:
      - /data/pdns
      - /var/www/html/
    - name: configure pdns to use sqlite3 as backend
      ansible.builtin.lineinfile:
        dest: /etc/powerdns/pdns.conf
        regexp: "launch=.*"
        line: "launch=gsqlite3"
      notify: restart pdns
    - name: configure pdns to use sqlite3 as backend
      ansible.builtin.lineinfile:
        dest: /etc/powerdns/pdns.conf
        regexp: "^api=.*$"
        line: "api=yes"
      notify: restart pdns
    - name: configure pdns to use sqlite3 as backend
      ansible.builtin.lineinfile:
        dest: /etc/powerdns/pdns.conf
        regexp: "^# api-key=.*|^api-key=.*$"
        line: "api-key={{ api_key }}"
      notify: restart pdns
    - name: configure pdns to use sqlite3 as backend
      ansible.builtin.lineinfile:
        dest: /etc/powerdns/pdns.conf
        regexp: "^# webserver=.*$|^webserver=.*"
        line: "webserver=yes"
      notify: restart pdns
    - name: configure pdns to use sqlite3 as backend
      ansible.builtin.lineinfile:
        dest: /etc/powerdns/pdns.conf
        regexp: "^gsqlite3-database=.*$"
        line: "gsqlite3-database=/data/pdns/pdns.sqlite3"
        state: present
      notify: restart pdns
    - name: Create the PowerDNS SQLite databases
      ansible.builtin.command:
        cmd: >
          sqlite3 -init /usr/share/doc/pdns-backend-sqlite3/schema.sqlite3.sql
          /data/pdns/pdns.sqlite3 .quit
        creates: /data/pdns/pdns.sqlite3
# PowerDNS Admin
    - name: clone pdns admin
      ansible.builtin.git:
        repo: https://github.com/ngoduykhanh/PowerDNS-Admin.git
        dest: /var/www/html/pdns
        force: yes
    - name: chown owner for installation
      ansible.builtin.file:
        owner: "{{ ansible_user }}"
        group: "{{ ansible_user }}"
        recurse: true
        path: /var/www/html/pdns
    - name: Add itdangerous
      ansible.builtin.lineinfile:
        path: /var/www/html/pdns/requirements.txt
        line: "itsdangerous==2.0.1"
        state: present
    - name: comment mysql
      ansible.builtin.replace:
        path: /var/www/html/pdns/requirements.txt
        regexp: '(mysqlclient.*)'
        replace: '#\1'
    - name: install requirements for pdns admin
      become: false
      ansible.builtin.pip:
        requirements: requirements.txt
        virtualenv:  /var/www/html/pdns/flask
        chdir: /var/www/html/pdns/
    - name: copy config file
      ansible.builtin.copy:
        src: files/config.py
        dest: /var/www/html/pdns/powerdnsadmin/default_config.py
        owner: "{{ ansible_user }}"
        group: "{{ ansible_user }}"
        mode: 0644
    - name: Upgrade database
      ansible.builtin.shell:
        cmd: "/var/www/html/pdns/flask/bin/flask db upgrade"
        chdir: /var/www/html/pdns/
      environment:
      - FLASK_APP: /var/www/html/pdns/powerdnsadmin/__init__.py
    - name: yarn
      ansible.builtin.shell:
        cmd: "yarn install --pure-lockfile"
        chdir: /var/www/html/pdns/
      environment:
      - FLASK_APP: /var/www/html/pdns/powerdnsadmin/__init__.py
    - name: Build flask assets
      ansible.builtin.shell:
        cmd: "/var/www/html/pdns/flask/bin/flask assets build"
        chdir: /var/www/html/pdns/
      environment:
      - FLASK_APP: /var/www/html/pdns/powerdnsadmin/__init__.py
    - name: chown owner of pdns
      ansible.builtin.file:
        owner: www-data
        group: www-data
        recurse: true
        path: /var/www/html/pdns
    - name: chown owner to pdns of pdnsadmin
      ansible.builtin.file:
        owner: pdns
        recurse: true
        path: /var/www/html/pdns
    - name: chown owner db
      ansible.builtin.file:
        owner: pdns
        group: pdns
        recurse: true
        path: /data/pdns
    - name: create pdnsadmin run_dir
      ansible.builtin.file:
        path: /run/pdnsadmin/
        state: directory
        owner: pdns
    - name: Create pdnsadmin service
      ansible.builtin.copy:
        src: "files/{{ item }}"
        dest: /etc/systemd/system/
        owner: root
        group: root
        mode: 0644
      with_items:
        - pdnsadmin.service
        - pdnsadmin.socket
      notify: restart pdnsadmin_service
    - name: start pdnsadmin_service
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
        enabled: true
      with_items:
        - pdnsadmin.service
        - pdnsadmin.socket
    - name: Remove default nginx config
      ansible.builtin.file:
        path: /etc/nginx/sites-enabled/default_config
        state: absent
    - name: copy nginx config
      ansible.builtin.copy:
        src: files/nginx.conf
        dest: /etc/nginx
        mode: 0644
      notify: restart nginx
    - name: Copy nginx config
      ansible.builtin.template:
        src: templates/pdns.conf
        dest: /etc/nginx/conf.d
        owner: root
        group: root
        mode: 0644
      notify: restart nginx
    - name: copy certificate
      ansible.builtin.copy:
        src: "files/{{ item }}"
        dest: "/etc/ssl/{{ item }}"
        mode: 0640
      with_items:
        - "{{ cert_file }}"
        - "{{ cert_key }}"
      notify: restart nginx

  handlers:
    - name: restart pdns
      ansible.builtin.service:
        name: pdns
        state: restarted
    - name: restart nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
    - name: restart pdnsadmin_service
      ansible.builtin.service:
        name: "{{ item }}"
        state: restarted
        enabled: true
      with_items:
        - pdnsadmin.service
        - pdnsadmin.socket

L'installation est assez proche de celle que j'ai utilisé pour Nexus. Les données sont sauvegardées sur un montage NFS dans /data/pdns.

On crée la base si elle n'existe pas. Et finit par l'installation de nginx.

Une fois terminé il suffit de se rendre dans le navigateur sur cette adresse : https://localhost:8443

Lors du premier démarrage il faudra :

  • créer le user admin

  • indiquer l'adresse et la clé de l'api powerdns

Installation sur une machine du home Lab avec Terraform

La aussi nous sommes très proches de ce que j'ai utilisé pour nexus. Mis à part le nom de la vm, la mac address le nombre de cpu et de mémoire. Changez-les en fonction de votre besoin.

terraform {
  required_providers {
    libvirt = {
      source = "dmacvicar/libvirt"
    }
  }
}

// instance the provider
provider "libvirt" {
  // uri = "qemu:///system"
  uri = "qemu+ssh://admuser@devbox3.robert.local/system"
}

// variables that can be overriden
variable "hostname" { default = "pdns" }
variable "domain" { default = "robert.local" }
variable "ip_type" { default = "dhcp" } # dhcp is other valid type
variable "memoryMB" { default = 1024*1 }
variable "cpu" { default = 2 }

// fetch the latest ubuntu release image from their mirrors
resource "libvirt_volume" "os_image" {
  name = "${var.hostname}-os_image"
  pool = "devbox"
  source = "impish-server-cloudimg-amd64.img"
  format = "qcow2"
}

// Use CloudInit ISO to add ssh-key to the instance
resource "libvirt_cloudinit_disk" "commoninit" {
          name = "${var.hostname}-commoninit.iso"
          pool = "devbox"
          user_data      = data.template_cloudinit_config.config.rendered
          network_config = data.template_file.network_config.rendered
}

data "template_file" "user_data" {
  template = file("${path.module}/cloud_init.cfg")
  vars = {
    hostname = var.hostname
    fqdn = "${var.hostname}.${var.domain}"
    public_key = file("~/.ssh/id_ed25519.pub")
  }
}

data "template_cloudinit_config" "config" {
  gzip = false
  base64_encode = false
  part {
    filename = "init.cfg"
    content_type = "text/cloud-config"
    content = "${data.template_file.user_data.rendered}"
  }
}

data "template_file" "network_config" {
  template = file("${path.module}/network_config_${var.ip_type}.cfg")
}

// Create the machine
resource "libvirt_domain" "domain-ubuntu" {
  # domain name in libvirt, not hostname
  name = "${var.hostname}"
  memory = var.memoryMB
  vcpu = var.cpu
  autostart = true
  qemu_agent = true

  disk {
      volume_id = libvirt_volume.os_image.id
  }
  network_interface {
      network_name = "bridged-network"
      mac = "52:54:00:36:14:e7"
  }

  cloudinit = libvirt_cloudinit_disk.commoninit.id

  # IMPORTANT
  # Ubuntu can hang is a isa-serial is not present at boot time.
  # If you find your CPU 100% and never is available this is why
  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }

  graphics {
    type = "spice"
    listen_type = "address"
    autoport = "true"
  }
}

terraform {
  required_version = ">= 0.12"
}


output "ips" {
  #value = libvirt_domain.domain-ubuntu
  #value = libvirt_domain.domain-ubuntu.*.network_interface
  # show IP, run 'terraform refresh' if not populated
    value = libvirt_domain.domain-ubuntu.*.network_interface.0.addresses.0
}

Ce code fait appel à du cloud-init pour configurer la VM. Encore ici peu de changements.

#cloud-config
# https://cloudinit.readthedocs.io/en/latest/topics/modules.html
timezone: Europe/Paris

fqdn: pdns.robert.local
manage_etc_hosts: true
resize_rootfs: true

users:
  - name: admuser
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, wheel
    home: /home/admuser
    shell: /bin/bash
    lock_passwd: false
    ssh-authorized-keys:
      - ${public_key}

# only cert auth via ssh (console access can still login)
## debug - ssh_pwauth: true
disable_root: false
ssh_pwauth: true
chpasswd:
  list: |
    root:passwd
    admuser:123456
  expire: false
growpart:
  mode: auto
  devices: ['/']
  ignore_growroot_disabled: false

packages:
  - qemu-guest-agent
write_files:
  - path: /etc/sysctl.d/10-disable-ipv6.conf
    permissions: 0644
    owner: root
    content: |
      net.ipv6.conf.all.disable_ipv6 = 1
      net.ipv6.conf.default.disable_ipv6 = 1
# every boot
bootcmd:
    - [ sh, -c, 'echo $(date) | sudo tee -a /root/bootcmd.log' ]
# run once for setup

runcmd:
  - sed -i 's/#UseDNS yes/UseDNS no/' /etc/ssh/sshd_config
  - systemctl restart sshd
  - sysctl --load /etc/sysctl.d/10-disable-ipv6.conf
  - localectl set-keymap fr
  - localectl set-locale LANG=fr_FR.UTF8
  - domainname robert.local

Modifier le nom de domain !!

Et pour la partie réseau :

version: 2
ethernets:
  ens3:
    dhcp4: true
    nameservers:
      addresses: [192.168.1.1]
      search: [robert.local]

Modifier le nom de domain et la gateway !!

Démarrage de la VM

Avant de démarrer nous allons télécharger l'image et modifier sa taille. Elle ne fait que 2 Go.

wget https://cloud-images.ubuntu.com/impish/current/impish-server-cloudimg-amd64.img
qemu-img resize impish-server-cloudimg-amd64.img 10G

Maintenant que tout est prêt allez on démarre le tout :

terraform init
terraform apply -auto-approve

Allez dans cockpit et dans machine virtuelle cliquez sur pdns.

Ouvrez la console série et entrez root, passwd pour vous connecter.

cloud-init status
status: done

C'est bon elle est configurée !

On peut lancer le playbook Ansible.

ansible-playbook -i inventory deploy_powerdns.yml

La aussi il faudra être patient .... Une fois l'installation terminée, ouvrez votre navigateur sur l'adresse que vous avez défini : https://pnds.robert.local

Plus loin

Le code source de l'ensemble est disponible sur gitlab

Il y a encore un peu de travail pour rendre le playbook tip-top au petits oignons. Mais je vous laisse jouer avec pour apprendre :)