From fbee3b1095912f9aa461856b9d7aa40c9b84f3a2 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Fri, 10 May 2024 11:38:48 -0700 Subject: [PATCH] Introduce a `regional-service` module (#328) Signed-off-by: Matt Moore --- .github/workflows/documentation.yaml | 1 + modules/regional-go-service/README.md | 15 +- modules/regional-go-service/main.tf | 407 +++--------------- modules/regional-go-service/outputs.tf | 4 +- modules/regional-go-service/variables.tf | 2 +- modules/regional-service/README.md | 111 +++++ modules/regional-service/main.tf | 377 ++++++++++++++++ .../otel-config/config.yaml | 0 modules/regional-service/outputs.tf | 5 + modules/regional-service/variables.tf | 170 ++++++++ 10 files changed, 719 insertions(+), 373 deletions(-) create mode 100644 modules/regional-service/README.md create mode 100644 modules/regional-service/main.tf rename modules/{regional-go-service => regional-service}/otel-config/config.yaml (100%) create mode 100644 modules/regional-service/outputs.tf create mode 100644 modules/regional-service/variables.tf diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index a571ac04..dbc0c1e1 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -18,6 +18,7 @@ jobs: - cloudevent-trigger - cloudevent-recorder - regional-go-service + - regional-service - serverless-gclb - networking - dashboard/service diff --git a/modules/regional-go-service/README.md b/modules/regional-go-service/README.md index e1dd0f57..c80fd80b 100644 --- a/modules/regional-go-service/README.md +++ b/modules/regional-go-service/README.md @@ -66,31 +66,20 @@ No requirements. | Name | Version | |------|---------| | [cosign](#provider\_cosign) | n/a | -| [google](#provider\_google) | n/a | -| [google-beta](#provider\_google-beta) | n/a | | [ko](#provider\_ko) | n/a | ## Modules | Name | Source | Version | |------|--------|---------| -| [audit-serviceaccount](#module\_audit-serviceaccount) | ../audit-serviceaccount | n/a | +| [this](#module\_this) | ../regional-service | n/a | ## Resources | Name | Type | |------|------| | [cosign_sign.this](https://registry.terraform.io/providers/chainguard-dev/cosign/latest/docs/resources/sign) | resource | -| [google-beta_google_cloud_run_v2_service.this](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_cloud_run_v2_service) | resource | -| [google_cloud_run_v2_service_iam_member.public-services-are-unauthenticated](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_service_iam_member) | resource | -| [google_monitoring_alert_policy.anomalous-service-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | -| [google_monitoring_alert_policy.bad-rollout](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | -| [google_project_iam_member.metrics-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | -| [google_project_iam_member.profiler-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | -| [google_project_iam_member.trace-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [ko_build.this](https://registry.terraform.io/providers/ko-build/ko/latest/docs/resources/build) | resource | -| [google_client_openid_userinfo.me](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_openid_userinfo) | data source | -| [google_project.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source | ## Inputs @@ -106,7 +95,7 @@ No requirements. | [otel\_collector\_image](#input\_otel\_collector\_image) | The otel collector image to use as a base. Must be on gcr.io or dockerhub. | `string` | `"chainguard/opentelemetry-collector-contrib:latest"` | no | | [project\_id](#input\_project\_id) | n/a | `string` | n/a | yes | | [regional-volumes](#input\_regional-volumes) | The volumes to make available to the containers in the service for mounting. |
list(object({
name = string
gcs = optional(map(object({
bucket = string
read_only = optional(bool, true)
})), {})
nfs = optional(map(object({
server = string
path = string
read_only = optional(bool, true)
})), {})
}))
| `[]` | no | -| [regions](#input\_regions) | A map from region names to a network and subnetwork. A pub/sub topic and ingress service (publishing to the respective topic) will be created in each region, with the ingress service configured to egress all traffic via the specified subnetwork. |
map(object({
network = string
subnet = string
}))
| n/a | yes | +| [regions](#input\_regions) | A map from region names to a network and subnetwork. A service will be created in each region configured to egress the specified traffic via the specified subnetwork. |
map(object({
network = string
subnet = string
}))
| n/a | yes | | [request\_timeout\_seconds](#input\_request\_timeout\_seconds) | The timeout for requests to the service, in seconds. | `number` | `300` | no | | [scaling](#input\_scaling) | The scaling configuration for the service. |
object({
min_instances = optional(number, 0)
max_instances = optional(number, 100)
max_instance_request_concurrency = optional(number)
})
| `{}` | no | | [service\_account](#input\_service\_account) | The service account as which to run the service. | `string` | n/a | yes | diff --git a/modules/regional-go-service/main.tf b/modules/regional-go-service/main.tf index dfd3390d..a4f6d4e6 100644 --- a/modules/regional-go-service/main.tf +++ b/modules/regional-go-service/main.tf @@ -5,35 +5,24 @@ terraform { } } -module "audit-serviceaccount" { - source = "../audit-serviceaccount" - - project_id = var.project_id - service-account = var.service_account - - # The absence of authorized identities here means that - # nothing is authorized to act as this service account. - # Note: Cloud Run's usage doesn't show up in the audit logs. - - notification_channels = var.notification_channels +moved { + from = module.audit-serviceaccount + to = module.this.module.audit-serviceaccount } -resource "google_project_iam_member" "metrics-writer" { - project = var.project_id - role = "roles/monitoring.metricWriter" - member = "serviceAccount:${var.service_account}" +moved { + from = google_project_iam_member.metrics-writer + to = module.this.google_project_iam_member.metrics-writer } -resource "google_project_iam_member" "trace-writer" { - project = var.project_id - role = "roles/cloudtrace.agent" - member = "serviceAccount:${var.service_account}" +moved { + from = google_project_iam_member.trace-writer + to = module.this.google_project_iam_member.trace-writer } -resource "google_project_iam_member" "profiler-writer" { - project = var.project_id - role = "roles/cloudprofiler.agent" - member = "serviceAccount:${var.service_account}" +moved { + from = google_project_iam_member.profiler-writer + to = module.this.google_project_iam_member.profiler-writer } // Build each of the application images from source. @@ -50,349 +39,55 @@ resource "cosign_sign" "this" { conflict = "REPLACE" } -locals { - // Pull out the containers from var.containers that has no port - sidecars = { - for key, value in var.containers : key => value if length(value.ports) == 0 - } - // Pull out the main container from var.containers that has a port. - // There should be only one of them, but using a map to make it easier to - // iterate over and look up the ko_builds. - has_port = { - for key, value in var.containers : key => value if length(value.ports) > 0 - } - - main_container_idx = keys(local.has_port)[0] - main_container = local.has_port[local.main_container_idx] -} - -check "exactly_one_main_container" { - assert { - condition = length(local.has_port) == 1 - error_message = "Exactly one container with ports must be specified." - } -} - -// Deploy the service into each of our regions. -resource "google_cloud_run_v2_service" "this" { - for_each = var.regions - - provider = google-beta # For empty_dir - project = var.project_id - name = var.name - location = each.key - labels = var.labels - ingress = var.ingress - - launch_stage = "BETA" // Needed for vpc_access below - - - template { - scaling { - min_instance_count = var.scaling.min_instances - max_instance_count = var.scaling.max_instances - } - max_instance_request_concurrency = var.scaling.max_instance_request_concurrency - execution_environment = var.execution_environment - vpc_access { - network_interfaces { - network = each.value.network - subnetwork = each.value.subnet - } - egress = var.egress - // TODO(mattmoor): When direct VPC egress supports network tags - // for NAT egress, then we should incorporate those here. - } - service_account = var.service_account - timeout = "${var.request_timeout_seconds}s" - - // A main container has ports. It needs to go first to avoid a bug in the - // Cloud Run terraform provider where omitting the port{} block does not - // remove the port from the service. - containers { - image = cosign_sign.this[local.main_container_idx].signed_ref - args = local.main_container.args - - dynamic "ports" { - for_each = local.main_container.ports - content { - name = ports.value.name - container_port = ports.value.container_port - } - } - - dynamic "resources" { - for_each = local.main_container.resources != null ? { "" : local.main_container.resources } : {} - content { - limits = resources.value.limits - cpu_idle = resources.value.cpu_idle - startup_cpu_boost = resources.value.startup_cpu_boost - } - } - - dynamic "env" { - for_each = local.main_container.env - content { - name = env.value.name - value = env.value.value - dynamic "value_source" { - for_each = env.value.value_source != null ? { "" : env.value.value_source } : {} - content { - secret_key_ref { - secret = value_source.value.secret_key_ref.secret - version = value_source.value.secret_key_ref.version - } - } - } - } - } - - // Iterate over regional environment variables and look up the - // appropriate value to pass to each region. - dynamic "env" { - for_each = local.main_container.regional-env - content { - name = env.value.name - value = env.value.value[each.key] - } - } - - dynamic "volume_mounts" { - for_each = local.main_container.volume_mounts - content { - name = volume_mounts.value.name - mount_path = volume_mounts.value.mount_path - } - } - - } - - // Now the sidecar containers can be added. - dynamic "containers" { - for_each = local.sidecars - content { - image = cosign_sign.this[containers.key].signed_ref - args = containers.value.args - - dynamic "resources" { - for_each = containers.value.resources != null ? { "" : containers.value.resources } : {} - content { - limits = resources.value.limits - cpu_idle = resources.value.cpu_idle - startup_cpu_boost = resources.value.startup_cpu_boost - } - } - - dynamic "env" { - for_each = containers.value.env - content { - name = env.value.name - value = env.value.value - dynamic "value_source" { - for_each = env.value.value_source != null ? { "" : env.value.value_source } : {} - content { - secret_key_ref { - secret = value_source.value.secret_key_ref.secret - version = value_source.value.secret_key_ref.version - } - } - } - } - } - - // Iterate over regional environment variables and look up the - // appropriate value to pass to each region. - dynamic "env" { - for_each = containers.value.regional-env - content { - name = env.value.name - value = env.value.value[each.key] - } - } - - dynamic "volume_mounts" { - for_each = containers.value.volume_mounts - content { - name = volume_mounts.value.name - mount_path = volume_mounts.value.mount_path - } - } - } - } - containers { - image = var.otel_collector_image - // config via env is an option; https://pkg.go.dev/go.opentelemetry.io/collector/service#section-readme - args = ["--config=env:OTEL_CONFIG"] - env { - name = "OTEL_CONFIG" - value = file("${path.module}/otel-config/config.yaml") - } - } - - dynamic "volumes" { - for_each = var.volumes - content { - name = volumes.value.name - - dynamic "secret" { - for_each = volumes.value.secret != null ? { "" : volumes.value.secret } : {} - content { - secret = secret.value.secret - dynamic "items" { - for_each = secret.value.items - content { - version = items.value.version - path = items.value.path - } - } - } - } - - dynamic "empty_dir" { - for_each = volumes.value.empty_dir != null ? { "" : volumes.value.empty_dir } : {} - content { - medium = empty_dir.value.medium - size_limit = empty_dir.value.size_limit - } - } - } - } - - // Regional volumes - dynamic "volumes" { - for_each = var.regional-volumes - content { - name = volumes.value.name - - dynamic "gcs" { - for_each = length(volumes.value.gcs) > 0 ? { "" : volumes.value.gcs[each.key] } : {} - content { - bucket = gcs.value.bucket - read_only = gcs.value.read_only - } - } - dynamic "nfs" { - for_each = length(volumes.value.nfs) > 0 ? { "" : volumes.value.nfs[each.key] } : {} - content { - server = nfs.value.server - path = nfs.value.path - read_only = nfs.value.read_only - } - } - } +module "this" { + source = "../regional-service" + + project_id = var.project_id + name = var.name + regions = var.regions + ingress = var.ingress + egress = var.egress + + service_account = var.service_account + containers = { + for name, container in var.containers : name => { + image = cosign_sign.this[name].signed_ref + args = container.args + ports = container.ports + resources = container.resources + env = container.env + regional-env = container.regional-env + volume_mounts = container.volume_mounts } } -} - -// Get a project number for this project ID. -data "google_project" "project" { project_id = var.project_id } -// What identity is deploying this? -data "google_client_openid_userinfo" "me" {} + labels = var.labels + scaling = var.scaling + volumes = var.volumes + regional-volumes = var.regional-volumes -// Create an alert policy to notify if the service is accessed by an unauthorized entity. -resource "google_monitoring_alert_policy" "anomalous-service-access" { - # In the absence of data, incident will auto-close after an hour - alert_strategy { - auto_close = "3600s" - - notification_rate_limit { - period = "3600s" // re-alert hourly if condition still valid. - } - } - - display_name = "Abnormal Service Access: ${var.name}" - combiner = "OR" - - conditions { - display_name = "Abnormal Service Access: ${var.name}" - - condition_matched_log { - filter = < v.name - } + value = module.this.names } diff --git a/modules/regional-go-service/variables.tf b/modules/regional-go-service/variables.tf index 7eb01fc0..33747503 100644 --- a/modules/regional-go-service/variables.tf +++ b/modules/regional-go-service/variables.tf @@ -7,7 +7,7 @@ variable "name" { } variable "regions" { - description = "A map from region names to a network and subnetwork. A pub/sub topic and ingress service (publishing to the respective topic) will be created in each region, with the ingress service configured to egress all traffic via the specified subnetwork." + description = "A map from region names to a network and subnetwork. A service will be created in each region configured to egress the specified traffic via the specified subnetwork." type = map(object({ network = string subnet = string diff --git a/modules/regional-service/README.md b/modules/regional-service/README.md new file mode 100644 index 00000000..e1a5a83c --- /dev/null +++ b/modules/regional-service/README.md @@ -0,0 +1,111 @@ +# `regional-service` + +This module provisions a regionalizied Cloud Run service. The simplest example +service can be seen here: + +```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 = [...] +} + +module "foo-service" { + source = "chainguard-dev/common/infra//modules/regional-service" + + project_id = var.project_id + name = "foo" + regions = module.networking.regional-networks + + service_account = google_service_account.foo.email + containers = { + "foo" = { + image = "..." + ports = [{ container_port = 8080 }] + } + } +} +``` + +The module is intended to encapsulate Chainguard best practices around deploying +Cloud Run services including: + +- More secure default for ingress +- More secure default for egress +- Intentionally not exposing a `uri` output (use + [`authorize-private-service`](../authorize-private-service/README.md)) +- Requiring a service-account name to run as (so as not to use the default + compute service account!) +- Running an `otel-collector` sidecar container that can collect and publish + telemetry data from out services (for use with the dashboard modules). + +For the most part, we have tried to expose a roughly compatible shape to the +cloud run v2 service itself, with one primary change: + +1. In addition to `env` we support `regional-env`, where the value is a map from + region to regional value. This can be used to pass different environment + values to services based on the region they are running in (e.g. + `cloudevent-broker` ingress endpoint or another regionalized service's + localized URI). + + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [google](#provider\_google) | n/a | +| [google-beta](#provider\_google-beta) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [audit-serviceaccount](#module\_audit-serviceaccount) | ../audit-serviceaccount | n/a | + +## Resources + +| Name | Type | +|------|------| +| [google-beta_google_cloud_run_v2_service.this](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_cloud_run_v2_service) | resource | +| [google_cloud_run_v2_service_iam_member.public-services-are-unauthenticated](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_service_iam_member) | resource | +| [google_monitoring_alert_policy.anomalous-service-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | +| [google_monitoring_alert_policy.bad-rollout](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/monitoring_alert_policy) | resource | +| [google_project_iam_member.metrics-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | +| [google_project_iam_member.profiler-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | +| [google_project_iam_member.trace-writer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | +| [google_client_openid_userinfo.me](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_openid_userinfo) | data source | +| [google_project.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [containers](#input\_containers) | The containers to run in the service. Each container will be run in each region. |
map(object({
image = string
args = optional(list(string), [])
ports = optional(list(object({
name = optional(string, "http1")
container_port = number
})), [])
resources = optional(
object(
{
limits = optional(object(
{
cpu = string
memory = string
}
), null)
cpu_idle = optional(bool, true)
startup_cpu_boost = optional(bool, false)
}
),
{
cpu_idle = true
}
)
env = optional(list(object({
name = string
value = optional(string)
value_source = optional(object({
secret_key_ref = object({
secret = string
version = string
})
}), null)
})), [])
regional-env = optional(list(object({
name = string
value = map(string)
})), [])
volume_mounts = optional(list(object({
name = string
mount_path = string
})), [])
}))
| n/a | yes | +| [egress](#input\_egress) | Which type of egress traffic to send through the VPC.

- ALL\_TRAFFIC sends all traffic through regional VPC network
- PRIVATE\_RANGES\_ONLY sends only traffic to private IP addresses through regional VPC network | `string` | `"ALL_TRAFFIC"` | no | +| [execution\_environment](#input\_execution\_environment) | The execution environment for the service | `string` | `"EXECUTION_ENVIRONMENT_GEN1"` | no | +| [ingress](#input\_ingress) | Which type of ingress traffic to accept for the service.

- INGRESS\_TRAFFIC\_ALL accepts all traffic, enabling the public .run.app URL for the service
- INGRESS\_TRAFFIC\_INTERNAL\_LOAD\_BALANCER accepts traffic only from a load balancer
- INGRESS\_TRAFFIC\_INTERNAL\_ONLY accepts internal traffic only | `string` | `"INGRESS_TRAFFIC_INTERNAL_ONLY"` | no | +| [labels](#input\_labels) | Labels to apply to the service. | `map(string)` | `{}` | no | +| [name](#input\_name) | n/a | `string` | n/a | yes | +| [notification\_channels](#input\_notification\_channels) | List of notification channels to alert. | `list(string)` | n/a | yes | +| [otel\_collector\_image](#input\_otel\_collector\_image) | The otel collector image to use as a base. Must be on gcr.io or dockerhub. | `string` | `"chainguard/opentelemetry-collector-contrib:latest"` | no | +| [project\_id](#input\_project\_id) | n/a | `string` | n/a | yes | +| [regional-volumes](#input\_regional-volumes) | The volumes to make available to the containers in the service for mounting. |
list(object({
name = string
gcs = optional(map(object({
bucket = string
read_only = optional(bool, true)
})), {})
nfs = optional(map(object({
server = string
path = string
read_only = optional(bool, true)
})), {})
}))
| `[]` | no | +| [regions](#input\_regions) | A map from region names to a network and subnetwork. A service will be created in each region configured to egress the specified traffic via the specified subnetwork. |
map(object({
network = string
subnet = string
}))
| n/a | yes | +| [request\_timeout\_seconds](#input\_request\_timeout\_seconds) | The timeout for requests to the service, in seconds. | `number` | `300` | no | +| [scaling](#input\_scaling) | The scaling configuration for the service. |
object({
min_instances = optional(number, 0)
max_instances = optional(number, 100)
max_instance_request_concurrency = optional(number)
})
| `{}` | no | +| [service\_account](#input\_service\_account) | The service account as which to run the service. | `string` | n/a | yes | +| [volumes](#input\_volumes) | The volumes to make available to the containers in the service for mounting. |
list(object({
name = string
empty_dir = optional(object({
medium = optional(string, "MEMORY")
size_limit = optional(string, "2G")
}))
secret = optional(object({
secret = string
items = list(object({
version = string
path = string
}))
}))
}))
| `[]` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [names](#output\_names) | n/a | + diff --git a/modules/regional-service/main.tf b/modules/regional-service/main.tf new file mode 100644 index 00000000..e398281a --- /dev/null +++ b/modules/regional-service/main.tf @@ -0,0 +1,377 @@ +module "audit-serviceaccount" { + source = "../audit-serviceaccount" + + project_id = var.project_id + service-account = var.service_account + + # The absence of authorized identities here means that + # nothing is authorized to act as this service account. + # Note: Cloud Run's usage doesn't show up in the audit logs. + + notification_channels = var.notification_channels +} + +resource "google_project_iam_member" "metrics-writer" { + project = var.project_id + role = "roles/monitoring.metricWriter" + member = "serviceAccount:${var.service_account}" +} + +resource "google_project_iam_member" "trace-writer" { + project = var.project_id + role = "roles/cloudtrace.agent" + member = "serviceAccount:${var.service_account}" +} + +resource "google_project_iam_member" "profiler-writer" { + project = var.project_id + role = "roles/cloudprofiler.agent" + member = "serviceAccount:${var.service_account}" +} + +locals { + // Pull out the containers from var.containers that has no port + sidecars = { + for key, value in var.containers : key => value if length(value.ports) == 0 + } + // Pull out the main container from var.containers that has a port. + // There should be only one of them, but using a map to make it easier to + // iterate over and look up the ko_builds. + has_port = { + for key, value in var.containers : key => value if length(value.ports) > 0 + } + + main_container_idx = keys(local.has_port)[0] + main_container = local.has_port[local.main_container_idx] +} + +check "exactly_one_main_container" { + assert { + condition = length(local.has_port) == 1 + error_message = "Exactly one container with ports must be specified." + } +} + +// Deploy the service into each of our regions. +resource "google_cloud_run_v2_service" "this" { + for_each = var.regions + + provider = google-beta # For empty_dir + project = var.project_id + name = var.name + location = each.key + labels = var.labels + ingress = var.ingress + + launch_stage = "BETA" // Needed for vpc_access below + + + template { + scaling { + min_instance_count = var.scaling.min_instances + max_instance_count = var.scaling.max_instances + } + max_instance_request_concurrency = var.scaling.max_instance_request_concurrency + execution_environment = var.execution_environment + vpc_access { + network_interfaces { + network = each.value.network + subnetwork = each.value.subnet + } + egress = var.egress + // TODO(mattmoor): When direct VPC egress supports network tags + // for NAT egress, then we should incorporate those here. + } + service_account = var.service_account + timeout = "${var.request_timeout_seconds}s" + + // A main container has ports. It needs to go first to avoid a bug in the + // Cloud Run terraform provider where omitting the port{} block does not + // remove the port from the service. + containers { + image = local.main_container.image + args = local.main_container.args + + dynamic "ports" { + for_each = local.main_container.ports + content { + name = ports.value.name + container_port = ports.value.container_port + } + } + + dynamic "resources" { + for_each = local.main_container.resources != null ? { "" : local.main_container.resources } : {} + content { + limits = resources.value.limits + cpu_idle = resources.value.cpu_idle + startup_cpu_boost = resources.value.startup_cpu_boost + } + } + + dynamic "env" { + for_each = local.main_container.env + content { + name = env.value.name + value = env.value.value + dynamic "value_source" { + for_each = env.value.value_source != null ? { "" : env.value.value_source } : {} + content { + secret_key_ref { + secret = value_source.value.secret_key_ref.secret + version = value_source.value.secret_key_ref.version + } + } + } + } + } + + // Iterate over regional environment variables and look up the + // appropriate value to pass to each region. + dynamic "env" { + for_each = local.main_container.regional-env + content { + name = env.value.name + value = env.value.value[each.key] + } + } + + dynamic "volume_mounts" { + for_each = local.main_container.volume_mounts + content { + name = volume_mounts.value.name + mount_path = volume_mounts.value.mount_path + } + } + + } + + // Now the sidecar containers can be added. + dynamic "containers" { + for_each = local.sidecars + content { + image = containers.value.image + args = containers.value.args + + dynamic "resources" { + for_each = containers.value.resources != null ? { "" : containers.value.resources } : {} + content { + limits = resources.value.limits + cpu_idle = resources.value.cpu_idle + startup_cpu_boost = resources.value.startup_cpu_boost + } + } + + dynamic "env" { + for_each = containers.value.env + content { + name = env.value.name + value = env.value.value + dynamic "value_source" { + for_each = env.value.value_source != null ? { "" : env.value.value_source } : {} + content { + secret_key_ref { + secret = value_source.value.secret_key_ref.secret + version = value_source.value.secret_key_ref.version + } + } + } + } + } + + // Iterate over regional environment variables and look up the + // appropriate value to pass to each region. + dynamic "env" { + for_each = containers.value.regional-env + content { + name = env.value.name + value = env.value.value[each.key] + } + } + + dynamic "volume_mounts" { + for_each = containers.value.volume_mounts + content { + name = volume_mounts.value.name + mount_path = volume_mounts.value.mount_path + } + } + } + } + containers { + image = var.otel_collector_image + // config via env is an option; https://pkg.go.dev/go.opentelemetry.io/collector/service#section-readme + args = ["--config=env:OTEL_CONFIG"] + env { + name = "OTEL_CONFIG" + value = file("${path.module}/otel-config/config.yaml") + } + } + + dynamic "volumes" { + for_each = var.volumes + content { + name = volumes.value.name + + dynamic "secret" { + for_each = volumes.value.secret != null ? { "" : volumes.value.secret } : {} + content { + secret = secret.value.secret + dynamic "items" { + for_each = secret.value.items + content { + version = items.value.version + path = items.value.path + } + } + } + } + + dynamic "empty_dir" { + for_each = volumes.value.empty_dir != null ? { "" : volumes.value.empty_dir } : {} + content { + medium = empty_dir.value.medium + size_limit = empty_dir.value.size_limit + } + } + } + } + + // Regional volumes + dynamic "volumes" { + for_each = var.regional-volumes + content { + name = volumes.value.name + + dynamic "gcs" { + for_each = length(volumes.value.gcs) > 0 ? { "" : volumes.value.gcs[each.key] } : {} + content { + bucket = gcs.value.bucket + read_only = gcs.value.read_only + } + } + dynamic "nfs" { + for_each = length(volumes.value.nfs) > 0 ? { "" : volumes.value.nfs[each.key] } : {} + content { + server = nfs.value.server + path = nfs.value.path + read_only = nfs.value.read_only + } + } + } + } + } +} + +// Get a project number for this project ID. +data "google_project" "project" { project_id = var.project_id } + +// What identity is deploying this? +data "google_client_openid_userinfo" "me" {} + +// Create an alert policy to notify if the service is accessed by an unauthorized entity. +resource "google_monitoring_alert_policy" "anomalous-service-access" { + # In the absence of data, incident will auto-close after an hour + alert_strategy { + auto_close = "3600s" + + notification_rate_limit { + period = "3600s" // re-alert hourly if condition still valid. + } + } + + display_name = "Abnormal Service Access: ${var.name}" + combiner = "OR" + + conditions { + display_name = "Abnormal Service Access: ${var.name}" + + condition_matched_log { + filter = < v.name + } +} diff --git a/modules/regional-service/variables.tf b/modules/regional-service/variables.tf new file mode 100644 index 00000000..87498c7c --- /dev/null +++ b/modules/regional-service/variables.tf @@ -0,0 +1,170 @@ +variable "project_id" { + type = string +} + +variable "name" { + type = string +} + +variable "regions" { + description = "A map from region names to a network and subnetwork. A service will be created in each region configured to egress the specified traffic via the specified subnetwork." + type = map(object({ + network = string + subnet = string + })) +} + +variable "ingress" { + type = string + description = <