mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat(terraform/gcp): dependencies-only mode and bring-your-own-network for GKE (#39695)
* feat(terraform/gcp): add dependencies-only mode Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(terraform/gcp): review fixes for dependencies-only mode Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
730d789418
commit
b290dd410e
17 changed files with 540 additions and 120 deletions
31
.github/workflows/test-terraform-modules.yml
vendored
31
.github/workflows/test-terraform-modules.yml
vendored
|
|
@ -4,6 +4,7 @@ on:
|
|||
push:
|
||||
paths:
|
||||
- "terraform/litellm/aws/**"
|
||||
- "terraform/litellm/gcp/**"
|
||||
- ".github/workflows/test-terraform-modules.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
|
|
@ -13,6 +14,7 @@ on:
|
|||
- "litellm_**"
|
||||
paths:
|
||||
- "terraform/litellm/aws/**"
|
||||
- "terraform/litellm/gcp/**"
|
||||
- ".github/workflows/test-terraform-modules.yml"
|
||||
|
||||
permissions:
|
||||
|
|
@ -52,3 +54,32 @@ jobs:
|
|||
# Plan-only, mock_provider-backed: no AWS credentials, no API calls.
|
||||
- name: test
|
||||
run: terraform test
|
||||
|
||||
gcp-module:
|
||||
name: fmt, validate, test (gcp)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: terraform/litellm/gcp
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
with:
|
||||
terraform_version: 1.13.3
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: fmt
|
||||
run: terraform fmt -recursive -check -diff
|
||||
|
||||
- name: init
|
||||
run: terraform init -backend=false -input=false
|
||||
|
||||
- name: validate
|
||||
run: terraform validate
|
||||
|
||||
- name: test
|
||||
run: terraform test
|
||||
|
|
|
|||
|
|
@ -392,6 +392,63 @@ with its own provider config (one `examples/default`-style root per project),
|
|||
or fork the module to add `configuration_aliases` and pass per-instance
|
||||
`providers = { ... }`.
|
||||
|
||||
## Dependencies only (run LiteLLM on GKE)
|
||||
|
||||
Set `create_runtime = false` to provision Cloud SQL, Memorystore, GCS,
|
||||
Secret Manager, and the runtime service account without Cloud Run or the
|
||||
load balancer. For a Shared VPC, set the full host-project network ID and
|
||||
skip PSA creation after the host project has configured it:
|
||||
|
||||
```hcl
|
||||
create_runtime = false
|
||||
network_id = "projects/<host>/global/networks/<vpc>"
|
||||
create_psa_connection = false
|
||||
```
|
||||
|
||||
The host project must already have Private Services Access configured on
|
||||
that network and the Service Networking API enabled; the module cannot set
|
||||
PSA up from a service project. GKE nodes must sit on the same Shared VPC so
|
||||
the Cloud SQL and Memorystore private IPs are routable from the pods. Run
|
||||
the root with its provider pointed at the project that should own the
|
||||
dependencies. `create_runtime = true` with `network_id` set is also allowed,
|
||||
but the Serverless VPC Access connector has to live in the same project as
|
||||
the network, so that combination only works when the VPC is in the
|
||||
deployment project
|
||||
|
||||
Map the outputs into the Helm values as follows:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
writer:
|
||||
host: <cloudsql_writer_ip>
|
||||
dbname: <db_name>
|
||||
passwordSecret:
|
||||
name: <kubernetes-secret-with-db-credentials>
|
||||
reader:
|
||||
host: <cloudsql_reader_ip>
|
||||
dbname: <db_name>
|
||||
passwordSecret:
|
||||
name: <kubernetes-secret-with-db-credentials>
|
||||
redis:
|
||||
host: <redis_host>
|
||||
port: <redis_port>
|
||||
masterKey:
|
||||
secretName: <kubernetes-secret-with-master-key>
|
||||
```
|
||||
|
||||
Create the database Secret with keys `username` (the `db_username` output)
|
||||
and `password` (read it with `gcloud secrets versions access latest
|
||||
--secret=<db_password_secret_id>`), and the master key Secret from
|
||||
`master_key_secret_id` the same way. Memorystore only accepts TLS by
|
||||
default, so store the `redis_server_ca_pem` output in a third Secret,
|
||||
mount it into the gateway and backend pods via `volumes` / `volumeMounts`,
|
||||
and add `REDIS_SSL=true` and `REDIS_SSL_CA_CERTS=<mount path>` to each
|
||||
component's `extraEnv`. Setting `redis_transit_encryption = false` removes
|
||||
the CA plumbing at the cost of plaintext Redis traffic inside the VPC
|
||||
|
||||
The chart's pre-install/pre-upgrade migration hook runs the Prisma
|
||||
migration, so nothing replaces the Cloud Run migrations Job in this mode
|
||||
|
||||
## Storage and database retention
|
||||
|
||||
Two opt-in tripwires guard against accidental data loss on
|
||||
|
|
@ -409,14 +466,15 @@ Flip `cloudsql_deletion_protection` to `false` or `gcs_force_destroy` to
|
|||
|
||||
## Redis encryption
|
||||
|
||||
Memorystore runs with `transit_encryption_mode = "SERVER_AUTHENTICATION"`,
|
||||
so the proxy connects via `rediss://`. The instance's self-signed CA cert
|
||||
(`server_ca_certs[0].cert`) is shipped to gateway + backend as
|
||||
`REDIS_CA_PEM_B64`; their entrypoint shell decodes it to `/tmp/redis-ca.pem`
|
||||
before uvicorn starts and points `REDIS_SSL_CA_CERTS` at that path. No
|
||||
extra config needed — but if you ever swap Memorystore for an external
|
||||
Redis, override `REDIS_HOST`/`REDIS_PORT` and either drop these env vars
|
||||
or point them at your own CA.
|
||||
By default, Memorystore runs with
|
||||
`transit_encryption_mode = "SERVER_AUTHENTICATION"`, so Cloud Run connects
|
||||
via `rediss://`. The instance's self-signed CA cert
|
||||
(`server_ca_certs[0].cert`) is shipped to gateway and backend as
|
||||
`REDIS_CA_PEM_B64`; their entrypoint shell decodes it to
|
||||
`/tmp/redis-ca.pem` before uvicorn starts and points `REDIS_SSL_CA_CERTS` at
|
||||
that path. Set `redis_transit_encryption = false` to use plaintext Redis.
|
||||
For GKE, use `redis_server_ca_pem` as described in the dependencies-only
|
||||
section, or accept the security tradeoff of disabling transit encryption
|
||||
|
||||
## Files
|
||||
|
||||
|
|
@ -434,4 +492,5 @@ or point them at your own CA.
|
|||
| `iam.tf` | Runtime SA + Cloud SQL client + Secret Manager accessor |
|
||||
| `cloudrun.tf` | 3 Cloud Run services + Cloud Run Job for migrations |
|
||||
| `load_balancer.tf`| External HTTPS LB, serverless NEGs, URL map for path routing |
|
||||
| `outputs.tf` | LB IP, service URLs, secret IDs, migration `execute` command |
|
||||
| `outputs.tf` | LB IP, service URLs, dependency endpoints, secret IDs, migration command |
|
||||
| `tests/` | Plan-only mock-provider coverage for deployment modes and Redis encryption |
|
||||
|
|
|
|||
|
|
@ -15,15 +15,17 @@
|
|||
# enough to invoke Cloud Run admin APIs (`gcloud auth login`).
|
||||
|
||||
resource "terraform_data" "migration" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
triggers_replace = {
|
||||
job_id = google_cloud_run_v2_job.migrations.id
|
||||
job_id = google_cloud_run_v2_job.migrations[0].id
|
||||
job_image = local.migrations_image
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
interpreter = ["bash", "-c"]
|
||||
environment = {
|
||||
JOB = google_cloud_run_v2_job.migrations.name
|
||||
JOB = google_cloud_run_v2_job.migrations[0].name
|
||||
REGION = var.region
|
||||
PROJECT = var.project_id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,25 +6,28 @@ locals {
|
|||
# Memorystore exposes a self-signed CA cert per instance; we ship it as
|
||||
# a base64 env var and decode it to a file at container startup so the
|
||||
# rediss:// connection can validate. Public cert, not sensitive.
|
||||
redis_ca_pem_b64 = base64encode(google_redis_instance.this.server_ca_certs[0].cert)
|
||||
redis_ca_pem_b64 = var.redis_transit_encryption ? base64encode(google_redis_instance.this.server_ca_certs[0].cert) : ""
|
||||
|
||||
shared_env_kv = [
|
||||
{ name = "DATABASE_HOST", value = google_sql_database_instance.writer.private_ip_address },
|
||||
{ name = "DATABASE_PORT", value = "5432" },
|
||||
{ name = "DATABASE_USER", value = var.db_username },
|
||||
{ name = "DATABASE_NAME", value = var.db_name },
|
||||
{ name = "DATABASE_HOST_READ_REPLICA", value = google_sql_database_instance.reader.private_ip_address },
|
||||
{ name = "DATABASE_PORT_READ_REPLICA", value = "5432" },
|
||||
{ name = "REDIS_HOST", value = google_redis_instance.this.host },
|
||||
{ name = "REDIS_PORT", value = tostring(google_redis_instance.this.port) },
|
||||
# _redis.get_redis_url_from_environment honors REDIS_SSL to flip the
|
||||
# scheme to rediss://; REDIS_SSL_CA_CERTS is mapped via
|
||||
# _get_redis_env_kwarg_mapping → ssl_ca_certs on the redis-py client.
|
||||
{ name = "REDIS_SSL", value = "true" },
|
||||
{ name = "REDIS_SSL_CA_CERTS", value = "/tmp/redis-ca.pem" },
|
||||
{ name = "REDIS_CA_PEM_B64", value = local.redis_ca_pem_b64 },
|
||||
{ name = "GCS_BUCKET_NAME", value = google_storage_bucket.this.name },
|
||||
]
|
||||
shared_env_kv = concat(
|
||||
[
|
||||
{ name = "DATABASE_HOST", value = google_sql_database_instance.writer.private_ip_address },
|
||||
{ name = "DATABASE_PORT", value = "5432" },
|
||||
{ name = "DATABASE_USER", value = var.db_username },
|
||||
{ name = "DATABASE_NAME", value = var.db_name },
|
||||
{ name = "DATABASE_HOST_READ_REPLICA", value = google_sql_database_instance.reader.private_ip_address },
|
||||
{ name = "DATABASE_PORT_READ_REPLICA", value = "5432" },
|
||||
{ name = "REDIS_HOST", value = google_redis_instance.this.host },
|
||||
{ name = "REDIS_PORT", value = tostring(google_redis_instance.this.port) },
|
||||
],
|
||||
var.redis_transit_encryption ? [
|
||||
{ name = "REDIS_SSL", value = "true" },
|
||||
{ name = "REDIS_SSL_CA_CERTS", value = "/tmp/redis-ca.pem" },
|
||||
{ name = "REDIS_CA_PEM_B64", value = local.redis_ca_pem_b64 },
|
||||
] : [],
|
||||
[
|
||||
{ name = "GCS_BUCKET_NAME", value = google_storage_bucket.this.name },
|
||||
],
|
||||
)
|
||||
|
||||
# OTel v2 is opt-in and gated on otel_endpoint, matching the AWS stack —
|
||||
# nothing OTel-related is added to the container env until an endpoint is
|
||||
|
|
@ -126,9 +129,9 @@ locals {
|
|||
# Decode the Memorystore CA cert (passed as REDIS_CA_PEM_B64) to the
|
||||
# path REDIS_SSL_CA_CERTS points at, so the redis-py client can validate
|
||||
# the rediss:// handshake.
|
||||
redis_ca_fragment = [
|
||||
redis_ca_fragment = var.redis_transit_encryption ? [
|
||||
"python -c \"import os, base64, pathlib; pathlib.Path(os.environ['REDIS_SSL_CA_CERTS']).write_bytes(base64.b64decode(os.environ['REDIS_CA_PEM_B64']))\""
|
||||
]
|
||||
] : []
|
||||
|
||||
database_url_fragment = [
|
||||
"export DATABASE_URL=\"postgresql://$${DATABASE_USER}:$${DATABASE_PASSWORD}@$${DATABASE_HOST}:$${DATABASE_PORT}/$${DATABASE_NAME}\"",
|
||||
|
|
@ -171,29 +174,7 @@ locals {
|
|||
|
||||
# ---------- Gateway ----------
|
||||
resource "google_cloud_run_v2_service" "gateway" {
|
||||
# Metering needs a client certificate AND its key. Each secret is created only
|
||||
# when its own PEM is supplied, so an endpoint set with a missing key would
|
||||
# otherwise apply cleanly and leave the proxy logging "missing config" and
|
||||
# never exporting. ca_cert_pem stays optional: empty means fall back to the
|
||||
# system trust store.
|
||||
#
|
||||
# The guard lives here, on an unconditional resource, rather than on the cert
|
||||
# secret: that secret is count-gated on the cert itself, so it has zero
|
||||
# instances in exactly the case this must catch. Adding count or for_each to
|
||||
# this resource would silently stop the guard from evaluating.
|
||||
#
|
||||
# endpoint cert key -> result
|
||||
# "" any any -> metering off, no secrets created
|
||||
# set set set -> metering on
|
||||
# set any-missing -> plan fails here
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = var.billing_metrics_endpoint == "" || (
|
||||
var.billing_metrics_client_cert_pem != "" && var.billing_metrics_client_key_pem != ""
|
||||
)
|
||||
error_message = "billing_metrics_client_cert_pem and billing_metrics_client_key_pem are both required when billing_metrics_endpoint is set."
|
||||
}
|
||||
}
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-gateway"
|
||||
location = var.region
|
||||
|
|
@ -206,7 +187,7 @@ resource "google_cloud_run_v2_service" "gateway" {
|
|||
max_instance_request_concurrency = var.gateway_max_instance_request_concurrency
|
||||
|
||||
vpc_access {
|
||||
connector = google_vpc_access_connector.this.id
|
||||
connector = google_vpc_access_connector.this[0].id
|
||||
egress = "PRIVATE_RANGES_ONLY"
|
||||
}
|
||||
|
||||
|
|
@ -312,17 +293,7 @@ resource "google_cloud_run_v2_service" "gateway" {
|
|||
|
||||
# ---------- Backend ----------
|
||||
resource "google_cloud_run_v2_service" "backend" {
|
||||
# Same guard as the gateway: the backend meters too (it serves the named-server
|
||||
# MCP transport), and a targeted apply of just this resource must not slip a
|
||||
# billing endpoint through without the credentials to use it.
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = var.billing_metrics_endpoint == "" || (
|
||||
var.billing_metrics_client_cert_pem != "" && var.billing_metrics_client_key_pem != ""
|
||||
)
|
||||
error_message = "billing_metrics_client_cert_pem and billing_metrics_client_key_pem are both required when billing_metrics_endpoint is set."
|
||||
}
|
||||
}
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-backend"
|
||||
location = var.region
|
||||
|
|
@ -335,7 +306,7 @@ resource "google_cloud_run_v2_service" "backend" {
|
|||
max_instance_request_concurrency = var.backend_max_instance_request_concurrency
|
||||
|
||||
vpc_access {
|
||||
connector = google_vpc_access_connector.this.id
|
||||
connector = google_vpc_access_connector.this[0].id
|
||||
egress = "PRIVATE_RANGES_ONLY"
|
||||
}
|
||||
|
||||
|
|
@ -443,6 +414,8 @@ resource "google_cloud_run_v2_service" "backend" {
|
|||
# with zero IAM bindings, so a compromised UI container can't pivot to
|
||||
# Secret Manager / Cloud SQL via the metadata service.
|
||||
resource "google_cloud_run_v2_service" "ui" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-ui"
|
||||
location = var.region
|
||||
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
|
||||
|
|
@ -450,7 +423,7 @@ resource "google_cloud_run_v2_service" "ui" {
|
|||
deletion_protection = false
|
||||
|
||||
template {
|
||||
service_account = google_service_account.ui_runtime.email
|
||||
service_account = google_service_account.ui_runtime[0].email
|
||||
max_instance_request_concurrency = var.ui_max_instance_request_concurrency
|
||||
|
||||
scaling {
|
||||
|
|
@ -491,25 +464,31 @@ resource "google_cloud_run_v2_service" "ui" {
|
|||
# (LITELLM_MASTER_KEY); these IAM bindings just open up Cloud Run's invoker
|
||||
# gate so the LB request makes it to the container.
|
||||
resource "google_cloud_run_v2_service_iam_member" "gateway_allusers" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
project = var.project_id
|
||||
location = google_cloud_run_v2_service.gateway.location
|
||||
name = google_cloud_run_v2_service.gateway.name
|
||||
location = google_cloud_run_v2_service.gateway[0].location
|
||||
name = google_cloud_run_v2_service.gateway[0].name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "backend_allusers" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
project = var.project_id
|
||||
location = google_cloud_run_v2_service.backend.location
|
||||
name = google_cloud_run_v2_service.backend.name
|
||||
location = google_cloud_run_v2_service.backend[0].location
|
||||
name = google_cloud_run_v2_service.backend[0].name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "ui_allusers" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
project = var.project_id
|
||||
location = google_cloud_run_v2_service.ui.location
|
||||
name = google_cloud_run_v2_service.ui.name
|
||||
location = google_cloud_run_v2_service.ui[0].location
|
||||
name = google_cloud_run_v2_service.ui[0].name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
|
@ -519,6 +498,8 @@ resource "google_cloud_run_v2_service_iam_member" "ui_allusers" {
|
|||
# assembles DATABASE_URL from the DATABASE_* env vars and runs `prisma
|
||||
# migrate deploy`. No proxy_config, no master key, no shell wrapper.
|
||||
resource "google_cloud_run_v2_job" "migrations" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-migrations"
|
||||
location = var.region
|
||||
labels = local.labels
|
||||
|
|
@ -529,7 +510,7 @@ resource "google_cloud_run_v2_job" "migrations" {
|
|||
service_account = google_service_account.runtime.email
|
||||
|
||||
vpc_access {
|
||||
connector = google_vpc_access_connector.this.id
|
||||
connector = google_vpc_access_connector.this[0].id
|
||||
egress = "PRIVATE_RANGES_ONLY"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ resource "google_sql_database_instance" "writer" {
|
|||
|
||||
ip_configuration {
|
||||
ipv4_enabled = false
|
||||
private_network = google_compute_network.this.id
|
||||
private_network = local.network_id
|
||||
}
|
||||
|
||||
insights_config {
|
||||
|
|
@ -55,6 +55,11 @@ resource "google_sql_database_instance" "writer" {
|
|||
# (full data loss). Set the initial size only; let Cloud SQL own it
|
||||
# thereafter.
|
||||
ignore_changes = [settings[0].disk_size]
|
||||
|
||||
precondition {
|
||||
condition = var.create_psa_connection || var.network_id != ""
|
||||
error_message = "create_psa_connection must be true unless network_id references an existing VPC with Private Services Access configured."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +81,7 @@ resource "google_sql_database_instance" "reader" {
|
|||
|
||||
ip_configuration {
|
||||
ipv4_enabled = false
|
||||
private_network = google_compute_network.this.id
|
||||
private_network = local.network_id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ module "litellm" {
|
|||
tenant = var.tenant
|
||||
env = var.env
|
||||
|
||||
create_runtime = var.create_runtime
|
||||
network_id = var.network_id
|
||||
create_psa_connection = var.create_psa_connection
|
||||
redis_transit_encryption = var.redis_transit_encryption
|
||||
|
||||
litellm_master_key = var.litellm_master_key
|
||||
litellm_license = var.litellm_license
|
||||
ui_password = var.ui_password
|
||||
|
|
|
|||
|
|
@ -38,6 +38,31 @@ output "redis_endpoint" {
|
|||
value = module.litellm.redis_endpoint
|
||||
}
|
||||
|
||||
output "redis_host" {
|
||||
description = "Memorystore Redis host."
|
||||
value = module.litellm.redis_host
|
||||
}
|
||||
|
||||
output "redis_port" {
|
||||
description = "Memorystore Redis port."
|
||||
value = module.litellm.redis_port
|
||||
}
|
||||
|
||||
output "redis_server_ca_pem" {
|
||||
description = "Memorystore server CA PEM."
|
||||
value = module.litellm.redis_server_ca_pem
|
||||
}
|
||||
|
||||
output "db_username" {
|
||||
description = "Cloud SQL application username."
|
||||
value = module.litellm.db_username
|
||||
}
|
||||
|
||||
output "db_name" {
|
||||
description = "Cloud SQL database name."
|
||||
value = module.litellm.db_name
|
||||
}
|
||||
|
||||
output "gcs_bucket" {
|
||||
description = "GCS bucket name."
|
||||
value = module.litellm.gcs_bucket
|
||||
|
|
@ -53,6 +78,11 @@ output "db_password_secret_id" {
|
|||
value = module.litellm.db_password_secret_id
|
||||
}
|
||||
|
||||
output "runtime_service_account_email" {
|
||||
description = "Runtime service account email."
|
||||
value = module.litellm.runtime_service_account_email
|
||||
}
|
||||
|
||||
output "migration_run_command" {
|
||||
description = "Break-glass command to re-run the one-off migration job."
|
||||
value = module.litellm.migration_run_command
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@ region = "us-central1"
|
|||
tenant = "acme"
|
||||
env = "stage"
|
||||
|
||||
# Deployment mode. For dependencies only on a Shared VPC, set
|
||||
# create_runtime = false, network_id to the full host-project network ID, and
|
||||
# create_psa_connection = false after configuring PSA on that network.
|
||||
# create_runtime = true
|
||||
# network_id = ""
|
||||
# create_psa_connection = true
|
||||
# redis_transit_encryption = true
|
||||
|
||||
# Tenant-supplied secrets. Prefer TF_VAR_litellm_master_key /
|
||||
# TF_VAR_litellm_license / TF_VAR_ui_password env vars so the values don't
|
||||
# end up in a committed tfvars file. All three are optional — when
|
||||
|
|
|
|||
|
|
@ -26,6 +26,30 @@ variable "env" {
|
|||
type = string
|
||||
}
|
||||
|
||||
variable "create_runtime" {
|
||||
description = "Create Cloud Run and load balancer resources."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "network_id" {
|
||||
description = "Existing VPC network resource ID. Empty creates a VPC."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "create_psa_connection" {
|
||||
description = "Create Private Services Access resources."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "redis_transit_encryption" {
|
||||
description = "Enable Memorystore transit encryption."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
# Sensitive — prefer TF_VAR_litellm_master_key / TF_VAR_litellm_license /
|
||||
# TF_VAR_ui_password so values stay out of any committed tfvars file.
|
||||
variable "litellm_master_key" {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@
|
|||
resource "google_service_account" "runtime" {
|
||||
account_id = "${local.name}-runtime"
|
||||
display_name = "LiteLLM Cloud Run runtime"
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = !var.create_runtime || var.billing_metrics_endpoint == "" || (
|
||||
var.billing_metrics_client_cert_pem != "" && var.billing_metrics_client_key_pem != ""
|
||||
)
|
||||
error_message = "billing_metrics_client_cert_pem and billing_metrics_client_key_pem are both required when billing_metrics_endpoint is set and create_runtime is true."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# UI runtime SA — no role bindings. The UI is static nginx with no DB,
|
||||
|
|
@ -14,6 +23,8 @@ resource "google_service_account" "runtime" {
|
|||
# project's serverless service agent (not this SA), so it doesn't need
|
||||
# artifactregistry.reader either.
|
||||
resource "google_service_account" "ui_runtime" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
account_id = "${local.name}-ui-runtime"
|
||||
display_name = "LiteLLM Cloud Run UI runtime (no data-plane access)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,77 +14,93 @@ locals {
|
|||
}
|
||||
|
||||
resource "google_compute_global_address" "lb" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-lb-ip"
|
||||
labels = local.labels
|
||||
}
|
||||
|
||||
# Serverless NEGs — one per Cloud Run service.
|
||||
resource "google_compute_region_network_endpoint_group" "gateway" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-gateway-neg"
|
||||
region = var.region
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.gateway.name
|
||||
service = google_cloud_run_v2_service.gateway[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_region_network_endpoint_group" "backend" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-backend-neg"
|
||||
region = var.region
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.backend.name
|
||||
service = google_cloud_run_v2_service.backend[0].name
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_region_network_endpoint_group" "ui" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-ui-neg"
|
||||
region = var.region
|
||||
network_endpoint_type = "SERVERLESS"
|
||||
|
||||
cloud_run {
|
||||
service = google_cloud_run_v2_service.ui.name
|
||||
service = google_cloud_run_v2_service.ui[0].name
|
||||
}
|
||||
}
|
||||
|
||||
# Backend services wrap each NEG.
|
||||
resource "google_compute_backend_service" "gateway" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-gateway-bs"
|
||||
protocol = "HTTP"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.gateway.id
|
||||
group = google_compute_region_network_endpoint_group.gateway[0].id
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_backend_service" "backend" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-backend-bs"
|
||||
protocol = "HTTP"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.backend.id
|
||||
group = google_compute_region_network_endpoint_group.backend[0].id
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_backend_service" "ui" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-ui-bs"
|
||||
protocol = "HTTP"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
|
||||
backend {
|
||||
group = google_compute_region_network_endpoint_group.ui.id
|
||||
group = google_compute_region_network_endpoint_group.ui[0].id
|
||||
}
|
||||
}
|
||||
|
||||
# URL map. Default → backend (management API). Path matchers route the
|
||||
# gateway and UI prefixes elsewhere.
|
||||
resource "google_compute_url_map" "this" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = local.name
|
||||
default_service = google_compute_backend_service.backend.id
|
||||
default_service = google_compute_backend_service.backend[0].id
|
||||
|
||||
host_rule {
|
||||
hosts = ["*"]
|
||||
|
|
@ -93,13 +109,13 @@ resource "google_compute_url_map" "this" {
|
|||
|
||||
path_matcher {
|
||||
name = "main"
|
||||
default_service = google_compute_backend_service.backend.id
|
||||
default_service = google_compute_backend_service.backend[0].id
|
||||
|
||||
# UI paths (catch them before any /v1/* gateway rules so /favicon.ico
|
||||
# and / take precedence).
|
||||
path_rule {
|
||||
paths = local.ui_path_prefixes
|
||||
service = google_compute_backend_service.ui.id
|
||||
service = google_compute_backend_service.ui[0].id
|
||||
}
|
||||
|
||||
# Gateway path prefixes. GCP URL maps cap a path_rule at 10 path globs,
|
||||
|
|
@ -108,7 +124,7 @@ resource "google_compute_url_map" "this" {
|
|||
for_each = { for idx, chunk in chunklist(local.gateway_path_prefixes, 10) : idx => chunk }
|
||||
content {
|
||||
paths = path_rule.value
|
||||
service = google_compute_backend_service.gateway.id
|
||||
service = google_compute_backend_service.gateway[0].id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +134,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 = local.tls_enabled ? 1 : 0
|
||||
count = var.create_runtime && local.tls_enabled ? 1 : 0
|
||||
name = "${local.name}-redirect"
|
||||
|
||||
default_url_redirect {
|
||||
|
|
@ -129,8 +145,10 @@ resource "google_compute_url_map" "https_redirect" {
|
|||
}
|
||||
|
||||
resource "google_compute_target_http_proxy" "this" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-http"
|
||||
url_map = local.tls_enabled ? google_compute_url_map.https_redirect[0].id : google_compute_url_map.this.id
|
||||
url_map = local.tls_enabled ? google_compute_url_map.https_redirect[0].id : google_compute_url_map.this[0].id
|
||||
|
||||
# Default-deny on the HTTP-only path: TLS is the supported posture.
|
||||
# Operators must either supply DNS names or explicitly opt in.
|
||||
|
|
@ -143,12 +161,14 @@ resource "google_compute_target_http_proxy" "this" {
|
|||
}
|
||||
|
||||
resource "google_compute_global_forwarding_rule" "http" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-http"
|
||||
ip_protocol = "TCP"
|
||||
port_range = "80"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
ip_address = google_compute_global_address.lb.address
|
||||
target = google_compute_target_http_proxy.this.id
|
||||
ip_address = google_compute_global_address.lb[0].address
|
||||
target = google_compute_target_http_proxy.this[0].id
|
||||
labels = local.labels
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +181,7 @@ resource "google_compute_global_forwarding_rule" "http" {
|
|||
# transitions to ACTIVE.
|
||||
|
||||
resource "google_compute_managed_ssl_certificate" "this" {
|
||||
count = local.tls_enabled ? 1 : 0
|
||||
count = var.create_runtime && local.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
|
||||
|
|
@ -181,19 +201,19 @@ resource "google_compute_managed_ssl_certificate" "this" {
|
|||
}
|
||||
|
||||
resource "google_compute_target_https_proxy" "this" {
|
||||
count = local.tls_enabled ? 1 : 0
|
||||
count = var.create_runtime && local.tls_enabled ? 1 : 0
|
||||
name = "${local.name}-https"
|
||||
url_map = google_compute_url_map.this.id
|
||||
url_map = google_compute_url_map.this[0].id
|
||||
ssl_certificates = [google_compute_managed_ssl_certificate.this[0].id]
|
||||
}
|
||||
|
||||
resource "google_compute_global_forwarding_rule" "https" {
|
||||
count = local.tls_enabled ? 1 : 0
|
||||
count = var.create_runtime && local.tls_enabled ? 1 : 0
|
||||
name = "${local.name}-https"
|
||||
ip_protocol = "TCP"
|
||||
port_range = "443"
|
||||
load_balancing_scheme = "EXTERNAL_MANAGED"
|
||||
ip_address = google_compute_global_address.lb.address
|
||||
ip_address = google_compute_global_address.lb[0].address
|
||||
target = google_compute_target_https_proxy.this[0].id
|
||||
labels = local.labels
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ locals {
|
|||
var.labels,
|
||||
)
|
||||
|
||||
create_network = var.network_id == ""
|
||||
network_id = local.create_network ? google_compute_network.this[0].id : var.network_id
|
||||
|
||||
gateway_path_prefixes = [
|
||||
"/v1/chat/*", "/chat/*",
|
||||
"/v1/completions*", "/completions*",
|
||||
|
|
@ -74,7 +77,7 @@ locals {
|
|||
"/ui/*",
|
||||
]
|
||||
|
||||
proxy_config_enabled = length(keys(var.proxy_config)) > 0
|
||||
proxy_config_enabled = var.create_runtime && length(keys(var.proxy_config)) > 0
|
||||
proxy_config_yaml = local.proxy_config_enabled ? yamlencode(var.proxy_config) : ""
|
||||
|
||||
proxy_config_mount_path = "/etc/litellm"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
resource "google_compute_network" "this" {
|
||||
count = local.create_network ? 1 : 0
|
||||
|
||||
name = local.name
|
||||
auto_create_subnetworks = false
|
||||
routing_mode = "REGIONAL"
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "this" {
|
||||
count = local.create_network ? 1 : 0
|
||||
|
||||
name = "${local.name}-${var.region}"
|
||||
region = var.region
|
||||
network = google_compute_network.this.id
|
||||
network = google_compute_network.this[0].id
|
||||
ip_cidr_range = var.subnet_cidr
|
||||
private_ip_google_access = true
|
||||
}
|
||||
|
|
@ -16,17 +20,21 @@ resource "google_compute_subnetwork" "this" {
|
|||
# managed services peer with the VPC over the connection below using
|
||||
# addresses from this range.
|
||||
resource "google_compute_global_address" "psa" {
|
||||
count = var.create_psa_connection ? 1 : 0
|
||||
|
||||
name = "${local.name}-psa"
|
||||
purpose = "VPC_PEERING"
|
||||
address_type = "INTERNAL"
|
||||
prefix_length = 16
|
||||
network = google_compute_network.this.id
|
||||
network = local.network_id
|
||||
}
|
||||
|
||||
resource "google_service_networking_connection" "psa" {
|
||||
network = google_compute_network.this.id
|
||||
count = var.create_psa_connection ? 1 : 0
|
||||
|
||||
network = local.network_id
|
||||
service = "servicenetworking.googleapis.com"
|
||||
reserved_peering_ranges = [google_compute_global_address.psa.name]
|
||||
reserved_peering_ranges = [google_compute_global_address.psa[0].name]
|
||||
}
|
||||
|
||||
# Serverless VPC Access connector — required so Cloud Run can reach
|
||||
|
|
@ -37,9 +45,11 @@ resource "google_service_networking_connection" "psa" {
|
|||
# for low-to-moderate Cloud Run egress; bump max if your services push
|
||||
# heavy private-network traffic.
|
||||
resource "google_vpc_access_connector" "this" {
|
||||
count = var.create_runtime ? 1 : 0
|
||||
|
||||
name = "${local.name}-conn"
|
||||
region = var.region
|
||||
network = google_compute_network.this.name
|
||||
network = local.network_id
|
||||
ip_cidr_range = var.vpc_connector_cidr
|
||||
min_instances = 2
|
||||
max_instances = 3
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
output "lb_ip" {
|
||||
description = "Global anycast IP of the external HTTPS load balancer."
|
||||
value = google_compute_global_address.lb.address
|
||||
description = "Global anycast IP of the external HTTPS load balancer. Null when create_runtime is false."
|
||||
value = var.create_runtime ? one(google_compute_global_address.lb[*].address) : null
|
||||
}
|
||||
|
||||
output "lb_url" {
|
||||
description = "Proxy URL. 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/*."
|
||||
value = local.tls_enabled ? "https://${var.lb_domains[0]}" : "http://${google_compute_global_address.lb.address}"
|
||||
description = "Proxy URL, or null when create_runtime is false. Switches scheme based on whether lb_domains is set."
|
||||
value = var.create_runtime ? (local.tls_enabled ? "https://${var.lb_domains[0]}" : "http://${one(google_compute_global_address.lb[*].address)}") : null
|
||||
}
|
||||
|
||||
output "gateway_service_url" {
|
||||
description = "Default Cloud Run URL for the gateway (bypasses the LB)."
|
||||
value = google_cloud_run_v2_service.gateway.uri
|
||||
description = "Default Cloud Run URL for the gateway, or null when create_runtime is false."
|
||||
value = var.create_runtime ? one(google_cloud_run_v2_service.gateway[*].uri) : null
|
||||
}
|
||||
|
||||
output "backend_service_url" {
|
||||
description = "Default Cloud Run URL for the backend (bypasses the LB)."
|
||||
value = google_cloud_run_v2_service.backend.uri
|
||||
description = "Default Cloud Run URL for the backend, or null when create_runtime is false."
|
||||
value = var.create_runtime ? one(google_cloud_run_v2_service.backend[*].uri) : null
|
||||
}
|
||||
|
||||
output "ui_service_url" {
|
||||
description = "Default Cloud Run URL for the UI (bypasses the LB)."
|
||||
value = google_cloud_run_v2_service.ui.uri
|
||||
description = "Default Cloud Run URL for the UI, or null when create_runtime is false."
|
||||
value = var.create_runtime ? one(google_cloud_run_v2_service.ui[*].uri) : null
|
||||
}
|
||||
|
||||
output "cloudsql_writer_ip" {
|
||||
|
|
@ -38,6 +38,36 @@ output "redis_endpoint" {
|
|||
value = "${google_redis_instance.this.host}:${google_redis_instance.this.port}"
|
||||
}
|
||||
|
||||
output "runtime_service_account_email" {
|
||||
description = "Runtime service account email for Cloud Run or GKE Workload Identity."
|
||||
value = google_service_account.runtime.email
|
||||
}
|
||||
|
||||
output "redis_host" {
|
||||
description = "Memorystore Redis host."
|
||||
value = google_redis_instance.this.host
|
||||
}
|
||||
|
||||
output "redis_port" {
|
||||
description = "Memorystore Redis port."
|
||||
value = google_redis_instance.this.port
|
||||
}
|
||||
|
||||
output "redis_server_ca_pem" {
|
||||
description = "Memorystore server CA PEM. Mount it in the pod and set REDIS_SSL=true and REDIS_SSL_CA_CERTS=<path> via extraEnv when transit encryption is enabled."
|
||||
value = var.redis_transit_encryption ? google_redis_instance.this.server_ca_certs[0].cert : null
|
||||
}
|
||||
|
||||
output "db_username" {
|
||||
description = "Cloud SQL application username."
|
||||
value = var.db_username
|
||||
}
|
||||
|
||||
output "db_name" {
|
||||
description = "Cloud SQL database name."
|
||||
value = var.db_name
|
||||
}
|
||||
|
||||
output "gcs_bucket" {
|
||||
description = "GCS bucket name. Exposed to gateway + backend as GCS_BUCKET_NAME. Reference from proxy_config via `os.environ/GCS_BUCKET_NAME`."
|
||||
value = google_storage_bucket.this.name
|
||||
|
|
@ -54,11 +84,11 @@ output "db_password_secret_id" {
|
|||
}
|
||||
|
||||
output "migration_run_command" {
|
||||
description = "Shell command that executes the one-off migration job against Cloud SQL. Run this once after the first apply."
|
||||
value = format(
|
||||
description = "Shell command that executes the one-off migration job against Cloud SQL, or null when create_runtime is false."
|
||||
value = var.create_runtime ? format(
|
||||
"gcloud run jobs execute %s --region %s --project %s --wait",
|
||||
google_cloud_run_v2_job.migrations.name,
|
||||
one(google_cloud_run_v2_job.migrations[*].name),
|
||||
var.region,
|
||||
var.project_id,
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ resource "google_redis_instance" "this" {
|
|||
memory_size_gb = var.redis_memory_size_gb
|
||||
region = var.region
|
||||
|
||||
authorized_network = google_compute_network.this.id
|
||||
authorized_network = local.network_id
|
||||
connect_mode = "PRIVATE_SERVICE_ACCESS"
|
||||
|
||||
redis_version = "REDIS_7_0"
|
||||
|
|
@ -16,7 +16,7 @@ resource "google_redis_instance" "this" {
|
|||
# and passed to the proxy as REDIS_CA_PEM_B64); the proxy decodes it to
|
||||
# /tmp/redis-ca.pem at startup and uses it to validate the rediss://
|
||||
# handshake. Mirrors `transit_encryption_enabled = true` on AWS.
|
||||
transit_encryption_mode = "SERVER_AUTHENTICATION"
|
||||
transit_encryption_mode = var.redis_transit_encryption ? "SERVER_AUTHENTICATION" : "DISABLED"
|
||||
|
||||
depends_on = [google_service_networking_connection.psa]
|
||||
}
|
||||
|
|
|
|||
175
terraform/litellm/gcp/tests/deps_only.tftest.hcl
Normal file
175
terraform/litellm/gcp/tests/deps_only.tftest.hcl
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
mock_provider "google" {
|
||||
mock_resource "google_redis_instance" {
|
||||
defaults = {
|
||||
host = "10.0.0.4"
|
||||
port = 6379
|
||||
server_ca_certs = [{
|
||||
cert = "-----BEGIN CERTIFICATE-----\nmock\n-----END CERTIFICATE-----"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mock_provider "google-beta" {}
|
||||
mock_provider "random" {}
|
||||
|
||||
variables {
|
||||
project_id = "test-project"
|
||||
tenant = "tenant"
|
||||
env = "test"
|
||||
allow_plaintext_lb = true
|
||||
image_registry = "us-central1-docker.pkg.dev/test-project/litellm"
|
||||
}
|
||||
|
||||
run "default_creates_everything" {
|
||||
command = plan
|
||||
|
||||
assert {
|
||||
condition = alltrue([
|
||||
length(google_compute_network.this) == 1,
|
||||
length(google_compute_subnetwork.this) == 1,
|
||||
length(google_compute_global_address.psa) == 1,
|
||||
length(google_service_networking_connection.psa) == 1,
|
||||
length(google_vpc_access_connector.this) == 1,
|
||||
length(google_cloud_run_v2_service.gateway) == 1,
|
||||
length(google_cloud_run_v2_service.backend) == 1,
|
||||
length(google_cloud_run_v2_service.ui) == 1,
|
||||
length(google_cloud_run_v2_job.migrations) == 1,
|
||||
length(google_compute_global_address.lb) == 1,
|
||||
length(terraform_data.migration) == 1,
|
||||
])
|
||||
error_message = "The default mode must create networking, runtime services, the load balancer, and migrations."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = google_redis_instance.this.transit_encryption_mode == "SERVER_AUTHENTICATION"
|
||||
error_message = "Redis transit encryption must remain enabled by default."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.shared_env_kv) == 12
|
||||
error_message = "The default runtime environment must include GCS and the three Redis TLS entries."
|
||||
}
|
||||
}
|
||||
|
||||
run "deps_only_creates_no_runtime" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
create_runtime = false
|
||||
proxy_config = {
|
||||
model_list = []
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = alltrue([
|
||||
length(google_cloud_run_v2_service.gateway) == 0,
|
||||
length(google_cloud_run_v2_service.backend) == 0,
|
||||
length(google_cloud_run_v2_service.ui) == 0,
|
||||
length(google_cloud_run_v2_job.migrations) == 0,
|
||||
length(google_cloud_run_v2_service_iam_member.gateway_allusers) == 0,
|
||||
length(google_cloud_run_v2_service_iam_member.backend_allusers) == 0,
|
||||
length(google_cloud_run_v2_service_iam_member.ui_allusers) == 0,
|
||||
length(google_compute_global_address.lb) == 0,
|
||||
length(google_compute_region_network_endpoint_group.gateway) == 0,
|
||||
length(google_compute_region_network_endpoint_group.backend) == 0,
|
||||
length(google_compute_region_network_endpoint_group.ui) == 0,
|
||||
length(google_compute_backend_service.gateway) == 0,
|
||||
length(google_compute_backend_service.backend) == 0,
|
||||
length(google_compute_backend_service.ui) == 0,
|
||||
length(google_compute_url_map.this) == 0,
|
||||
length(google_compute_url_map.https_redirect) == 0,
|
||||
length(google_compute_target_http_proxy.this) == 0,
|
||||
length(google_compute_global_forwarding_rule.http) == 0,
|
||||
length(google_compute_managed_ssl_certificate.this) == 0,
|
||||
length(google_compute_target_https_proxy.this) == 0,
|
||||
length(google_compute_global_forwarding_rule.https) == 0,
|
||||
length(terraform_data.migration) == 0,
|
||||
length(google_vpc_access_connector.this) == 0,
|
||||
length(google_service_account.ui_runtime) == 0,
|
||||
length(google_storage_bucket.proxy_config) == 0,
|
||||
])
|
||||
error_message = "Dependencies-only mode must omit all runtime, load balancer, connector, UI identity, and proxy config resources."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = alltrue([
|
||||
google_sql_database_instance.writer.name == "tenant-litellm-test",
|
||||
google_sql_database_instance.reader.name == "tenant-litellm-test-reader",
|
||||
google_redis_instance.this.name == "tenant-litellm-test",
|
||||
google_storage_bucket.this.force_destroy == false,
|
||||
google_secret_manager_secret.master_key.secret_id == "tenant-litellm-test-master-key",
|
||||
google_secret_manager_secret.db_password.secret_id == "tenant-litellm-test-db-password",
|
||||
google_service_account.runtime.account_id == "tenant-litellm-test-runtime",
|
||||
])
|
||||
error_message = "Dependencies-only mode must retain data stores, secrets, and the runtime service account."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.lb_url == null && output.migration_run_command == null
|
||||
error_message = "Runtime outputs must be null while dependency outputs remain available."
|
||||
}
|
||||
}
|
||||
|
||||
run "existing_network_attaches_data_stores" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
network_id = "projects/host-proj/global/networks/shared"
|
||||
create_psa_connection = false
|
||||
create_runtime = false
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = alltrue([
|
||||
length(google_compute_network.this) == 0,
|
||||
length(google_compute_subnetwork.this) == 0,
|
||||
length(google_compute_global_address.psa) == 0,
|
||||
length(google_service_networking_connection.psa) == 0,
|
||||
google_sql_database_instance.writer.settings[0].ip_configuration[0].private_network == var.network_id,
|
||||
google_redis_instance.this.authorized_network == var.network_id,
|
||||
])
|
||||
error_message = "An existing VPC must receive the Cloud SQL and Memorystore private-network attachments."
|
||||
}
|
||||
}
|
||||
|
||||
run "psa_required_without_existing_network" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
create_psa_connection = false
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
google_sql_database_instance.writer,
|
||||
]
|
||||
}
|
||||
|
||||
run "redis_plaintext_drops_tls_env" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
redis_transit_encryption = false
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = google_redis_instance.this.transit_encryption_mode == "DISABLED"
|
||||
error_message = "Redis transit encryption must be disabled when requested."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.shared_env_kv) == 9
|
||||
error_message = "Plaintext Redis mode must include GCS and omit the three Redis TLS entries."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length([for env in local.shared_env_kv : env if env.name == "REDIS_SSL"]) == 0
|
||||
error_message = "Plaintext Redis mode must not set REDIS_SSL."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.redis_ca_fragment) == 0
|
||||
error_message = "Plaintext Redis mode must not decode a Redis CA at startup."
|
||||
}
|
||||
}
|
||||
|
|
@ -79,16 +79,42 @@ variable "ui_password" {
|
|||
sensitive = true
|
||||
}
|
||||
|
||||
# ---------- Deployment mode ----------
|
||||
|
||||
variable "create_runtime" {
|
||||
description = "Create Cloud Run, load balancer, VPC connector, runtime support resources, and the migration job. Set false for GKE or another external runtime."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "network_id" {
|
||||
description = "Existing VPC network resource ID (`projects/<host-project>/global/networks/<name>`). When set, no VPC or subnet is created. A VPC connector requires this network to be in the deployment project when create_runtime is true."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "create_psa_connection" {
|
||||
description = "Create the Private Services Access range and connection for Cloud SQL and Memorystore. Set false when the existing network already has PSA configured."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "redis_transit_encryption" {
|
||||
description = "Enable Memorystore transit encryption and inject Redis TLS settings into Cloud Run. Set false to use plaintext Redis."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
# ---------- Networking ----------
|
||||
|
||||
variable "subnet_cidr" {
|
||||
description = "Primary CIDR block for the LiteLLM subnet."
|
||||
description = "Primary CIDR block for the LiteLLM subnet. Unused when network_id is set."
|
||||
type = string
|
||||
default = "10.40.0.0/16"
|
||||
}
|
||||
|
||||
variable "vpc_connector_cidr" {
|
||||
description = "CIDR for the Serverless VPC Access connector. /28 required."
|
||||
description = "CIDR for the Serverless VPC Access connector. /28 required. Unused when create_runtime is false."
|
||||
type = string
|
||||
default = "10.41.0.0/28"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue