Building a Zero-Maintenance Dynamic DNS Service with Cloud Functions and Terraform
Most residential internet service providers assign dynamic public IPv4 addresses that periodically change whenever your modem renews its lease.
If you host home services (VPN, WireGuard, self-hosted dashboards, media servers), you need dynamic DNS (DDNS). For years, people used services like No-IP or DynDNS. But free tiers have become increasingly hostile, forcing you to solve monthly CAPTCHAs to keep your hostname active, while commercial options cost $30 to $50 a year.
I already manage my domains on Google Cloud DNS. Instead of paying for a third-party service, I built gcp-diy-dyndns.
How It Works
- Endpoint: A lightweight Go Cloud Function (Gen 2) triggered by HTTP.
- Caller IP Detection: The function inspects the
X-Forwarded-Forheader provided by Google Cloud's edge proxy to obtain the caller's true public IP. - Authentication: Verifies an API key passed in query parameters.
- Cloud DNS API: Compares the caller IP with the current
Arecord in Cloud DNS. If it changed, it executes an atomic ResourceRecordSets change batch.
func HandleHTTP(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
if key != expectedKey {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
clientIP := extractPublicIP(r.Header.Get("X-Forwarded-For"))
zone := r.URL.Query().Get("zone")
domain := r.URL.Query().Get("domain")
if err := updateDnsRecord(zone, domain, clientIP); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Updated %s to %s\n", domain, clientIP)
}
Infrastructure as Code with Terraform
The entire infrastructure (IAM roles, Cloud Function deployment, Cloud DNS permissions) is defined in a single Terraform module:
resource "google_cloudfunctions2_function" "dyndns" {
name = "diydns-function"
location = var.region
description = "Dynamic DNS updater for Cloud DNS"
build_config {
runtime = "go122"
entry_point = "HandleHTTP"
source {
storage_source {
bucket = google_storage_bucket.source.name
object = google_storage_bucket_object.archive.name
}
}
}
}
Setting up the Cron on a Home Server
On a home server or router running 24/7, a single cron line keeps the record in sync:
*/30 * * * * curl -s "https://REGION-PROJECT.cloudfunctions.net/diydns-function?key=SECRET&zone=my-zone&domain=home.mydomain.com." > /dev/null 2>&1
Because the function only runs twice an hour and executes in under 80 milliseconds, it stays well within GCP's free tier limits (2 million invocations per month). It has been running for over two years without a single hitch.