From 4ac05e8058051fa10fa25940be3aa1da405b21b5 Mon Sep 17 00:00:00 2001 From: Yannik Zbick Date: Fri, 16 Oct 2026 15:20:08 +0200 Subject: [PATCH] feature(proxy): setup dev environment --- .github/workflows/deploy-data-dev.yml | 300 ++++++++++++++++++ .../litellm/aws/config/dev/backend.tfbackend | 4 + .../litellm/aws/config/dev/terraform.tfvars | 69 ++++ terraform/litellm/aws/ecr.tf | 12 +- terraform/litellm/aws/rds.tf | 17 +- terraform/litellm/aws/variables.tf | 12 + 6 files changed, 404 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/deploy-data-dev.yml create mode 100644 terraform/litellm/aws/config/dev/backend.tfbackend create mode 100644 terraform/litellm/aws/config/dev/terraform.tfvars diff --git a/.github/workflows/deploy-data-dev.yml b/.github/workflows/deploy-data-dev.yml new file mode 100644 index 000000000000..16f1d4631c59 --- /dev/null +++ b/.github/workflows/deploy-data-dev.yml @@ -0,0 +1,300 @@ +name: Deploy Data Reply DEV Instance + +on: + workflow_dispatch: + inputs: + force_build: + description: 'Force rebuild all images' + type: boolean + default: true + + +permissions: + id-token: write + contents: read + +defaults: + run: + shell: bash + working-directory: ./terraform/litellm/aws/ + +env: + AWS_REGION: eu-central-1 + IMAGE_TAG: ${{ github.sha }} + +jobs: + terraform: + name: Terraform Deploy DEV + if: github.ref == 'refs/heads/datareply/dev' + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + fetch-depth: 0 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + with: + audience: sts.amazonaws.com + role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-oidc-provider-role + role-session-name: github-actions + aws-region: ${{ env.AWS_REGION }} + + - name: Setup Terraform + uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 + with: + terraform_version: "1.15.7" + + - name: Detect image-related changes + id: image_changes + env: + BEFORE_SHA: ${{ github.event.before }} + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "${{ inputs.force_build }}" = "true" ]; then + base_ref="" # force all builds + else + base_ref="HEAD^" + fi + elif [ "${{ github.event_name }}" = "push" ] && \ + [ -n "$BEFORE_SHA" ] && \ + [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then + base_ref="$BEFORE_SHA" + elif git rev-parse --verify HEAD^ >/dev/null 2>&1; then + base_ref="HEAD^" + else + base_ref="" + fi + + if [ -z "$base_ref" ]; then + { + echo "gateway_changed=true" + echo "backend_changed=true" + echo "ui_changed=true" + echo "migrations_changed=true" + } >> "$GITHUB_OUTPUT" + exit 0 + fi + + if git diff --quiet "$base_ref"...HEAD -- \ + .dockerignore \ + gateway/ \ + litellm/ \ + litellm-proxy-extras/ \ + enterprise/ \ + pyproject.toml \ + uv.lock \ + schema.prisma; then + gateway_changed=false + else + gateway_changed=true + fi + + if git diff --quiet "$base_ref"...HEAD -- \ + .dockerignore \ + backend/ \ + litellm/ \ + litellm-proxy-extras/ \ + enterprise/ \ + pyproject.toml \ + uv.lock \ + schema.prisma; then + backend_changed=false + else + backend_changed=true + fi + + if git diff --quiet "$base_ref"...HEAD -- \ + .dockerignore \ + ui/; then + ui_changed=false + else + ui_changed=true + fi + + if git diff --quiet "$base_ref"...HEAD -- \ + .dockerignore \ + migrations/ \ + litellm/ \ + litellm-proxy-extras/ \ + enterprise/ \ + pyproject.toml \ + uv.lock \ + schema.prisma; then + migrations_changed=false + else + migrations_changed=true + fi + + { + echo "gateway_changed=$gateway_changed" + echo "backend_changed=$backend_changed" + echo "ui_changed=$ui_changed" + echo "migrations_changed=$migrations_changed" + } >> "$GITHUB_OUTPUT" + + - name: Terraform Init + run: terraform init -backend-config=config/dev/backend.tfbackend + + - name: Terraform Validate + run: terraform validate + + - name: Bootstrap ECR repositories + run: | + terraform apply -auto-approve \ + -var-file=config/dev/terraform.tfvars \ + -target=aws_ecr_repository.gateway \ + -target=aws_ecr_repository.backend \ + -target=aws_ecr_repository.ui \ + -target=aws_ecr_repository.migrations + + - name: Read ECR repository URLs + id: ecr + run: | + { + echo "gateway_repo=$(terraform output -raw gateway_ecr_repository_url)" + echo "backend_repo=$(terraform output -raw backend_ecr_repository_url)" + echo "ui_repo=$(terraform output -raw ui_ecr_repository_url)" + echo "migrations_repo=$(terraform output -raw migrations_ecr_repository_url)" + } >> "$GITHUB_OUTPUT" + + - name: Resolve ECS image refs + id: image_refs + env: + GATEWAY_CHANGED: ${{ steps.image_changes.outputs.gateway_changed }} + BACKEND_CHANGED: ${{ steps.image_changes.outputs.backend_changed }} + UI_CHANGED: ${{ steps.image_changes.outputs.ui_changed }} + MIGRATIONS_CHANGED: ${{ steps.image_changes.outputs.migrations_changed }} + GATEWAY_CANDIDATE: ${{ steps.ecr.outputs.gateway_repo }}:${{ env.IMAGE_TAG }} + BACKEND_CANDIDATE: ${{ steps.ecr.outputs.backend_repo }}:${{ env.IMAGE_TAG }} + UI_CANDIDATE: ${{ steps.ecr.outputs.ui_repo }}:${{ env.IMAGE_TAG }} + MIGRATIONS_CANDIDATE: ${{ steps.ecr.outputs.migrations_repo }}:${{ env.IMAGE_TAG }} + run: | + state_resources="$(terraform state list 2>/dev/null || true)" + + current_image() { + terraform show -json 2>/dev/null \ + | jq -r --arg res "$1" ' + .values.root_module.resources[] + | select(.address == $res) + | .values.container_definitions + | fromjson + | .[0].image // empty + ' \ + | head -n1 + } + + gateway_current_image="$(current_image aws_ecs_task_definition.gateway)" + backend_current_image="$(current_image aws_ecs_task_definition.backend)" + ui_current_image="$(current_image aws_ecs_task_definition.ui)" + migrations_current_image="$(current_image aws_ecs_task_definition.migrations)" + + if [ "$GATEWAY_CHANGED" = "true" ] || [ -z "$gateway_current_image" ]; then + gateway_build=true + gateway_image="$GATEWAY_CANDIDATE" + else + gateway_build=false + gateway_image="$gateway_current_image" + fi + + if [ "$BACKEND_CHANGED" = "true" ] || [ -z "$backend_current_image" ]; then + backend_build=true + backend_image="$BACKEND_CANDIDATE" + else + backend_build=false + backend_image="$backend_current_image" + fi + + if [ "$UI_CHANGED" = "true" ] || [ -z "$ui_current_image" ]; then + ui_build=true + ui_image="$UI_CANDIDATE" + else + ui_build=false + ui_image="$ui_current_image" + fi + + if [ "$MIGRATIONS_CHANGED" = "true" ] || [ -z "$migrations_current_image" ]; then + migrations_build=true + migrations_image="$MIGRATIONS_CANDIDATE" + else + migrations_build=false + migrations_image="$migrations_current_image" + fi + + if [ "$gateway_build" = "true" ] || [ "$backend_build" = "true" ] || [ "$ui_build" = "true" ] || [ "$migrations_build" = "true" ]; then + any_build=true + else + any_build=false + fi + + { + echo "any_build=$any_build" + echo "gateway_build=$gateway_build" + echo "backend_build=$backend_build" + echo "ui_build=$ui_build" + echo "migrations_build=$migrations_build" + echo "gateway_image=$gateway_image" + echo "backend_image=$backend_image" + echo "ui_image=$ui_image" + echo "migrations_image=$migrations_image" + } >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + if: steps.image_refs.outputs.any_build == 'true' + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Login to Amazon ECR + if: steps.image_refs.outputs.any_build == 'true' + uses: aws-actions/amazon-ecr-login@4625ce35226a7557230889aae2f52eb50ec3dcda # v2.0.1 + + - name: Build and push gateway image + if: steps.image_refs.outputs.gateway_build == 'true' + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0 + with: + context: . + file: ./gateway/Dockerfile + push: true + tags: ${{ steps.ecr.outputs.gateway_repo }}:${{ env.IMAGE_TAG }} + + - name: Build and push backend image + if: steps.image_refs.outputs.backend_build == 'true' + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0 + with: + context: . + file: ./backend/Dockerfile + push: true + tags: ${{ steps.ecr.outputs.backend_repo }}:${{ env.IMAGE_TAG }} + + - name: Build and push UI image + if: steps.image_refs.outputs.ui_build == 'true' + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0 + with: + context: . + file: ./ui/Dockerfile + push: true + tags: ${{ steps.ecr.outputs.ui_repo }}:${{ env.IMAGE_TAG }} + + - name: Build and push migrations image + if: steps.image_refs.outputs.migrations_build == 'true' + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0 + with: + context: . + file: ./migrations/Dockerfile + push: true + tags: ${{ steps.ecr.outputs.migrations_repo }}:${{ env.IMAGE_TAG }} + + - name: Terraform Plan + run: | + terraform plan \ + -var-file=config/dev/terraform.tfvars \ + -var="gateway_image=${{ steps.image_refs.outputs.gateway_image }}" \ + -var="backend_image=${{ steps.image_refs.outputs.backend_image }}" \ + -var="ui_image=${{ steps.image_refs.outputs.ui_image }}" \ + -var="migrations_image=${{ steps.image_refs.outputs.migrations_image }}" \ + -out=tfplan + + - name: Terraform Apply + if: github.ref == 'refs/heads/datareply/dev' + run: terraform apply -auto-approve tfplan diff --git a/terraform/litellm/aws/config/dev/backend.tfbackend b/terraform/litellm/aws/config/dev/backend.tfbackend new file mode 100644 index 000000000000..b84ce4ecc19a --- /dev/null +++ b/terraform/litellm/aws/config/dev/backend.tfbackend @@ -0,0 +1,4 @@ +bucket = "data-prod-ai-llm-gateway-terraform-751812493785-eu-central-1-an" +key = "litellm/dev/terraform.tfstate" +region = "eu-central-1" + diff --git a/terraform/litellm/aws/config/dev/terraform.tfvars b/terraform/litellm/aws/config/dev/terraform.tfvars new file mode 100644 index 000000000000..12c7344453cd --- /dev/null +++ b/terraform/litellm/aws/config/dev/terraform.tfvars @@ -0,0 +1,69 @@ +region = "eu-central-1" +azs = ["eu-central-1a", "eu-central-1b"] + +tenant = "data-reply" +env = "dev" + +ecr_force_delete = true + +vpc_id = "" +vpc_cidr = "10.41.0.0/16" + +gateway_desired_count = 1 +backend_desired_count = 1 +ui_desired_count = 1 +gateway_autoscaling_enabled = false +backend_autoscaling_enabled = false +ui_autoscaling_enabled = false + +create_database = true +db_enable_reader = false +db_writer_instance_class = "db.serverless" +db_serverless_min_capacity = 0.5 +db_serverless_max_capacity = 2 +db_engine_version = "16.11" + +db_randomize_final_snapshot_name = true + +create_redis = true +redis_num_replicas = 0 + +acm_certificate_domain_name = "litellm-dev.datareply.de" +route53_zone_id = "Z0891217221E1PTCMOPWZ" + +proxy_config = { + model_list = [ + { + model_name = "gpt-6-luna" + litellm_params = { + model = "openai/gpt-6-luna" + api_key = "os.environ/OPENAI_API_KEY" + } + } + ] + general_settings = { + master_key = "os.environ/LITELLM_MASTER_KEY" + database_url = "os.environ/DATABASE_URL" + store_prompts_in_spend_logs = false + } + litellm_settings = { + route_all_chat_openai_to_responses = true + redact_messages_in_exceptions = true + ui_theme_config = { + logo_url = "https://www.reply.com/favicon.ico" + favicon_url = "https://www.reply.com/favicon.ico" + } + } +} + +backend_extra_env = { + PROXY_BASE_URL = "https://litellm-dev.datareply.de" +} + +gateway_extra_secrets = { + OPENAI_API_KEY = "arn:aws:secretsmanager:eu-central-1:751812493785:secret:data-reply/litellm/openai-api-key-nNpCF1" +} + +/*backend_extra_secrets = { + OPENAI_API_KEY = "arn:aws:secretsmanager:eu-central-1:751812493785:secret:data-reply/litellm/openai-api-key-nNpCF1" +}*/ diff --git a/terraform/litellm/aws/ecr.tf b/terraform/litellm/aws/ecr.tf index 00a028dc1672..133f4c9f0487 100644 --- a/terraform/litellm/aws/ecr.tf +++ b/terraform/litellm/aws/ecr.tf @@ -1,5 +1,6 @@ resource "aws_ecr_repository" "gateway" { - name = "${local.name}-gateway" + name = "${local.name}-gateway" + force_delete = var.ecr_force_delete image_scanning_configuration { scan_on_push = true @@ -9,7 +10,8 @@ resource "aws_ecr_repository" "gateway" { } resource "aws_ecr_repository" "backend" { - name = "${local.name}-backend" + name = "${local.name}-backend" + force_delete = var.ecr_force_delete image_scanning_configuration { scan_on_push = true @@ -19,7 +21,8 @@ resource "aws_ecr_repository" "backend" { } resource "aws_ecr_repository" "ui" { - name = "${local.name}-ui" + name = "${local.name}-ui" + force_delete = var.ecr_force_delete image_scanning_configuration { scan_on_push = true @@ -29,7 +32,8 @@ resource "aws_ecr_repository" "ui" { } resource "aws_ecr_repository" "migrations" { - name = "${local.name}-migrations" + name = "${local.name}-migrations" + force_delete = var.ecr_force_delete image_scanning_configuration { scan_on_push = true diff --git a/terraform/litellm/aws/rds.tf b/terraform/litellm/aws/rds.tf index cbf65474d4de..2c8d59a656e9 100644 --- a/terraform/litellm/aws/rds.tf +++ b/terraform/litellm/aws/rds.tf @@ -42,6 +42,11 @@ locals { db_reader_instance_class = coalesce(var.db_reader_instance_class, var.db_instance_class) } +resource "random_id" "final_snapshot" { + count = var.create_database && !var.skip_final_snapshot && var.db_randomize_final_snapshot_name ? 1 : 0 + byte_length = 8 +} + resource "aws_rds_cluster" "this" { count = var.create_database ? 1 : 0 cluster_identifier = local.name @@ -73,12 +78,12 @@ resource "aws_rds_cluster" "this" { } } - # Final-snapshot guard. With the safe default (skip_final_snapshot = false), - # `terraform destroy` takes a snapshot named `-final-` - # before dropping the cluster. The short SHA disambiguates repeated - # destroy/recreate cycles so each snapshot has a unique name. - skip_final_snapshot = var.skip_final_snapshot - final_snapshot_identifier = var.skip_final_snapshot ? null : "${local.name}-final-${substr(md5(local.name), 0, 8)}" + skip_final_snapshot = var.skip_final_snapshot + final_snapshot_identifier = var.skip_final_snapshot ? null : format( + "%s-final-%s", + local.name, + var.db_randomize_final_snapshot_name ? random_id.final_snapshot[0].hex : substr(md5(local.name), 0, 8), + ) backup_retention_period = 7 preferred_backup_window = "07:00-09:00" diff --git a/terraform/litellm/aws/variables.tf b/terraform/litellm/aws/variables.tf index 65d4f3b583bd..baceb509be40 100644 --- a/terraform/litellm/aws/variables.tf +++ b/terraform/litellm/aws/variables.tf @@ -139,6 +139,12 @@ variable "azs" { # GHCR. Override on a per-component basis in tfvars when bumping; bump them # together when bumping the LiteLLM release. +variable "ecr_force_delete" { + description = "Allow deletion of ECR repositories containing images." + type = bool + default = false +} + variable "gateway_image" { description = "Container image for the gateway (data plane, port 4000). Tag must match a tag actually published to GHCR — the split images use the `v`-prefixed semver convention." type = string @@ -600,6 +606,12 @@ variable "skip_final_snapshot" { default = false } +variable "db_randomize_final_snapshot_name" { + description = "Use a random final snapshot suffix, retained in state until destroy and regenerated on recreation." + type = bool + default = false +} + variable "s3_force_destroy" { description = <<-EOT Allow `terraform destroy` to delete the S3 bucket even when it still