Managing DNS Records with deSEC.io and Terraform
rwxd July 01, 2026 #terraform #dns #desec.ioI manage all my DNS records using deSEC.io as the DNS provider and Terraform to define them as code. This keeps everything version-controlled, reviewable, and reproducible. OpenTofu works as well since the setup is fully compatible.
Why deSEC.io?
deSEC is a free, non-profit DNS hosting service based in Germany. It supports DNSSEC out of the box and has a clean REST API.
Repository Structure
The setup is minimal:
tf-desecio/
├── main.tf # Terraform logic
├── providers.tf # Provider configuration
├── records.yaml # All DNS records
└── .forgejo/workflows/terraform.yaml # CI pipelineProvider Configuration
Using the Valodim/desec Terraform provider:
terraform {
required_providers {
desec = {
source = "Valodim/desec"
version = "0.6.1"
}
}
}
Authentication is done via the DESEC_API_TOKEN environment variable.
Defining Records in YAML
All DNS records live in a single records.yaml file, grouped by domain:
example.com:
- subname: ""
type: "MX"
records:
- "10 mail.example.com."
- subname: "mail"
type: "A"
records:
- "203.0.113.1"
- subname: "mail"
type: "AAAA"
records:
- "2001:db8::1"
- subname: ""
type: "TXT"
records:
- "v=spf1 mx -all"
another-domain.de:
- subname: "www"
type: "CNAME"
records:
- another-domain.de.
This makes it easy to see all records at a glance and add new ones without touching any HCL.
Terraform Logic
The main.tf flattens the YAML structure into individual desec_rrset resources:
locals {
raw_records = yamldecode(file("records.yaml"))
records_flattened = flatten([
for domain_name, record_set in local.raw_records : [
for item in record_set : {
domain = domain_name
subname = item.subname
type = item.type
records = item.records
ttl = lookup(item, "ttl", 3600)
}
]
])
}
resource "desec_rrset" "rrset" {
for_each = { for rr in local.records_flattened : "${rr.domain}-${rr.subname}-${rr.type}" => rr }
domain = each.value.domain
subname = each.value.subname
type = each.value.type
records = each.value.records
ttl = each.value.ttl
}
Each record gets a unique key based on domain, subname, and type. Adding a new record is just appending a few lines to the YAML file.
CI Pipeline
A Forgejo Actions workflow runs on every push and PR:
on:
push:
branches: [main]
pull_request:
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: hashicorp/setup-terraform@v4
- run: terraform init
- run: terraform fmt -check
- run: terraform validate
This catches formatting issues and invalid configurations.
Workflow
Adding or changing a DNS record:
- Edit
records.yaml - Run
terraform planto verify changes - Run
terraform applyto push to deSEC - Commit and push
That's it. No web UI clicking, no forgetting what you changed, and full git history of every DNS modification.