From b1fce379cd1c110b5562a6b62531131d9b4755ef Mon Sep 17 00:00:00 2001 From: "cristiano.hoshikawa" Date: Fri, 21 Aug 2026 14:24:31 -0300 Subject: [PATCH] k8s multiple regions config --- docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md | 61 +++++++++++-------- docs/regional/CHANGELOG_IMPLEMENTACAO.md | 9 +++ docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md | 22 ++++--- docs/regional/TESTES_TIA_XAI_REGIONAL.md | 22 +++---- k8s/regional/regions.env.example | 34 +++++++++-- scripts/deploy-regional-k8s.sh | 24 +++++--- scripts/render-regional-k8s.sh | 60 +++++++++++++++--- tests/test_render_regional_multi_target.sh | 49 +++++++++++++++ 8 files changed, 213 insertions(+), 68 deletions(-) create mode 100644 tests/test_render_regional_multi_target.sh diff --git a/docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md b/docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md index 9921532..052a487 100644 --- a/docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md +++ b/docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md @@ -48,10 +48,10 @@ A solução tem os seguintes objetivos: +--------------------+--------------------+ | | v v - Deployment ORD Deployment IAD + Deployment Target A Deployment Target B / ... / N | | +--------+--------+ +--------+--------+ - | Pod TIA ORD | | Pod TIA IAD | + | Pod TIA A | | Pod TIA B/N | | | | | | bridge | | bridge | | agent/livekit | | agent/livekit | @@ -62,10 +62,20 @@ A solução tem os seguintes objetivos: +-------+---------+ +-------+---------+ | | v v - OCI xAI ORD OCI xAI IAD + OCI xAI endpoint A OCI xAI endpoint B/N ``` -O mesmo `Service` Kubernetes seleciona Pods ORD e IAD. Cada deployment acrescenta o label `tia-region`, útil para métricas e operação, mas ambos compartilham `app=${APP_NAME}-regional`. +O mesmo `Service` Kubernetes seleciona Pods de todos os targets configurados em `XAI_TARGETS`. Cada deployment acrescenta o label `tia-region`, útil para métricas e operação, e todos compartilham `app=${APP_NAME}-regional`. A quantidade de targets é arbitrária e dois targets podem inclusive apontar para endpoints diferentes na mesma região OCI. + +## 4.1 Configuração dinâmica de targets + +A topologia não possui lista fixa de regiões. `XAI_TARGETS` declara os pools/endpoints lógicos, por exemplo: + +```bash +XAI_TARGETS="ORD IAD GRU FRA" +``` + +Cada target define `REGION_ID`, réplicas, endpoint upstream e Secret por prefixo. Um target é uma unidade de pool/deployment, portanto vários targets podem apontar para endpoints distintos na mesma região OCI. Adicionar ou remover target não exige alteração dos scripts nem dos templates Kubernetes. ## 5. Componentes @@ -126,8 +136,9 @@ Cada deployment recebe seu endpoint próprio por `XAI_POOL_UPSTREAM_URL`. Exemplo: ```text -ORD -> wss://...us-chicago-1.../xai/v1/tts -IAD -> wss://...us-ashburn-1.../xai/v1/tts +TARGET_A -> wss:///xai/v1/tts +TARGET_B -> wss:///xai/v1/tts +TARGET_N -> wss:///xai/v1/tts ``` As credenciais reais ficam somente no sidecar de pool. O container `agent` usa uma credencial local dummy porque se conecta apenas a `localhost`. @@ -303,13 +314,13 @@ Operação normal: ```text LB -|-- ORD Pod 1 -> ready -|-- ORD Pod 2 -> ready -|-- IAD Pod 1 -> ready -`-- IAD Pod 2 -> ready +|-- Target A Pod 1 -> ready +|-- Target A Pod 2 -> ready +|-- Target B Pod 1 -> ready +`-- Target B Pod 2 -> ready ``` -Se ORD perder saúde/capacidade xAI, os slots começam a falhar e a quantidade de conexões saudáveis/livres cai. Quando a readiness cruza o threshold, os Pods ORD saem dos endpoints e novas chamadas passam a ser atendidas pelos Pods IAD disponíveis. +Se qualquer target perder saúde/capacidade xAI, seus slots começam a falhar e a quantidade de conexões saudáveis/livres cai. Quando a readiness cruza o threshold, os Pods desse target saem dos endpoints e novas chamadas passam a ser atendidas pelos demais targets Ready. Não há necessidade de alterar o cliente ou o LiveKit para escolher a região. @@ -324,16 +335,16 @@ capacidade regional de sockets = replicas_region * XAI_POOL_SIZE Exemplo: ```text -ORD: 2 pods x 25 sockets = 50 -IAD: 2 pods x 25 sockets = 50 +Target A: 2 pods x 25 sockets = 50 +Target B: 2 pods x 25 sockets = 50 TOTAL = 100 sockets prewarmed ``` ou, se a OCI conceder capacidade independente suficiente: ```text -ORD: 2 pods x 50 = 100 -IAD: 2 pods x 50 = 100 +Target A: 2 pods x 50 = 100 +Target B: 2 pods x 50 = 100 TOTAL = 200 ``` @@ -355,24 +366,24 @@ e deve ser comparado com a **capacidade efetivamente negociada/provisionada** pa Carga baixa ========= LB - |-- ORD-1 [pool 50: 10 leased / 40 free] - `-- IAD-1 [pool 50: 8 leased / 42 free] + |-- target-a-1 [pool 50: 10 leased / 40 free] + `-- target-b-1 [pool 50: 8 leased / 42 free] Carga aumenta ============= LB - |-- ORD-1 [47 leased / 3 free] READY - `-- IAD-1 [30 leased /20 free] READY + |-- target-a-1 [47 leased / 3 free] READY + `-- target-b-1 [30 leased /20 free] READY -ORD satura +Target A satura =========== LB - |-- ORD-1 [48 leased /2 free] NOT READY -> sem novas chamadas - `-- IAD-1 [31 leased /19 free] READY -> recebe novas chamadas + |-- target-a-1 [48 leased /2 free] NOT READY -> sem novas chamadas + `-- target-b-1 [31 leased /19 free] READY -> recebe novas chamadas -ORD recupera +Target A recupera ============= -ORD-1 chega a 45 leased /5 free +target-a-1 chega a 45 leased /5 free /readyz volta a 200 LB volta a considerá-lo para novas conexões ``` @@ -445,7 +456,7 @@ Estas métricas devem ser correlacionadas com as métricas já existentes no TIA | conexão presa a uma chamada | lease somente durante utterance | | LB envia tráfego a Pod sem capacidade TTS | `/readyz` baseado no pool | | flapping de health | histerese unavailable/recover | -| indisponibilidade regional | Deployments ORD/IAD no mesmo Service | +| indisponibilidade regional | N deployments/targets no mesmo Service | | rollout derruba novas sessões | drain + RollingUpdate | | chave xAI em todos os processos | credencial real somente no sidecar | | observabilidade de capacidade | `/pool/status` + `/metrics` | diff --git a/docs/regional/CHANGELOG_IMPLEMENTACAO.md b/docs/regional/CHANGELOG_IMPLEMENTACAO.md index bccb733..264e2a2 100644 --- a/docs/regional/CHANGELOG_IMPLEMENTACAO.md +++ b/docs/regional/CHANGELOG_IMPLEMENTACAO.md @@ -1,3 +1,12 @@ +## Correção multi-target / N regiões + +- removido hardcode `render_region ORD` / `render_region IAD`; +- nova variável `XAI_TARGETS` define qualquer quantidade de pools/endpoints; +- `XAI_REGIONS` mantida somente como alias de compatibilidade; +- deployment e rollout descobrem dinamicamente todos os manifests renderizados; +- suporte a múltiplos targets na mesma região OCI usando `REGION_ID` distintos; +- documentação e plano de testes generalizados para N targets. + # Changelog — TIA Regional / xAI Pool ## Implementação adicionada diff --git a/docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md b/docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md index b25a268..c740092 100644 --- a/docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md +++ b/docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md @@ -1,5 +1,8 @@ # Deployment — TIA Regional com Pool xAI +> **Modelo multi-target:** a lista de regiões/pools não é fixa. Declare `XAI_TARGETS="TARGET_A TARGET_B ..."` em `k8s/regional/regions.env`. Cada target define `_REGION_ID`, `_REGION_REPLICAS`, `_XAI_UPSTREAM_URL` e `_XAI_SECRET_NAME`. Para adicionar uma região ou um segundo pool na mesma região, altere somente configuração; os scripts e manifests não precisam ser modificados. `XAI_REGIONS` é aceito apenas como alias de compatibilidade. + + ## 1. Pré-requisitos - cluster Kubernetes/OKE funcional; @@ -312,20 +315,21 @@ Um Pod NotReady não deve ser usado para novas conexões do Service. ## 15. Testar failover regional -### Teste controlado de ORD +### Teste controlado de qualquer target -Coloque ORD em drain: +Escolha um `TARGET_REGION_ID` e coloque esse target em drain: ```bash -ORD_POD=$(kubectl -n "$K8S_NAMESPACE" get pod \ - -l app=${APP_NAME}-regional,tia-region=ord \ +TARGET_REGION_ID=${TARGET_REGION_ID:-ord} +TARGET_POD=$(kubectl -n "$K8S_NAMESPACE" get pod \ + -l app=${APP_NAME}-regional,tia-region=${TARGET_REGION_ID} \ -o jsonpath='{.items[0].metadata.name}') -kubectl -n "$K8S_NAMESPACE" exec "$ORD_POD" -c xai-pool -- \ +kubectl -n "$K8S_NAMESPACE" exec "$TARGET_POD" -c xai-pool -- \ python -c "import urllib.request; urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:18100/drain', method='POST')).read()" ``` -Confirme `/readyz=503` e gere **nova chamada**. Ela deve ser entregue a um Pod IAD ainda Ready. +Confirme `/readyz=503` e gere **nova chamada**. Ela deve ser entregue a qualquer outro target que permaneça Ready. Esse teste não deve ser interpretado como migração de uma chamada existente; somente novas conexões são rebalanceadas. @@ -334,7 +338,7 @@ Esse teste não deve ser interpretado como migração de uma chamada existente; O drain manual é intencional e não é revertido. Para voltar o Pod, reinicie-o: ```bash -kubectl -n "$K8S_NAMESPACE" delete pod "$ORD_POD" +kubectl -n "$K8S_NAMESPACE" delete pod "$TARGET_POD" ``` O novo Pod deve: @@ -378,7 +382,7 @@ A alteração não remove suporte ao modo anterior. ## 20. Checklist de produção - [ ] confirmar quota real de WebSockets por endpoint/região/tenancy; -- [ ] confirmar se ORD e IAD têm pools de capacidade independentes; +- [ ] confirmar capacidade independente/adequada para cada target configurado; - [ ] validar `XAI_POOL_SIZE * replicas` por região; - [ ] validar Secret/Vault; - [ ] medir tempo de prewarm dos 50 sockets; @@ -388,7 +392,7 @@ A alteração não remove suporte ao modo anterior. - [ ] testar recovery -> `/readyz=200`; - [ ] testar drain durante chamada ativa; - [ ] testar rollout sem perda de novas chamadas; -- [ ] testar perda total de ORD e entrada em IAD; +- [ ] testar perda total de cada target e redistribuição para os demais targets Ready; - [ ] correlacionar TTFB/gap/underflow com `pool_free`; - [ ] validar LB/Service com WebSocket real; - [ ] executar stress test com perfil semelhante ao tráfego de produção. diff --git a/docs/regional/TESTES_TIA_XAI_REGIONAL.md b/docs/regional/TESTES_TIA_XAI_REGIONAL.md index 063d547..81b25c7 100644 --- a/docs/regional/TESTES_TIA_XAI_REGIONAL.md +++ b/docs/regional/TESTES_TIA_XAI_REGIONAL.md @@ -6,7 +6,7 @@ Validar separadamente capacidade do pool, comportamento do Kubernetes, failover ## Camada 1 — Smoke -1. subir 1 Pod ORD com pool pequeno (`XAI_POOL_SIZE=3`); +1. subir 1 Pod de um target de teste com pool pequeno (`XAI_POOL_SIZE=3`); 2. confirmar 3 conexões healthy; 3. realizar uma chamada e uma síntese; 4. confirmar que `leased` vai 0 -> 1 -> 0; @@ -91,15 +91,15 @@ Critérios: ## Camada 7 — Falha upstream -Bloqueie ORD ou aponte temporariamente para endpoint inválido. +Bloqueie um target (por exemplo TARGET_A) ou aponte temporariamente seu upstream para endpoint inválido. Esperado: -- slots ORD tornam-se unhealthy; +- slots do TARGET_A tornam-se unhealthy; - maintenance tenta recuperação; -- readiness ORD cai; -- Service deixa de enviar novas chamadas a ORD; -- IAD continua Ready. +- readiness do TARGET_A cai; +- Service deixa de enviar novas chamadas ao TARGET_A; +- os demais targets continuam Ready. ## Camada 8 — Latência @@ -141,7 +141,7 @@ Rodar pelo menos: Com chamadas ativas: ```bash -kubectl rollout restart deployment/ +kubectl rollout restart deployment/ ``` Validar: @@ -153,11 +153,11 @@ Validar: ## Camada 10 — Failover regional -1. ORD e IAD Ready; +1. configurar pelo menos dois targets Ready; 2. iniciar tráfego contínuo; -3. tornar ORD NotReady; -4. verificar novas chamadas em IAD; -5. recuperar ORD; +3. tornar um target NotReady; +4. verificar novas chamadas nos demais targets Ready; +5. recuperar o target; 6. verificar reentrada progressiva. ## Evidências a guardar diff --git a/k8s/regional/regions.env.example b/k8s/regional/regions.env.example index 6230b32..5bf7d77 100644 --- a/k8s/regional/regions.env.example +++ b/k8s/regional/regions.env.example @@ -8,6 +8,11 @@ TERMINATION_GRACE_SECONDS=600 DRAIN_SECONDS=30 PDB_MIN_AVAILABLE=2 +# Logical xAI targets. Add/remove targets here without modifying scripts/YAML. +# A target represents one pool/upstream endpoint and does not need to be 1:1 +# with an OCI region. This also supports multiple pools in the same region. +XAI_TARGETS="ORD IAD" + # Existing TIA resources CPU_TIA_BRIDGE_REQ=250m MEM_TIA_BRIDGE_REQ=512Mi @@ -22,9 +27,8 @@ MEM_XAI_POOL_REQ=256Mi CPU_XAI_POOL_LIM=1000m MEM_XAI_POOL_LIM=768Mi -# Pool profile - 50 means 50 prewarmed upstream WebSockets PER POD. -# Tamanho do pool por réplica TIA. NÃO representa limite do OCI/xAI. -# Ajuste conforme capacidade negociada para o ambiente. +# Pool profile - XAI_POOL_SIZE means prewarmed upstream WebSockets PER POD. +# It is NOT the OCI/xAI service limit; tune it to the negotiated capacity. XAI_POOL_SIZE=50 XAI_POOL_UNAVAILABLE_FREE=2 XAI_POOL_RECOVER_FREE=5 @@ -36,19 +40,37 @@ XAI_TTS_VOICE=c8x2ieiocufs XAI_TTS_LANGUAGE=pt-BR XAI_UPSTREAM_AUTH_METHOD=API_KEY -# HPA. replicas * XAI_POOL_SIZE deve respeitar a capacidade efetivamente negociada/provisionada para cada região/endpoint. +# HPA. replicas * XAI_POOL_SIZE must fit each target/endpoint capacity. HPA_MIN_REPLICAS=1 HPA_MAX_REPLICAS=3 HPA_CPU_TARGET=65 -# ORD +# Example target ORD ORD_REGION_ID=ord ORD_REGION_REPLICAS=1 ORD_XAI_UPSTREAM_URL=wss://peordagnt002prd.pe.inference.generativeai.us-chicago-1.oci.oraclecloud.com/xai/v1/tts ORD_XAI_SECRET_NAME=xai-ord-credentials -# IAD +# Example target IAD IAD_REGION_ID=iad IAD_REGION_REPLICAS=1 IAD_XAI_UPSTREAM_URL=wss://peiadagnt003prd.pe.inference.generativeai.us-ashburn-1.oci.oraclecloud.com/xai/v1/tts IAD_XAI_SECRET_NAME=xai-iad-credentials + +# Add another region without changing code: +# XAI_TARGETS="ORD IAD GRU" +# GRU_REGION_ID=gru +# GRU_REGION_REPLICAS=1 +# GRU_XAI_UPSTREAM_URL=wss:///xai/v1/tts +# GRU_XAI_SECRET_NAME=xai-gru-credentials + +# Multiple independent pools in one OCI region are also supported: +# XAI_TARGETS="ORD_PRIMARY ORD_SECONDARY IAD" +# ORD_PRIMARY_REGION_ID=ord-primary +# ORD_PRIMARY_REGION_REPLICAS=1 +# ORD_PRIMARY_XAI_UPSTREAM_URL=wss:///xai/v1/tts +# ORD_PRIMARY_XAI_SECRET_NAME=xai-ord-primary-credentials +# ORD_SECONDARY_REGION_ID=ord-secondary +# ORD_SECONDARY_REGION_REPLICAS=1 +# ORD_SECONDARY_XAI_UPSTREAM_URL=wss:///xai/v1/tts +# ORD_SECONDARY_XAI_SECRET_NAME=xai-ord-secondary-credentials diff --git a/scripts/deploy-regional-k8s.sh b/scripts/deploy-regional-k8s.sh index ec26b12..ca44612 100644 --- a/scripts/deploy-regional-k8s.sh +++ b/scripts/deploy-regional-k8s.sh @@ -3,13 +3,23 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_FILE="${1:-$ROOT/k8s/regional/regions.env}" OUT="${2:-$ROOT/k8s/regional/rendered}" + "$ROOT/scripts/render-regional-k8s.sh" "$ENV_FILE" "$OUT" +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + kubectl apply -f "$OUT/service.yaml" kubectl apply -f "$OUT/pdb.yaml" -kubectl apply -f "$OUT/deployment-ord.yaml" -kubectl apply -f "$OUT/deployment-iad.yaml" -kubectl apply -f "$OUT/hpa-ord.yaml" -kubectl apply -f "$OUT/hpa-iad.yaml" -kubectl rollout status -f "$OUT/deployment-ord.yaml" --timeout=10m -kubectl rollout status -f "$OUT/deployment-iad.yaml" --timeout=10m -kubectl get pods -l app="$(grep '^APP_NAME=' "$ENV_FILE" | cut -d= -f2)-regional" -o wide + +shopt -s nullglob +deployments=("$OUT"/deployment-*.yaml) +hpas=("$OUT"/hpa-*.yaml) +((${#deployments[@]} > 0)) || { echo "No regional deployments rendered" >&2; exit 2; } + +for manifest in "${deployments[@]}"; do kubectl apply -f "$manifest"; done +for manifest in "${hpas[@]}"; do kubectl apply -f "$manifest"; done +for manifest in "${deployments[@]}"; do kubectl rollout status -f "$manifest" --timeout=10m; done + +kubectl -n "$K8S_NAMESPACE" get pods -l "app=${APP_NAME}-regional" -o wide diff --git a/scripts/render-regional-k8s.sh b/scripts/render-regional-k8s.sh index 8da0992..44deafa 100644 --- a/scripts/render-regional-k8s.sh +++ b/scripts/render-regional-k8s.sh @@ -4,21 +4,61 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_FILE="${1:-$ROOT/k8s/regional/regions.env}" OUT="${2:-$ROOT/k8s/regional/rendered}" [[ -f "$ENV_FILE" ]] || { echo "Missing $ENV_FILE (copy regions.env.example)" >&2; exit 2; } -set -a; source "$ENV_FILE"; set +a +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a command -v envsubst >/dev/null || { echo "envsubst is required (gettext package)" >&2; exit 2; } -rm -rf "$OUT"; mkdir -p "$OUT" -render_region() { - local prefix="$1" + +XAI_TARGETS="${XAI_TARGETS:-${XAI_REGIONS:-}}" +[[ -n "${XAI_TARGETS// }" ]] || { + echo 'XAI_TARGETS is required. Example: XAI_TARGETS="ORD IAD GRU"' >&2 + exit 2 +} + +rm -rf "$OUT" +mkdir -p "$OUT" + +required_target_var() { + local target="$1" suffix="$2" var value + var="${target}_${suffix}" + value="${!var:-}" + [[ -n "$value" ]] || { echo "Missing required configuration: $var" >&2; exit 2; } + printf '%s' "$value" +} + +render_target() { + local target="$1" + [[ "$target" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || { + echo "Invalid XAI target '$target'. Use letters, numbers and underscores only." >&2 + exit 2 + } + export REGION_ID REGION_REPLICAS XAI_UPSTREAM_URL XAI_SECRET_NAME - REGION_ID="$(eval echo \"\${${prefix}_REGION_ID}\")" - REGION_REPLICAS="$(eval echo \"\${${prefix}_REGION_REPLICAS}\")" - XAI_UPSTREAM_URL="$(eval echo \"\${${prefix}_XAI_UPSTREAM_URL}\")" - XAI_SECRET_NAME="$(eval echo \"\${${prefix}_XAI_SECRET_NAME}\")" + REGION_ID="$(required_target_var "$target" REGION_ID)" + REGION_REPLICAS="$(required_target_var "$target" REGION_REPLICAS)" + XAI_UPSTREAM_URL="$(required_target_var "$target" XAI_UPSTREAM_URL)" + XAI_SECRET_NAME="$(required_target_var "$target" XAI_SECRET_NAME)" + + [[ "$REGION_ID" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]] || { + echo "Invalid ${target}_REGION_ID='$REGION_ID'. Use a lowercase Kubernetes label." >&2 + exit 2 + } + [[ ! -e "$OUT/deployment-${REGION_ID}.yaml" ]] || { + echo "Duplicate REGION_ID '$REGION_ID'. Each target must have a unique REGION_ID." >&2 + echo "For multiple pools in one OCI region use ids such as ord-primary and ord-secondary." >&2 + exit 2 + } + envsubst < "$ROOT/k8s/regional/deployment-region.yaml" > "$OUT/deployment-${REGION_ID}.yaml" envsubst < "$ROOT/k8s/regional/hpa-region.yaml" > "$OUT/hpa-${REGION_ID}.yaml" + echo "Rendered target=$target region_id=$REGION_ID" } -render_region ORD -render_region IAD + +for target in $XAI_TARGETS; do + render_target "$target" +done + envsubst < "$ROOT/k8s/regional/service.yaml" > "$OUT/service.yaml" envsubst < "$ROOT/k8s/regional/pdb.yaml" > "$OUT/pdb.yaml" echo "Rendered manifests in $OUT" diff --git a/tests/test_render_regional_multi_target.sh b/tests/test_render_regional_multi_target.sh new file mode 100644 index 0000000..2f3e358 --- /dev/null +++ b/tests/test_render_regional_multi_target.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/repo/scripts" "$TMP/repo/k8s/regional" "$TMP/bin" +cp "$ROOT/scripts/render-regional-k8s.sh" "$TMP/repo/scripts/" +cat > "$TMP/bin/envsubst" <<'PY' +#!/usr/bin/env python3 +import os,re,sys +s=sys.stdin.read() +s=re.sub(r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}',lambda m:os.environ.get(m.group(1),m.group(0)),s) +sys.stdout.write(s) +PY +chmod +x "$TMP/bin/envsubst" +cat > "$TMP/repo/k8s/regional/deployment-region.yaml" <<'EOF2' +id=${REGION_ID};replicas=${REGION_REPLICAS};url=${XAI_UPSTREAM_URL};secret=${XAI_SECRET_NAME} +EOF2 +cat > "$TMP/repo/k8s/regional/hpa-region.yaml" <<'EOF2' +hpa=${REGION_ID} +EOF2 +cat > "$TMP/repo/k8s/regional/service.yaml" <<'EOF2' +service=${APP_NAME} +EOF2 +cat > "$TMP/repo/k8s/regional/pdb.yaml" <<'EOF2' +pdb=${APP_NAME} +EOF2 +cat > "$TMP/regions.env" <<'EOF2' +APP_NAME=tia +XAI_TARGETS="ORD IAD GRU" +ORD_REGION_ID=ord +ORD_REGION_REPLICAS=1 +ORD_XAI_UPSTREAM_URL=wss://ord +ORD_XAI_SECRET_NAME=secret-ord +IAD_REGION_ID=iad +IAD_REGION_REPLICAS=2 +IAD_XAI_UPSTREAM_URL=wss://iad +IAD_XAI_SECRET_NAME=secret-iad +GRU_REGION_ID=gru +GRU_REGION_REPLICAS=3 +GRU_XAI_UPSTREAM_URL=wss://gru +GRU_XAI_SECRET_NAME=secret-gru +EOF2 +PATH="$TMP/bin:$PATH" "$TMP/repo/scripts/render-regional-k8s.sh" "$TMP/regions.env" "$TMP/out" +test -f "$TMP/out/deployment-ord.yaml" +test -f "$TMP/out/deployment-iad.yaml" +test -f "$TMP/out/deployment-gru.yaml" +grep -q 'replicas=3' "$TMP/out/deployment-gru.yaml" +echo 'test_render_regional_multi_target: PASS'