[lab] Prepara serviço para gates de promoção lab -> homolog #9
@ -11,8 +11,9 @@ CLICKHOUSE_USER=wren_ia
|
|||||||
CLICKHOUSE_PASSWORD=
|
CLICKHOUSE_PASSWORD=
|
||||||
CLICKHOUSE_SECURE=true
|
CLICKHOUSE_SECURE=true
|
||||||
|
|
||||||
# CORS (separar por vírgula)
|
# CORS: origens permitidas por ambiente, separadas por vírgula.
|
||||||
VANNA_CORS_ORIGINS=https://lab.clubpetro.com,https://homologation.clubpetro.com
|
# Definir por ambiente (no k8s vem do secret/env do overlay — nunca hardcode de host aqui).
|
||||||
|
VANNA_CORS_ORIGINS=
|
||||||
|
|
||||||
# RLS defaults (apenas pra `python ask.py` na CLI; servidor web extrai de query string)
|
# RLS defaults (apenas pra `python ask.py` na CLI; servidor web extrai de query string)
|
||||||
RLS_PROGRAM_ID=
|
RLS_PROGRAM_ID=
|
||||||
|
|||||||
@ -82,9 +82,10 @@ jobs:
|
|||||||
gcloud container clusters get-credentials ${{ secrets.GKE_CLUSTER }} --region ${{ secrets.GKE_REGION }} --project ${{ secrets.GCP_PROJECT }}
|
gcloud container clusters get-credentials ${{ secrets.GKE_CLUSTER }} --region ${{ secrets.GKE_REGION }} --project ${{ secrets.GCP_PROJECT }}
|
||||||
NS=${{ secrets.K8S_NAMESPACE }}
|
NS=${{ secrets.K8S_NAMESPACE }}
|
||||||
|
|
||||||
# 1) Aplica manifests (idempotente — cria PVC/Service/Ingress/Deployment se faltarem)
|
# 1) Aplica manifests do LAB (idempotente — cria PVC/Service/Ingress/Deployment se faltarem).
|
||||||
if [ -d k8s ]; then
|
# Overlay do lab vive em k8s/lab/; homolog usa k8s/hml/ (promoção via pipeline lab->homolog).
|
||||||
kubectl apply -n "$NS" -f k8s/
|
if [ -d k8s/lab ]; then
|
||||||
|
kubectl apply -n "$NS" -f k8s/lab/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2) Atualiza image
|
# 2) Atualiza image
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@ -51,8 +51,16 @@ RUN pip install -r requirements.txt
|
|||||||
# Código do app
|
# Código do app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# data dirs
|
# Gate E — usuário non-root. uid/gid 10001 batem com o securityContext do
|
||||||
RUN mkdir -p /app/chroma_db /app/data_storage
|
# k8s/hml/deployment.yaml (runAsUser/fsGroup). HOME=/home/app pro cache do
|
||||||
|
# ChromaDB (~/.cache/chroma) cair num diretório gravável pelo usuário.
|
||||||
|
ENV HOME=/home/app
|
||||||
|
RUN groupadd -g 10001 app \
|
||||||
|
&& useradd -u 10001 -g 10001 -m -d /home/app -s /usr/sbin/nologin app \
|
||||||
|
&& mkdir -p /app/chroma_db /app/data_storage /home/app/.cache/chroma \
|
||||||
|
&& chown -R 10001:10001 /app /home/app
|
||||||
|
|
||||||
|
USER 10001
|
||||||
|
|
||||||
EXPOSE 8765
|
EXPOSE 8765
|
||||||
|
|
||||||
|
|||||||
85
k8s/hml/deployment.yaml
Normal file
85
k8s/hml/deployment.yaml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: vanna-clubpetro-deployment
|
||||||
|
labels:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
spec:
|
||||||
|
replicas: 1 # ChromaDB SQLite-based — múltiplas réplicas corrompem o vector store
|
||||||
|
strategy:
|
||||||
|
type: Recreate # com PVC ReadWriteOnce não dá pra ter 2 pods montando ao mesmo tempo
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
spec:
|
||||||
|
# Gate E: pod roda como non-root. fsGroup dá posse dos volumes (PVC) ao
|
||||||
|
# grupo 10001 pra que o processo non-root consiga escrever no store/cache.
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 10001
|
||||||
|
runAsGroup: 10001
|
||||||
|
fsGroup: 10001
|
||||||
|
containers:
|
||||||
|
- name: vanna-clubpetro
|
||||||
|
# A tag é sobrescrita pelo pipeline (kubectl set image). Placeholder:
|
||||||
|
image: us-central1-docker.pkg.dev/corepetro/clubpetro/vanna-clubpetro:hml-latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8765
|
||||||
|
env:
|
||||||
|
- name: HOME
|
||||||
|
value: /home/app
|
||||||
|
- name: VANNA_CORS_ORIGINS
|
||||||
|
value: "https://homologation.clubpetro.com"
|
||||||
|
envFrom:
|
||||||
|
# Gate G: credenciais do ClickHouse num secret DEDICADO ao serviço.
|
||||||
|
- secretRef:
|
||||||
|
name: vanna-clubpetro-db
|
||||||
|
# Config de app (OpenAI, RLS defaults etc.) em secret separado.
|
||||||
|
- secretRef:
|
||||||
|
name: vanna-clubpetro-secret
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "200m"
|
||||||
|
memory: "1Gi"
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: "2Gi"
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8765
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8765
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/chroma_db
|
||||||
|
subPath: chroma_db
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/data_storage
|
||||||
|
subPath: data_storage
|
||||||
|
- name: data
|
||||||
|
mountPath: /home/app/.cache/chroma
|
||||||
|
subPath: chroma-onnx-cache
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: vanna-clubpetro-data
|
||||||
32
k8s/hml/ingress.yaml
Normal file
32
k8s/hml/ingress.yaml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: vanna-clubpetro
|
||||||
|
labels:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: nginx
|
||||||
|
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
||||||
|
# SSE/WebSocket precisam de timeout grande pra streaming não ser cortado
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: homologation.clubpetro.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /api/vanna(/|$)(.*)
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: vanna-clubpetro
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- homologation.clubpetro.com
|
||||||
|
secretName: homologation-tls
|
||||||
53
k8s/hml/secret.example.yaml
Normal file
53
k8s/hml/secret.example.yaml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Exemplo dos Secrets do homolog. NÃO versionar os valores reais — este arquivo
|
||||||
|
# é só documentação. Crie os secrets no cluster via kubectl (fora do git):
|
||||||
|
#
|
||||||
|
# # Gate G — credenciais de banco em secret DEDICADO ao serviço:
|
||||||
|
# kubectl -n <ns> create secret generic vanna-clubpetro-db \
|
||||||
|
# --from-literal=CLICKHOUSE_HOST=... \
|
||||||
|
# --from-literal=CLICKHOUSE_PORT=8443 \
|
||||||
|
# --from-literal=CLICKHOUSE_DATABASE=gold \
|
||||||
|
# --from-literal=CLICKHOUSE_USER=wren_ia \
|
||||||
|
# --from-literal=CLICKHOUSE_PASSWORD=... \
|
||||||
|
# --from-literal=CLICKHOUSE_SECURE=true
|
||||||
|
#
|
||||||
|
# # Config de app (não-banco): OpenAI + RLS defaults:
|
||||||
|
# kubectl -n <ns> create secret generic vanna-clubpetro-secret \
|
||||||
|
# --from-literal=OPENAI_API_KEY=... \
|
||||||
|
# --from-literal=OPENAI_MODEL=gpt-5 \
|
||||||
|
# --from-literal=OPENAI_TEMPERATURE=1.0 \
|
||||||
|
# --from-literal=RLS_PROGRAM_ID=... \
|
||||||
|
# --from-literal=RLS_STORE_ID=... \
|
||||||
|
# --from-literal=RLS_USER_ID=...
|
||||||
|
#
|
||||||
|
# As chaves acima são as mesmas do .env.example. O Deployment injeta ambos
|
||||||
|
# via envFrom.secretRef. Nunca colocar valor real em arquivo versionado.
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: vanna-clubpetro-db
|
||||||
|
labels:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
CLICKHOUSE_HOST: ""
|
||||||
|
CLICKHOUSE_PORT: "8443"
|
||||||
|
CLICKHOUSE_DATABASE: "gold"
|
||||||
|
CLICKHOUSE_USER: "wren_ia"
|
||||||
|
CLICKHOUSE_PASSWORD: ""
|
||||||
|
CLICKHOUSE_SECURE: "true"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: vanna-clubpetro-secret
|
||||||
|
labels:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
OPENAI_API_KEY: ""
|
||||||
|
OPENAI_MODEL: "gpt-5"
|
||||||
|
OPENAI_TEMPERATURE: "1.0"
|
||||||
|
RLS_PROGRAM_ID: ""
|
||||||
|
RLS_STORE_ID: ""
|
||||||
|
RLS_USER_ID: ""
|
||||||
13
k8s/lab/pvc.yaml
Normal file
13
k8s/lab/pvc.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: vanna-clubpetro-data
|
||||||
|
labels:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
storageClassName: standard-rwo
|
||||||
15
k8s/lab/service.yaml
Normal file
15
k8s/lab/service.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: vanna-clubpetro
|
||||||
|
labels:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: vanna-clubpetro
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 8765
|
||||||
|
protocol: TCP
|
||||||
115
promotion-manifest.yaml
Normal file
115
promotion-manifest.yaml
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# promotion-manifest.yaml
|
||||||
|
# Manifesto de promoção lab -> homolog (ClubPetro).
|
||||||
|
# A PR de promoção deve ser marcada com [lab] e conter este arquivo.
|
||||||
|
# O pipeline lê os campos abaixo pra rodar os gates 0 + A-F + G + admissao.
|
||||||
|
#
|
||||||
|
# OBS: o schema exato é definido pelo parser do pipeline (referências vivas:
|
||||||
|
# backend=external_apis, seed=action-plan). As chaves aqui são auto-descritivas;
|
||||||
|
# reconcilie com o parser se ele exigir nomes específicos.
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PromotionManifest
|
||||||
|
|
||||||
|
service:
|
||||||
|
name: vanna-clubpetro
|
||||||
|
kind: backend # serviço HTTP (não é MFE)
|
||||||
|
description: >
|
||||||
|
Deploy do Vanna 2.0 (text-to-SQL sobre LLM) que responde perguntas em
|
||||||
|
pt-BR consultando o ClickHouse Cloud (database gold) com RLS por tenant
|
||||||
|
(program_id + store_id).
|
||||||
|
owner: dados-plataforma # TODO: confirmar squad/owner
|
||||||
|
runtimePort: 8765
|
||||||
|
|
||||||
|
promotion:
|
||||||
|
from: lab
|
||||||
|
to: homolog
|
||||||
|
# Onde o pipeline re-materializa o código (branch limpa, sem histórico do lab).
|
||||||
|
kubernetesOverlay: k8s/hml
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gate 0 — Stack
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
stack:
|
||||||
|
language: python
|
||||||
|
runtime: python-3.11
|
||||||
|
framework: vanna-2.0 + fastapi + uvicorn
|
||||||
|
packageManager: pip
|
||||||
|
# Stack padrão do core é NestJS>=10/TypeORM0.3 (yarn) OU MFE React17/MUI5.
|
||||||
|
# Python é NO-GO por padrão -> exceção formal abaixo (Gate 0).
|
||||||
|
stackException:
|
||||||
|
requested: true
|
||||||
|
reason: >
|
||||||
|
O serviço é um deploy da biblioteca Vanna 2.0, que é Python e não tem
|
||||||
|
equivalente em NestJS. O núcleo (agente LLM, memória vetorial ChromaDB,
|
||||||
|
runner RLS do ClickHouse e o web component <vanna-chat>) vem do upstream
|
||||||
|
vanna-ai/vanna. Reescrever em Node significaria reimplementar toda essa
|
||||||
|
cadeia — inviável e sem ganho. Mantém-se Python; todos os demais gates
|
||||||
|
aplicáveis são cumpridos (ver abaixo).
|
||||||
|
approvedBy: "" # TODO: preencher com o aprovador humano do stackException
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gate B — Migrations
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
migrations:
|
||||||
|
applicable: false
|
||||||
|
reason: >
|
||||||
|
O serviço não possui schema relacional próprio. O ClickHouse Cloud
|
||||||
|
(database gold) é gerido fora do serviço; o "treino" (train.py) apenas
|
||||||
|
popula um vector store ChromaDB local, idempotente e reconstruível
|
||||||
|
(rm -rf chroma_db/ && python train.py). Não há migrations TypeORM.
|
||||||
|
Toda query ao ClickHouse é parametrizada via settings do clickhouse_connect
|
||||||
|
(RLS), nunca por interpolação de string — ver rls_runner.py.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gate D — Atalhos / segurança
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
security:
|
||||||
|
envVersioned: false # apenas .env.example versionado; .env no .gitignore
|
||||||
|
hardcodedSecrets: false # segredos vêm de k8s Secret (envFrom), nunca do git
|
||||||
|
hardcodedLabHost: false # overlay homolog usa homologation.clubpetro.com
|
||||||
|
npmrcVersioned: false # sem .npmrc no repo
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gate G — Isolamento de credenciais de banco
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
database:
|
||||||
|
# Credenciais do ClickHouse em secret DEDICADO ao serviço (não genérico).
|
||||||
|
credentialsSecret: vanna-clubpetro-db
|
||||||
|
genericSecret: false # NÃO usa sqlhomologation / sqluserhomolgeneric etc.
|
||||||
|
engine: clickhouse-cloud
|
||||||
|
database: gold
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Admissão de capacidade
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
capacity:
|
||||||
|
replicas: 1 # ChromaDB é SQLite-based; réplicas > 1 corrompem o store
|
||||||
|
strategy: Recreate # PVC ReadWriteOnce — não dá 2 pods montando ao mesmo tempo
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "200m"
|
||||||
|
memory: "1Gi"
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: "2Gi"
|
||||||
|
persistentVolume:
|
||||||
|
size: 5Gi
|
||||||
|
accessMode: ReadWriteOnce # store vetorial + cache ONNX + CSVs
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Deltas (avisos — não bloqueiam)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
deltas:
|
||||||
|
A_architecture: >
|
||||||
|
Tenant (program_id/store_id) resolvido via RequestContext e validado por
|
||||||
|
regex ^[A-Za-z0-9_-]+$ (agent.py). O embed confiável passa os IDs por query
|
||||||
|
param do <vanna-chat> — não há token JWT nesta arquitetura de embed.
|
||||||
|
C_contract: >
|
||||||
|
OpenAPI auto-servido pelo FastAPI (/openapi.json, /docs). Rotas de chat já
|
||||||
|
versionadas: /api/vanna/v2/chat_sse|chat_websocket|chat_poll (upstream).
|
||||||
|
E_ops: >
|
||||||
|
Probes de liveness/readiness em GET /health; replicas: 1; Dockerfile
|
||||||
|
multi-stage rodando como usuário non-root (uid 10001).
|
||||||
|
F_ci: >
|
||||||
|
CI/CD via .gitea/workflows/cd.yml (build remoto no Cloud Build + rollout
|
||||||
|
no GKE). bitbucket-pipelines.yml N/A: o repo vive no Gitea self-hosted.
|
||||||
@ -42,6 +42,12 @@ def _build_app():
|
|||||||
if os.path.isdir(dist):
|
if os.path.isdir(dist):
|
||||||
fastapi_app.mount("/static", StaticFiles(directory=dist), name="static")
|
fastapi_app.mount("/static", StaticFiles(directory=dist), name="static")
|
||||||
|
|
||||||
|
@fastapi_app.get("/health")
|
||||||
|
async def health():
|
||||||
|
# Alvo das probes de liveness/readiness (Gate E). Leve, sem tocar
|
||||||
|
# ClickHouse/LLM — só sinaliza que o processo subiu e serve HTTP.
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
@fastapi_app.get("/vanna-theme.css")
|
@fastapi_app.get("/vanna-theme.css")
|
||||||
async def vanna_theme():
|
async def vanna_theme():
|
||||||
path = os.path.join(here, "static", "vanna-theme.css")
|
path = os.path.join(here, "static", "vanna-theme.css")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user