Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br>More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes | <pre>object({<br> failure_threshold = optional(number, null)<br> initial_delay_seconds = optional(number, null)<br> timeout_seconds = optional(number, null)<br> period_seconds = optional(number, null)<br> http_get = optional(object({<br> path = optional(string)<br> http_headers = optional(list(object({<br> name = string<br> value = string<br> })), null)<br> }), null)<br> grpc = optional(object({<br> port = optional(number)<br> service = optional(string)<br> }), null)<br> })</pre> | `null` | no |
| location | Cloud Run service deployment location | `string` | n/a | yes |
Expand All @@ -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) | <pre>list(object({<br> mount_path = string<br> name = string<br> }))</pre> | `[]` | no |
| volumes | [Beta] Volumes needed for environment variables (when using secret) | <pre>list(object({<br> name = string<br> secret = set(object({<br> secret_name = string<br> items = map(string)<br> }))<br> }))</pre> | `[]` | 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

Expand Down
104 changes: 93 additions & 11 deletions examples/v2_multi_regions/README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,118 @@
# 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

This example assumes that below mentioned prerequisites are in place before consuming the example.

* 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)

<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## 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 <IP>.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)` | <pre>[<br> "us-west1",<br> "europe-west1"<br>]</pre> | no |
| vpc\_connectors | Configuration for Serverless VPC Access connectors by regions. | <pre>map(object({<br> name = string<br> region = string<br> subnet_name = string<br> }))</pre> | n/a | yes |
| regions | Regions where serverless VPC Access connectors will be created. | `list(string)` | <pre>[<br> "us-west1",<br> "europe-west1"<br>]</pre> | no |
| service\_name | Cloud Run service name. | `string` | n/a | yes |
| vpc\_connectors | Configuration for Serverless VPC Access connectors by region. | <pre>map(object({<br> name = string<br> region = string<br> subnet_name = string<br> }))</pre> | `{}` | 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. |

<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->

Expand All @@ -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).

Expand All @@ -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`
17 changes: 17 additions & 0 deletions examples/v2_multi_regions/custom_image/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
42 changes: 42 additions & 0 deletions examples/v2_multi_regions/custom_image/main.go
Original file line number Diff line number Diff line change
@@ -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, `
<div style="text-align:center; padding-top:50px; font-family:sans-serif;">
<h1>Hello Cloud Run!</h1>
<p>Path: %s</p>
</div>
`, 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")
}
105 changes: 65 additions & 40 deletions examples/v2_multi_regions/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
}
]
}
Loading