Terraform et Gestion DNS
Introduction
SdV propose une API compatible PowerDNS pour la gestion programmatique de vos zones DNS. Cette API permet d'adopter l'Infrastructure as Code (IaC) pour automatiser la création, modification et suppression des enregistrements DNS.
Documentation API PowerDNS : https://doc.powerdns.com/authoritative/http-routingtable.html
Architecture de l'API
L'API PowerDNS n'étant pas multi-tenant nativement, SdV a mis en place un middleware qui apporte les fonctionnalités suivantes :
- Gestion multi-tenant avec isolation par clé API
- Accès à l'ensemble de vos domaines via une unique clé d'API (si souhaité)
- Création de clés API avec portée limitée à un ou plusieurs domaines spécifiques
- Filtrage IP spécifique à chaque clé
- Restrictions de sécurité sur certaines fonctionnalités de l'API PowerDNS
Endpoint API : https://powerdns-endpoint-dns.sdv.fr/
Avantages de Terraform
- Infrastructure as Code : versioning, revue de code, reproductibilité
- Plan avant exécution : visualisation des changements avant application
- État partagé : collaboration en équipe
- Idempotence : application sûre et répétée
- Intégration CI/CD : automatisation complète
Prérequis
Installation de Terraform
# Télécharger Terraform
wget https://releases.hashicorp.com/terraform/1.14.6/terraform_1.14.6_linux_amd64.zip
# Extraire et installer
unzip terraform_1.14.6_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Vérifier l'installation
terraform --versionObtention de la clé API
Pour obtenir votre clé API PowerDNS, contactez SdV :
- Via l'outil de ticket
- Ou contactez votre commercial
Informations à fournir :
- Liste des domaines concernés
- Portée souhaitée (tous les domaines ou domaines spécifiques)
- Adresses IP sources autorisées
- Environnement (production, préproduction, développement)
Configuration
Structure du projet
Organisation recommandée pour un projet Terraform DNS :
terraform-dns/
├── main.tf # Ressources principales
├── provider.tf # Configuration du provider
├── variables.tf # Variables d'entrée
├── outputs.tf # Outputs
├── terraform.tfvars # Valeurs des variables (ne pas versionner)
├── versions.tf # Contraintes de version
└── environments/
├── prod/
│ └── terraform.tfvars
├── preprod/
│ └── terraform.tfvars
└── dev/
└── terraform.tfvarsConfiguration du provider
Déclaration du provider
terraform {
required_version = ">= 1.0.0"
required_providers {
powerdns = {
source = "pan-net/powerdns"
version = "~> 1.5.0"
}
}
}Configuration de base
provider "powerdns" {
api_key = var.powerdns_api_key
server_url = "https://powerdns-endpoint-dns.sdv.fr/"
}Variables
variable "powerdns_api_key" {
description = "API key for PowerDNS"
type = string
sensitive = true
}
variable "zone" {
description = "DNS zone name"
type = string
}
variable "environment" {
description = "Environment name (prod, preprod, dev)"
type = string
default = "prod"
}Fichier de valeurs
Fichier terraform.tfvars (à ne pas versionner) :
powerdns_api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
zone = "monsite.fr."
environment = "prod"Ajoutez terraform.tfvars dans votre .gitignore :
echo "terraform.tfvars" >> .gitignore
echo "*.tfvars" >> .gitignore
echo ".terraform/" >> .gitignore
echo "terraform.tfstate*" >> .gitignoreMéthodes de configuration sécurisées
Via variables d'environnement
export TF_VAR_powerdns_api_key="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
terraform planVia fichier de variables externe
terraform plan -var-file="secrets.tfvars"Via gestionnaire de secrets
Avec HashiCorp Vault :
data "vault_generic_secret" "powerdns" {
path = "secret/powerdns"
}
provider "powerdns" {
api_key = data.vault_generic_secret.powerdns.data["api_key"]
server_url = "https://powerdns-endpoint-dns.sdv.fr/"
}Avec AWS Secrets Manager :
data "aws_secretsmanager_secret_version" "powerdns" {
secret_id = "powerdns/api_key"
}
provider "powerdns" {
api_key = jsondecode(data.aws_secretsmanager_secret_version.powerdns.secret_string)["api_key"]
server_url = "https://powerdns-endpoint-dns.sdv.fr/"
}Via git-crypt
Pour les projets nécessitant le versioning des secrets chiffrés :
# Installer git-crypt
apt-get install git-crypt # Debian/Ubuntu
brew install git-crypt # macOS
# Initialiser git-crypt
cd terraform-dns/
git-crypt init
# Ajouter les fichiers à chiffrer dans .gitattributes
echo "terraform.tfvars filter=git-crypt diff=git-crypt" >> .gitattributes
echo "*.tfvars filter=git-crypt diff=git-crypt" >> .gitattributes
# Ajouter une clé GPG (chaque collaborateur)
git-crypt add-gpg-user user@example.com
# Les fichiers .tfvars seront automatiquement chiffrés dans GitInitialisation
Première utilisation
# Initialiser le projet Terraform
terraform init
# Télécharge les providers
# Initialise le backend
# Crée le répertoire .terraform/Sortie attendue :
Initializing the backend...
Initializing provider plugins...
- Finding pan-net/powerdns versions matching "~> 1.5.0"...
- Installing pan-net/powerdns v1.5.0...
- Installed pan-net/powerdns v1.5.0 (signed by a HashiCorp partner)
Terraform has been successfully initialized!Mise à jour des providers
terraform init -upgradeGestion des enregistrements DNS
Record type A
Enregistrement simple
resource "powerdns_record" "www" {
zone = var.zone
name = "www.${var.zone}"
type = "A"
ttl = 300
records = ["192.0.2.10"]
}Enregistrement avec plusieurs IPs
resource "powerdns_record" "app" {
zone = var.zone
name = "app.${var.zone}"
type = "A"
ttl = 300
records = [
"192.0.2.10",
"192.0.2.11",
"192.0.2.12"
]
}Record type AAAA (IPv6)
resource "powerdns_record" "www_ipv6" {
zone = var.zone
name = "www.${var.zone}"
type = "AAAA"
ttl = 300
records = ["2001:db8::1"]
}Record type CNAME
resource "powerdns_record" "blog" {
zone = var.zone
name = "blog.${var.zone}"
type = "CNAME"
ttl = 300
records = ["www.${var.zone}"]
}Record type MX
resource "powerdns_record" "mail" {
zone = var.zone
name = var.zone
type = "MX"
ttl = 300
records = [
"10 mail1.${var.zone}",
"20 mail2.${var.zone}"
]
}Record type TXT
resource "powerdns_record" "spf" {
zone = var.zone
name = var.zone
type = "TXT"
ttl = 300
records = [
"\"v=spf1 include:_spf.google.com ~all\""
]
}
resource "powerdns_record" "dkim" {
zone = var.zone
name = "default._domainkey.${var.zone}"
type = "TXT"
ttl = 300
records = [
"\"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...\""
]
}Record type SRV
resource "powerdns_record" "xmpp" {
zone = var.zone
name = "_xmpp-client._tcp.${var.zone}"
type = "SRV"
ttl = 300
records = [
"5 0 5222 xmpp.${var.zone}"
]
}Record type CAA
resource "powerdns_record" "caa" {
zone = var.zone
name = var.zone
type = "CAA"
ttl = 300
records = [
"0 issue \"letsencrypt.org\"",
"0 issuewild \"letsencrypt.org\"",
"0 iodef \"mailto:security@monsite.fr\""
]
}Record type NS (sous-délégation)
resource "powerdns_record" "subdomain_ns" {
zone = var.zone
name = "sub.${var.zone}"
type = "NS"
ttl = 3600
records = [
"ns1.provider.com.",
"ns2.provider.com."
]
}Workflow Terraform
Plan
Visualiser les changements avant application :
terraform planExemple de sortie :
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# powerdns_record.www will be created
+ resource "powerdns_record" "www" {
+ id = (known after apply)
+ name = "www.monsite.fr."
+ records = [
+ "192.0.2.10",
]
+ ttl = 300
+ type = "A"
+ zone = "monsite.fr."
}
Plan: 1 to add, 0 to change, 0 to destroy.Sauvegarder un plan
terraform plan -out=tfplan
terraform apply tfplanApply
Appliquer les changements :
terraform applyTerraform affiche le plan et demande confirmation :
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yesApplication automatique (CI/CD)
terraform apply -auto-approveDestroy
Supprimer toutes les ressources gérées :
terraform destroySupprimer une ressource spécifique
terraform destroy -target=powerdns_record.wwwVérification DNS post-application
# Vérifier l'enregistrement A
dig A www.monsite.fr +short
# Vérifier l'enregistrement MX
dig MX monsite.fr +short
# Vérifier l'enregistrement TXT
dig TXT monsite.fr +shortExemples complets
Infrastructure web standard
# Record A pour le domaine racine
resource "powerdns_record" "root" {
zone = var.zone
name = var.zone
type = "A"
ttl = 300
records = ["192.0.2.10"]
}
# Record A pour www
resource "powerdns_record" "www" {
zone = var.zone
name = "www.${var.zone}"
type = "A"
ttl = 300
records = ["192.0.2.10"]
}
# Records MX pour les emails
resource "powerdns_record" "mail" {
zone = var.zone
name = var.zone
type = "MX"
ttl = 300
records = [
"10 mail.${var.zone}",
]
}
# Record A pour le serveur mail
resource "powerdns_record" "mail_server" {
zone = var.zone
name = "mail.${var.zone}"
type = "A"
ttl = 300
records = ["192.0.2.20"]
}
# SPF
resource "powerdns_record" "spf" {
zone = var.zone
name = var.zone
type = "TXT"
ttl = 300
records = [
"\"v=spf1 mx a:mail.${var.zone} ~all\""
]
}
# DMARC
resource "powerdns_record" "dmarc" {
zone = var.zone
name = "_dmarc.${var.zone}"
type = "TXT"
ttl = 300
records = [
"\"v=DMARC1; p=quarantine; rua=mailto:dmarc@${var.zone}\""
]
}Infrastructure microservices
# API principale
resource "powerdns_record" "api" {
zone = var.zone
name = "api.${var.zone}"
type = "A"
ttl = 60
records = ["192.0.2.30"]
}
# API version 2
resource "powerdns_record" "api_v2" {
zone = var.zone
name = "api-v2.${var.zone}"
type = "A"
ttl = 60
records = ["192.0.2.31"]
}
# Load balancer avec plusieurs IPs
resource "powerdns_record" "lb" {
zone = var.zone
name = "lb.${var.zone}"
type = "A"
ttl = 30
records = [
"192.0.2.40",
"192.0.2.41",
"192.0.2.42"
]
}
# Services backend
resource "powerdns_record" "auth_service" {
zone = var.zone
name = "auth.${var.zone}"
type = "A"
ttl = 300
records = ["192.0.2.50"]
}
resource "powerdns_record" "data_service" {
zone = var.zone
name = "data.${var.zone}"
type = "A"
ttl = 300
records = ["192.0.2.51"]
}Utilisation de boucles (for_each)
variable "subdomains" {
description = "Map of subdomains to IP addresses"
type = map(object({
ip = string
ttl = number
}))
default = {
"www" = {
ip = "192.0.2.10"
ttl = 300
}
"api" = {
ip = "192.0.2.20"
ttl = 60
}
"admin" = {
ip = "192.0.2.30"
ttl = 300
}
}
}
resource "powerdns_record" "subdomains" {
for_each = var.subdomains
zone = var.zone
name = "${each.key}.${var.zone}"
type = "A"
ttl = each.value.ttl
records = [each.value.ip]
}Utilisation de modules
Fichier modules/dns-record/main.tf :
variable "zone" {
type = string
}
variable "name" {
type = string
}
variable "type" {
type = string
}
variable "ttl" {
type = number
default = 300
}
variable "records" {
type = list(string)
}
resource "powerdns_record" "this" {
zone = var.zone
name = var.name
type = var.type
ttl = var.ttl
records = var.records
}
output "fqdn" {
value = powerdns_record.this.name
}Utilisation du module :
module "www" {
source = "./modules/dns-record"
zone = var.zone
name = "www.${var.zone}"
type = "A"
ttl = 300
records = ["192.0.2.10"]
}
module "api" {
source = "./modules/dns-record"
zone = var.zone
name = "api.${var.zone}"
type = "A"
ttl = 60
records = ["192.0.2.20"]
}Gestion de l'état (State)
Backend local
Par défaut, Terraform stocke l'état localement dans terraform.tfstate.
Backend distant
S3 Backend (AWS)
terraform {
backend "s3" {
bucket = "terraform-state-monsite"
key = "dns/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}GCS Backend (Google Cloud)
terraform {
backend "gcs" {
bucket = "terraform-state-monsite"
prefix = "dns"
}
}Terraform Cloud
terraform {
cloud {
organization = "mon-organisation"
workspaces {
name = "dns-production"
}
}
}Manipulation de l'état
Lister les ressources
terraform state listAfficher une ressource
terraform state show powerdns_record.wwwSupprimer une ressource de l'état
terraform state rm powerdns_record.wwwImporter une ressource existante
terraform import powerdns_record.www monsite.fr.:::A:::www.monsite.fr.Intégration CI/CD
GitLab CI
Fichier .gitlab-ci.yml :
variables:
TF_ROOT: ${CI_PROJECT_DIR}
TF_STATE_NAME: dns
stages:
- validate
- plan
- apply
before_script:
- cd ${TF_ROOT}
- export TF_VAR_powerdns_api_key=${POWERDNS_API_KEY}
validate:
stage: validate
image: hashicorp/terraform:latest
script:
- terraform init -backend=false
- terraform validate
- terraform fmt -check
only:
- merge_requests
- main
plan:
stage: plan
image: hashicorp/terraform:latest
script:
- terraform init
- terraform plan -out=tfplan
artifacts:
paths:
- ${TF_ROOT}/tfplan
expire_in: 1 week
only:
- merge_requests
- main
apply:
stage: apply
image: hashicorp/terraform:latest
script:
- terraform init
- terraform apply -auto-approve tfplan
dependencies:
- plan
only:
- main
when: manualGitHub Actions
Fichier .github/workflows/terraform.yml :
name: Terraform DNS
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
TF_VAR_powerdns_api_key: ${{ secrets.POWERDNS_API_KEY }}
jobs:
terraform:
name: Terraform
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.14.6
- name: Terraform Init
run: terraform init
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -no-color
continue-on-error: true
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approveJenkins Pipeline
Fichier Jenkinsfile :
pipeline {
agent any
environment {
TF_VAR_powerdns_api_key = credentials('powerdns-api-key')
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Terraform Init') {
steps {
sh 'terraform init'
}
}
stage('Terraform Validate') {
steps {
sh 'terraform validate'
}
}
stage('Terraform Plan') {
steps {
sh 'terraform plan -out=tfplan'
}
}
stage('Terraform Apply') {
when {
branch 'main'
}
steps {
input message: 'Apply Terraform changes?', ok: 'Apply'
sh 'terraform apply tfplan'
}
}
}
post {
always {
cleanWs()
}
}
}Bonnes pratiques
Organisation du code
Séparation des environnements
terraform-dns/
├── modules/
│ └── dns-zone/
├── environments/
│ ├── prod/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── preprod/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── dev/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvarsUtilisation de workspaces
# Créer un workspace
terraform workspace new prod
# Lister les workspaces
terraform workspace list
# Sélectionner un workspace
terraform workspace select prod
# Afficher le workspace actuel
terraform workspace showSécurité
Protection des secrets
- Ne jamais versionner les clés API en clair
- Utiliser des variables sensibles (
sensitive = true) - Chiffrer l'état Terraform (backend S3 avec encryption)
- Utiliser git-crypt pour les fichiers sensibles versionnés
- Privilégier les gestionnaires de secrets (Vault, AWS Secrets Manager)
Gestion des accès
- Clés API avec portée limitée (domaines spécifiques)
- Filtrage IP sur les clés API
- Rotation régulière des clés API
- Audit des modifications via logs
Validation
Pre-commit hooks
Fichier .pre-commit-config.yaml :
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.5
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docsInstallation :
pip install pre-commit
pre-commit installLinting
# TFLint
tflint --init
tflint
# Checkov (sécurité)
checkov -d .Documentation
Génération automatique
# Installer terraform-docs
brew install terraform-docs # macOS
# ou
wget https://github.com/terraform-docs/terraform-docs/releases/download/v0.16.0/terraform-docs-v0.16.0-linux-amd64.tar.gz
# Générer la documentation
terraform-docs markdown table . > README.mdCommentaires dans le code
# Record A pour le load balancer principal
# TTL court pour permettre un basculement rapide
resource "powerdns_record" "lb" {
zone = var.zone
name = "lb.${var.zone}"
type = "A"
ttl = 30 # 30 secondes pour basculement rapide
records = [
"192.0.2.40",
"192.0.2.41"
]
}Tests
Validation syntaxique
terraform validateTests avec Terratest
Fichier test/dns_test.go :
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestDNSRecords(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../",
Vars: map[string]interface{}{
"zone": "test.example.com.",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Vérifications
output := terraform.Output(t, terraformOptions, "www_fqdn")
assert.Equal(t, "www.test.example.com.", output)
}Points d'attention
TTL et propagation
- TTL court (30-60s) : basculement rapide, charge DNS élevée
- TTL moyen (300-3600s) : bon compromis pour la plupart des usages
- TTL long (86400s) : records stables, propagation lente des changements
Recommandations par type :
- Load balancers : 30-60s
- Serveurs web : 300-600s
- Serveurs mail : 3600s
- NS records : 86400s
Limitations de l'API
Certaines fonctionnalités de l'API PowerDNS sont filtrées par le middleware SdV pour des raisons de sécurité :
- Création/suppression de zones : non disponible
- Modification des paramètres de zone : restreint
- Opérations de maintenance : non disponible
Pour ces opérations, contactez le support SdV.
Performance
- Utilisez des modules pour éviter la duplication
- Limitez le nombre de ressources par fichier (< 100)
- Utilisez
-parallelismpour contrôler le parallélisme - Backend distant pour les projets d'équipe
# Limiter le parallélisme
terraform apply -parallelism=10Impacts opérationnels
- Plan avant chaque apply en production
- Testez dans un environnement de préproduction
- Sauvegardes de l'état Terraform
- Monitoring des changements DNS
- Documentation des modifications
Troubleshooting
Erreurs courantes
Erreur d'authentification
Error: API authentication failedVérifications :
# Vérifier la variable
echo $TF_VAR_powerdns_api_key
# Tester l'API manuellement
curl -H "X-API-Key: $TF_VAR_powerdns_api_key" \
https://powerdns-endpoint-dns.sdv.fr/api/v1/serversRecord déjà existant
Error: record already existsSolution : Importer la ressource existante
terraform import powerdns_record.www monsite.fr.:::A:::www.monsite.fr.État verrouillé
Error: state lockedSolution : Déverrouiller l'état (avec précaution)
terraform force-unlock LOCK_IDDebug
Mode verbose
TF_LOG=DEBUG terraform applyNiveaux de log
# TRACE (le plus verbeux)
TF_LOG=TRACE terraform apply
# DEBUG
TF_LOG=DEBUG terraform apply
# INFO
TF_LOG=INFO terraform apply
# WARN
TF_LOG=WARN terraform apply
# ERROR
TF_LOG=ERROR terraform applyLogs dans un fichier
TF_LOG=DEBUG TF_LOG_PATH=terraform.log terraform applyCommandes utiles
Gestion basique
# Initialiser
terraform init
# Formater le code
terraform fmt -recursive
# Valider la syntaxe
terraform validate
# Planifier les changements
terraform plan
# Appliquer les changements
terraform apply
# Détruire les ressources
terraform destroyÉtat et ressources
# Lister les ressources
terraform state list
# Afficher une ressource
terraform state show powerdns_record.www
# Rafraîchir l'état
terraform refresh
# Importer une ressource
terraform import powerdns_record.www monsite.fr.:::A:::www.monsite.fr.
# Supprimer de l'état (sans détruire)
terraform state rm powerdns_record.www
# Déplacer une ressource
terraform state mv powerdns_record.www powerdns_record.www_newOutputs
# Afficher tous les outputs
terraform output
# Afficher un output spécifique
terraform output www_ip
# Format JSON
terraform output -jsonWorkspaces
# Créer un workspace
terraform workspace new prod
# Lister
terraform workspace list
# Sélectionner
terraform workspace select prod
# Supprimer
terraform workspace delete devContact et support
En cas de problème ou pour améliorer cette documentation, contactez :
- Support SdV : Outil de ticket
- Documentation Terraform : https://www.terraform.io/docs
- Provider PowerDNS : https://registry.terraform.io/providers/pan-net/powerdns/latest/docs