From b8d6ff7eb3398f4f0fae959dbfbf6f3d57862116 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Sat, 16 May 2026 18:44:44 -0700 Subject: [PATCH] refactor: convert AWS and GCP Terraform stacks into reusable modules with examples/default entry point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove `provider` blocks from both AWS and GCP stack roots so the modules can be consumed with `count`, `for_each`, `depends_on`, assumed-role or aliased providers — patterns that are forbidden when a module owns its own provider configuration - Add `examples/default/` thin-root wrappers for both stacks that wire the provider (AWS) / providers (google + google-beta) and call the module with a curated variable surface, preserving the one-command deploy experience - Move `terraform.tfvars.example` files into `examples/default/` alongside the new roots; update example comments to reflect the curated variable surface - Thread `local.tags` (containing `litellm:stack`, `managed-by`, and `var.tags`) explicitly onto every taggable AWS resource since the module no longer controls the provider's `default_tags`; GCP resource labels already flow through the module's `labels` input - Add `examples/default/variables.tf` and `outputs.tf` for both stacks, exposing the most-used knobs and re-exporting all module outputs - Commit provider lock files for both examples so `terraform init` is reproducible without a network fetch - Update top-level and per-stack READMEs to document the module-first design, the `for_each` multi-tenant pattern, and the `examples/default/` quick-start path --- terraform/litellm/README.md | 30 ++++- terraform/litellm/aws/README.md | 64 +++++++++- terraform/litellm/aws/alb.tf | 18 +++ terraform/litellm/aws/bootstrap.tf | 6 + terraform/litellm/aws/ecs.tf | 22 ++++ .../aws/examples/default/.terraform.lock.hcl | 46 +++++++ .../litellm/aws/examples/default/main.tf | 40 ++++++ .../litellm/aws/examples/default/outputs.tf | 54 ++++++++ .../litellm/aws/examples/default/providers.tf | 18 +++ .../default}/terraform.tfvars.example | 19 +-- .../litellm/aws/examples/default/variables.tf | 98 ++++++++++++++ .../litellm/aws/examples/default/versions.tf | 14 ++ terraform/litellm/aws/iam.tf | 10 ++ terraform/litellm/aws/locals.tf | 14 ++ terraform/litellm/aws/migrations.tf | 2 + terraform/litellm/aws/network.tf | 24 ++-- terraform/litellm/aws/providers.tf | 13 -- terraform/litellm/aws/rds.tf | 10 ++ terraform/litellm/aws/redis.tf | 4 + terraform/litellm/aws/s3.tf | 4 + terraform/litellm/aws/secrets.tf | 8 ++ terraform/litellm/aws/variables.tf | 2 +- terraform/litellm/gcp/README.md | 57 ++++++++- terraform/litellm/gcp/cloudsql.tf | 15 +++ .../gcp/examples/default/.terraform.lock.hcl | 63 +++++++++ .../litellm/gcp/examples/default/main.tf | 45 +++++++ .../litellm/gcp/examples/default/outputs.tf | 59 +++++++++ .../litellm/gcp/examples/default/providers.tf | 17 +++ .../default}/terraform.tfvars.example | 16 +-- .../litellm/gcp/examples/default/variables.tf | 120 ++++++++++++++++++ .../litellm/gcp/examples/default/versions.tf | 18 +++ terraform/litellm/gcp/load_balancer.tf | 13 +- terraform/litellm/gcp/providers.tf | 9 -- 33 files changed, 880 insertions(+), 72 deletions(-) create mode 100644 terraform/litellm/aws/examples/default/.terraform.lock.hcl create mode 100644 terraform/litellm/aws/examples/default/main.tf create mode 100644 terraform/litellm/aws/examples/default/outputs.tf create mode 100644 terraform/litellm/aws/examples/default/providers.tf rename terraform/litellm/aws/{ => examples/default}/terraform.tfvars.example (79%) create mode 100644 terraform/litellm/aws/examples/default/variables.tf create mode 100644 terraform/litellm/aws/examples/default/versions.tf delete mode 100644 terraform/litellm/aws/providers.tf create mode 100644 terraform/litellm/gcp/examples/default/.terraform.lock.hcl create mode 100644 terraform/litellm/gcp/examples/default/main.tf create mode 100644 terraform/litellm/gcp/examples/default/outputs.tf create mode 100644 terraform/litellm/gcp/examples/default/providers.tf rename terraform/litellm/gcp/{ => examples/default}/terraform.tfvars.example (81%) create mode 100644 terraform/litellm/gcp/examples/default/variables.tf create mode 100644 terraform/litellm/gcp/examples/default/versions.tf delete mode 100644 terraform/litellm/gcp/providers.tf diff --git a/terraform/litellm/README.md b/terraform/litellm/README.md index 5ca704b96dd..f1fa455f65e 100644 --- a/terraform/litellm/README.md +++ b/terraform/litellm/README.md @@ -1,18 +1,34 @@ # LiteLLM Terraform stacks -Two self-contained Terraform root modules that deploy the **componentized** -LiteLLM proxy — the gateway, backend, and UI as three independent containers -(see `helm/litellm/` for the canonical chart with the same split). +Two self-contained, reusable Terraform **modules** that deploy the +**componentized** LiteLLM proxy — the gateway, backend, and UI as three +independent containers (see `helm/litellm/` for the canonical chart with the +same split). + +Each module declares **no `provider` block of its own**, so it can be called +with `count` / `for_each` / `depends_on` and the caller controls region, +assume-role / impersonation, aliases, and `default_tags`. A ready-to-run root +that wires the provider lives at `/examples/default/` — that's the +one-command deploy path. To embed a stack in your own config, call the module +by source: + +```hcl +module "litellm" { + source = "github.com/BerriAI/litellm//terraform/litellm/aws?ref=" + # ... inputs ... +} +``` | Stack | Compute | Database (writer + reader) | Cache | Object store | Public entrypoint | | ------ | ----------- | ---------------------------------- | ----------- | ------------ | ------------------ | | `aws/` | ECS Fargate | Aurora Postgres (IAM auth) | ElastiCache | S3 | Application LB | | `gcp/` | Cloud Run | Cloud SQL Postgres (password auth) | Memorystore | GCS | External HTTPS LB | -Each stack creates its own VPC and managed data stores — drop in a tfvars -file and run `terraform apply`. Both stacks support a typed `proxy_config` -input (mirrors `helm/litellm`'s `gateway.config.proxy_config`) and per-component -extra env vars / secret-manager refs. +Each stack creates its own VPC and managed data stores — from +`/examples/default/`, drop in a tfvars file and run `terraform apply`. +Both stacks support a typed `proxy_config` input (mirrors `helm/litellm`'s +`gateway.config.proxy_config`) and per-component extra env vars / +secret-manager refs. ## Components diff --git a/terraform/litellm/aws/README.md b/terraform/litellm/aws/README.md index 8638ea800ec..80ee18c667d 100644 --- a/terraform/litellm/aws/README.md +++ b/terraform/litellm/aws/README.md @@ -132,10 +132,11 @@ pair differs: | `acme` | `prod` | `acme-litellm-prod-master-key` | | `globex` | `dev` | `globex-litellm-dev-license` | -For a per-tenant instance, the only inputs that change are the tenant -slug, env, and the two pre-issued secrets: +For a per-tenant instance via the example root, the only inputs that +change are the tenant slug, env, and the two pre-issued secrets: ```bash +cd terraform/litellm/aws/examples/default export TF_VAR_litellm_master_key="sk-..." # the tenant's master key export TF_VAR_litellm_license="lic-..." # their LITELLM_LICENSE @@ -146,6 +147,22 @@ terraform apply \ -var "env=stage" ``` +To run *many* tenants from a single config, call the module with +`for_each` instead of one root per tenant (see "Using as a module"): + +```hcl +module "litellm" { + for_each = toset(["acme", "globex"]) + source = "github.com/BerriAI/litellm//terraform/litellm/aws?ref=" + tenant = each.key + env = "prod" + region = "us-west-2" + azs = ["us-west-2a", "us-west-2b"] +} +``` +(This `for_each` form is only possible because the module declares no +provider block — the original root-with-provider layout forbade it.) + Both `litellm_master_key` and `litellm_license` are optional: - Omit `litellm_master_key` → the stack auto-generates a random `sk-…` value (trial/dev path). @@ -159,14 +176,21 @@ example files. ## Quick start ```bash -cd terraform/litellm/aws +cd terraform/litellm/aws/examples/default cp terraform.tfvars.example terraform.tfvars -# Edit: region, tenant, env, azs, *_image, proxy_config, gateway_extra_secrets. +# Edit: region, tenant, env, azs, proxy_config, gateway_extra_secrets. terraform init terraform apply ``` +`examples/default/` is a thin root that configures the `aws` provider and +calls the module (`../../`). It exposes a curated variable surface; for +advanced knobs (per-component CPU/memory/workers, autoscaling, RDS/Redis +sizing, per-component image pins) set them on the `module "litellm"` block +in `examples/default/main.tf`, or call the module from your own config — +see "Using as a module" below. + That single apply provisions everything, runs the DB user bootstrap, runs the schema migration, and only then starts the gateway/backend services. When it returns, the stack is serving traffic. @@ -179,6 +203,34 @@ aws secretsmanager get-secret-value \ --query SecretString --output text ``` +## Using as a module + +The directory itself is a module with **no `provider` block** — the caller +owns provider config. That means you can call it directly with `for_each` +(many tenants from one config), `count` (conditional stacks), `depends_on`, +an assume-role / aliased provider, etc.: + +```hcl +provider "aws" { + region = "us-west-2" + assume_role { role_arn = "arn:aws:iam::111122223333:role/deployer" } +} + +module "litellm" { + source = "github.com/BerriAI/litellm//terraform/litellm/aws?ref=" + + region = "us-west-2" + tenant = "acme" + env = "prod" + azs = ["us-west-2a", "us-west-2b"] + # ...any of the inputs in variables.tf... +} +``` + +Tags: the module threads its own `litellm:stack` / `managed-by` / `var.tags` +onto every taggable resource. Any `default_tags` on your provider merge on +top — set org-wide tags there, per-deployment tags via the `tags` input. + ## Image pulls The defaults pull from `ghcr.io/berriai/litellm-:v1.86.0-dev`, @@ -238,8 +290,8 @@ losing the contents. | File | What's in it | | ----------------- | --------------------------------------------------------------------- | -| `versions.tf` | Terraform + provider version constraints | -| `providers.tf` | AWS provider (region + default tags) | +| `versions.tf` | Terraform + `required_providers` constraints (module declares no provider config) | +| `examples/default/` | Thin root: `aws` provider + `default_tags` + a call to the module. The one-command deploy path. | | `variables.tf` | All input variables | | `locals.tf` | Path-prefix lists for ALB routing (mirror of `helm/.../ingress.yaml`) | | `network.tf` | VPC, subnets, IGW, NAT, route tables, security groups | diff --git a/terraform/litellm/aws/alb.tf b/terraform/litellm/aws/alb.tf index de0d9c2310f..786b9d9a5b9 100644 --- a/terraform/litellm/aws/alb.tf +++ b/terraform/litellm/aws/alb.tf @@ -6,6 +6,8 @@ resource "aws_lb" "this" { subnets = aws_subnet.public[*].id idle_timeout = 120 + + tags = local.tags } locals { @@ -35,6 +37,8 @@ resource "aws_lb_target_group" "gateway" { } deregistration_delay = 30 + + tags = local.tags } resource "aws_lb_target_group" "backend" { @@ -54,6 +58,8 @@ resource "aws_lb_target_group" "backend" { } deregistration_delay = 30 + + tags = local.tags } resource "aws_lb_target_group" "ui" { @@ -73,6 +79,8 @@ resource "aws_lb_target_group" "ui" { } deregistration_delay = 30 + + tags = local.tags } # HTTP listener. When TLS is enabled this only serves a permanent @@ -106,6 +114,8 @@ resource "aws_lb_listener" "http" { error_message = "ALB has no HTTPS listener. Either set `acm_certificate_arn` to enable TLS, or set `allow_plaintext_alb = true` to opt into HTTP-only (trial / dev only)." } } + + tags = local.tags } # HTTPS listener. Only created when an ACM cert ARN is supplied — terminates @@ -122,6 +132,8 @@ resource "aws_lb_listener" "https" { type = "forward" target_group_arn = aws_lb_target_group.backend.arn } + + tags = local.tags } # UI exact paths (/, /favicon.ico, /ui) — priority 10. @@ -139,6 +151,8 @@ resource "aws_lb_listener_rule" "ui_exact" { values = local.ui_exact_paths } } + + tags = local.tags } # UI prefix paths (/_next/*, /litellm-asset-prefix/*, /assets/*, /ui/*) — priority 20. @@ -156,6 +170,8 @@ resource "aws_lb_listener_rule" "ui_prefix" { values = local.ui_path_prefixes } } + + tags = local.tags } # Gateway prefix rules — one per chunk-of-5 because ALB caps a path-pattern @@ -176,4 +192,6 @@ resource "aws_lb_listener_rule" "gateway" { values = each.value } } + + tags = local.tags } diff --git a/terraform/litellm/aws/bootstrap.tf b/terraform/litellm/aws/bootstrap.tf index e9a56dedbb5..b0bc38d44fb 100644 --- a/terraform/litellm/aws/bootstrap.tf +++ b/terraform/litellm/aws/bootstrap.tf @@ -32,6 +32,8 @@ resource "aws_iam_policy" "bootstrap_secrets" { Resource = [aws_secretsmanager_secret.db_master_password.arn] }] }) + + tags = local.tags } resource "aws_iam_role_policy_attachment" "task_execution_bootstrap_secrets" { @@ -43,6 +45,8 @@ resource "aws_iam_role_policy_attachment" "task_execution_bootstrap_secrets" { resource "aws_cloudwatch_log_group" "bootstrap_db" { name = "/ecs/${local.name}/bootstrap-db" retention_in_days = var.log_retention_days + + tags = local.tags } locals { @@ -101,6 +105,8 @@ resource "aws_ecs_task_definition" "bootstrap_db" { } } }]) + + tags = local.tags } # ---------- Bootstrap trigger ---------- diff --git a/terraform/litellm/aws/ecs.tf b/terraform/litellm/aws/ecs.tf index a6d2350c681..aee8f0cfc73 100644 --- a/terraform/litellm/aws/ecs.tf +++ b/terraform/litellm/aws/ecs.tf @@ -5,26 +5,36 @@ resource "aws_ecs_cluster" "this" { name = "containerInsights" value = "enabled" } + + tags = local.tags } resource "aws_cloudwatch_log_group" "gateway" { name = "/ecs/${local.name}/gateway" retention_in_days = var.log_retention_days + + tags = local.tags } resource "aws_cloudwatch_log_group" "backend" { name = "/ecs/${local.name}/backend" retention_in_days = var.log_retention_days + + tags = local.tags } resource "aws_cloudwatch_log_group" "ui" { name = "/ecs/${local.name}/ui" retention_in_days = var.log_retention_days + + tags = local.tags } resource "aws_cloudwatch_log_group" "migrations" { name = "/ecs/${local.name}/migrations" retention_in_days = var.log_retention_days + + tags = local.tags } # Shared env block fed to gateway, backend, and the migration task. Mirrors @@ -169,6 +179,8 @@ resource "aws_ecs_task_definition" "gateway" { local.gateway_proxy_overrides, ) ]) + + tags = local.tags } resource "aws_ecs_service" "gateway" { @@ -206,6 +218,8 @@ resource "aws_ecs_service" "gateway" { aws_lb_listener.https, terraform_data.migration, ] + + tags = local.tags } # ---------- Backend ---------- @@ -246,6 +260,8 @@ resource "aws_ecs_task_definition" "backend" { local.backend_proxy_overrides, ) ]) + + tags = local.tags } resource "aws_ecs_service" "backend" { @@ -279,6 +295,8 @@ resource "aws_ecs_service" "backend" { aws_lb_listener.https, terraform_data.migration, ] + + tags = local.tags } # ---------- UI ---------- @@ -312,6 +330,8 @@ resource "aws_ecs_task_definition" "ui" { } } ]) + + tags = local.tags } resource "aws_ecs_service" "ui" { @@ -344,4 +364,6 @@ resource "aws_ecs_service" "ui" { aws_lb_listener.http, aws_lb_listener.https, ] + + tags = local.tags } diff --git a/terraform/litellm/aws/examples/default/.terraform.lock.hcl b/terraform/litellm/aws/examples/default/.terraform.lock.hcl new file mode 100644 index 00000000000..4a059b2b268 --- /dev/null +++ b/terraform/litellm/aws/examples/default/.terraform.lock.hcl @@ -0,0 +1,46 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.60" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.9.0" + constraints = "~> 3.6" + hashes = [ + "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=", + "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", + "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", + "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", + "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", + "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", + "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", + "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", + "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", + "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", + "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", + "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", + ] +} diff --git a/terraform/litellm/aws/examples/default/main.tf b/terraform/litellm/aws/examples/default/main.tf new file mode 100644 index 00000000000..0cbd48701aa --- /dev/null +++ b/terraform/litellm/aws/examples/default/main.tf @@ -0,0 +1,40 @@ +# One-command deploy of the LiteLLM AWS stack. +# +# cd terraform/litellm/aws/examples/default +# cp terraform.tfvars.example terraform.tfvars # edit it +# terraform init +# terraform apply +# +# This root just wires the provider (see providers.tf) to the module. The +# module itself (../../) declares no provider, so it can also be consumed +# from your own config with count/for_each/aliased or assume-role providers: +# +# module "litellm" { +# source = "github.com/BerriAI/litellm//terraform/litellm/aws?ref=" +# ... +# } +# +# Knobs not surfaced as variables here (per-component sizing, autoscaling, +# RDS/Redis tuning) can be set directly on this block — see ../../variables.tf. +module "litellm" { + source = "../../" + + region = var.region + tenant = var.tenant + env = var.env + azs = var.azs + + litellm_master_key = var.litellm_master_key + litellm_license = var.litellm_license + ui_password = var.ui_password + + acm_certificate_arn = var.acm_certificate_arn + allow_plaintext_alb = var.allow_plaintext_alb + s3_force_destroy = var.s3_force_destroy + + proxy_config = var.proxy_config + gateway_extra_env = var.gateway_extra_env + backend_extra_env = var.backend_extra_env + gateway_extra_secrets = var.gateway_extra_secrets + backend_extra_secrets = var.backend_extra_secrets +} diff --git a/terraform/litellm/aws/examples/default/outputs.tf b/terraform/litellm/aws/examples/default/outputs.tf new file mode 100644 index 00000000000..235c069933c --- /dev/null +++ b/terraform/litellm/aws/examples/default/outputs.tf @@ -0,0 +1,54 @@ +output "alb_dns_name" { + description = "Public DNS name of the LiteLLM ALB." + value = module.litellm.alb_dns_name +} + +output "alb_url" { + description = "Proxy URL. Dashboard at /, API at /v1/*." + value = module.litellm.alb_url +} + +output "ecs_cluster" { + description = "ECS cluster name." + value = module.litellm.ecs_cluster +} + +output "aurora_writer_endpoint" { + description = "Aurora writer endpoint." + value = module.litellm.aurora_writer_endpoint +} + +output "aurora_reader_endpoint" { + description = "Aurora reader endpoint." + value = module.litellm.aurora_reader_endpoint +} + +output "redis_endpoint" { + description = "ElastiCache Redis primary endpoint (TLS)." + value = module.litellm.redis_endpoint +} + +output "s3_bucket" { + description = "S3 bucket name." + value = module.litellm.s3_bucket +} + +output "master_key_secret_arn" { + description = "Secrets Manager ARN holding LITELLM_MASTER_KEY." + value = module.litellm.master_key_secret_arn +} + +output "db_master_password_secret_arn" { + description = "Secrets Manager ARN holding the Aurora master credentials (bootstrap-only)." + value = module.litellm.db_master_password_secret_arn +} + +output "db_bootstrap_sql" { + description = "Run once as the master DB user to create the IAM-authed app user." + value = module.litellm.db_bootstrap_sql +} + +output "migration_run_command" { + description = "Break-glass command to re-run the one-off prisma migration task." + value = module.litellm.migration_run_command +} diff --git a/terraform/litellm/aws/examples/default/providers.tf b/terraform/litellm/aws/examples/default/providers.tf new file mode 100644 index 00000000000..aaad4ae916d --- /dev/null +++ b/terraform/litellm/aws/examples/default/providers.tf @@ -0,0 +1,18 @@ +# The provider is configured HERE, in the root, not in the module. That is +# the whole point of the split: a module that declares its own configured +# `provider` block can't be called with count/for_each/depends_on and gives +# the caller no way to set assume-role, custom endpoints, or aliases. +# +# `default_tags` set here still flow into every resource the module creates +# (provider default_tags propagate through module calls) and merge with the +# module's own `litellm:stack` / `managed-by` / var.tags. Use this block for +# org-wide tags; use the module's `tags` input for per-deployment tags. +provider "aws" { + region = var.region + + default_tags { + tags = { + "managed-by" = "terraform" + } + } +} diff --git a/terraform/litellm/aws/terraform.tfvars.example b/terraform/litellm/aws/examples/default/terraform.tfvars.example similarity index 79% rename from terraform/litellm/aws/terraform.tfvars.example rename to terraform/litellm/aws/examples/default/terraform.tfvars.example index 2be573949ef..88d12cf26e2 100644 --- a/terraform/litellm/aws/terraform.tfvars.example +++ b/terraform/litellm/aws/examples/default/terraform.tfvars.example @@ -26,19 +26,12 @@ env = "stage" # non-empty bucket. Flip to true only for ephemeral / CI stacks. # s3_force_destroy = false -# Component images. Defaults pin all four to the same GHCR release tag — -# bump them together when bumping LiteLLM. Override here to pull from a -# private registry or to mix-and-match versions. -# gateway_image = "ghcr.io/berriai/litellm-gateway:1.86.0-dev" -# backend_image = "ghcr.io/berriai/litellm-backend:1.86.0-dev" -# ui_image = "ghcr.io/berriai/litellm-ui:1.86.0-dev" -# migrations_image = "ghcr.io/berriai/litellm-migrations:1.86.0-dev" - -# Per-task sizing for the gateway. Defaults are 1 vCPU / 4 GiB / 1 worker. -# uvicorn rule of thumb for CPU-bound work is (2 * vCPU) + 1 workers. -# gateway_cpu = 1024 # 1024 = 1 vCPU -# gateway_memory = 4096 # MiB -# gateway_num_workers = 1 +# Component images and per-task sizing/autoscaling are NOT exposed as +# variables in this example (it keeps the curated surface small). They +# default to working public GHCR images. To pin images or tune +# CPU/memory/workers/autoscaling, set those inputs directly on the +# `module "litellm"` block in main.tf — the full list is in +# ../../variables.tf — or call the module from your own root config. # ---------- proxy_config (mirrors helm gateway.config.proxy_config) ---------- # proxy_config = { diff --git a/terraform/litellm/aws/examples/default/variables.tf b/terraform/litellm/aws/examples/default/variables.tf new file mode 100644 index 00000000000..f8950ca2eca --- /dev/null +++ b/terraform/litellm/aws/examples/default/variables.tf @@ -0,0 +1,98 @@ +# Curated surface for the one-command deploy path. The module (../../) +# exposes far more knobs (per-component CPU/memory, autoscaling, RDS/Redis +# sizing, …). To tune those, set them directly on the `module "litellm"` +# block in main.tf, or call the module from your own root config. Full +# per-variable docs live in ../../variables.tf — the module is the source +# of truth; descriptions here are intentionally terse. + +variable "region" { + description = "AWS region to deploy into." + type = string +} + +variable "tenant" { + description = "Tenant slug — prefix for every resource (-litellm-)." + type = string +} + +variable "env" { + description = "Environment suffix (stage, prod, dev)." + type = string +} + +variable "azs" { + description = "Availability zones for subnets. At least 2 (RDS + ALB)." + type = list(string) +} + +# 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" { + description = "Pre-existing LITELLM_MASTER_KEY (sk-…). Empty → auto-generated." + type = string + default = "" + sensitive = true +} + +variable "litellm_license" { + description = "LiteLLM enterprise license. Empty → OSS-only." + type = string + default = "" + sensitive = true +} + +variable "ui_password" { + description = "UI admin password. Empty → falls back to LITELLM_MASTER_KEY." + type = string + default = "" + sensitive = true +} + +# TLS — provide an ACM cert for production, or opt into HTTP-only for dev. +variable "acm_certificate_arn" { + description = "ACM cert ARN for the ALB HTTPS listener. Empty → no TLS." + type = string + default = "" +} + +variable "allow_plaintext_alb" { + description = "Opt into HTTP-only ALB (trial/dev only)." + type = bool + default = false +} + +variable "s3_force_destroy" { + description = "Allow destroy of a non-empty S3 bucket (ephemeral/CI only)." + type = bool + default = false +} + +variable "proxy_config" { + description = "LiteLLM proxy config (contents of config.yaml). Empty → defaults." + type = any + default = {} +} + +variable "gateway_extra_env" { + description = "Plain-text env vars layered onto the gateway." + type = map(string) + default = {} +} + +variable "backend_extra_env" { + description = "Plain-text env vars layered onto the backend." + type = map(string) + default = {} +} + +variable "gateway_extra_secrets" { + description = "Gateway env vars sourced from Secrets Manager (name → ARN)." + type = map(string) + default = {} +} + +variable "backend_extra_secrets" { + description = "Backend env vars sourced from Secrets Manager (name → ARN)." + type = map(string) + default = {} +} diff --git a/terraform/litellm/aws/examples/default/versions.tf b/terraform/litellm/aws/examples/default/versions.tf new file mode 100644 index 00000000000..73b88e91dce --- /dev/null +++ b/terraform/litellm/aws/examples/default/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.60" + } + random = { + source = "hashicorp/random" + version = "~> 3.6" + } + } +} diff --git a/terraform/litellm/aws/iam.tf b/terraform/litellm/aws/iam.tf index 504e0fe1d63..f425f1a00e7 100644 --- a/terraform/litellm/aws/iam.tf +++ b/terraform/litellm/aws/iam.tf @@ -13,6 +13,8 @@ data "aws_iam_policy_document" "task_assume" { resource "aws_iam_role" "task_execution" { name = "${local.name}-task-execution" assume_role_policy = data.aws_iam_policy_document.task_assume.json + + tags = local.tags } resource "aws_iam_role_policy_attachment" "task_execution" { @@ -59,6 +61,8 @@ data "aws_iam_policy_document" "secrets_access" { resource "aws_iam_policy" "secrets_access" { name = "${local.name}-secrets-access" policy = data.aws_iam_policy_document.secrets_access.json + + tags = local.tags } resource "aws_iam_role_policy_attachment" "task_execution_secrets" { @@ -75,6 +79,8 @@ resource "aws_iam_role_policy_attachment" "task_execution_secrets" { resource "aws_iam_role" "task" { name = "${local.name}-task" assume_role_policy = data.aws_iam_policy_document.task_assume.json + + tags = local.tags } data "aws_caller_identity" "current" {} @@ -91,6 +97,8 @@ data "aws_iam_policy_document" "rds_iam_connect" { resource "aws_iam_policy" "rds_iam_connect" { name = "${local.name}-rds-iam-connect" policy = data.aws_iam_policy_document.rds_iam_connect.json + + tags = local.tags } resource "aws_iam_role_policy_attachment" "task_rds_iam_connect" { @@ -111,4 +119,6 @@ resource "aws_iam_role_policy_attachment" "task_rds_iam_connect" { resource "aws_iam_role" "ui_task" { name = "${local.name}-ui-task" assume_role_policy = data.aws_iam_policy_document.task_assume.json + + tags = local.tags } diff --git a/terraform/litellm/aws/locals.tf b/terraform/litellm/aws/locals.tf index 85c3b6eaaad..b5e28272d04 100644 --- a/terraform/litellm/aws/locals.tf +++ b/terraform/litellm/aws/locals.tf @@ -11,6 +11,20 @@ locals { # the stack can reference local.name. name = "${var.tenant}-litellm-${var.env}" + # This is a reusable module — it declares no `provider` block, so the AWS + # provider's `default_tags` is the caller's concern, not ours. To keep the + # same per-resource tagging the stack had when it owned the provider, the + # module threads `local.tags` onto every taggable resource itself. Callers + # may layer org-wide tags on top via their own provider `default_tags` + # (those merge with these). `var.tags` is the per-deployment override. + tags = merge( + { + "litellm:stack" = local.name + "managed-by" = "terraform" + }, + var.tags, + ) + gateway_path_prefixes = [ "/v1/chat/*", "/chat/*", "/v1/completions*", "/completions*", diff --git a/terraform/litellm/aws/migrations.tf b/terraform/litellm/aws/migrations.tf index fc4e2ce0cab..62880ebf165 100644 --- a/terraform/litellm/aws/migrations.tf +++ b/terraform/litellm/aws/migrations.tf @@ -42,4 +42,6 @@ resource "aws_ecs_task_definition" "migrations" { } } }]) + + tags = local.tags } diff --git a/terraform/litellm/aws/network.tf b/terraform/litellm/aws/network.tf index d5ed49c1b8a..2f104da6a6b 100644 --- a/terraform/litellm/aws/network.tf +++ b/terraform/litellm/aws/network.tf @@ -7,12 +7,12 @@ resource "aws_vpc" "this" { enable_dns_hostnames = true enable_dns_support = true - tags = { Name = local.name } + tags = merge(local.tags, { Name = local.name }) } resource "aws_internet_gateway" "this" { vpc_id = aws_vpc.this.id - tags = { Name = local.name } + tags = merge(local.tags, { Name = local.name }) } # Public subnets (ALB + NAT). One per AZ. @@ -23,7 +23,7 @@ resource "aws_subnet" "public" { availability_zone = var.azs[count.index] map_public_ip_on_launch = true - tags = { Name = "${local.name}-public-${var.azs[count.index]}" } + tags = merge(local.tags, { Name = "${local.name}-public-${var.azs[count.index]}" }) } # Private subnets (ECS tasks, RDS, ElastiCache). One per AZ, separate from @@ -34,12 +34,12 @@ resource "aws_subnet" "private" { cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10) availability_zone = var.azs[count.index] - tags = { Name = "${local.name}-private-${var.azs[count.index]}" } + tags = merge(local.tags, { Name = "${local.name}-private-${var.azs[count.index]}" }) } resource "aws_eip" "nat" { domain = "vpc" - tags = { Name = "${local.name}-nat" } + tags = merge(local.tags, { Name = "${local.name}-nat" }) depends_on = [aws_internet_gateway.this] } @@ -50,7 +50,7 @@ resource "aws_nat_gateway" "this" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public[0].id - tags = { Name = local.name } + tags = merge(local.tags, { Name = local.name }) depends_on = [aws_internet_gateway.this] } @@ -63,7 +63,7 @@ resource "aws_route_table" "public" { gateway_id = aws_internet_gateway.this.id } - tags = { Name = "${local.name}-public" } + tags = merge(local.tags, { Name = "${local.name}-public" }) } resource "aws_route_table_association" "public" { @@ -80,7 +80,7 @@ resource "aws_route_table" "private" { nat_gateway_id = aws_nat_gateway.this.id } - tags = { Name = "${local.name}-private" } + tags = merge(local.tags, { Name = "${local.name}-private" }) } resource "aws_route_table_association" "private" { @@ -119,6 +119,8 @@ resource "aws_security_group" "alb" { protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } + + tags = local.tags } resource "aws_security_group" "tasks" { @@ -141,6 +143,8 @@ resource "aws_security_group" "tasks" { protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } + + tags = local.tags } resource "aws_security_group" "rds" { @@ -155,6 +159,8 @@ resource "aws_security_group" "rds" { protocol = "tcp" security_groups = [aws_security_group.tasks.id] } + + tags = local.tags } resource "aws_security_group" "redis" { @@ -169,4 +175,6 @@ resource "aws_security_group" "redis" { protocol = "tcp" security_groups = [aws_security_group.tasks.id] } + + tags = local.tags } diff --git a/terraform/litellm/aws/providers.tf b/terraform/litellm/aws/providers.tf deleted file mode 100644 index 5e7d506c23f..00000000000 --- a/terraform/litellm/aws/providers.tf +++ /dev/null @@ -1,13 +0,0 @@ -provider "aws" { - region = var.region - - default_tags { - tags = merge( - { - "litellm:stack" = local.name - "managed-by" = "terraform" - }, - var.tags, - ) - } -} diff --git a/terraform/litellm/aws/rds.tf b/terraform/litellm/aws/rds.tf index 8e3b70a8d62..d9b7351a805 100644 --- a/terraform/litellm/aws/rds.tf +++ b/terraform/litellm/aws/rds.tf @@ -19,12 +19,16 @@ resource "aws_db_subnet_group" "this" { name = "${local.name}-db" subnet_ids = aws_subnet.private[*].id + + tags = local.tags } resource "aws_rds_cluster_parameter_group" "this" { name = "${local.name}-cluster-pg" family = "aurora-postgresql${split(".", var.db_engine_version)[0]}" description = "LiteLLM Aurora Postgres cluster parameters." + + tags = local.tags } resource "aws_rds_cluster" "this" { @@ -52,6 +56,8 @@ resource "aws_rds_cluster" "this" { backup_retention_period = 7 preferred_backup_window = "07:00-09:00" + + tags = local.tags } resource "aws_rds_cluster_instance" "writer" { @@ -67,6 +73,8 @@ resource "aws_rds_cluster_instance" "writer" { # Promotion tier 0 — first in line during failover, so this instance stays # the writer unless it goes unhealthy. promotion_tier = 0 + + tags = local.tags } resource "aws_rds_cluster_instance" "reader" { @@ -82,4 +90,6 @@ resource "aws_rds_cluster_instance" "reader" { # Higher promotion tier — won't be picked as writer during a failover # unless the writer instance itself is gone. promotion_tier = 15 + + tags = local.tags } diff --git a/terraform/litellm/aws/redis.tf b/terraform/litellm/aws/redis.tf index 2a6fab2d89f..071cbc6d46f 100644 --- a/terraform/litellm/aws/redis.tf +++ b/terraform/litellm/aws/redis.tf @@ -1,6 +1,8 @@ resource "aws_elasticache_subnet_group" "this" { name = "${local.name}-redis" subnet_ids = aws_subnet.private[*].id + + tags = local.tags } # Replication group (not aws_elasticache_cluster, which is the @@ -30,4 +32,6 @@ resource "aws_elasticache_replication_group" "this" { transit_encryption_enabled = true apply_immediately = true + + tags = local.tags } diff --git a/terraform/litellm/aws/s3.tf b/terraform/litellm/aws/s3.tf index 375bc73bb71..218949ebd03 100644 --- a/terraform/litellm/aws/s3.tf +++ b/terraform/litellm/aws/s3.tf @@ -18,6 +18,8 @@ resource "aws_s3_bucket" "this" { # cached responses, archived request logs, and /v1/files storage stay put. # Flip to true only for ephemeral / CI stacks (`var.s3_force_destroy`). force_destroy = var.s3_force_destroy + + tags = local.tags } resource "aws_s3_bucket_versioning" "this" { @@ -72,6 +74,8 @@ data "aws_iam_policy_document" "s3_access" { resource "aws_iam_policy" "s3_access" { name = "${local.name}-s3-access" policy = data.aws_iam_policy_document.s3_access.json + + tags = local.tags } resource "aws_iam_role_policy_attachment" "task_s3_access" { diff --git a/terraform/litellm/aws/secrets.tf b/terraform/litellm/aws/secrets.tf index dd13fdc1239..300d38e4053 100644 --- a/terraform/litellm/aws/secrets.tf +++ b/terraform/litellm/aws/secrets.tf @@ -22,6 +22,8 @@ resource "aws_secretsmanager_secret" "master_key" { name = "${local.name}-master-key" description = "LITELLM_MASTER_KEY for gateway + backend." recovery_window_in_days = 0 + + tags = local.tags } resource "aws_secretsmanager_secret_version" "master_key" { @@ -40,6 +42,8 @@ resource "aws_secretsmanager_secret" "license" { name = "${local.name}-license" description = "LITELLM_LICENSE for gateway + backend." recovery_window_in_days = 0 + + tags = local.tags } resource "aws_secretsmanager_secret_version" "license" { @@ -59,6 +63,8 @@ resource "aws_secretsmanager_secret" "ui_password" { name = "${local.name}-ui-password" description = "UI_PASSWORD for the backend (UI admin login)." recovery_window_in_days = 0 + + tags = local.tags } resource "aws_secretsmanager_secret_version" "ui_password" { @@ -72,6 +78,8 @@ resource "aws_secretsmanager_secret" "db_master_password" { name = "${local.name}-db-master-password" description = "Aurora master-user password - bootstrap only. Runtime auth is IAM-token." recovery_window_in_days = 0 + + tags = local.tags } resource "aws_secretsmanager_secret_version" "db_master_password" { diff --git a/terraform/litellm/aws/variables.tf b/terraform/litellm/aws/variables.tf index 946cd7ebbf3..8bd505eb9a0 100644 --- a/terraform/litellm/aws/variables.tf +++ b/terraform/litellm/aws/variables.tf @@ -24,7 +24,7 @@ variable "env" { } variable "tags" { - description = "Additional tags merged into the provider default_tags." + description = "Per-deployment tags applied to every taggable resource the module creates, on top of the module's own `litellm:stack` / `managed-by` tags. Caller-level provider `default_tags` (if any) merge with these." type = map(string) default = {} } diff --git a/terraform/litellm/gcp/README.md b/terraform/litellm/gcp/README.md index 504cfa066e4..140741bcff6 100644 --- a/terraform/litellm/gcp/README.md +++ b/terraform/litellm/gcp/README.md @@ -173,10 +173,11 @@ pair differs: | `acme` | `prod` | `acme-litellm-prod-master-key` | | `globex` | `dev` | `globex-litellm-dev-license` | -For a per-tenant instance, the only inputs that change are the tenant -slug, env, and the two pre-issued secrets: +For a per-tenant instance via the example root, the only inputs that +change are the tenant slug, env, and the two pre-issued secrets: ```bash +cd terraform/litellm/gcp/examples/default export TF_VAR_litellm_master_key="sk-..." # the tenant's master key export TF_VAR_litellm_license="lic-..." # their LITELLM_LICENSE @@ -187,6 +188,10 @@ terraform apply \ -var "env=stage" ``` +To run *many* tenants from a single config, call the module with +`for_each` instead of one root per tenant — only possible because the +module declares no provider block (see "Using as a module"). + Both `litellm_master_key` and `litellm_license` are optional: - Omit `litellm_master_key` → the stack auto-generates a random `sk-…` value (trial/dev path). @@ -200,14 +205,22 @@ example files. ## Quick start ```bash -cd terraform/litellm/gcp +cd terraform/litellm/gcp/examples/default cp terraform.tfvars.example terraform.tfvars -# Edit: project, region, tenant, env, *_image, proxy_config, gateway_extra_secrets. +# Edit: project, region, tenant, env, image_registry, proxy_config, gateway_extra_secrets. terraform init terraform apply ``` +`examples/default/` is a thin root that configures the `google` / +`google-beta` providers and calls the module (`../../`). It exposes a +curated variable surface; for advanced knobs (per-component +CPU/memory/instances, Cloud SQL tier/edition, Memorystore tier, +per-component image pins) set them on the `module "litellm"` block in +`examples/default/main.tf`, or call the module from your own config — see +"Using as a module" below. + That single apply provisions everything, runs the prisma schema migration via the Cloud Run job (auto-triggered by `bootstrap.tf`), and only then starts the gateway/backend services. When it returns, the stack is serving traffic. @@ -251,6 +264,38 @@ Set `allow_plaintext_lb = true` and leave `lb_domains = []`. 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 + +The directory itself is a module with **no `provider` block** — the caller +owns provider config. You can call it directly with `for_each` (many +tenants from one config), `count`, `depends_on`, or providers configured +to impersonate a service account / target a different project: + +```hcl +provider "google" { + project = "my-gcp-project" + region = "us-central1" +} +provider "google-beta" { + project = "my-gcp-project" + region = "us-central1" +} + +module "litellm" { + source = "github.com/BerriAI/litellm//terraform/litellm/gcp?ref=" + + project = "my-gcp-project" + region = "us-central1" + tenant = "acme" + env = "prod" + # ...any of the inputs in variables.tf... +} +``` + +Both the default `google` and `google-beta` configs are inherited by the +module automatically through the call — declare both in the caller. +Resource labels are controlled by the module's `labels` input. + ## Storage and database retention Two opt-in tripwires guard against accidental data loss on @@ -281,8 +326,8 @@ or point them at your own CA. | File | What's in it | | ----------------- | -------------------------------------------------------------------- | -| `versions.tf` | Terraform + provider version constraints | -| `providers.tf` | Google + Google-Beta providers | +| `versions.tf` | Terraform + `required_providers` constraints (module declares no provider config) | +| `examples/default/` | Thin root: `google` / `google-beta` providers + a call to the module. The one-command deploy path. | | `variables.tf` | All input variables | | `locals.tf` | Path-prefix lists (mirror of `helm/.../ingress.yaml`) + proxy_config helpers | | `network.tf` | VPC, subnet, PSA range, Serverless VPC connector | diff --git a/terraform/litellm/gcp/cloudsql.tf b/terraform/litellm/gcp/cloudsql.tf index 70939c049c3..e3394fefc0f 100644 --- a/terraform/litellm/gcp/cloudsql.tf +++ b/terraform/litellm/gcp/cloudsql.tf @@ -45,6 +45,15 @@ resource "google_sql_database_instance" "writer" { } deletion_protection = var.cloudsql_deletion_protection + + lifecycle { + # disk_autoresize grows storage but never shrinks it. Without this, + # the first plan after any auto-grow reads disk_size as a shrink, which + # is an immutable change and forces a destroy/recreate of the instance + # (full data loss). Set the initial size only; let Cloud SQL own it + # thereafter. + ignore_changes = [settings[0].disk_size] + } } resource "google_sql_database_instance" "reader" { @@ -68,6 +77,12 @@ resource "google_sql_database_instance" "reader" { } deletion_protection = var.cloudsql_deletion_protection + + lifecycle { + # Same autoresize footgun as the writer — the replica grows its disk + # independently. Never let a perceived shrink replace the instance. + ignore_changes = [settings[0].disk_size] + } } resource "google_sql_database" "this" { diff --git a/terraform/litellm/gcp/examples/default/.terraform.lock.hcl b/terraform/litellm/gcp/examples/default/.terraform.lock.hcl new file mode 100644 index 00000000000..e6285567315 --- /dev/null +++ b/terraform/litellm/gcp/examples/default/.terraform.lock.hcl @@ -0,0 +1,63 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "6.50.0" + constraints = "~> 6.10" + hashes = [ + "h1:79CwMTsp3Ud1nOl5hFS5mxQHyT0fGVye7pqpU0PPlHI=", + "zh:1f3513fcfcbf7ca53d667a168c5067a4dd91a4d4cccd19743e248ff31065503c", + "zh:3da7db8fc2c51a77dd958ea8baaa05c29cd7f829bd8941c26e2ea9cb3aadc1e5", + "zh:3e09ac3f6ca8111cbb659d38c251771829f4347ab159a12db195e211c76068bb", + "zh:7bb9e41c568df15ccf1a8946037355eefb4dfb4e35e3b190808bb7c4abae547d", + "zh:81e5d78bdec7778e6d67b5c3544777505db40a826b6eb5abe9b86d4ba396866b", + "zh:8d309d020fb321525883f5c4ea864df3d5942b6087f6656d6d8b3a1377f340fc", + "zh:93e112559655ab95a523193158f4a4ac0f2bfed7eeaa712010b85ebb551d5071", + "zh:d3efe589ffd625b300cef5917c4629513f77e3a7b111c9df65075f76a46a63c7", + "zh:d4a4d672bbef756a870d8f32b35925f8ce2ef4f6bbd5b71a3cb764f1b6c85421", + "zh:e13a86bca299ba8a118e80d5f84fbdd708fe600ecdceea1a13d4919c068379fe", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fec30c095647b583a246c39d557704947195a1b7d41f81e369ba377d997faef6", + ] +} + +provider "registry.terraform.io/hashicorp/google-beta" { + version = "6.50.0" + constraints = "~> 6.10" + hashes = [ + "h1:P2GiUJM1frlPtBViwKn1A9V2dVBdGuWcX80w9TdH8ZE=", + "zh:18b442bd0a05321d39dda1e9e3f1bdede4e61bc2ac62cc7a67037a3864f75101", + "zh:2e387c51455862828bec923a3ec81abf63a4d998da470cf00e09003bda53d668", + "zh:3942e708fa84ebe54996086f4b1398cb747fe19cbcd0be07ace528291fb35dee", + "zh:496287dd48b34ae6197cb1f887abeafd07c33f389dbe431bb01e24846754cfdd", + "zh:6eca885419969ce5c2a706f34dce1f10bde9774757675f2d8a92d12e5a1be390", + "zh:710dbef826c3fe7f76f844dae47937e8e4c1279dd9205ec4610be04cf3327244", + "zh:777ebf44b24bfc7bdbf770dc089f1a72f143b4718fdedb8c6bd75983115a1ec2", + "zh:9c8703bba37b8c7ad857efc3513392c5a096c519397c1cb822d7612f38e4262f", + "zh:c4f1d3a73de2702277c99d5348ad6d374705bcfdd367ad964ff4cfd2cf06c281", + "zh:eca8df11af3f5a948492d5b8b5d01b4ec705aad10bc30ec1524205508ae28393", + "zh:f41e7fd5f2628e8fd6b8ea136366923858f54428d1729898925469b862c275c2", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.9.0" + constraints = "~> 3.6" + hashes = [ + "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=", + "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", + "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", + "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", + "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", + "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", + "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", + "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", + "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", + "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", + "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", + "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", + ] +} diff --git a/terraform/litellm/gcp/examples/default/main.tf b/terraform/litellm/gcp/examples/default/main.tf new file mode 100644 index 00000000000..745b79383db --- /dev/null +++ b/terraform/litellm/gcp/examples/default/main.tf @@ -0,0 +1,45 @@ +# One-command deploy of the LiteLLM GCP stack. +# +# cd terraform/litellm/gcp/examples/default +# cp terraform.tfvars.example terraform.tfvars # edit it +# terraform init +# terraform apply +# +# This root just wires the providers (see providers.tf) to the module. The +# module itself (../../) declares no provider, so it can also be consumed +# from your own config with count/for_each or impersonated-SA providers: +# +# module "litellm" { +# source = "github.com/BerriAI/litellm//terraform/litellm/gcp?ref=" +# ... +# } +# +# Knobs not surfaced as variables here (per-component sizing/instances, +# Cloud SQL tier/edition, Memorystore tier, per-component image overrides) +# can be set directly on this block — see ../../variables.tf. +module "litellm" { + source = "../../" + + project = var.project + region = var.region + tenant = var.tenant + env = var.env + + litellm_master_key = var.litellm_master_key + litellm_license = var.litellm_license + ui_password = var.ui_password + + image_registry = var.image_registry + image_tag = var.image_tag + + 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 + + proxy_config = var.proxy_config + gateway_extra_env = var.gateway_extra_env + backend_extra_env = var.backend_extra_env + gateway_extra_secrets = var.gateway_extra_secrets + backend_extra_secrets = var.backend_extra_secrets +} diff --git a/terraform/litellm/gcp/examples/default/outputs.tf b/terraform/litellm/gcp/examples/default/outputs.tf new file mode 100644 index 00000000000..3a9343c4850 --- /dev/null +++ b/terraform/litellm/gcp/examples/default/outputs.tf @@ -0,0 +1,59 @@ +output "lb_ip" { + description = "Global anycast IP of the external load balancer." + value = module.litellm.lb_ip +} + +output "lb_url" { + description = "Proxy URL. Dashboard at /, API at /v1/*." + value = module.litellm.lb_url +} + +output "gateway_service_url" { + description = "Default Cloud Run URL for the gateway (bypasses the LB)." + value = module.litellm.gateway_service_url +} + +output "backend_service_url" { + description = "Default Cloud Run URL for the backend (bypasses the LB)." + value = module.litellm.backend_service_url +} + +output "ui_service_url" { + description = "Default Cloud Run URL for the UI (bypasses the LB)." + value = module.litellm.ui_service_url +} + +output "cloudsql_writer_ip" { + description = "Private IP of the Cloud SQL writer." + value = module.litellm.cloudsql_writer_ip +} + +output "cloudsql_reader_ip" { + description = "Private IP of the Cloud SQL read replica." + value = module.litellm.cloudsql_reader_ip +} + +output "redis_endpoint" { + description = "Memorystore Redis endpoint." + value = module.litellm.redis_endpoint +} + +output "gcs_bucket" { + description = "GCS bucket name." + value = module.litellm.gcs_bucket +} + +output "master_key_secret_id" { + description = "Secret Manager resource ID holding LITELLM_MASTER_KEY." + value = module.litellm.master_key_secret_id +} + +output "db_password_secret_id" { + description = "Secret Manager resource ID holding the Cloud SQL app-user password." + value = module.litellm.db_password_secret_id +} + +output "migration_run_command" { + description = "Break-glass command to re-run the one-off migration job." + value = module.litellm.migration_run_command +} diff --git a/terraform/litellm/gcp/examples/default/providers.tf b/terraform/litellm/gcp/examples/default/providers.tf new file mode 100644 index 00000000000..4b79367fe09 --- /dev/null +++ b/terraform/litellm/gcp/examples/default/providers.tf @@ -0,0 +1,17 @@ +# Providers are configured HERE, in the root, not in the module. A module +# that declares its own configured `provider` block can't be called with +# count/for_each/depends_on and gives the caller no way to set an +# impersonated service account, a different project, or aliases. +# +# The module's resources inherit these default (unaliased) `google` / +# `google-beta` configs automatically through the module call, so project +# and region set here flow into every resource that doesn't pass its own. +provider "google" { + project = var.project + region = var.region +} + +provider "google-beta" { + project = var.project + region = var.region +} diff --git a/terraform/litellm/gcp/terraform.tfvars.example b/terraform/litellm/gcp/examples/default/terraform.tfvars.example similarity index 81% rename from terraform/litellm/gcp/terraform.tfvars.example rename to terraform/litellm/gcp/examples/default/terraform.tfvars.example index 5c22a14c6d6..eff338ca240 100644 --- a/terraform/litellm/gcp/terraform.tfvars.example +++ b/terraform/litellm/gcp/examples/default/terraform.tfvars.example @@ -28,14 +28,14 @@ env = "stage" # cloudsql_deletion_protection = true # default: refuse destroy on the DB # gcs_force_destroy = false # default: refuse destroy on a non-empty bucket -# Component images. Defaults pin all four to the same GHCR release tag — -# bump them together when bumping LiteLLM. To use private images, mirror -# them into Artifact Registry first — Cloud Run only authenticates against -# AR / gcr.io. -# gateway_image = "us-central1-docker.pkg.dev/my-gcp-project/litellm/gateway:1.86.0-dev" -# backend_image = "us-central1-docker.pkg.dev/my-gcp-project/litellm/backend:1.86.0-dev" -# ui_image = "us-central1-docker.pkg.dev/my-gcp-project/litellm/ui:1.86.0-dev" -# migrations_image = "us-central1-docker.pkg.dev/my-gcp-project/litellm/migrations:1.86.0-dev" +# Images. Cloud Run rejects ghcr.io, so a real deploy must point +# image_registry at an Artifact Registry remote repo (see README "Image +# pulls"); image_tag is applied to all four litellm-* images. Per-component +# *_image overrides are NOT exposed here — set them directly on the +# `module "litellm"` block in main.tf (see ../../variables.tf) if you need +# to mix-and-match versions. +# image_registry = "us-central1-docker.pkg.dev/my-gcp-project/litellm/berriai" +# image_tag = "v1.86.0-dev" # ---------- proxy_config (mirrors helm gateway.config.proxy_config) ---------- # proxy_config = { diff --git a/terraform/litellm/gcp/examples/default/variables.tf b/terraform/litellm/gcp/examples/default/variables.tf new file mode 100644 index 00000000000..745a5e5d76b --- /dev/null +++ b/terraform/litellm/gcp/examples/default/variables.tf @@ -0,0 +1,120 @@ +# Curated surface for the one-command deploy path. The module (../../) +# exposes far more knobs (per-component CPU/memory/instances, Cloud SQL +# tier/edition, Memorystore tier, per-component image overrides, …). To +# tune those, set them directly on the `module "litellm"` block in +# main.tf, or call the module from your own root config. Full per-variable +# docs live in ../../variables.tf — the module is the source of truth. + +variable "project" { + description = "GCP project ID." + type = string +} + +variable "region" { + description = "GCP region for VPC, Cloud SQL, Memorystore, Cloud Run, and the LB IP." + type = string + default = "us-central1" +} + +variable "tenant" { + description = "Tenant slug — prefix for every resource (-litellm-)." + type = string +} + +variable "env" { + description = "Environment suffix (stage, prod, dev)." + type = string +} + +# 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" { + description = "Pre-existing LITELLM_MASTER_KEY (sk-…). Empty → auto-generated." + type = string + default = "" + sensitive = true +} + +variable "litellm_license" { + description = "LiteLLM enterprise license. Empty → OSS-only." + type = string + default = "" + sensitive = true +} + +variable "ui_password" { + description = "UI admin password. Empty → falls back to LITELLM_MASTER_KEY." + type = string + default = "" + sensitive = true +} + +# Image source. Cloud Run rejects ghcr.io, so a real deploy must point +# image_registry at an Artifact Registry remote repo (see README "Image +# pulls"). Per-component overrides live in ../../variables.tf. +variable "image_registry" { + description = "Registry path prefix; images composed as /litellm-:." + type = string + default = "ghcr.io/berriai" +} + +variable "image_tag" { + description = "Tag applied to all four litellm-* images. Bump in lockstep." + type = string + default = "v1.86.0-dev" +} + +# 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. Empty → no TLS." + type = list(string) + default = [] +} + +variable "allow_plaintext_lb" { + description = "Opt into HTTP-only LB (trial/dev only)." + type = bool + default = false +} + +variable "cloudsql_deletion_protection" { + description = "Cloud SQL deletion protection (writer + reader)." + type = bool + default = true +} + +variable "gcs_force_destroy" { + description = "Allow destroy of a non-empty GCS bucket (ephemeral/CI only)." + type = bool + default = false +} + +variable "proxy_config" { + description = "LiteLLM proxy config (contents of config.yaml). Empty → defaults." + type = any + default = {} +} + +variable "gateway_extra_env" { + description = "Plain-text env vars layered onto the gateway." + type = map(string) + default = {} +} + +variable "backend_extra_env" { + description = "Plain-text env vars layered onto the backend." + type = map(string) + default = {} +} + +variable "gateway_extra_secrets" { + description = "Gateway env vars sourced from Secret Manager (name → secret resource ID)." + type = map(string) + default = {} +} + +variable "backend_extra_secrets" { + description = "Backend env vars sourced from Secret Manager (name → secret resource ID)." + type = map(string) + default = {} +} diff --git a/terraform/litellm/gcp/examples/default/versions.tf b/terraform/litellm/gcp/examples/default/versions.tf new file mode 100644 index 00000000000..a630c59afd0 --- /dev/null +++ b/terraform/litellm/gcp/examples/default/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 6.10" + } + google-beta = { + source = "hashicorp/google-beta" + version = "~> 6.10" + } + random = { + source = "hashicorp/random" + version = "~> 3.6" + } + } +} diff --git a/terraform/litellm/gcp/load_balancer.tf b/terraform/litellm/gcp/load_balancer.tf index b0081786f13..3fce96eaf74 100644 --- a/terraform/litellm/gcp/load_balancer.tf +++ b/terraform/litellm/gcp/load_balancer.tf @@ -160,11 +160,22 @@ resource "google_compute_global_forwarding_rule" "http" { resource "google_compute_managed_ssl_certificate" "this" { count = local.tls_enabled ? 1 : 0 - name = "${local.name}-cert" + + # A managed cert's `domains` is immutable, so changing var.lb_domains + # forces replacement, and the cert is referenced by the HTTPS target + # proxy — a destroy-then-create replacement fails with + # `resourceInUseByAnotherResource`. Hashing the domains into the name + # makes the name change with the domain set, so create_before_destroy + # builds the new cert + repoints the proxy before deleting the old one. + name = "${local.name}-cert-${substr(sha1(join(",", var.lb_domains)), 0, 8)}" managed { domains = var.lb_domains } + + lifecycle { + create_before_destroy = true + } } resource "google_compute_target_https_proxy" "this" { diff --git a/terraform/litellm/gcp/providers.tf b/terraform/litellm/gcp/providers.tf deleted file mode 100644 index fd1584463f8..00000000000 --- a/terraform/litellm/gcp/providers.tf +++ /dev/null @@ -1,9 +0,0 @@ -provider "google" { - project = var.project - region = var.region -} - -provider "google-beta" { - project = var.project - region = var.region -}