Domain Parking and Redirection in Google Cloud (GCP)

Parking

Many organisations have a stockpile of DNS domains they've collected over the years. Chances are, some of them are for brand protection, some are "marketing" domains which redirect to specific pages on their main website, some are SEO related and some are just plain old legacy that needs to be maintained.

You can easily run up a server with Apache, Nginx or something on it and setup the configuration to receive requests for those domains and perform 301/302 redirects or serve up some sort of generic "this domain is parked" type page.

However, in the cloud, you can go serverless - do all of that, but without a server to build, run, maintain, backup, upgrade, security audit and manage. This document explains how to setup such a service in Google Cloud (GCP).

Build your Domain Parking and Redirector

You can do all of this using the Cloud Console, but we like infrastructure as code, so we're going to do it all in Terraform. Firstly, we'll need an Internet IP address:

resource "google_compute_global_address" "redirector" {
  name = "redirector"
}

Next, we'll need a "forwarder". This is the "front end" of a load balancer which receives traffic and passes it on to be handled by the processing logic (this is roughly a "listener" if you're thinking in AWS terms):

resource "google_compute_global_forwarding_rule" "redirector" {
  name       = "redirector-port-80"
  ip_address = google_compute_global_address.redirector.address
  port_range = "80"
  target     = google_compute_target_http_proxy.redirector.self_link
}

Here we're going to receive on port 80, so plain HTTP. With a little bit of extra work we can do the same for port 443 and receive SSL (with valid certificates, generated and maintained by Google).

Next, we need an "HTTP proxy". In this case this is just a bit of fluff, but in other use-cases you may need more from this. In our case it's pretty simple:

resource "google_compute_target_http_proxy" "redirector" {
  name    = "redirector"
  url_map = google_compute_url_map.redirector.self_link
}

This needs a url map. This is the main logic of the redirector, and so we're going to break out the config into a variable. This is how we declare the variable:

variable "redirections" {
  description = "A list of maps describing the redirections required"
  type        = list(map(string))
}

When we actually set this variable, it'll look something like this:

redirections = [
  {
    domain:        "example.com",
    redirect_host: "destination.example.com",
    redirect_path: "/some/path/",
    redirect_ssl:  true
  }
]

Back to the url map... We need to configure rules that say "if the incoming request is for a host called example.com, then redirect it to https://destination.example.com/some/path/". We also want to be able to add in more redirects from other domains to other destinations, so we use the for_each resource and dynamic config to do it:

resource "google_compute_url_map" "redirector" {
  name        = "redirector"
  default_service = google_compute_backend_bucket.domain-parking-backend.self_link

  dynamic "host_rule" {
    for_each = var.redirections
    content {
      hosts        = [ host_rule.value["host"] ]
      path_matcher = replace(host_rule.value["host"], ".", "-")
    }
  }

  dynamic "path_matcher" {
    for_each = var.redirections
    content {
      name = replace(path_matcher.value["host"], ".", "-")

      default_url_redirect {
        host_redirect          = path_matcher.value["host"]
        https_redirect         = path_matcher.value["ssl"]
        path_redirect          = path_matcher.value["path"]
        redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
        strip_query            = false
      }
    }
  }
}

This will do our redirects for our chosen domains and paths, but it won't yet handle anything that's not in a rule. For this we need a default backend service. You could use an actual server here, but we can use a Storage Bucket to avoid needing that server:

resource "google_compute_backend_bucket" "domain-parking-backend" {
  name        = "redirector-domain-parking-site"
  description = "Provides a default web server for the domain parking load balancer"
  bucket_name = google_storage_bucket.domain-parking.name
  enable_cdn  = false
}

resource "google_storage_bucket" "domain-parking" {
  name     = "http-redirector-domain-parking-site"
  location = "EU"
}

Now we've got everything we need, except a landing page that says "this domain is parked". If you hit up the service we've got here, it'll return an ugly looking XML document saying something like "key not found".

To fix this, we'll create a simple landing page:

<!doctype html>
<html lang=en>
 <head>
  <meta charset=utf-8>
  <meta name="robots" content="noindex, nofollow, nosnippet, noarchive, nocache">
  <title>Domain Parking</title>
 </head>
<body>
 <p>This domain is parked.</p>
</body>
</html>

Then we just need to put that file into the root of the bucket and make it publicly readable:

resource "google_storage_bucket_object" "index-html" {
  name   = "index.html"
  source = "${path.module}/index.html"
  bucket = google_storage_bucket.domain-parking.name
}

resource "google_storage_object_access_control" "index-html" {
  object = google_storage_bucket_object.index-html.output_name
  bucket = google_storage_bucket.domain-parking.name
  role   = "READER"
  entity = "allUsers"
}

There we go - all done. Now if you visit the IP address made in the first step you'll see "this domain is parked". If you point some actual DNS domains at that IP address, they'll either redirect or show the landing page. You can do some presentation and branding on the landing page if you want to make it look a little better.

For all the code, checkout our Gitlab Snippet.

Bootnote

Creating this took a lot of head scratching and even a call to Google support. The documentation around it isn't terribly comprehensive, and even just getting it to work in the Console is tricky. Hopefully this helps someone out one day!

We also looked at making this a regional rather than global service (as a way to save a bit of money). After some hours of hitting problem after problem, we decided against it. If you get it working, please let us know how!

Image credit: https://flic.kr/p/XbkNsn