Aller au contenu principal

Installation de PowerDNS et PowerDNS-Admin

· 10 minutes de lecture
Stéphane ROBERT

logo

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

    create user powerdns

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

    create user 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 :)