From fc3068496f38262f4d8f076a7db7269903ab31f2 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Sun, 21 Jan 2024 16:45:22 -0800 Subject: [PATCH] Add a `serverless-gclb` module. This makes putting regionalized services behind GCLB with DNS and TLS much easier. Signed-off-by: Matt Moore --- .github/workflows/documentation.yaml | 1 + modules/dashboard/service/dashboard.tf | 8 +- modules/regional-go-service/main.tf | 2 +- modules/serverless-gclb/README.md | 84 +++++++++++++++++ modules/serverless-gclb/main.tf | 121 +++++++++++++++++++++++++ modules/serverless-gclb/variables.tf | 29 ++++++ 6 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 modules/serverless-gclb/README.md create mode 100644 modules/serverless-gclb/main.tf create mode 100644 modules/serverless-gclb/variables.tf diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 0a633f57..5f83a72d 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -15,6 +15,7 @@ jobs: - cloudevent-trigger - cloudevent-recorder - regional-go-service + - serverless-gclb - otel-collector - networking - dashboard/service diff --git a/modules/dashboard/service/dashboard.tf b/modules/dashboard/service/dashboard.tf index c91c4566..c4d91118 100644 --- a/modules/dashboard/service/dashboard.tf +++ b/modules/dashboard/service/dashboard.tf @@ -12,10 +12,10 @@ module "http" { } module "grpc" { - source = "../sections/grpc" - title = "GRPC" - filter = [] - service_name = var.service_name + source = "../sections/grpc" + title = "GRPC" + filter = [] + service_name = var.service_name } module "resources" { diff --git a/modules/regional-go-service/main.tf b/modules/regional-go-service/main.tf index d7785877..9824e62c 100644 --- a/modules/regional-go-service/main.tf +++ b/modules/regional-go-service/main.tf @@ -82,7 +82,7 @@ resource "google_cloud_run_v2_service" "this" { for_each = env.value.value_source != null ? { "" : env.value.value_source } : {} content { secret_key_ref { - secret = value_source.value.secret_key_ref.secret + secret = value_source.value.secret_key_ref.secret version = value_source.value.secret_key_ref.version } } diff --git a/modules/serverless-gclb/README.md b/modules/serverless-gclb/README.md new file mode 100644 index 00000000..6e83b880 --- /dev/null +++ b/modules/serverless-gclb/README.md @@ -0,0 +1,84 @@ +# `serverless-gclb` + +This module provisions a Google Cloud Load Balancer (GCLB) that sits in front of +some number of regionalized Cloud Run services. + +```hcl +// Create a network with several regional subnets +module "networking" { + source = "chainguard-dev/common/infra//modules/networking" + + name = "my-networking" + project_id = var.project_id + regions = [...] +} + +resource "google_dns_managed_zone" "top-level-zone" { + project = var.project_id + name = "example-com" + dns_name = "example.com." +} + +module "serverless-gclb" { + source = "chainguard-dev/common/infra//modules/serverless-gclb" + + name = "my-gclb" + project_id = var.project_id + dns_zone = google_dns_managed_zone.top-level-zone.name + + // Regions are all of the places that we have backends deployed. + // Regions must be removed from serving before they are torn down. + regions = keys(module.networking.regional-networks) + serving_regions = keys(module.networking.regional-networks) + + public-services = { + "foo.example.com" = { + name = "my-foo-service" // e.g. from regional-go-service + } + } +} +``` + + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [google](#provider\_google) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [google_compute_backend_service.public-services](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_backend_service) | resource | +| [google_compute_global_address.this](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_global_address) | resource | +| [google_compute_global_forwarding_rule.this](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_global_forwarding_rule) | resource | +| [google_compute_managed_ssl_certificate.public-service](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_managed_ssl_certificate) | resource | +| [google_compute_region_network_endpoint_group.regional-backends](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_region_network_endpoint_group) | resource | +| [google_compute_target_https_proxy.public-service](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_target_https_proxy) | resource | +| [google_compute_url_map.public-service](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map) | resource | +| [google_dns_record_set.public-service](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_record_set) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [dns\_zone](#input\_dns\_zone) | The managed DNS zone in which to create record sets. | `string` | n/a | yes | +| [name](#input\_name) | n/a | `string` | n/a | yes | +| [project\_id](#input\_project\_id) | n/a | `string` | n/a | yes | +| [public-services](#input\_public-services) | A map from hostnames (managed by dns\_zone), to the name of the regionalized cloud run service to which the hostname should be routed. A managed SSL certificate will be created for each hostname, and a DNS record set will be created for each hostname pointing to the load balancer's global IP address. |
map(object({
name = string
}))
| n/a | yes | +| [regions](#input\_regions) | The set of regions containing backends for the load balancer (regions must be added here before they can be added as serving regions). | `list` |
[
"us-central1"
]
| no | +| [serving\_regions](#input\_serving\_regions) | The set of regions with backends suitable for serving traffic from the load balancer (regions must be removed from here before they can be removed from regions). | `list` |
[
"us-central1"
]
| no | + +## Outputs + +No outputs. + diff --git a/modules/serverless-gclb/main.tf b/modules/serverless-gclb/main.tf new file mode 100644 index 00000000..11739cf7 --- /dev/null +++ b/modules/serverless-gclb/main.tf @@ -0,0 +1,121 @@ +// Create the IP address for our LB to serve on. +resource "google_compute_global_address" "this" { + project = var.project_id + name = var.name +} + +// Create A records for each of our public service hostnames. +resource "google_dns_record_set" "public-service" { + for_each = var.public-services + + project = var.project_id + name = "${each.key}." + managed_zone = var.dns_zone + type = "A" + ttl = 60 + + rrdatas = [google_compute_global_address.this.address] +} + +// Provision a managed SSL certificate for each of our public services. +resource "google_compute_managed_ssl_certificate" "public-service" { + for_each = var.public-services + + name = each.value.name + + managed { + domains = [google_dns_record_set.public-service[each.key].name] + } +} + +// Create the cross-product of public services and regions so we can for_each over it. +locals { + regional-backends = merge([ + for svcinfo in values(var.public-services) : merge([ + for region in var.regions : { + "${svcinfo.name}-${region}" : { + name = svcinfo.name + region = region + } + } + ]...) + ]...) +} + +// Create a network endpoint group for each service in each region. +resource "google_compute_region_network_endpoint_group" "regional-backends" { + for_each = local.regional-backends + + name = each.value.name + network_endpoint_type = "SERVERLESS" + region = each.value.region + cloud_run { + service = each.value.name + } +} + +// Create a backend service for each public service with a backend in each region. +resource "google_compute_backend_service" "public-services" { + for_each = var.public-services + + project = var.project_id + name = each.value.name + + // Create a backend for each region hosting this cloud run service. + dynamic "backend" { + for_each = toset(var.serving_regions) + content { + group = google_compute_region_network_endpoint_group.regional-backends["${each.value.name}-${backend.key}"]["id"] + } + } +} + +// Create a URL map that routes each hostname to the appropriate backend service. +resource "google_compute_url_map" "public-service" { + project = var.project_id + name = var.name + + default_url_redirect { + host_redirect = "chainguard.dev" + strip_query = true + } + + // For each of the public services create a host rule. + dynamic "host_rule" { + for_each = var.public-services + content { + hosts = [host_rule.key] + path_matcher = host_rule.value.name + } + } + + // For each of the public services create an empty path matcher + // that routes to its backend service. + dynamic "path_matcher" { + for_each = var.public-services + content { + name = path_matcher.value.name + default_service = google_compute_backend_service.public-services[path_matcher.key].id + } + } +} + +// Create an HTTPS proxy for our URL map. +resource "google_compute_target_https_proxy" "public-service" { + project = var.project_id + name = var.name + url_map = google_compute_url_map.public-service.id + + ssl_certificates = [for domain, cert in google_compute_managed_ssl_certificate.public-service : cert.id] +} + +// Attach the HTTPS proxy to the global IP address via a forwarding rule. +resource "google_compute_global_forwarding_rule" "this" { + project = var.project_id + name = var.name + ip_protocol = "TCP" + load_balancing_scheme = "EXTERNAL" + port_range = 443 + ip_address = google_compute_global_address.this.id + target = google_compute_target_https_proxy.public-service.id +} diff --git a/modules/serverless-gclb/variables.tf b/modules/serverless-gclb/variables.tf new file mode 100644 index 00000000..d5697071 --- /dev/null +++ b/modules/serverless-gclb/variables.tf @@ -0,0 +1,29 @@ +variable "name" { + type = string +} + +variable "project_id" { + type = string +} + +variable "regions" { + description = "The set of regions containing backends for the load balancer (regions must be added here before they can be added as serving regions)." + default = ["us-central1"] +} + +variable "serving_regions" { + description = "The set of regions with backends suitable for serving traffic from the load balancer (regions must be removed from here before they can be removed from regions)." + default = ["us-central1"] +} + +variable "dns_zone" { + type = string + description = "The managed DNS zone in which to create record sets." +} + +variable "public-services" { + description = "A map from hostnames (managed by dns_zone), to the name of the regionalized cloud run service to which the hostname should be routed. A managed SSL certificate will be created for each hostname, and a DNS record set will be created for each hostname pointing to the load balancer's global IP address." + type = map(object({ + name = string + })) +}