Last year, I wrote a tutorial on deploying a serverless application to all Google Cloud regions, and route your users to the closer region using a load balancer with anycast IP. (I’ve since moved that article into our documentation). It was a 13+ step tutorial to get it working.

Naturally, I scratched the itch and released a Terraform module that makes this much easier. I posted it on Twitter, and it got over 200 likes, so I decided to write about how this works. In this article I’ll show you how you can automate deploying to “all regions”much easier using this new Terraform module. This repository contains all the code and examples I’ll talk here.

Several of our customers and apps like gcpping.com are already using this Terraform module in production to deploy to all regions.

Why deploy everywhere?

Long story short: Round-trip latencies. Assume you live in Singapore, and trying to connect to your application running in Iowa datacenter. Establishing a TCP connection and a TLS handshake takes, 3 full round-trips between these points. If your round-trip latency to the server is 200ms, your users will take 600ms just to connect to your application before they receive any data. For most applications, that’s not acceptable.

Enter global anycast IPs

As I explained in my earlier article, Google Cloud’s HTTPS Load Balancer works with anycast public IPs, that are advertised globally and routed to the nearest Google datacenter.

If a user from Signapore connects to your app, they are routed to the nearest datacenter or edge PoP location (likely ⪅30ms round-trip latency) so the whole handshake will complete in 100ms or so. After the TLS is terminated at the edge, your traffic will now flow inside Google’s global network (still encrypted) and will be routed to the nearest Cloud Run service you deployed behind the load balancer.

How to do this in Terraform?

To make this easier on Terraform, I worked on extending our lb-http module to support serverless network endpoint groups.

First, we need a data source to retrieve all Cloud Run regions as a list:

data "google_cloud_run_locations" "default" { }

Then, we deploy the Cloud Run service using for_each construct in HCL:

# deploy Cloud Run Service
resource "google_cloud_run_service" "default" {
  for_each = toset(data.google_cloud_run_locations.default.locations)

  name     = "${var.name}--${each.value}"
  location = each.value
  project  = var.project_id

  template {
    spec {
      containers {
        image = var.image
      }
    }
  }
}

Optionally, we make the service publicly accessible by everyone (assuming it’s a public website or API) by letting allUsers invoke this service, using IAM. Note that we’re using for_each again.

resource "google_cloud_run_service_iam_member" "default" {
  for_each = toset(data.google_cloud_run_locations.default.locations)

  location = google_cloud_run_service.default[each.key].location
  project  = google_cloud_run_service.default[each.key].project
  service  = google_cloud_run_service.default[each.key].name
  role     = "roles/run.invoker"
  member   = "allUsers"
}

Then, we need to create a serverless network endpoint group to be able to add the Cloud Run service as a backend to the load balancer. Note that we’re using for_each here as well.

resource "google_compute_region_network_endpoint_group" "default" {
  for_each = toset(data.google_cloud_run_locations.default.locations)

  provider              = google-beta
  name                  = "${var.name}--neg--${each.key}"
  network_endpoint_type = "SERVERLESS"
  region                = google_cloud_run_service.default[each.key].location
  cloud_run {
    service = google_cloud_run_service.default[each.key].name
  }
}

Finally, create the load balancer, using our lb-http module’s serverless_negs submodule. (I omitted some details here, but you can find a working example in the repo.) This is where you configure your domain name, IP address, CDN settings, TLS certificate (automatic or bring-your-own) etc. for the load balancer. We use for_each here to add the NEGs we created above as a backend here.

module "lb-http" {
  source            = "GoogleCloudPlatform/lb-http/google//modules/serverless_negs"
  version           = "~> 4.5"

  project = var.project_id
  name    = var.name

  # ...
  backends = {
    # ...
    default = {
      # ...
      groups = [
        for neg in google_compute_region_network_endpoint_group.default:
        {
          group = neg.id
        }
      ]

      # ...
    }
  }

That’s it! Even with the parts omitted, in less than 100 lines of Terraform, you can configure an automated way to deploy a containerized app to all regions, and serve it on a global load balancer (with a domain name and TLS certificate).

Every time we roll out new regions, all you need to do is re-apply this configuration and it will pick up the newly added regions from the API.

Right now, when I deploy this configuration to all 19 regions, it creates about 68 resources on various APIs, and the resulting Cloud Run services look like this (and thanks to terraform destroy, it cleanly removes them all):

$ gcloud run services list
SERVICE                               REGION                   URL                                                                 
zoneprinter--asia-east2               asia-east2               https://zoneprinter--asia-east2-2wvlk7vg3a-df.a.run.app             
zoneprinter--asia-northeast1          asia-northeast1          https://zoneprinter--asia-northeast1-2wvlk7vg3a-an.a.run.app        
zoneprinter--asia-northeast2          asia-northeast2          https://zoneprinter--asia-northeast2-2wvlk7vg3a-dt.a.run.app        
zoneprinter--asia-northeast3          asia-northeast3          https://zoneprinter--asia-northeast3-2wvlk7vg3a-du.a.run.app        
zoneprinter--asia-south1              asia-south1              https://zoneprinter--asia-south1-2wvlk7vg3a-el.a.run.app            
zoneprinter--asia-southeast1          asia-southeast1          https://zoneprinter--asia-southeast1-2wvlk7vg3a-as.a.run.app        
zoneprinter--asia-southeast2          asia-southeast2          https://zoneprinter--asia-southeast2-2wvlk7vg3a-et.a.run.app        
zoneprinter--australia-southeast1     australia-southeast1     https://zoneprinter--australia-southeast1-2wvlk7vg3a-ts.a.run.app   
zoneprinter--europe-north1            europe-north1            https://zoneprinter--europe-north1-2wvlk7vg3a-lz.a.run.app          
zoneprinter--europe-west1             europe-west1             https://zoneprinter--europe-west1-2wvlk7vg3a-ew.a.run.app           
zoneprinter--europe-west2             europe-west2             https://zoneprinter--europe-west2-2wvlk7vg3a-nw.a.run.app           
zoneprinter--europe-west3             europe-west3             https://zoneprinter--europe-west3-2wvlk7vg3a-ey.a.run.app           
zoneprinter--europe-west4             europe-west4             https://zoneprinter--europe-west4-2wvlk7vg3a-ez.a.run.app           
zoneprinter--europe-west6             europe-west6             https://zoneprinter--europe-west6-2wvlk7vg3a-oa.a.run.app           
zoneprinter--northamerica-northeast1  northamerica-northeast1  https://zoneprinter--northamerica-northeast1-2wvlk7vg3a-nn.a.run.app
zoneprinter--southamerica-east1       southamerica-east1       https://zoneprinter--southamerica-east1-2wvlk7vg3a-rj.a.run.app     
zoneprinter--us-central1              us-central1              https://zoneprinter--us-central1-2wvlk7vg3a-uc.a.run.app            
zoneprinter--us-east1                 us-east1                 https://zoneprinter--us-east1-2wvlk7vg3a-ue.a.run.app               
zoneprinter--us-east4                 us-east4                 https://zoneprinter--us-east4-2wvlk7vg3a-uk.a.run.app               
zoneprinter--us-west1                 us-west1                 https://zoneprinter--us-west1-2wvlk7vg3a-uw.a.run.app               

Hopefully, this is useful to you. Let me know on Twitter if you end up using it. Make sure to read the Terraform module documentation to see what else you can tune and fork the example repo if you want to give this a try.