Skip to content

Commit

Permalink
Add a serverless-gclb module.
Browse files Browse the repository at this point in the history
This makes putting regionalized services behind GCLB with DNS and TLS much easier.

Signed-off-by: Matt Moore <[email protected]>
  • Loading branch information
mattmoor committed Jan 22, 2024
1 parent d093fc1 commit 6cd9ba6
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 5 deletions.
8 changes: 4 additions & 4 deletions modules/dashboard/service/dashboard.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
2 changes: 1 addition & 1 deletion modules/regional-go-service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
98 changes: 98 additions & 0 deletions modules/serverless-gclb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# `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
}
}
}
```

<!-- BEGIN_TF_DOCS -->
## Requirements

No requirements.

## Providers

| Name | Version |
|------|---------|
| <a name="provider_google"></a> [google](#provider\_google) | n/a |
| <a name="provider_random"></a> [random](#provider\_random) | n/a |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_recorder-dashboard"></a> [recorder-dashboard](#module\_recorder-dashboard) | ../dashboard/cloudevent-receiver | n/a |
| <a name="module_this"></a> [this](#module\_this) | ../regional-go-service | n/a |
| <a name="module_triggers"></a> [triggers](#module\_triggers) | ../cloudevent-trigger | n/a |

## Resources

| Name | Type |
|------|------|
| [google_bigquery_data_transfer_config.import-job](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_data_transfer_config) | resource |
| [google_bigquery_dataset.this](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_dataset) | resource |
| [google_bigquery_table.types](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table) | resource |
| [google_bigquery_table_iam_binding.import-writes-to-tables](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table_iam_binding) | resource |
| [google_service_account.import-identity](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource |
| [google_service_account.recorder](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource |
| [google_service_account_iam_binding.bq-dts-assumes-import-identity](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource |
| [google_service_account_iam_binding.provisioner-acts-as-import-identity](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource |
| [google_storage_bucket.recorder](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket) | resource |
| [google_storage_bucket_iam_binding.import-reads-from-gcs-buckets](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket_iam_binding) | resource |
| [google_storage_bucket_iam_binding.recorder-writes-to-gcs-buckets](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket_iam_binding) | resource |
| [random_id.suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |
| [random_id.trigger-suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |
| [google_project.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_broker"></a> [broker](#input\_broker) | A map from each of the input region names to the name of the Broker topic in that region. | `map(string)` | n/a | yes |
| <a name="input_deletion_protection"></a> [deletion\_protection](#input\_deletion\_protection) | Whether to enable deletion protection on data resources. | `bool` | `true` | no |
| <a name="input_location"></a> [location](#input\_location) | The location to create the BigQuery dataset in, and in which to run the data transfer jobs from GCS. | `string` | `"US"` | no |
| <a name="input_name"></a> [name](#input\_name) | n/a | `string` | n/a | yes |
| <a name="input_project_id"></a> [project\_id](#input\_project\_id) | n/a | `string` | n/a | yes |
| <a name="input_provisioner"></a> [provisioner](#input\_provisioner) | The identity as which this module will be applied (so it may be granted permission to 'act as' the DTS service account). This should be in the form expected by an IAM subject (e.g. user:sally@example.com) | `string` | n/a | yes |
| <a name="input_regions"></a> [regions](#input\_regions) | A map from region names to a network and subnetwork. A recorder service and cloud storage bucket (into which the service writes events) will be created in each region. | <pre>map(object({<br> network = string<br> subnet = string<br> }))</pre> | n/a | yes |
| <a name="input_retention-period"></a> [retention-period](#input\_retention-period) | The number of days to retain data in BigQuery. | `number` | n/a | yes |
| <a name="input_types"></a> [types](#input\_types) | A map from cloudevent types to the BigQuery schema associated with them. | `map(string)` | n/a | yes |

## Outputs

No outputs.
<!-- END_TF_DOCS -->
121 changes: 121 additions & 0 deletions modules/serverless-gclb/main.tf
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions modules/serverless-gclb/variables.tf
Original file line number Diff line number Diff line change
@@ -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
}))
}

Check failure on line 29 in modules/serverless-gclb/variables.tf

View workflow job for this annotation

GitHub Actions / Lint

[EOF Newline] reported by reviewdog 🐶 Missing newline Raw Output: modules/serverless-gclb/variables.tf:29: Missing newline

0 comments on commit 6cd9ba6

Please sign in to comment.