diff --git a/terraform/litellm/gcp/README.md b/terraform/litellm/gcp/README.md index 85363074086..2577567737a 100644 --- a/terraform/litellm/gcp/README.md +++ b/terraform/litellm/gcp/README.md @@ -442,13 +442,15 @@ vpcaccess, compute, servicenetworking, storage, artifactregistry). ## TLS -TLS and `lb_domains` apply only when `load_balancing_scheme = "EXTERNAL_MANAGED"`. -For `INTERNAL_MANAGED`, set `lb_domains = []` (or leave it unset) and `allow_plaintext_lb = true`. +TLS is supported for both LB schemes, with different certificate inputs: + +- `EXTERNAL_MANAGED`: set `lb_domains`; this module creates a Google-managed cert +- `INTERNAL_MANAGED`: set both `lb_domains` and `certificate_manager_certificates`; this module uses your pre-existing Certificate Manager certificates `terraform plan` refuses to provision an HTTP-only LB by default — TLS is the supported posture. Two paths: -**Production / staging — set `lb_domains`:** +**Production / staging (INTERNAL_MANAGED) — set `lb_domains`:** 1. `terraform apply` once with `allow_plaintext_lb = true` (intentional chicken-and-egg escape hatch) to provision the LB and read the anycast @@ -464,10 +466,22 @@ managed cert sits in `PROVISIONING` for ~15-60 min on first apply until DNS propagation completes — `gcloud compute ssl-certificates describe -litellm--cert` shows the state. +**Production / staging (INTERNAL_MANAGED) — set both hostnames and cert refs:** + +Set: + +- `lb_domains = ["proxy.internal.example.com"]` +- `certificate_manager_certificates = ["projects//locations/global/certificates/"]` + +Result: a 443 internal forwarding rule and HTTPS target proxy using the +provided certificates. If exactly one of the two variables is set, +`terraform plan` fails with a clear precondition error. + **Trial / dev — explicitly opt into HTTP-only:** -Set `allow_plaintext_lb = true` and leave `lb_domains = []`. Without the -flag, plan fails with a clear error pointing at the precondition. +Set `allow_plaintext_lb = true`, leave `lb_domains = []`, and leave +`certificate_manager_certificates = []`. Without the flag, plan fails +with a clear error pointing at the precondition. Intended for short-lived trial / dev stacks only. ## Using as a module diff --git a/terraform/litellm/gcp/examples/default/TUTORIAL.md b/terraform/litellm/gcp/examples/default/TUTORIAL.md index 5c7144619d6..78e98446158 100644 --- a/terraform/litellm/gcp/examples/default/TUTORIAL.md +++ b/terraform/litellm/gcp/examples/default/TUTORIAL.md @@ -81,7 +81,12 @@ gcloud secrets versions access latest \ ## Going to TLS -If you picked `allow_plaintext_lb=true` to bootstrap but want HTTPS for real, point a DNS A record at the LB IP, then re-run terraform with `lb_domains` set and `allow_plaintext_lb` removed: +If you picked `allow_plaintext_lb=true` to bootstrap but want HTTPS for real: + +- EXTERNAL_MANAGED: point a DNS A record at the LB IP and set `lb_domains` +- INTERNAL_MANAGED: set both `lb_domains` and `certificate_manager_certificates` + +Then re-run terraform with `allow_plaintext_lb` removed: ```bash terraform apply \ diff --git a/terraform/litellm/gcp/examples/default/deploystack.json b/terraform/litellm/gcp/examples/default/deploystack.json index 47d1fd914ce..947f0516764 100644 --- a/terraform/litellm/gcp/examples/default/deploystack.json +++ b/terraform/litellm/gcp/examples/default/deploystack.json @@ -34,7 +34,7 @@ }, { "name": "allow_plaintext_lb", - "description": "Skip TLS on the load balancer (HTTP-only). Set true for trial/dev. For production, leave false and add lb_domains to terraform.tfvars after the first apply", + "description": "Skip TLS on the load balancer (HTTP-only). Set true for trial/dev. For EXTERNAL_MANAGED production, leave false and add lb_domains after bootstrap. For INTERNAL_MANAGED TLS, set both lb_domains and certificate_manager_certificates", "default": "true", "options": ["true", "false"] } diff --git a/terraform/litellm/gcp/examples/default/main.tf b/terraform/litellm/gcp/examples/default/main.tf index 23cde567b8e..16aad90a59c 100644 --- a/terraform/litellm/gcp/examples/default/main.tf +++ b/terraform/litellm/gcp/examples/default/main.tf @@ -43,11 +43,12 @@ module "litellm" { image_registry = var.image_registry image_tag = var.image_tag - load_balancing_scheme = var.load_balancing_scheme - lb_domains = var.lb_domains - allow_plaintext_lb = var.allow_plaintext_lb - cloudsql_deletion_protection = var.cloudsql_deletion_protection - gcs_force_destroy = var.gcs_force_destroy + load_balancing_scheme = var.load_balancing_scheme + lb_domains = var.lb_domains + certificate_manager_certificates = var.certificate_manager_certificates + allow_plaintext_lb = var.allow_plaintext_lb + cloudsql_deletion_protection = var.cloudsql_deletion_protection + gcs_force_destroy = var.gcs_force_destroy proxy_config = var.proxy_config gateway_extra_env = var.gateway_extra_env diff --git a/terraform/litellm/gcp/examples/default/terraform.tfvars.example b/terraform/litellm/gcp/examples/default/terraform.tfvars.example index 553fb1aab85..c68f5d538fc 100644 --- a/terraform/litellm/gcp/examples/default/terraform.tfvars.example +++ b/terraform/litellm/gcp/examples/default/terraform.tfvars.example @@ -27,10 +27,11 @@ env = "stage" # TLS: provide DNS names already pointing at the LB IP for a Google-managed # cert. Without one, plan fails unless allow_plaintext_lb = true is set -# explicitly (trial/dev only). INTERNAL_MANAGED is private-IP only and -# should keep lb_domains empty. +# explicitly (trial/dev only). INTERNAL_MANAGED TLS requires both +# lb_domains and certificate_manager_certificates. # load_balancing_scheme = "EXTERNAL_MANAGED" # or "INTERNAL_MANAGED" -# lb_domains = ["proxy.example.com"] +# lb_domains = ["proxy.example.com"] +# certificate_manager_certificates = ["projects//locations/global/certificates/"] # allow_plaintext_lb = true # Storage and database retention. Defaults are safe — destroy preserves diff --git a/terraform/litellm/gcp/examples/default/variables.tf b/terraform/litellm/gcp/examples/default/variables.tf index cf66b58f20a..2ac5470d5ba 100644 --- a/terraform/litellm/gcp/examples/default/variables.tf +++ b/terraform/litellm/gcp/examples/default/variables.tf @@ -90,7 +90,13 @@ variable "image_tag" { # TLS — provide DNS names for a managed cert, or opt into HTTP-only for dev. variable "lb_domains" { - description = "DNS names (already pointing at lb_ip) for a Google-managed cert in EXTERNAL_MANAGED mode. Empty → no TLS." + description = "TLS hostnames. EXTERNAL_MANAGED creates a Google-managed cert; INTERNAL_MANAGED requires this plus certificate_manager_certificates. Empty -> no TLS." + type = list(string) + default = [] +} + +variable "certificate_manager_certificates" { + description = "Pre-existing Certificate Manager certificate self_links for INTERNAL_MANAGED TLS." type = list(string) default = [] } diff --git a/terraform/litellm/gcp/load_balancer.tf b/terraform/litellm/gcp/load_balancer.tf index 86bf977ffeb..c316296fc6a 100644 --- a/terraform/litellm/gcp/load_balancer.tf +++ b/terraform/litellm/gcp/load_balancer.tf @@ -12,9 +12,12 @@ # is rewritten to redirect HTTP→HTTPS via a redirect-only URL map. locals { - tls_enabled = length(var.lb_domains) > 0 is_external = var.load_balancing_scheme == "EXTERNAL_MANAGED" is_internal = var.load_balancing_scheme == "INTERNAL_MANAGED" + + external_tls_enabled = local.is_external && length(var.lb_domains) > 0 + internal_tls_enabled = local.is_internal && length(var.lb_domains) > 0 && length(var.certificate_manager_certificates) > 0 + tls_enabled = local.external_tls_enabled || local.internal_tls_enabled } resource "google_compute_global_address" "lb" { @@ -139,7 +142,7 @@ resource "google_compute_url_map" "this" { # target proxy when TLS is enabled; otherwise the regular path-routing # URL map is attached to the HTTP proxy and everything stays plaintext. resource "google_compute_url_map" "https_redirect" { - count = var.create_runtime && local.tls_enabled && local.is_external ? 1 : 0 + count = var.create_runtime && local.tls_enabled ? 1 : 0 name = "${local.name}-redirect" default_url_redirect { @@ -159,8 +162,11 @@ resource "google_compute_target_http_proxy" "this" { # Operators must either supply DNS names or explicitly opt in. lifecycle { precondition { - condition = var.load_balancing_scheme != "INTERNAL_MANAGED" || length(var.lb_domains) == 0 - error_message = "INTERNAL_MANAGED does not support lb_domains/TLS in this module. Set load_balancing_scheme = \"EXTERNAL_MANAGED\" to use lb_domains, or leave lb_domains empty for INTERNAL_MANAGED." + condition = !local.is_internal || ( + (length(var.lb_domains) == 0 && length(var.certificate_manager_certificates) == 0) || + (length(var.lb_domains) > 0 && length(var.certificate_manager_certificates) > 0) + ) + error_message = "INTERNAL_MANAGED TLS requires both `lb_domains` and `certificate_manager_certificates` to be set (or both empty for HTTP-only)." } precondition { @@ -205,7 +211,7 @@ resource "google_compute_global_forwarding_rule" "http_internal" { # transitions to ACTIVE. resource "google_compute_managed_ssl_certificate" "this" { - count = var.create_runtime && local.tls_enabled && local.is_external ? 1 : 0 + count = var.create_runtime && local.external_tls_enabled ? 1 : 0 # A managed cert's `domains` is immutable, so changing var.lb_domains # forces replacement, and the cert is referenced by the HTTPS target @@ -225,19 +231,29 @@ resource "google_compute_managed_ssl_certificate" "this" { } resource "google_compute_target_https_proxy" "this" { - count = var.create_runtime && local.tls_enabled && local.is_external ? 1 : 0 - name = "${local.name}-https" - url_map = google_compute_url_map.this[0].id - ssl_certificates = [google_compute_managed_ssl_certificate.this[0].id] + count = var.create_runtime && local.tls_enabled ? 1 : 0 + + name = "${local.name}-https" + url_map = google_compute_url_map.this[0].id + ssl_certificates = local.is_external ? [google_compute_managed_ssl_certificate.this[0].id] : null + certificate_manager_certificates = local.is_internal ? var.certificate_manager_certificates : null } resource "google_compute_global_forwarding_rule" "https" { - count = var.create_runtime && local.tls_enabled && local.is_external ? 1 : 0 + count = var.create_runtime && local.external_tls_enabled ? 1 : 0 name = "${local.name}-https" ip_protocol = "TCP" port_range = "443" load_balancing_scheme = var.load_balancing_scheme - ip_address = google_compute_global_address.lb[0].address target = google_compute_target_https_proxy.this[0].id labels = local.labels + + # Configuration for Global External LB + ip_address = local.is_external ? google_compute_global_address.lb[0].address : null + + # Configuration for Global Internal LB + network = local.is_internal ? google_compute_network.this[0].id : null + subnetwork = local.is_internal ? google_compute_subnetwork.this[0].id : null + + depends_on = [google_compute_subnetwork.managed_proxy] } diff --git a/terraform/litellm/gcp/outputs.tf b/terraform/litellm/gcp/outputs.tf index cd015594c85..b4831768a39 100644 --- a/terraform/litellm/gcp/outputs.tf +++ b/terraform/litellm/gcp/outputs.tf @@ -1,19 +1,19 @@ output "lb_ip" { description = "Load balancer IP. Global anycast for EXTERNAL_MANAGED, global private IP for INTERNAL_MANAGED. Null when create_runtime is false." - value = var.create_runtime ? ( + value = var.create_runtime ? ( var.load_balancing_scheme == "EXTERNAL_MANAGED" ? google_compute_global_address.lb[0].address : google_compute_global_forwarding_rule.http_internal[0].ip_address - ) : ( + ) : ( null ) } output "lb_url" { - description = "Proxy URL, or null when create_runtime is false. Switches scheme based on whether lb_domains is set; when TLS is enabled the URL points at the first listed domain (since managed certs are tied to the hostname, not the anycast IP). The dashboard is served at /, the API at /v1/*." + description = "Proxy URL, or null when create_runtime is false. Switches scheme based on whether TLS is enabled; when TLS is enabled the URL points at the first domain in lb_domains. The dashboard is served at /, the API at /v1/*." value = var.create_runtime ? (local.tls_enabled ? ( "https://${var.lb_domains[0]}" - ) : ( - format("http://%s", var.load_balancing_scheme == "EXTERNAL_MANAGED" ? google_compute_global_address.lb[0].address : google_compute_global_forwarding_rule.http_internal[0].ip_address) - )) : ( + ) : ( + var.load_balancing_scheme == "EXTERNAL_MANAGED" ? "https://${google_compute_global_address.lb[0].address}" : "https://${google_compute_global_forwarding_rule.http_internal[0].ip_address}" + )) : ( null ) } diff --git a/terraform/litellm/gcp/variables.tf b/terraform/litellm/gcp/variables.tf index 857b34af1e1..056d104344c 100644 --- a/terraform/litellm/gcp/variables.tf +++ b/terraform/litellm/gcp/variables.tf @@ -373,14 +373,30 @@ variable "db_username" { variable "lb_domains" { description = <<-EOT - DNS names for a Google-managed SSL certificate fronting the LB. When - non-empty, the stack provisions a 443 forwarding rule + HTTPS target - proxy + managed cert covering these domains, and the existing 80 - forwarding rule serves a permanent 301 redirect to HTTPS. Leave empty - ([]) to disable TLS (must combine with `allow_plaintext_lb = true` for - the plan to succeed — see README.md "TLS"). Each domain must already - resolve to the LB's anycast IP (`lb_ip` output) for managed-cert - provisioning to succeed. + DNS names fronting the LB when TLS is enabled. + + EXTERNAL_MANAGED: when non-empty, the stack provisions a Google-managed + SSL certificate covering these domains, creates a 443 forwarding rule, + and rewrites port 80 to a permanent HTTPS redirect. + + INTERNAL_MANAGED: when enabling TLS, `lb_domains` must be non-empty and + `certificate_manager_certificates` must also be provided. + + Leave empty ([]) to disable TLS (must combine with + `allow_plaintext_lb = true` for the plan to succeed — see README.md + "TLS"). + EOT + type = list(string) + default = [] +} + +variable "certificate_manager_certificates" { + description = <<-EOT + Pre-existing Certificate Manager certificate resource references for + INTERNAL_MANAGED TLS. Provide full certificate self_links. + + This input is ignored for EXTERNAL_MANAGED, where `lb_domains` continues + to drive Google-managed SSL certificate creation in this module. EOT type = list(string) default = []