diff --git a/README.md b/README.md index 44aeac172..d29981edf 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ module "cloud_run" { | force\_override | Option to force override existing mapping | `bool` | `false` | no | | generate\_revision\_name | Option to enable revision name generation | `bool` | `true` | no | | image | GCR hosted image URL to deploy | `string` | n/a | yes | +| ingress | Restricts network access to your Cloud Run service | `string` | `"INGRESS_TRAFFIC_ALL"` | no | | limits | Resource limits to the container | `map(string)` | `null` | no | | liveness\_probe | Periodic probe of container liveness. Container will be restarted if the probe fails.
More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes |
object({
failure_threshold = optional(number, null)
initial_delay_seconds = optional(number, null)
timeout_seconds = optional(number, null)
period_seconds = optional(number, null)
http_get = optional(object({
path = optional(string)
http_headers = optional(list(object({
name = string
value = string
})), null)
}), null)
grpc = optional(object({
port = optional(number)
service = optional(string)
}), null)
})
| `null` | no | | location | Cloud Run service deployment location | `string` | n/a | yes | @@ -82,6 +83,8 @@ module "cloud_run" { | verified\_domain\_name | List of Custom Domain Name | `list(string)` | `[]` | no | | volume\_mounts | [Beta] Volume Mounts to be attached to the container (when using secret) |
list(object({
mount_path = string
name = string
}))
| `[]` | no | | volumes | [Beta] Volumes needed for environment variables (when using secret) |
list(object({
name = string
secret = set(object({
secret_name = string
items = map(string)
}))
}))
| `[]` | no | +| vpc\_connector | The full resource name of the VPC Access connector to use. Leave null to use Direct VPC Egress. | `string` | `null` | no | +| vpc\_egress | The outbound traffic setting for VPC. Use 'PRIVATE\_RANGES\_ONLY' with a connector. Use 'ALL\_TRAFFIC' for Direct VPC Egress. Set to null to disable all VPC egress. | `string` | `"ALL_TRAFFIC"` | no | ## Outputs diff --git a/examples/v2_multi_regions/README.md b/examples/v2_multi_regions/README.md index bc14ca762..e0e089adb 100644 --- a/examples/v2_multi_regions/README.md +++ b/examples/v2_multi_regions/README.md @@ -1,13 +1,18 @@ # Cloud Run Service using v2 API and multi-regions Example -This example showcases the basic deployment of containerized applications on Cloud Run and IAM policy for the service in multi-regions. +This example showcases the basic deployment of containerized applications on Cloud Run and IAM policy for the service in multi-regions. It allows for the optional provisioning of a Global Load Balancer for traffic distribution and SSL termination depending on the configuration. The resources/services/activations/deletions that this example will create/trigger are: * Deploys a Cloud Run V2 service across multiple regions. * Creates a Service Account to be used by Cloud Run Service. -* Creates a Serverless VPC Access Connectors per region. -* Cloud Run -> VPC integration through regional connectors. +* Creates Serverless VPC Access Connectors per region (or configures Direct VPC Egress). +* **If `enable_load_balancer` is set to `true`:** + * Reserves a Global Static IP Address (if not provided). + * Creates Serverless Network Endpoint Groups (NEGs) per region to bridge the Load Balancer and Cloud Run. + * Provisions a Global External Application Load Balancer (HTTP/S) with URL Maps and Backend Services. + * Sets up Google-managed SSL Certificates for secure HTTPS access. + * Configures outlier detection to handle failover between regions automatically. ## Assumptions and Prerequisites @@ -15,26 +20,99 @@ This example assumes that below mentioned prerequisites are in place before cons * All required APIs are enabled in the GCP Project +## Usage + +- Rename the `tfvars` file by running `mv terraform.tfvars.example terraform.tfvars` and update `terraform.tfvars` with values from your environment. + + ```bash + mv terraform.tfvars.example terraform.tfvars + ``` + +- Run `terraform init` to get the plugins. + + ```bash + terraform init + ``` + +- Run `terraform plan` and review the plan. + + ```bash + terraform plan + ``` + +- Run `terraform apply` to apply the infrastructure build. + + ```bash + terraform apply + ``` + + +### Clean up + +- Run `terraform destroy` to clean up your environment. +The input `delete_contents_on_destroy` must have been set to `true` in the original `apply` for the `terraform destroy` command to work. + + ```bash + terraform destroy + ``` + +> **DISCLAIMER**: Please pay attention to the following important details regarding the Cloud Run **Service Health** feature used in this project: + +* **Pre-GA Feature:** This feature is subject to the "Pre-GA Offerings Terms" in the General Service Terms section of the Service Specific Terms. Pre-GA features are available "as is" and might have limited support. +* **Load Balancer Behavior:** Revisions without configured probes are treated as **unknown**. Please note that the Load Balancer considers instances with "unknown" status as **healthy** and will route traffic to them. +* **Manual Configuration Required:** Currently, the `google_cloud_run_v2_service` Terraform resource does not natively support `readiness_probe` configuration. Due to this limitation, you must manually execute the following `gcloud` command to correctly enable this check after deployment: + +```bash +gcloud beta run deploy SERVICE \ + --image=IMAGE_URL \ + --readiness-probe httpGet.path=PATH,httpGet.port=CONTAINER_PORT,successThreshold=SUCCESS_THRESHOLD,failureThreshold=FAILURE_THRESHOLD,timeoutSeconds=TIMEOUT,periodSeconds=PERIOD +``` +For more information, please visit: [Cloud Run Service Health Documentation](https://docs.cloud.google.com/run/docs/configuring/healthchecks#readiness-probes) + ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | cloud\_run\_deletion\_protection | Prevents Terraform from destroying/recreating Cloud Run jobs/services. | `bool` | `true` | no | +| cloud\_run\_vpc\_egress\_mode | Defines how Cloud Run connects to the VPC for outbound traffic. Modes can be default, direct-vpc-egress or vpc-access-connector. | `string` | `"default"` | no | +| enable\_load\_balancer | If true, creates the Global Load Balancer resources. Defaults to false. | `bool` | `false` | no | +| image | Name of the image used by cloud run. | `string` | `"us-docker.pkg.dev/cloudrun/container/hello:latest"` | no | +| lb\_domain | Optional: Use an existing domain. Leave empty to use .sslip.io. | `string` | `null` | no | +| lb\_ip\_address | Optional: Use an existing Global IP for Load Balancer. Leave empty to create a new one. | `string` | `null` | no | +| location | Settings for creating a Multi-Region Service. Make sure to use region = 'global' if deploying a multi-region cloud run. | `string` | `"global"` | no | +| primary\_region | Primary region for reference. | `string` | `"us-west1"` | no | | project\_id | Project where the Cloud Run v2 will be deployed. | `string` | n/a | yes | -| regions | Regions where serverless vpc access connectors will be created. | `list(string)` |
[
"us-west1",
"europe-west1"
]
| no | -| vpc\_connectors | Configuration for Serverless VPC Access connectors by regions. |
map(object({
name = string
region = string
subnet_name = string
}))
| n/a | yes | +| regions | Regions where serverless VPC Access connectors will be created. | `list(string)` |
[
"us-west1",
"europe-west1"
]
| no | +| service\_name | Cloud Run service name. | `string` | n/a | yes | +| vpc\_connectors | Configuration for Serverless VPC Access connectors by region. |
map(object({
name = string
region = string
subnet_name = string
}))
| `{}` | no | +| vpc\_egress\_traffic | Defines which outbound traffic from Cloud Run is routed through the VPC (private IP ranges only or all traffic) when VPC egress is enabled. | `string` | `"private-ranges-only"` | no | +| vpc\_network | Configuration of VPC Network by region (only for direct-vpc-egress). | `string` | `null` | no | +| vpc\_subnets | Configuration of VPC subnets by region (only for direct-vpc-egress). | `map(string)` | `{}` | no | ## Outputs | Name | Description | |------|-------------| +| backend\_service\_global | Global backend service backing the Cloud Run multi-region service. | +| cloud\_run\_regions | Regions where the Cloud Run service is deployed. | | cloud\_run\_service\_account | Service account used by Cloud Run instances. | -| service\_id | Service IDs per region | -| service\_name | Service names per region | -| service\_uri | Service URIs per region | -| vpc\_connectors\_ids | Serverless VPC Access Connectors IDs by region. | -| vpc\_connectors\_names | VPC Access Connectors. | +| cloud\_run\_service\_id | Cloud Run service ID. | +| cloud\_run\_vpc\_egress\_mode | Cloud Run VPC egress mode in use. | +| cloud\_run\_vpc\_egress\_setting | Cloud Run VPC egress setting. | +| global\_entrypoint | Global HTTPS entrypoint for the Cloud Run multi-region service. | +| global\_forwarding\_rule\_id | ID of the global HTTPS forwarding rule. | +| https\_proxy\_id | ID of the HTTPS target proxy. | +| lb\_domain | Domain for the Load Balancer (IP.sslip.io if no custom domain is provided). | +| lb\_https\_url | HTTPS URL for the global Load Balancer. | +| lb\_ip | Global IP address of the Load Balancer. | +| serverless\_neg\_self\_links | Serverless NEG self links by region. | +| serverless\_negs | Serverless NEGs by region. | +| ssl\_certificate\_domains | Domains covered by the managed SSL certificate. | +| ssl\_certificate\_id | Managed SSL certificate ID. | +| url\_map\_id | ID of the URL map. | +| vpc\_connectors\_ids | VPC Access Connectors IDs by region. | +| vpc\_connectors\_names | VPC Access Connectors names by region. | @@ -54,6 +132,8 @@ These sections describe requirements for using this example. A service account can be used with required roles to execute this example: * Cloud Run Admin: `roles/run.admin` +* Compute Load Balancer Admin: `roles/compute.loadBalancerAdmin` (required for LB resources) +* Compute Network Admin: `roles/compute.networkAdmin` (required for NEGs and IP reservation) Know more about [Cloud Run Deployment Permissions](https://cloud.google.com/run/docs/reference/iam/roles#additional-configuration). @@ -64,6 +144,8 @@ The [Project Factory module](https://registry.terraform.io/modules/terraform-goo A project with the following APIs enabled must be used to host the main resource of this example: +* Google Artifact Registry `artifactregistry.googleapis.com` +* Google Cloud Build: `cloudbuild.googleapis.com` * Google Cloud Run: `run.googleapis.com` * Google Compute Engine: `compute.googleapis.com` -* Google Servless VPC Acces: `vpcaccess.googleapis.com` +* Google Network Services: `networkservices.googleapis.com` diff --git a/examples/v2_multi_regions/custom_image/Dockerfile b/examples/v2_multi_regions/custom_image/Dockerfile new file mode 100644 index 000000000..0445f538c --- /dev/null +++ b/examples/v2_multi_regions/custom_image/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.23-alpine as builder + +WORKDIR /app + +COPY main.go . + +RUN CGO_ENABLED=0 go build -o server main.go + +FROM alpine:3 + +WORKDIR /app + +COPY --from=builder /app/server . + +ENV PORT 8080 + +CMD ["./server"] diff --git a/examples/v2_multi_regions/custom_image/main.go b/examples/v2_multi_regions/custom_image/main.go new file mode 100644 index 000000000..e47a14116 --- /dev/null +++ b/examples/v2_multi_regions/custom_image/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/healthcheck" { + healthHandler(w, r) + return + } + + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, ` +
+

Hello Cloud Run!

+

Path: %s

+
+ `, r.URL.Path) + }) + + http.HandleFunc("/healthcheck", healthHandler) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Printf("Listening on port %s", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "OK") +} diff --git a/examples/v2_multi_regions/main.tf b/examples/v2_multi_regions/main.tf index 217f526a8..749a6eeb3 100644 --- a/examples/v2_multi_regions/main.tf +++ b/examples/v2_multi_regions/main.tf @@ -15,25 +15,23 @@ */ locals { - vpc_connectors_ids = { - for region, conn in google_vpc_access_connector.vpc_connectors : - region => conn.id - } - - vpc_config_valid = ( - (var.vpc_mode == "vpc-access-connector" && length(var.vpc_connectors) > 0) || - (var.vpc_mode == "direct-vpc-egress" && var.vpc_network != null && length(var.vpc_subnets) > 0) + vpc_flags = ( + var.cloud_run_vpc_egress_mode == "vpc-access-connector" ? + "--vpc-connector=${var.vpc_connectors[var.primary_region].name} --vpc-egress=${var.vpc_egress_traffic}" : + var.cloud_run_vpc_egress_mode == "direct-vpc-egress" ? + "--network=${var.vpc_network} --subnet=${var.vpc_subnets[var.primary_region]} --vpc-egress=${var.vpc_egress_traffic}" : + "" ) } resource "google_service_account" "sa" { project = var.project_id account_id = "ci-cloud-run-v2-sa" - display_name = "Service account for ci-cloud-run-v2-sa" + display_name = "Service account for Cloud Run multi-region" } resource "google_vpc_access_connector" "vpc_connectors" { - for_each = var.vpc_mode == "vpc-access-connector" ? var.vpc_connectors : {} + for_each = var.cloud_run_vpc_egress_mode == "vpc-access-connector" ? var.vpc_connectors : {} name = each.value.name project = var.project_id @@ -47,51 +45,78 @@ resource "google_vpc_access_connector" "vpc_connectors" { max_instances = 4 } +resource "null_resource" "validate_primary_region" { + count = contains(var.regions, var.primary_region) ? 0 : 1 + + provisioner "local-exec" { + command = "echo \"ERROR: primary_region (${var.primary_region}) must be one of: ${join(", ", var.regions)}\" && exit 1" + } +} + resource "null_resource" "validate_vpc" { - count = local.vpc_config_valid ? 0 : 1 + count = ( + var.cloud_run_vpc_egress_mode != "default" && ( + (var.cloud_run_vpc_egress_mode == "vpc-access-connector" && length(var.vpc_connectors) == 0) || + (var.cloud_run_vpc_egress_mode == "direct-vpc-egress" && + (var.vpc_network == null || length(var.vpc_subnets) == 0) + ) + ) + ) ? 1 : 0 provisioner "local-exec" { - command = "echo 'ERROR: VPC configuration invalid. Check your vpc_mode, vpc_connectors, vpc_network and vpc_subnets'' && exit 1" + command = "echo 'ERROR: Invalid VPC configuration for selected cloud_run_vpc_egress_mode' && exit 1" } } module "cloud_run_v2_multiregion" { source = "../../modules/v2" - for_each = toset(var.regions) - - service_name = "cloudrun-multiregion-${each.key}" - project_id = var.project_id - location = each.key - - create_service_account = false - service_account = google_service_account.sa.email - + service_name = var.service_name + location = var.location + project_id = var.project_id + create_service_account = false + service_account = google_service_account.sa.email cloud_run_deletion_protection = var.cloud_run_deletion_protection - vpc_access = ( - var.vpc_mode == "vpc-access-connector" - ? - { - connector = local.vpc_connectors_ids[each.key] - egress = "PRIVATE_RANGES_ONLY" - network_interfaces = null - } - : - { - connector = null - egress = var.vpc_egress - network_interfaces = { - network = var.vpc_network - subnetwork = var.vpc_subnets[each.key] - } - } - ) + multi_region_settings = { + regions = var.regions + } + + load_balancer_config = var.enable_load_balancer ? { + regions = var.regions + global_ip_address = var.lb_ip_address + domain = var.lb_domain + name_prefix = var.service_name + } : null containers = [ { - container_image = "us-docker.pkg.dev/cloudrun/container/hello" + container_image = var.image container_name = "hello-world" + + startup_probe = { + initial_delay_seconds = 10 + timeout_seconds = 3 + period_seconds = 3 + failure_threshold = 5 + + http_get = { + path = "/" + port = 8080 + } + } + + liveness_probe = { + initial_delay_seconds = 10 + timeout_seconds = 3 + period_seconds = 3 + failure_threshold = 5 + + http_get = { + path = "/" + port = 8080 + } + } } ] } diff --git a/examples/v2_multi_regions/outputs.tf b/examples/v2_multi_regions/outputs.tf index d0a0512f4..ff543edff 100644 --- a/examples/v2_multi_regions/outputs.tf +++ b/examples/v2_multi_regions/outputs.tf @@ -19,28 +19,54 @@ output "cloud_run_service_account" { value = google_service_account.sa.email } -output "service_name" { - description = "Service names per region." - value = { - for region, mod in module.cloud_run_v2_multiregion : - region => mod.service_name - } +output "cloud_run_service_id" { + description = "Cloud Run service ID." + value = module.cloud_run_v2_multiregion.service_id } -output "service_uri" { - description = "Service URIs per region." - value = { - for region, mod in module.cloud_run_v2_multiregion : - region => mod.service_uri - } +output "global_forwarding_rule_id" { + description = "ID of the global HTTPS forwarding rule." + value = module.cloud_run_v2_multiregion.global_forwarding_rule_id } -output "service_id" { - description = "Service IDs per region." - value = { - for region, mod in module.cloud_run_v2_multiregion : - region => mod.service_id - } +output "https_proxy_id" { + description = "ID of the HTTPS target proxy." + value = module.cloud_run_v2_multiregion.https_proxy_id +} + +output "url_map_id" { + description = "ID of the URL map." + value = module.cloud_run_v2_multiregion.url_map_id +} + +output "serverless_neg_self_links" { + description = "Serverless NEG self links by region." + value = module.cloud_run_v2_multiregion.serverless_negs +} + +output "ssl_certificate_domains" { + description = "Domains covered by the managed SSL certificate." + value = module.cloud_run_v2_multiregion.ssl_certificate_domains +} + +output "cloud_run_vpc_egress_mode" { + description = "Cloud Run VPC egress mode in use." + value = var.cloud_run_vpc_egress_mode +} + +output "cloud_run_vpc_egress_setting" { + description = "Cloud Run VPC egress setting." + value = var.vpc_egress_traffic +} + +output "cloud_run_regions" { + description = "Regions where the Cloud Run service is deployed." + value = var.regions +} + +output "serverless_negs" { + description = "Serverless NEGs by region." + value = module.cloud_run_v2_multiregion.serverless_negs } output "vpc_connectors_ids" { @@ -49,14 +75,42 @@ output "vpc_connectors_ids" { for region, connector in google_vpc_access_connector.vpc_connectors : region => connector.id } - sensitive = false } output "vpc_connectors_names" { - description = "VPC Access Connectors name by region." + description = "VPC Access Connectors names by region." value = { for region, connector in google_vpc_access_connector.vpc_connectors : region => connector.name } - sensitive = false +} + +output "backend_service_global" { + description = "Global backend service backing the Cloud Run multi-region service." + value = module.cloud_run_v2_multiregion.backend_service_global_id +} + +output "lb_ip" { + description = "Global IP address of the Load Balancer." + value = module.cloud_run_v2_multiregion.lb_ip +} + +output "lb_domain" { + description = "Domain for the Load Balancer (IP.sslip.io if no custom domain is provided)." + value = module.cloud_run_v2_multiregion.lb_domain +} + +output "lb_https_url" { + description = "HTTPS URL for the global Load Balancer." + value = module.cloud_run_v2_multiregion.lb_https_url +} + +output "ssl_certificate_id" { + description = "Managed SSL certificate ID." + value = module.cloud_run_v2_multiregion.ssl_certificate_id +} + +output "global_entrypoint" { + description = "Global HTTPS entrypoint for the Cloud Run multi-region service." + value = module.cloud_run_v2_multiregion.lb_https_url } diff --git a/examples/v2_multi_regions/terraform.tfvars.example b/examples/v2_multi_regions/terraform.tfvars.example new file mode 100644 index 000000000..dac1ffbbe --- /dev/null +++ b/examples/v2_multi_regions/terraform.tfvars.example @@ -0,0 +1,46 @@ +project_id = "YOUR-PROJECT-ID" +regions = ["us-west1", "europe-west1"] +primary_region = "us-west1" +service_name = "cloudrun-multiregion" + +# Choose between direct-vpc-egress or vpc-access-connector +cloud_run_vpc_egress_mode = "direct-vpc-egress" +vpc_egress_traffic = "all-traffic" + +# Only for Direct VPC Egress +#vpc_network = "projects/YOUR-PROJECT/global/networks/YOUR-VPCE" + +# vpc_subnets = { +# "us-west1" = "projects/YOUR-PROJECT/regions/us-west1/subnetworks/sb-restricted-us-west1" +# "europe-west1" = "projects/YOUR-PROJECT/regions/europe-west1/subnetworks/sb-restricted-europe-west1" +# } + +# Set to true to create the load balancer infrastructure +# enable_load_balancer = true +lb_domain = null # Opcional - Your LB domain +lb_ip_address = null # Opcional - Your LB Global IP + +# Un-comment if you want to use the default vpc network +# cloud_run_vpc_egress_mode = "default" +# vpc_egress = null +# vpc_network = null +# vpc_subnets = {} +# vpc_connectors = {} +# lb_ip_address = null +# lb_domain = null + +# Un-comment if you want to use the vpc-access-connector +# vpc_connectors = { +# "us-west1" = { +# name = "con-cloud-run-us" +# region = "us-west1" +# subnet_name = "cloudrun-us-west1" +# } +# "europe-west1" = { +# name = "con-cloud-run-eu" +# region = "europe-west1" +# subnet_name = "cloudrun-europe-west1" +# } +# } + +# cloud_run_deletion_protection = false diff --git a/examples/v2_multi_regions/variables.tf b/examples/v2_multi_regions/variables.tf index 120cf5906..884ff4963 100644 --- a/examples/v2_multi_regions/variables.tf +++ b/examples/v2_multi_regions/variables.tf @@ -21,35 +21,55 @@ variable "project_id" { variable "regions" { type = list(string) - description = "Regions where serverless vpc access connectors will be created." + description = "Regions where serverless VPC Access connectors will be created." default = ["us-west1", "europe-west1"] } +variable "location" { + type = string + description = " Settings for creating a Multi-Region Service. Make sure to use region = 'global' if deploying a multi-region cloud run." + default = "global" +} + +variable "service_name" { + type = string + description = "Cloud Run service name." +} + +variable "image" { + type = string + description = "Name of the image used by cloud run." + default = "us-docker.pkg.dev/cloudrun/container/hello:latest" +} + variable "cloud_run_deletion_protection" { type = bool description = "Prevents Terraform from destroying/recreating Cloud Run jobs/services." default = true } -variable "vpc_mode" { +variable "cloud_run_vpc_egress_mode" { type = string - description = "VPC Mode: direct-vpc-egress (default) or vpc-access-connector." - default = "direct-vpc-egress" + description = "Defines how Cloud Run connects to the VPC for outbound traffic. Modes can be default, direct-vpc-egress or vpc-access-connector." + default = "default" validation { - condition = contains(["direct-vpc-egress", "vpc-access-connector"], var.vpc_mode) - error_message = "vpc_mode must be 'direct-vpc-egress' or 'vpc-access-connector'." + condition = contains( + ["default", "direct-vpc-egress", "vpc-access-connector"], + var.cloud_run_vpc_egress_mode + ) + error_message = "cloud_run_vpc_egress_mode must be 'default', 'direct-vpc-egress' or 'vpc-access-connector'." } } variable "vpc_connectors" { - description = "Configuration for Serverless VPC Access connectors by regions." type = map(object({ name = string region = string subnet_name = string })) - default = {} + description = "Configuration for Serverless VPC Access connectors by region." + default = {} } variable "vpc_network" { @@ -64,11 +84,36 @@ variable "vpc_subnets" { default = {} } -variable "vpc_egress" { +variable "vpc_egress_traffic" { type = string - default = "PRIVATE_RANGES_ONLY" + description = "Defines which outbound traffic from Cloud Run is routed through the VPC (private IP ranges only or all traffic) when VPC egress is enabled." + default = "private-ranges-only" validation { - condition = var.vpc_egress == null || can(regex("^(PRIVATE_RANGES_ONLY|ALL_TRAFFIC)$", var.vpc_egress)) - error_message = "vpc_egress must be PRIVATE_RANGES_ONLY, ALL_TRAFFIC or null." + condition = var.vpc_egress_traffic == null || can(regex("^(private-ranges-only|all-traffic)$", var.vpc_egress_traffic)) + error_message = "vpc_egress_traffic must be private-ranges-only, all-traffic or null." } } + +variable "primary_region" { + type = string + description = "Primary region for reference." + default = "us-west1" +} + +variable "lb_ip_address" { + type = string + description = "Optional: Use an existing Global IP for Load Balancer. Leave empty to create a new one." + default = null +} + +variable "lb_domain" { + type = string + description = "Optional: Use an existing domain. Leave empty to use .sslip.io." + default = null +} + +variable "enable_load_balancer" { + type = bool + description = "If true, creates the Global Load Balancer resources. Defaults to false." + default = false +} diff --git a/metadata.yaml b/metadata.yaml index 46f92dd60..c7a6fbf72 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -64,6 +64,8 @@ spec: location: examples/simple_job_exec - name: v2 location: examples/v2 + - name: v2_multi_regions + location: examples/v2_multi_regions - name: v2_with_gmp location: examples/v2_with_gmp - name: v2_with_gpu @@ -119,6 +121,17 @@ spec: description: A set of key/value label pairs to assign to the container metadata varType: map(string) defaultValue: {} + - name: ingress + description: Restricts network access to your Cloud Run service + varType: string + defaultValue: INGRESS_TRAFFIC_ALL + - name: vpc_connector + description: The full resource name of the VPC Access connector to use. Leave null to use Direct VPC Egress. + varType: string + - name: vpc_egress + description: The outbound traffic setting for VPC. Use 'PRIVATE_RANGES_ONLY' with a connector. Use 'ALL_TRAFFIC' for Direct VPC Egress. Set to null to disable all VPC egress. + varType: string + defaultValue: ALL_TRAFFIC - name: template_annotations description: Annotations to the container metadata including VPC Connector and SQL. See [more details](https://cloud.google.com/run/docs/reference/rpc/google.cloud.run.v1#revisiontemplate) varType: map(string) @@ -323,13 +336,13 @@ spec: roles: - level: Project roles: - - roles/cloudkms.admin - - roles/resourcemanager.projectIamAdmin - - roles/run.admin - roles/iam.serviceAccountAdmin - roles/artifactregistry.admin - roles/iam.serviceAccountUser - roles/serviceusage.serviceUsageViewer + - roles/cloudkms.admin + - roles/resourcemanager.projectIamAdmin + - roles/run.admin services: - accesscontextmanager.googleapis.com - cloudbilling.googleapis.com diff --git a/modules/job-exec/metadata.yaml b/modules/job-exec/metadata.yaml index 8247c5733..6d90fcf47 100644 --- a/modules/job-exec/metadata.yaml +++ b/modules/job-exec/metadata.yaml @@ -48,6 +48,8 @@ spec: location: examples/simple_job_exec - name: v2 location: examples/v2 + - name: v2_multi_regions + location: examples/v2_multi_regions - name: v2_with_gmp location: examples/v2_with_gmp - name: v2_with_gpu @@ -219,13 +221,13 @@ spec: roles: - level: Project roles: - - roles/run.admin - - roles/iam.serviceAccountAdmin - roles/artifactregistry.admin - roles/iam.serviceAccountUser - roles/serviceusage.serviceUsageViewer - roles/cloudkms.admin - roles/resourcemanager.projectIamAdmin + - roles/run.admin + - roles/iam.serviceAccountAdmin services: - accesscontextmanager.googleapis.com - cloudbilling.googleapis.com diff --git a/modules/secure-cloud-run-core/metadata.yaml b/modules/secure-cloud-run-core/metadata.yaml index c2a6df4a7..00aa85803 100644 --- a/modules/secure-cloud-run-core/metadata.yaml +++ b/modules/secure-cloud-run-core/metadata.yaml @@ -48,6 +48,8 @@ spec: location: examples/simple_job_exec - name: v2 location: examples/v2 + - name: v2_multi_regions + location: examples/v2_multi_regions - name: v2_with_gmp location: examples/v2_with_gmp - name: v2_with_gpu @@ -306,13 +308,13 @@ spec: roles: - level: Project roles: - - roles/cloudkms.admin - - roles/resourcemanager.projectIamAdmin - roles/run.admin - roles/iam.serviceAccountAdmin - roles/artifactregistry.admin - roles/iam.serviceAccountUser - roles/serviceusage.serviceUsageViewer + - roles/cloudkms.admin + - roles/resourcemanager.projectIamAdmin services: - accesscontextmanager.googleapis.com - cloudbilling.googleapis.com diff --git a/modules/secure-cloud-run-security/metadata.yaml b/modules/secure-cloud-run-security/metadata.yaml index 0b9de8caf..3fe91395b 100644 --- a/modules/secure-cloud-run-security/metadata.yaml +++ b/modules/secure-cloud-run-security/metadata.yaml @@ -48,6 +48,8 @@ spec: location: examples/simple_job_exec - name: v2 location: examples/v2 + - name: v2_multi_regions + location: examples/v2_multi_regions - name: v2_with_gmp location: examples/v2_with_gmp - name: v2_with_gpu @@ -133,13 +135,13 @@ spec: roles: - level: Project roles: - - roles/cloudkms.admin - - roles/resourcemanager.projectIamAdmin - roles/run.admin - roles/iam.serviceAccountAdmin - roles/artifactregistry.admin - roles/iam.serviceAccountUser - roles/serviceusage.serviceUsageViewer + - roles/cloudkms.admin + - roles/resourcemanager.projectIamAdmin services: - accesscontextmanager.googleapis.com - cloudbilling.googleapis.com diff --git a/modules/secure-cloud-run/metadata.yaml b/modules/secure-cloud-run/metadata.yaml index 284d4d749..55e1f5b94 100644 --- a/modules/secure-cloud-run/metadata.yaml +++ b/modules/secure-cloud-run/metadata.yaml @@ -48,6 +48,8 @@ spec: location: examples/simple_job_exec - name: v2 location: examples/v2 + - name: v2_multi_regions + location: examples/v2_multi_regions - name: v2_with_gmp location: examples/v2_with_gmp - name: v2_with_gpu @@ -250,13 +252,13 @@ spec: roles: - level: Project roles: + - roles/iam.serviceAccountUser - roles/serviceusage.serviceUsageViewer - roles/cloudkms.admin - roles/resourcemanager.projectIamAdmin - roles/run.admin - roles/iam.serviceAccountAdmin - roles/artifactregistry.admin - - roles/iam.serviceAccountUser services: - accesscontextmanager.googleapis.com - cloudbilling.googleapis.com diff --git a/modules/secure-serverless-harness/metadata.yaml b/modules/secure-serverless-harness/metadata.yaml index 466cd1126..ce9fc6009 100644 --- a/modules/secure-serverless-harness/metadata.yaml +++ b/modules/secure-serverless-harness/metadata.yaml @@ -48,6 +48,8 @@ spec: location: examples/simple_job_exec - name: v2 location: examples/v2 + - name: v2_multi_regions + location: examples/v2_multi_regions - name: v2_with_gmp location: examples/v2_with_gmp - name: v2_with_gpu diff --git a/modules/secure-serverless-net/metadata.yaml b/modules/secure-serverless-net/metadata.yaml index f7c089b4f..ffc463ea7 100644 --- a/modules/secure-serverless-net/metadata.yaml +++ b/modules/secure-serverless-net/metadata.yaml @@ -48,6 +48,8 @@ spec: location: examples/simple_job_exec - name: v2 location: examples/v2 + - name: v2_multi_regions + location: examples/v2_multi_regions - name: v2_with_gmp location: examples/v2_with_gmp - name: v2_with_gpu diff --git a/modules/v2/README.md b/modules/v2/README.md index 017b81765..605514c4b 100644 --- a/modules/v2/README.md +++ b/modules/v2/README.md @@ -57,6 +57,7 @@ Functional examples are included in the | create\_service\_account | Create a new service account for cloud run service | `bool` | `true` | no | | custom\_audiences | One or more custom audiences that you want this service to support. Specify each custom audience as the full URL in a string. [Refer](https://cloud.google.com/run/docs/configuring/custom-audiences) | `list(string)` | `null` | no | | description | Cloud Run service description. This field currently has a 512-character limit. | `string` | `null` | no | +| enable\_load\_balancer | If true, creates the Global Load Balancer resources. Defaults to false. | `bool` | `false` | no | | enable\_prometheus\_sidecar | Enable Prometheus sidecar in Cloud Run instance. | `bool` | `false` | no | | encryption\_key | A reference to a customer managed encryption key (CMEK) to use to encrypt this container image. This is optional. | `string` | `null` | no | | execution\_environment | The sandbox environment to host this Revision. | `string` | `"EXECUTION_ENVIRONMENT_GEN2"` | no | @@ -64,9 +65,13 @@ Functional examples are included in the | iap\_members | Valid only when launch stage is set to 'BETA'. IAP is enabled automatically when users or service accounts (SAs) are provided. Use allUsers for public access, allAuthenticatedUsers for any Google-authenticated user, or specify individual users/SAs. [More info](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iap_web_cloud_run_service_iam#member/members-2) | `list(string)` | `[]` | no | | ingress | Restricts network access to your Cloud Run service | `string` | `"INGRESS_TRAFFIC_ALL"` | no | | launch\_stage | The launch stage as defined by Google Cloud Platform Launch Stages. Cloud Run supports ALPHA, BETA, and GA. If no value is specified, GA is assumed. | `string` | `"GA"` | no | +| lb\_domain | Optional: Use an existing domain. Leave empty to use .sslip.io. (Only used if enable\_load\_balancer is true) | `string` | `null` | no | +| lb\_ip\_address | Optional: Use an existing Global IP for Load Balancer. Leave empty to create a new one. (Only used if enable\_load\_balancer is true) | `string` | `null` | no | +| load\_balancer\_config | Configuration for the Load Balancer. If null, no LB resources are created. |
object({
regions = list(string)
global_ip_address = optional(string, null)
domain = optional(string, null)
name_prefix = optional(string, "cloudrun")
})
| `null` | no | | location | Cloud Run service deployment location | `string` | n/a | yes | | max\_instance\_request\_concurrency | Sets the maximum number of requests that each serving instance can receive. This is optional. | `string` | `null` | no | | members | Users/SAs to be given invoker access to the service. Grant invoker access by specifying the users or service accounts (SAs). Use allUsers for public access, allAuthenticatedUsers for access by logged-in Google users, or provide a list of specific users/SAs. [See the complete list of available options here](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_service_iam#member/members-1) | `list(string)` | `[]` | no | +| multi\_region\_settings | Settings for creating a Multi-Region Service. |
object({
regions = list(string)
})
| `null` | no | | node\_selector | Node Selector describes the hardware requirements of the GPU resource. [More info](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_v2_service#nested_template_node_selector). |
object({
accelerator = string
})
| `null` | no | | project\_id | The project ID to deploy to | `string` | n/a | yes | | revision | The unique name for the revision. If this field is omitted, it will be automatically generated based on the Service name | `string` | `null` | no | @@ -90,18 +95,28 @@ Functional examples are included in the | Name | Description | |------|-------------| | apphub\_service\_uri | Service URI in CAIS style to be used by Apphub. | +| backend\_service\_global\_id | n/a | | creator | Email address of the authenticated creator. | | effective\_annotations | All of annotations (key/value pairs) present on the resource in GCP, including the annotations configured through Terraform, other clients and services. | +| global\_forwarding\_rule\_id | n/a | +| https\_proxy\_id | n/a | | last\_modifier | Email address of the last authenticated modifier. | | latest\_created\_revision | Name of the last created revision. See comments in reconciling for additional information on reconciliation process in Cloud Run. | | latest\_ready\_revision | Name of the latest revision that is serving traffic. See comments in reconciling for additional information on reconciliation process in Cloud Run. | +| lb\_domain | n/a | +| lb\_https\_url | n/a | +| lb\_ip | n/a | | location | Location in which the Cloud Run service was created | | observed\_generation | The generation of this Service currently serving traffic. | | project\_id | Google Cloud project in which the service was created | +| serverless\_negs | n/a | | service\_account\_id | Service account id and email | | service\_id | Unique Identifier for the created service with format projects/{{project}}/locations/{{location}}/services/{{name}} | | service\_name | Name of the created service | | service\_uri | The main URI in which this Service is serving traffic. | +| ssl\_certificate\_domains | n/a | +| ssl\_certificate\_id | n/a | | traffic\_statuses | Detailed status information for corresponding traffic targets. | +| url\_map\_id | n/a | diff --git a/modules/v2/main.tf b/modules/v2/main.tf index e94534e66..89b6b1e3d 100644 --- a/modules/v2/main.tf +++ b/modules/v2/main.tf @@ -76,6 +76,146 @@ locals { startup_probe = [] liveness_probe = [] }] + + create_lb = var.load_balancer_config != null + + lb_prefix = try(var.load_balancer_config.name_prefix, "cloudrun") + + create_ip = local.create_lb && try(var.load_balancer_config.global_ip_address, null) == null + lb_ip_address = local.create_lb ? ( + local.create_ip ? google_compute_global_address.lb_ip[0].address : var.load_balancer_config.global_ip_address + ) : null + + lb_domain = local.create_lb ? ( + try(var.load_balancer_config.domain, null) != null + ? var.load_balancer_config.domain + : "${local.lb_ip_address}.sslip.io" + ) : null + + neg_regions = local.create_lb ? toset(var.load_balancer_config.regions) : toset([]) +} + +resource "google_compute_global_address" "lb_ip" { + count = local.create_ip ? 1 : 0 + name = "${local.lb_prefix}-global-ip" + project = var.project_id +} + +resource "google_compute_region_network_endpoint_group" "cloudrun_neg" { + for_each = local.neg_regions + + project = var.project_id + region = each.key + name = "${local.lb_prefix}-neg-${each.key}" + network_endpoint_type = "SERVERLESS" + + cloud_run { + service = google_cloud_run_v2_service.main.name + } +} + +resource "google_compute_backend_service" "cloudrun_backend_global" { + count = local.create_lb ? 1 : 0 + project = var.project_id + name = "${local.lb_prefix}-backend-global" + + protocol = "HTTP" + load_balancing_scheme = "EXTERNAL_MANAGED" + enable_cdn = false + + outlier_detection { + consecutive_errors = 3 + consecutive_gateway_failure = 5 + enforcing_consecutive_errors = 100 + enforcing_consecutive_gateway_failure = 100 + + base_ejection_time { seconds = 30 } + interval { seconds = 10 } + } + + dynamic "backend" { + for_each = google_compute_region_network_endpoint_group.cloudrun_neg + content { + group = backend.value.id + } + } +} + +resource "google_compute_url_map" "cloudrun_urlmap" { + count = local.create_lb ? 1 : 0 + name = "${local.lb_prefix}-urlmap" + project = var.project_id + + default_service = google_compute_backend_service.cloudrun_backend_global[0].id + + host_rule { + hosts = ["*"] + path_matcher = "allpaths" + } + + path_matcher { + name = "allpaths" + default_service = google_compute_backend_service.cloudrun_backend_global[0].id + + path_rule { + paths = ["/*"] + + route_action { + weighted_backend_services { + backend_service = google_compute_backend_service.cloudrun_backend_global[0].id + weight = 100 + } + + retry_policy { + num_retries = 10 + per_try_timeout { seconds = 30 } + retry_conditions = [ + "5xx", + "gateway-error", + "connect-failure", + "retriable-4xx", + "unavailable", + "dead-deadline-exceeded", + ] + } + } + } + } +} + +resource "google_compute_managed_ssl_certificate" "cloudrun_cert" { + count = local.create_lb ? 1 : 0 + name = "${local.lb_prefix}-cert" + project = var.project_id + + managed { + domains = [local.lb_domain] + } +} + +resource "google_compute_target_https_proxy" "cloudrun_https_proxy" { + count = local.create_lb ? 1 : 0 + name = "${local.lb_prefix}-https-proxy" + project = var.project_id + ssl_certificates = [google_compute_managed_ssl_certificate.cloudrun_cert[0].id] + url_map = google_compute_url_map.cloudrun_urlmap[0].id +} + +resource "google_compute_global_forwarding_rule" "cloudrun_https_rule" { + count = local.create_lb ? 1 : 0 + name = "${local.lb_prefix}-https-rule" + project = var.project_id + + ip_address = local.lb_ip_address + ip_protocol = "TCP" + port_range = "443" + target = google_compute_target_https_proxy.cloudrun_https_proxy[0].id + load_balancing_scheme = "EXTERNAL_MANAGED" + + depends_on = [ + google_compute_target_https_proxy.cloudrun_https_proxy, + google_compute_managed_ssl_certificate.cloudrun_cert + ] } resource "google_service_account" "sa" { @@ -104,6 +244,13 @@ resource "google_cloud_run_v2_service" "main" { iap_enabled = length(var.iap_members) > 0 deletion_protection = var.cloud_run_deletion_protection + dynamic "multi_region_settings" { + for_each = var.multi_region_settings == null ? [] : [var.multi_region_settings] + content { + regions = multi_region_settings.value.regions + } + } + template { revision = var.revision labels = var.template_labels diff --git a/modules/v2/metadata.yaml b/modules/v2/metadata.yaml index 56515638e..fe492834e 100644 --- a/modules/v2/metadata.yaml +++ b/modules/v2/metadata.yaml @@ -48,6 +48,8 @@ spec: location: examples/simple_job_exec - name: v2 location: examples/v2 + - name: v2_multi_regions + location: examples/v2_multi_regions - name: v2_with_gmp location: examples/v2_with_gmp - name: v2_with_gpu @@ -60,6 +62,12 @@ spec: description: The project ID to deploy to varType: string required: true + - name: multi_region_settings + description: " Settings for creating a Multi-Region Service." + varType: |- + object({ + regions = list(string) + }) - name: location description: Cloud Run service deployment location varType: string @@ -501,6 +509,25 @@ spec: description: The sandbox environment to host this Revision. varType: string defaultValue: EXECUTION_ENVIRONMENT_GEN2 + - name: load_balancer_config + description: Configuration for the Load Balancer. If null, no LB resources are created. + varType: |- + object({ + regions = list(string) + global_ip_address = optional(string, null) + domain = optional(string, null) + name_prefix = optional(string, "cloudrun") + }) + - name: enable_load_balancer + description: If true, creates the Global Load Balancer resources. Defaults to false. + varType: bool + defaultValue: false + - name: lb_ip_address + description: "Optional: Use an existing Global IP for Load Balancer. Leave empty to create a new one. (Only used if enable_load_balancer is true)" + varType: string + - name: lb_domain + description: "Optional: Use an existing domain. Leave empty to use .sslip.io. (Only used if enable_load_balancer is true)" + varType: string outputs: - name: apphub_service_uri description: Service URI in CAIS style to be used by Apphub. @@ -509,6 +536,7 @@ spec: - location: string service_id: string service_uri: string + - name: backend_service_global_id - name: creator description: Email address of the authenticated creator. type: string @@ -517,6 +545,8 @@ spec: type: - map - string + - name: global_forwarding_rule_id + - name: https_proxy_id - name: last_modifier description: Email address of the last authenticated modifier. type: string @@ -526,6 +556,9 @@ spec: - name: latest_ready_revision description: Name of the latest revision that is serving traffic. See comments in reconciling for additional information on reconciliation process in Cloud Run. type: string + - name: lb_domain + - name: lb_https_url + - name: lb_ip - name: location description: Location in which the Cloud Run service was created type: string @@ -535,6 +568,7 @@ spec: - name: project_id description: Google Cloud project in which the service was created type: string + - name: serverless_negs - name: service_account_id description: Service account id and email type: @@ -551,6 +585,8 @@ spec: - name: service_uri description: The main URI in which this Service is serving traffic. type: string + - name: ssl_certificate_domains + - name: ssl_certificate_id - name: traffic_statuses description: Detailed status information for corresponding traffic targets. type: @@ -561,17 +597,18 @@ spec: tag: string type: string uri: string + - name: url_map_id requirements: roles: - level: Project roles: - - roles/run.admin - - roles/iam.serviceAccountAdmin - roles/iam.serviceAccountUser - roles/serviceusage.serviceUsageViewer - roles/resourcemanager.projectIamAdmin - roles/compute.viewer - roles/iap.admin + - roles/run.admin + - roles/iam.serviceAccountAdmin services: - cloudresourcemanager.googleapis.com - compute.googleapis.com @@ -583,6 +620,6 @@ spec: - storage-api.googleapis.com providerVersions: - source: hashicorp/google - version: ">= 6, < 7" + version: ">= 6, < 8" - source: hashicorp/google-beta - version: ">= 6, < 7" + version: ">= 6, < 8" diff --git a/modules/v2/outputs.tf b/modules/v2/outputs.tf index 4e76d00f9..1c948563f 100644 --- a/modules/v2/outputs.tf +++ b/modules/v2/outputs.tf @@ -87,3 +87,45 @@ output "apphub_service_uri" { } description = "Service URI in CAIS style to be used by Apphub." } + +output "https_proxy_id" { + value = try(google_compute_target_https_proxy.cloudrun_https_proxy[0].id, null) +} + +output "url_map_id" { + value = try(google_compute_url_map.cloudrun_urlmap[0].id, null) +} + +output "ssl_certificate_domains" { + value = try(google_compute_managed_ssl_certificate.cloudrun_cert[0].managed[0].domains, []) +} + +output "ssl_certificate_id" { + value = try(google_compute_managed_ssl_certificate.cloudrun_cert[0].id, null) +} + +output "serverless_negs" { + value = { + for k, v in google_compute_region_network_endpoint_group.cloudrun_neg : k => v.self_link + } +} + +output "backend_service_global_id" { + value = try(google_compute_backend_service.cloudrun_backend_global[0].id, null) +} + +output "lb_ip" { + value = local.lb_ip_address +} + +output "lb_domain" { + value = local.lb_domain +} + +output "lb_https_url" { + value = local.lb_domain != null ? "https://${local.lb_domain}" : null +} + +output "global_forwarding_rule_id" { + value = try(google_compute_global_forwarding_rule.cloudrun_https_rule[0].id, null) +} \ No newline at end of file diff --git a/modules/v2/variables.tf b/modules/v2/variables.tf index c62537e7f..684345e9c 100644 --- a/modules/v2/variables.tf +++ b/modules/v2/variables.tf @@ -20,6 +20,14 @@ variable "project_id" { type = string } +variable "multi_region_settings" { + description = " Settings for creating a Multi-Region Service." + type = object({ + regions = list(string) + }) + default = null +} + variable "location" { description = "Cloud Run service deployment location" type = string @@ -357,3 +365,31 @@ variable "execution_environment" { } } +variable "load_balancer_config" { + description = "Configuration for the Load Balancer. If null, no LB resources are created." + type = object({ + regions = list(string) + global_ip_address = optional(string, null) + domain = optional(string, null) + name_prefix = optional(string, "cloudrun") + }) + default = null +} + +variable "enable_load_balancer" { + type = bool + description = "If true, creates the Global Load Balancer resources. Defaults to false." + default = false +} + +variable "lb_ip_address" { + type = string + description = "Optional: Use an existing Global IP for Load Balancer. Leave empty to create a new one. (Only used if enable_load_balancer is true)" + default = null +} + +variable "lb_domain" { + type = string + description = "Optional: Use an existing domain. Leave empty to use .sslip.io. (Only used if enable_load_balancer is true)" + default = null +}