Add net-lb-proxy-int-cross-region module and tests (#4017)

* Add net-lb-proxy-int-cross-region module and tests

* Add context support example and tests

* Update copyright to 2026 and support instance group backends

* docs: correct Instance Groups support note in README
This commit is contained in:
Simone Ruffilli
2026-06-11 09:56:01 +02:00
committed by GitHub
parent e753344bd2
commit 8adb84c95c
17 changed files with 1699 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
# Cross-region Internal Proxy Network Load Balancer Module
This module allows managing Cross-region Internal Proxy Network Load Balancers (L4 proxy ILBs). It's designed to expose the full configuration of the underlying resources, and to facilitate common usage patterns by providing sensible defaults, and optionally managing prerequisite resources like health checks, network endpoint groups (NEGs), etc.
> [!IMPORTANT]
> Cross-region internal proxy Network Load Balancers support Instance Groups (Managed or Unmanaged), zonal NEGs (`GCE_VM_IP_PORT`), and Hybrid NEGs as backends.
>
> Proxy-only subnets for cross-region load balancers must be created with the purpose set to `GLOBAL_MANAGED_PROXY` (in the VPC module, set `global = true` in the `subnets_proxy_only` configuration).
## Examples
<!-- BEGIN TOC -->
- [Examples](#examples)
- [Minimal Example](#minimal-example)
- [Health Checks](#health-checks)
- [Network Endpoint Groups (NEGs)](#network-endpoint-groups-negs)
- [Zonal NEG creation](#zonal-neg-creation)
- [Hybrid NEG creation](#hybrid-neg-creation)
- [Variables](#variables)
- [Outputs](#outputs)
<!-- END TOC -->
### Minimal Example
An internal TCP proxy cross-region load balancer with a backend service pointing to auto-created zonal NEGs. This example demonstrates the use of the [context pattern](../../adrs/20251013-context-locals.md) for symbolic variable interpolation:
```hcl
module "tcp-proxy-cross-region" {
source = "./fabric/modules/net-lb-proxy-int-cross-region"
name = "ilb-proxy-test"
project_id = "$project_ids:my-project"
port = 80
context = {
project_ids = {
my-project = var.project_id
}
networks = {
my-vpc = var.vpc.self_link
}
subnets = {
my-subnet-1 = var.subnet1.self_link
my-subnet-2 = var.subnet2.self_link
}
}
vpc_config = {
network = "$networks:my-vpc"
subnetworks = {
europe-west1 = "$subnets:my-subnet-1"
europe-west4 = "$subnets:my-subnet-2"
}
}
neg_configs = {
neg-a = {
gce = {
zone = "europe-west1-b"
network = "$networks:my-vpc"
subnetwork = "$subnets:my-subnet-1"
endpoints = {
vm-a = {
instance = "my-vm-a"
ip_address = "10.0.0.2"
port = 80
}
}
}
}
neg-b = {
gce = {
zone = "europe-west4-a"
network = "$networks:my-vpc"
subnetwork = "$subnets:my-subnet-2"
endpoints = {
vm-b = {
instance = "my-vm-b"
ip_address = "10.0.1.2"
port = 80
}
}
}
}
}
backend_service_config = {
backends = [
{
group = "neg-a"
max_connections = {
per_endpoint = 100
}
},
{
group = "neg-b"
max_connections = {
per_endpoint = 100
}
}
]
}
}
# tftest modules=1 resources=9
```
### Health Checks
You can leverage externally defined health checks for backend services, or have the module create them for you. By default a simple TCP health check is created, and used in backend services.
Defining a custom health check:
```hcl
module "tcp-proxy-cross-region" {
source = "./fabric/modules/net-lb-proxy-int-cross-region"
name = "ilb-proxy-test"
project_id = var.project_id
port = 80
vpc_config = {
network = var.vpc.self_link
subnetworks = {
europe-west1 = var.subnet1.self_link
}
}
backend_service_config = {
backends = [{
group = "projects/my-project/zones/europe-west1-b/networkEndpointGroups/my-neg"
max_connections = {
per_endpoint = 100
}
}]
}
health_check_config = {
tcp = {
port = 8080
}
}
}
# tftest modules=1 resources=4
```
### Network Endpoint Groups (NEGs)
#### Zonal NEG creation
You can have the module create zonal NEGs for you by defining the `neg_configs` variable:
```hcl
module "tcp-proxy-cross-region" {
source = "./fabric/modules/net-lb-proxy-int-cross-region"
name = "ilb-proxy-test"
project_id = var.project_id
port = 80
vpc_config = {
network = var.vpc.self_link
subnetworks = {
europe-west1 = var.subnet1.self_link
}
}
neg_configs = {
neg-a = {
gce = {
zone = "europe-west1-b"
network = var.vpc.self_link
subnetwork = var.subnet1.self_link
}
}
}
backend_service_config = {
backends = [{
group = "neg-a"
max_connections = {
per_endpoint = 100
}
}]
}
}
# tftest modules=1 resources=5
```
#### Hybrid NEG creation
You can also configure hybrid NEGs:
```hcl
module "tcp-proxy-cross-region" {
source = "./fabric/modules/net-lb-proxy-int-cross-region"
name = "ilb-proxy-test"
project_id = var.project_id
port = 80
vpc_config = {
network = var.vpc.self_link
subnetworks = {
europe-west1 = var.subnet1.self_link
}
}
neg_configs = {
hybrid-a = {
hybrid = {
network = var.vpc.self_link
zone = "europe-west1-b"
endpoints = {
endpoint-1 = {
ip_address = "10.10.10.1"
port = 80
}
}
}
}
}
backend_service_config = {
backends = [{
group = "hybrid-a"
max_connections = {
per_endpoint = 100
}
}]
}
}
# tftest modules=1 resources=6
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [name](variables.tf#L204) | Load balancer name. | <code>string</code> | ✓ | |
| [project_id](variables.tf#L258) | Project id. | <code>string</code> | ✓ | |
| [vpc_config](variables.tf#L274) | VPC-level configuration. | <code>object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [addresses](variables.tf#L17) | Optional IP addresses used for the forwarding rules, mapped by subnetwork key. | <code>map&#40;string&#41;</code> | | <code>null</code> |
| [backend_service_config](variables.tf#L23) | Backend service level configuration. | <code>object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [context](variables.tf#L72) | Context-specific interpolations. | <code>object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [description](variables.tf#L85) | Optional description used for resources. | <code>string</code> | | <code>&#34;Terraform managed.&#34;</code> |
| [group_configs](variables.tf#L91) | Optional unmanaged groups to create. Can be referenced in backends via key or outputs. | <code>map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [health_check](variables.tf#L105) | Name of existing health check to use, disables auto-created health check. | <code>string</code> | | <code>null</code> |
| [health_check_config](variables.tf#L111) | Optional auto-created health check configurations. | <code>object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#8230;&#125;</code> |
| [labels](variables.tf#L198) | Labels set on resources. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [neg_configs](variables.tf#L209) | Optional network endpoint groups to create. Can be referenced in backends via key or outputs. | <code>map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
| [port](variables.tf#L252) | Forwarding rule port. Cross-region internal proxy load balancers support a single port. | <code>number</code> | | <code>80</code> |
| [target_proxy_config](variables.tf#L263) | Target proxy configuration. | <code>object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [addresses](outputs.tf#L17) | Forwarding rule addresses. | |
| [backend_service](outputs.tf#L22) | Backend resource. | |
| [backend_service_id](outputs.tf#L27) | Backend id. | |
| [backend_service_self_link](outputs.tf#L32) | Backend self link. | |
| [forwarding_rules](outputs.tf#L37) | Forwarding rule resources. | |
| [group_self_links](outputs.tf#L42) | Optional unmanaged instance group self links. | |
| [groups](outputs.tf#L49) | Optional unmanaged instance group resources. | |
| [health_check](outputs.tf#L54) | Auto-created health-check resource. | |
| [health_check_id](outputs.tf#L59) | Auto-created health-check id. | |
| [health_check_self_link](outputs.tf#L64) | Auto-created health-check self link. | |
| [ids](outputs.tf#L69) | Fully qualified forwarding rule ids. | |
| [neg_ids](outputs.tf#L74) | Autogenerated network endpoint group ids. | |
| [psc_neg_ids](outputs.tf#L81) | Autogenerated PSC network endpoint group ids. | |
| [target_proxy](outputs.tf#L88) | Target proxy resource. | |
| [target_proxy_id](outputs.tf#L93) | Target proxy id. | |
<!-- END TFDOC -->

View File

@@ -0,0 +1,76 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Backend service resources.
locals {
group_ids = merge(
{
for k, v in google_compute_instance_group.default : k => v.id
},
{
for k, v in google_compute_network_endpoint_group.default : k => v.id
},
{
for k, v in google_compute_region_network_endpoint_group.psc : k => v.id
}
)
}
resource "google_compute_backend_service" "default" {
provider = google-beta
project = local.project_id
name = coalesce(var.backend_service_config.name, var.name)
description = var.backend_service_config.description
affinity_cookie_ttl_sec = var.backend_service_config.affinity_cookie_ttl_sec
connection_draining_timeout_sec = var.backend_service_config.connection_draining_timeout_sec
health_checks = [local.health_check]
load_balancing_scheme = "INTERNAL_MANAGED"
port_name = var.backend_service_config.port_name
protocol = "TCP"
session_affinity = var.backend_service_config.session_affinity
timeout_sec = var.backend_service_config.timeout_sec
dynamic "backend" {
for_each = { for b in coalesce(var.backend_service_config.backends, []) : b.group => b }
content {
group = lookup(local.group_ids, backend.key, backend.key)
balancing_mode = backend.value.balancing_mode
capacity_scaler = backend.value.capacity_scaler
description = backend.value.description
max_connections = try(
backend.value.max_connections.per_group, null
)
max_connections_per_endpoint = try(
backend.value.max_connections.per_endpoint, null
)
max_connections_per_instance = try(
backend.value.max_connections.per_instance, null
)
max_utilization = backend.value.max_utilization
}
}
dynamic "log_config" {
for_each = var.backend_service_config.log_config == null ? [] : [""]
content {
enable = var.backend_service_config.log_config.enable
sample_rate = var.backend_service_config.log_config.sample_rate
optional_mode = var.backend_service_config.log_config.optional_mode
optional_fields = var.backend_service_config.log_config.optional_fields
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Instance group resources.
resource "google_compute_instance_group" "default" {
for_each = var.group_configs
project = (
each.value.project_id == null
? local.project_id
: each.value.project_id
)
zone = try(local.ctx.locations[each.value.zone], each.value.zone)
name = coalesce(each.value.name, "${var.name}-${each.key}")
description = each.value.description
instances = each.value.instances
dynamic "named_port" {
for_each = each.value.named_ports
content {
name = named_port.key
port = named_port.value
}
}
}

View File

@@ -0,0 +1,121 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Health check resource.
locals {
hc = (
var.health_check != null ? null : var.health_check_config
)
hc_grpc = try(local.hc.grpc, null) != null
hc_http = try(local.hc.http, null) != null
hc_http2 = try(local.hc.http2, null) != null
hc_https = try(local.hc.https, null) != null
hc_ssl = try(local.hc.ssl, null) != null
hc_tcp = try(local.hc.tcp, null) != null
}
resource "google_compute_health_check" "default" {
provider = google-beta
count = local.hc != null ? 1 : 0
project = local.project_id
name = coalesce(local.hc.name, var.name)
description = local.hc.description
check_interval_sec = local.hc.check_interval_sec
healthy_threshold = local.hc.healthy_threshold
timeout_sec = local.hc.timeout_sec
unhealthy_threshold = local.hc.unhealthy_threshold
dynamic "grpc_health_check" {
for_each = local.hc_grpc ? [""] : []
content {
port = local.hc.grpc.port
port_name = local.hc.grpc.port_name
port_specification = local.hc.grpc.port_specification
grpc_service_name = local.hc.grpc.service_name
}
}
dynamic "http_health_check" {
for_each = local.hc_http ? [""] : []
content {
host = local.hc.http.host
port = local.hc.http.port
port_name = local.hc.http.port_name
port_specification = local.hc.http.port_specification
proxy_header = local.hc.http.proxy_header
request_path = local.hc.http.request_path
response = local.hc.http.response
}
}
dynamic "http2_health_check" {
for_each = local.hc_http2 ? [""] : []
content {
host = local.hc.http2.host
port = local.hc.http2.port
port_name = local.hc.http2.port_name
port_specification = local.hc.http2.port_specification
proxy_header = local.hc.http2.proxy_header
request_path = local.hc.http2.request_path
response = local.hc.http2.response
}
}
dynamic "https_health_check" {
for_each = local.hc_https ? [""] : []
content {
host = local.hc.https.host
port = local.hc.https.port
port_name = local.hc.https.port_name
port_specification = local.hc.https.port_specification
proxy_header = local.hc.https.proxy_header
request_path = local.hc.https.request_path
response = local.hc.https.response
}
}
dynamic "ssl_health_check" {
for_each = local.hc_ssl ? [""] : []
content {
port = local.hc.ssl.port
port_name = local.hc.ssl.port_name
port_specification = local.hc.ssl.port_specification
proxy_header = local.hc.ssl.proxy_header
request = local.hc.ssl.request
response = local.hc.ssl.response
}
}
dynamic "tcp_health_check" {
for_each = local.hc_tcp ? [""] : []
content {
port = local.hc.tcp.port
port_name = local.hc.tcp.port_name
port_specification = local.hc.tcp.port_specification
proxy_header = local.hc.tcp.proxy_header
request = local.hc.tcp.request
response = local.hc.tcp.response
}
}
dynamic "log_config" {
for_each = try(local.hc.enable_logging, false) ? [""] : []
content {
enable = true
}
}
}

View File

@@ -0,0 +1,135 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
ctx = {
for k, v in var.context : k => {
for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv
}
}
ctx_p = "$"
network = lookup(local.ctx.networks, var.vpc_config.network, var.vpc_config.network)
project_id = lookup(local.ctx.project_ids, var.project_id, var.project_id)
}
locals {
_neg_endpoints = flatten([
for k, v in local.neg_zonal : [
for kk, vv in v.endpoints : merge(vv, {
key = "${k}-${kk}", neg = k, zone = v.zone
})
]
])
addresses = {
for k, v in coalesce(var.addresses, {}) : k => lookup(local.ctx.addresses, v, v)
}
health_check = (
var.health_check != null
? var.health_check
: google_compute_health_check.default[0].self_link
)
neg_endpoints = {
for v in local._neg_endpoints : (v.key) => v
}
neg_zonal = {
for k, v in var.neg_configs : k => {
endpoints = v.gce != null ? v.gce.endpoints : v.hybrid.endpoints
network = v.gce != null ? v.gce.network : v.hybrid.network
project_id = v.project_id
subnetwork = v.gce != null ? v.gce.subnetwork : null
type = v.gce != null ? "GCE_VM_IP_PORT" : "NON_GCP_PRIVATE_IP_PORT"
zone = v.gce != null ? v.gce.zone : v.hybrid.zone
} if v.gce != null || v.hybrid != null
}
neg_regional_psc = {
for k, v in var.neg_configs :
k => v if v.psc != null
}
subnetworks = {
for k, v in var.vpc_config.subnetworks : k => lookup(local.ctx.subnets, v, v)
}
}
resource "google_compute_global_forwarding_rule" "default" {
for_each = local.subnetworks
provider = google-beta
project = local.project_id
name = "${var.name}-${each.key}"
description = var.description
ip_address = try(local.addresses[each.key], null)
ip_protocol = "TCP"
load_balancing_scheme = "INTERNAL_MANAGED"
network = local.network
port_range = tostring(var.port)
subnetwork = each.value
labels = var.labels
target = google_compute_target_tcp_proxy.default.id
}
resource "google_compute_target_tcp_proxy" "default" {
project = local.project_id
name = coalesce(var.target_proxy_config.name, var.name)
description = var.target_proxy_config.description
backend_service = google_compute_backend_service.default.id
proxy_header = var.target_proxy_config.proxy_header
}
resource "google_compute_network_endpoint_group" "default" {
for_each = local.neg_zonal
project = (
each.value.project_id == null
? local.project_id
: each.value.project_id
)
name = "${var.name}-${each.key}"
network = (
each.value.network == null
? null
: lookup(local.ctx.networks, each.value.network, each.value.network)
)
subnetwork = (
each.value.subnetwork == null
? null
: lookup(local.ctx.subnets, each.value.subnetwork, each.value.subnetwork)
)
network_endpoint_type = each.value.type
zone = lookup(local.ctx.locations, each.value.zone, each.value.zone)
}
resource "google_compute_network_endpoint" "default" {
for_each = local.neg_endpoints
project = (
local.neg_zonal[each.value.neg].project_id == null
? local.project_id
: local.neg_zonal[each.value.neg].project_id
)
network_endpoint_group = google_compute_network_endpoint_group.default[each.value.neg].name
zone = lookup(local.ctx.locations, each.value.zone, each.value.zone)
instance = try(each.value.instance, null)
ip_address = each.value.ip_address
port = each.value.port
}
resource "google_compute_region_network_endpoint_group" "psc" {
for_each = local.neg_regional_psc
project = coalesce(each.value.project_id, local.project_id)
name = "${var.name}-${each.key}"
region = lookup(local.ctx.locations, each.value.psc.region, each.value.psc.region)
network = lookup(local.ctx.networks, each.value.psc.network, each.value.psc.network)
subnetwork = lookup(local.ctx.subnets, each.value.psc.subnetwork, each.value.psc.subnetwork)
network_endpoint_type = "PRIVATE_SERVICE_CONNECT"
psc_target_service = each.value.psc.target_service
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
output "addresses" {
description = "Forwarding rule addresses."
value = { for k, v in google_compute_global_forwarding_rule.default : k => v.ip_address }
}
output "backend_service" {
description = "Backend resource."
value = google_compute_backend_service.default
}
output "backend_service_id" {
description = "Backend id."
value = google_compute_backend_service.default.id
}
output "backend_service_self_link" {
description = "Backend self link."
value = google_compute_backend_service.default.self_link
}
output "forwarding_rules" {
description = "Forwarding rule resources."
value = google_compute_global_forwarding_rule.default
}
output "group_self_links" {
description = "Optional unmanaged instance group self links."
value = {
for k, v in google_compute_instance_group.default : k => v.self_link
}
}
output "groups" {
description = "Optional unmanaged instance group resources."
value = google_compute_instance_group.default
}
output "health_check" {
description = "Auto-created health-check resource."
value = try(google_compute_health_check.default[0], null)
}
output "health_check_id" {
description = "Auto-created health-check id."
value = try(google_compute_health_check.default[0].id, null)
}
output "health_check_self_link" {
description = "Auto-created health-check self link."
value = try(google_compute_health_check.default[0].self_link, null)
}
output "ids" {
description = "Fully qualified forwarding rule ids."
value = { for k, v in google_compute_global_forwarding_rule.default : k => v.id }
}
output "neg_ids" {
description = "Autogenerated network endpoint group ids."
value = {
for k, v in google_compute_network_endpoint_group.default : k => v.id
}
}
output "psc_neg_ids" {
description = "Autogenerated PSC network endpoint group ids."
value = {
for k, v in google_compute_region_network_endpoint_group.psc : k => v.id
}
}
output "target_proxy" {
description = "Target proxy resource."
value = google_compute_target_tcp_proxy.default
}
output "target_proxy_id" {
description = "Target proxy id."
value = google_compute_target_tcp_proxy.default.id
}

View File

@@ -0,0 +1,281 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "addresses" {
description = "Optional IP addresses used for the forwarding rules, mapped by subnetwork key."
type = map(string)
default = null
}
variable "backend_service_config" {
description = "Backend service level configuration."
type = object({
name = optional(string)
description = optional(string, "Terraform managed.")
affinity_cookie_ttl_sec = optional(number)
connection_draining_timeout_sec = optional(number)
health_checks = optional(list(string), ["default"])
log_config = optional(object({
enable = optional(bool)
sample_rate = optional(number)
optional_mode = optional(string)
optional_fields = optional(list(string))
}))
port_name = optional(string)
project_id = optional(string)
session_affinity = optional(string, "NONE")
timeout_sec = optional(number)
backends = optional(list(object({
group = string
balancing_mode = optional(string, "CONNECTION")
capacity_scaler = optional(number, 1)
description = optional(string, "Terraform managed.")
max_connections = optional(object({
per_endpoint = optional(number)
per_group = optional(number)
per_instance = optional(number)
}))
max_utilization = optional(number)
})))
})
default = {}
nullable = false
validation {
condition = (var.backend_service_config == null || contains(["NONE", "CLIENT_IP"],
var.backend_service_config.session_affinity
))
error_message = "Invalid session affinity value."
}
validation {
condition = alltrue([
for b in var.backend_service_config.backends : contains(
["CONNECTION", "UTILIZATION"], coalesce(b.balancing_mode, "CONNECTION")
)
])
error_message = "When specified, balancing mode needs to be 'CONNECTION' or 'UTILIZATION'."
}
}
variable "context" {
description = "Context-specific interpolations."
type = object({
addresses = optional(map(string), {})
locations = optional(map(string), {})
networks = optional(map(string), {})
project_ids = optional(map(string), {})
subnets = optional(map(string), {})
})
default = {}
nullable = false
}
variable "description" {
description = "Optional description used for resources."
type = string
default = "Terraform managed."
}
variable "group_configs" {
description = "Optional unmanaged groups to create. Can be referenced in backends via key or outputs."
type = map(object({
name = optional(string)
description = optional(string, "Terraform managed.")
zone = string
instances = optional(list(string))
named_ports = optional(map(number), {})
project_id = optional(string)
}))
default = {}
nullable = false
}
variable "health_check" {
description = "Name of existing health check to use, disables auto-created health check."
type = string
default = null
}
variable "health_check_config" {
description = "Optional auto-created health check configurations."
type = object({
name = optional(string)
check_interval_sec = optional(number)
description = optional(string, "Terraform managed.")
enable_logging = optional(bool, false)
healthy_threshold = optional(number)
project_id = optional(string)
timeout_sec = optional(number)
unhealthy_threshold = optional(number)
grpc = optional(object({
port = optional(number)
port_name = optional(string)
port_specification = optional(string)
service_name = optional(string)
}))
http = optional(object({
host = optional(string)
port = optional(number)
port_name = optional(string)
port_specification = optional(string)
proxy_header = optional(string)
request_path = optional(string)
response = optional(string)
}))
http2 = optional(object({
host = optional(string)
port = optional(number)
port_name = optional(string)
port_specification = optional(string)
proxy_header = optional(string)
request_path = optional(string)
response = optional(string)
}))
https = optional(object({
host = optional(string)
port = optional(number)
port_name = optional(string)
port_specification = optional(string)
proxy_header = optional(string)
request_path = optional(string)
response = optional(string)
}))
tcp = optional(object({
port = optional(number)
port_name = optional(string)
port_specification = optional(string)
proxy_header = optional(string)
request = optional(string)
response = optional(string)
}))
ssl = optional(object({
port = optional(number)
port_name = optional(string)
port_specification = optional(string)
proxy_header = optional(string)
request = optional(string)
response = optional(string)
}))
})
default = {
tcp = {
port_specification = "USE_SERVING_PORT"
}
}
validation {
condition = (
(try(var.health_check_config.grpc, null) == null ? 0 : 1) +
(try(var.health_check_config.http, null) == null ? 0 : 1) +
(try(var.health_check_config.http2, null) == null ? 0 : 1) +
(try(var.health_check_config.https, null) == null ? 0 : 1) +
(try(var.health_check_config.tcp, null) == null ? 0 : 1) +
(try(var.health_check_config.ssl, null) == null ? 0 : 1) <= 1
)
error_message = "Only one health check type can be configured at a time."
}
validation {
condition = alltrue([
for k, v in var.health_check_config : contains([
"-", "USE_FIXED_PORT", "USE_NAMED_PORT", "USE_SERVING_PORT"
], coalesce(try(v.port_specification, null), "-"))
])
error_message = "Invalid 'port_specification' value. Supported values are 'USE_FIXED_PORT', 'USE_NAMED_PORT', 'USE_SERVING_PORT'."
}
}
variable "labels" {
description = "Labels set on resources."
type = map(string)
default = {}
}
variable "name" {
description = "Load balancer name."
type = string
}
variable "neg_configs" {
description = "Optional network endpoint groups to create. Can be referenced in backends via key or outputs."
type = map(object({
project_id = optional(string)
gce = optional(object({
zone = string
network = optional(string)
subnetwork = optional(string)
endpoints = optional(map(object({
instance = string
ip_address = string
port = number
})), {})
}))
hybrid = optional(object({
zone = string
network = optional(string)
endpoints = optional(map(object({
ip_address = string
port = number
})), {})
}))
psc = optional(object({
region = string
target_service = string
network = optional(string)
subnetwork = optional(string)
}))
}))
default = {}
nullable = false
validation {
condition = alltrue([
for k, v in var.neg_configs : (
(try(v.gce, null) == null ? 0 : 1) +
(try(v.hybrid, null) == null ? 0 : 1) +
(try(v.psc, null) == null ? 0 : 1) == 1
)
])
error_message = "Only one type of neg can be configured at a time."
}
}
variable "port" {
description = "Forwarding rule port. Cross-region internal proxy load balancers support a single port."
type = number
default = 80
}
variable "project_id" {
description = "Project id."
type = string
}
variable "target_proxy_config" {
description = "Target proxy configuration."
type = object({
description = optional(string, "Terraform managed.")
name = optional(string)
proxy_header = optional(string)
})
default = {}
nullable = false
}
variable "vpc_config" {
description = "VPC-level configuration."
type = object({
network = string
subnetworks = map(string)
})
nullable = false
}

View File

@@ -0,0 +1,35 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Fabric release: v56.1.0
terraform {
required_version = ">= 1.12.2"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 7.33.0, < 8.0.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 7.33.0, < 8.0.0" # tftest
}
}
provider_meta "google" {
module_name = "google-pso-tool/cloud-foundation-fabric/modules/net-lb-proxy-int-cross-region:v56.1.0-tf"
}
provider_meta "google-beta" {
module_name = "google-pso-tool/cloud-foundation-fabric/modules/net-lb-proxy-int-cross-region:v56.1.0-tf"
}
}

View File

@@ -0,0 +1,35 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Fabric release: v56.1.0
terraform {
required_version = ">= 1.11.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 7.33.0, < 8.0.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 7.33.0, < 8.0.0" # tftest
}
}
provider_meta "google" {
module_name = "google-pso-tool/cloud-foundation-fabric/modules/net-lb-proxy-int-cross-region:v56.1.0-tofu"
}
provider_meta "google-beta" {
module_name = "google-pso-tool/cloud-foundation-fabric/modules/net-lb-proxy-int-cross-region:v56.1.0-tofu"
}
}