Merge pull request '[lab] Prepara serviço para gates de promoção lab -> homolog' (#9) from lab/promotion-gates into main
All checks were successful
CD / build (push) Successful in 5m28s
All checks were successful
CD / build (push) Successful in 5m28s
This commit is contained in:
commit
2fe6460578
@ -11,8 +11,9 @@ CLICKHOUSE_USER=wren_ia
|
||||
CLICKHOUSE_PASSWORD=
|
||||
CLICKHOUSE_SECURE=true
|
||||
|
||||
# CORS (separar por vírgula)
|
||||
VANNA_CORS_ORIGINS=https://lab.clubpetro.com,https://homologation.clubpetro.com
|
||||
# CORS: origens permitidas por ambiente, separadas por vírgula.
|
||||
# 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_PROGRAM_ID=
|
||||
|
||||
@ -82,9 +82,10 @@ jobs:
|
||||
gcloud container clusters get-credentials ${{ secrets.GKE_CLUSTER }} --region ${{ secrets.GKE_REGION }} --project ${{ secrets.GCP_PROJECT }}
|
||||
NS=${{ secrets.K8S_NAMESPACE }}
|
||||
|
||||
# 1) Aplica manifests (idempotente — cria PVC/Service/Ingress/Deployment se faltarem)
|
||||
if [ -d k8s ]; then
|
||||
kubectl apply -n "$NS" -f k8s/
|
||||
# 1) Aplica manifests do LAB (idempotente — cria PVC/Service/Ingress/Deployment se faltarem).
|
||||
# Overlay do lab vive em k8s/lab/; homolog usa k8s/hml/ (promoção via pipeline lab->homolog).
|
||||
if [ -d k8s/lab ]; then
|
||||
kubectl apply -n "$NS" -f k8s/lab/
|
||||
fi
|
||||
|
||||
# 2) Atualiza image
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@ -51,8 +51,16 @@ RUN pip install -r requirements.txt
|
||||
# Código do app
|
||||
COPY . .
|
||||
|
||||
# data dirs
|
||||
RUN mkdir -p /app/chroma_db /app/data_storage
|
||||
# Gate E — usuário non-root. uid/gid 10001 batem com o securityContext do
|
||||
# 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
|
||||
|
||||
|
||||
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):
|
||||
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")
|
||||
async def vanna_theme():
|
||||
path = os.path.join(here, "static", "vanna-theme.css")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user