Generate DNS records with Python and Traefik API
· 4 min read
Introduction #
I finally tackled a tedious task that’s been bothering me: creating DNS records for container services.
I’m using Traefik as the reverse proxy for my containers, and I use Adguard Home as a local DNS resolver in my home. When I first started self-hosting I only had a single virtual server running all my containers which meant that pointing DNS records for the services was easy, as I only needed a wildcard entry pointing to the virtual machine running Traefik.
Recently I decided to separate my containers so they run on different VMs depending on their purpose, each with a separate Traefik instance. This, of course, means I can no longer use the wildcard entry in Adguard Home, instead I need to create a DNS record for every service I deploy, which can become quite tedious.
Fast forward past me agonizing over this and coming up with a solution for my selfhosted inflicted problem: using python to query Traefiks API and autogenerate DNS records.
The setup #
To make this work, I needed an alternative to Adguard for handling the auto-generated records and Unbound was a good fit, so I spun up a debian LXC in Proxmox and installed it:
sudo apt install unbound
---
services:
traefik:
environment:
- TRAEFIK_API=true
...
The script #
Next up is the python script. It’s quite short and nothing fancy. I’ll break it down for those who are interested, the full script is at the bottom of the post.
To start off we need to create a few variables, starting with one containing the IPs of all the Traefik servers.
traefik_servers = "10.50.10.10","10.50.10.11","10.50.10.12"
p = re.compile("\w+\.self-hosted\.dad")
.conf
file so we can use it for comparing later. This file needs to be created manually, before the script is run for the first time!
conf = open("/etc/unbound/unbound.conf.d/traefik.conf","r")
conf_content = conf.read()
conf.close()
dns = {}
/api/http/routers
and storing the response as JSON. Afterwards, we iterate again, this time through the JSON response, using our regex method to extract the FQDN from the Traefik rule (this is the router rule).
for server in traefik_servers: #Iterate through all traefik APIs
api_url = "http://" + server + ":8080/api/http/routers"
response = requests.get(api_url)
response_json = response.json() #Save the response as JSON.
for x in response_json: #Iterate through the JSON
match = p.search(x["rule"]) #Match any Traerik "rule" that contains a FQDN.
if match:
name = match.group(0)
dns.update({name: server}) #Save the domain and IP in the dict.
After we’ve filled our dictionary we compare it with our current config. If any FQDNs are new, we overwrite the config. Afterwards we loop through the dictionary, writing a new line per item, after which we reload the Unbound service.
for hostname in dns.items(): #Iterate through the items in the dict
if hostname[0] not in conf_content: #Compare with the Unbound conf
with open("/etc/unbound/unbound.conf.d/traefik.conf","w") as f:
f.write("server:\n")
for hostname, ip in dns.items():
f.write(f"\tlocal-data: \"{hostname} IN A {ip}\"\n")
subprocess.run(["systemctl", "reload", "unbound"], check=True) #Reload unbound
break #End the loop, no need to write more than once
import requests
import re
import subprocess
traefik_servers = "10.50.10.10","10.50.10.11","10.50.10.12"
p = re.compile("\w+\.self-hosted\.dad")
conf = open("/etc/unbound/unbound.conf.d/traefik.conf","r")
conf_content = conf.read()
conf.close()
dns = {}
for server in traefik_servers: #Iterate through all traefik APIs
api_url = "http://" + server + ":8080/api/http/routers"
response = requests.get(api_url)
response_json = response.json() #Save the response as JSON.
for x in response_json: #Iterate through the JSON
match = p.search(x["rule"]) #Match any Traerik "rule" that contains a FQDN.
if match:
name = match.group(0)
dns.update({name: server}) #Save the domain and IP in the dict.
for hostname in dns.items(): #Iterate through the items in the dict
if hostname[0] not in conf_content: #Compare with the Unbound conf
with open("/etc/unbound/unbound.conf.d/traefik.conf","w") as f:
f.write("server:\n")
for hostname, ip in dns.items():
f.write(f"\tlocal-data: \"{hostname} IN A {ip}\"\n")
subprocess.run(["systemctl", "reload", "unbound"], check=True) #Reload unbound
break #End the loop, no need to write more than once
The end #
Let this script run as a cron job on the same server running Unbound to have it periodically check your Traefik instances for any new domain names.
Finally, if one is using Adguard Home (like I am), the Unbound server needs to be specified so that Adguard knows where to ask for these domain names. This is done by going to Settings > DNS Settings > Upstream DNS Servers, and adding:
[/self-hosted.dad/]10.80.10.2
That’s it! Maybe someone else, who likes to overcomplicate their home infrastructure, will find this useful.