nikolai.one
● SYS_CONSOLE // type 'help' for commands
Nikolai Systems Terminal v1.0.4 [x86_64-linux]
Type 'help' to see all available commands. Press '~' or ESC to toggle console.
Try: 'whoami', 'articles' or especially 'showoff'
>
MY ARCHIVE Click the book on the shelf to view bio • Move mouse to look • Press ESC to exit
πŸ“– Inspect Book
← All writing

Building a Zero-Maintenance Dynamic DNS Service with Cloud Functions and Terraform

#gcp #terraform #golang #dns #devops

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

  1. Endpoint: A lightweight Go Cloud Function (Gen 2) triggered by HTTP.
  2. Caller IP Detection: The function inspects the X-Forwarded-For header provided by Google Cloud's edge proxy to obtain the caller's true public IP.
  3. Authentication: Verifies an API key passed in query parameters.
  4. Cloud DNS API: Compares the caller IP with the current A record 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.