chore: bootstrap deploy no hml2 (Dockerfile + k8s + workflow CD) #1

Merged
dalton.alvarenga merged 1 commits from chore/initial-deploy into main 2026-05-06 12:11:55 +00:00
10 changed files with 321 additions and 0 deletions

17
.dockerignore Normal file
View File

@ -0,0 +1,17 @@
.git
.github
.gitea
.env
.env.local
venv
__pycache__
*.pyc
.vanna
vanna
chroma_db
data_storage
.DS_Store
*.md
docs
test_*.py
node_modules

20
.env.example Normal file
View File

@ -0,0 +1,20 @@
# OpenAI
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5
OPENAI_TEMPERATURE=1.0
# ClickHouse Cloud (database `gold`, usuário `wren_ia`)
CLICKHOUSE_HOST=
CLICKHOUSE_PORT=8443
CLICKHOUSE_DATABASE=gold
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
# RLS defaults (apenas pra `python ask.py` na CLI; servidor web extrai de query string)
RLS_PROGRAM_ID=
RLS_STORE_ID=
RLS_USER_ID=

60
.gitea/workflows/cd.yml Normal file
View File

@ -0,0 +1,60 @@
name: CD
on:
push:
branches: [master, main]
pull_request:
env:
IMAGE_BASE: ${{ secrets.AR_LOCATION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ secrets.AR_REPO }}
jobs:
build:
runs-on: ubuntu-latest
if: ${{ hashFiles('Dockerfile') != '' }}
steps:
- uses: actions/checkout@v4
- name: Auth GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Setup gcloud
uses: google-github-actions/setup-gcloud@v2
with:
project_id: ${{ secrets.GCP_PROJECT }}
- name: Configure Docker auth
run: gcloud auth configure-docker ${{ secrets.AR_LOCATION }}-docker.pkg.dev --quiet
- name: Build image
run: |
IMG="${IMAGE_BASE}/${{ gitea.event.repository.name }}:lab-${{ gitea.run_number }}"
docker build --platform=linux/amd64 -t "$IMG" .
echo "IMG=$IMG" >> $GITHUB_ENV
- name: Push image (apenas em push pra master/main)
if: github.event_name == 'push'
run: docker push "$IMG"
- name: Deploy hml2 (apenas em push pra master/main)
if: github.event_name == 'push'
run: |
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/
fi
# 2) Atualiza image
DEPLOYMENT="${{ gitea.event.repository.name }}-deployment"
if kubectl get deployment "$DEPLOYMENT" -n "$NS" >/dev/null 2>&1; then
CONTAINER=$(kubectl get deployment "$DEPLOYMENT" -n "$NS" -o jsonpath='{.spec.template.spec.containers[0].name}')
kubectl set image deployment/"$DEPLOYMENT" -n "$NS" "$CONTAINER=$IMG"
kubectl rollout status deployment/"$DEPLOYMENT" -n "$NS" --timeout=300s
else
echo "Deployment $DEPLOYMENT não existe no ns $NS — pulei set image (provavelmente é o 1º deploy e o kubectl apply acabou de criar)"
fi

29
AI-HISTORY.md Normal file
View File

@ -0,0 +1,29 @@
# Histórico de mudanças com IA
Registro das alterações feitas neste repositório com apoio de IA (Claude), incluindo tempo aproximado. Ordem cronológica — **mais recente no topo**.
## Formato de entrada
```
### YYYY-MM-DD HH:MM — Título curto
- **Tempo:** ~X min
- **Prompt:** resumo do pedido
- **Mudanças:** o que foi feito
- **Artefatos:** branch, PR #, commit hash
```
---
### 2026-05-05 ~17:30 — Bootstrap de deploy no hml2
- **Tempo:** ~30 min
- **Prompt:** "fazer pull do repo vanna-clubpetro e fazer o deploy"
- **Mudanças:**
- Criado `Dockerfile` multi-stage: stage 1 (Node 18) clona vanna-ai/vanna upstream pinned em `365d061` e builda o webcomponent (~7.5MB); stage 2 (Python 3.11-slim) instala vanna editable + `requirements.txt` + código do app. CMD com `--workers 1` (constraint do ChromaDB SQLite).
- Criado `requirements.txt` (clickhouse-connect, chromadb, openai, fastapi, uvicorn, pandas, plotly, pydantic, python-dotenv).
- Criado `.env.example` baseado em `docs/deploy.md`.
- Criado `.dockerignore`.
- Criado `k8s/`: `deployment.yaml` (1 réplica, strategy Recreate, PVC montado em `/app/chroma_db`, `/app/data_storage` e `~/.cache/chroma`, probes TCP), `service.yaml` (ClusterIP 80→8765), `ingress.yaml` (`lab.clubpetro.com/api/vanna/*` com timeouts altos pra SSE/WebSocket), `pvc.yaml` (5Gi standard-rwo).
- Criado `.gitea/workflows/cd.yml` seguindo template do lab — PR só valida build, merge faz `kubectl apply -f k8s/` (idempotente) + `kubectl set image` na tag `lab-<run_number>`.
- **Artefatos:** branch `chore/initial-deploy`, PR a abrir em `clubpetro-lab/vanna-clubpetro`. Pendente: criar Secret K8s `vanna-clubpetro-secret` com `OPENAI_API_KEY` + `CLICKHOUSE_*` antes do pod subir.

60
Dockerfile Normal file
View File

@ -0,0 +1,60 @@
# syntax=docker/dockerfile:1.6
# Multi-stage:
# 1) Clona vanna-ai/vanna upstream (commit pinned em 365d061) e builda o webcomponent (~7.5MB)
# 2) Imagem Python 3.11 com vanna editable + requirements + código do app
ARG VANNA_UPSTREAM_COMMIT=365d0617c1a4567ffee1b19b40c27feb4206bfcf
# ============================================================================
# Stage 1 — webcomponent (Node)
# ============================================================================
FROM node:18-bookworm-slim AS webcomponent
ARG VANNA_UPSTREAM_COMMIT
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /vanna
RUN git init -q \
&& git remote add origin https://github.com/vanna-ai/vanna.git \
&& git fetch --depth 50 origin "$VANNA_UPSTREAM_COMMIT" \
&& git checkout FETCH_HEAD
WORKDIR /vanna/frontends/webcomponent
RUN npm install --no-audit --no-fund --loglevel=error \
&& npm run build \
&& ls -lh dist/vanna-components.js
# ============================================================================
# Stage 2 — runtime Python
# ============================================================================
FROM python:3.11-slim-bookworm
ARG VANNA_UPSTREAM_COMMIT
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates curl build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copia o vanna upstream + bundle já buildado do stage 1
COPY --from=webcomponent /vanna /app/vanna
# Instala vanna editable
RUN pip install -e ./vanna
# Instala deps do app
COPY requirements.txt .
RUN pip install -r requirements.txt
# Código do app
COPY . .
# data dirs
RUN mkdir -p /app/chroma_db /app/data_storage
EXPOSE 8765
# `--workers 1` é OBRIGATÓRIO — múltiplos workers corrompem o SQLite do Chroma
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8765", "--workers", "1"]

65
k8s/deployment.yaml Normal file
View File

@ -0,0 +1,65 @@
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:
containers:
- name: vanna-clubpetro
image: us-central1-docker.pkg.dev/corepetro/clubpetro-lab/vanna-clubpetro:lab-latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8765
env:
- name: VANNA_CORS_ORIGINS
value: "https://lab.clubpetro.com,https://homologation.clubpetro.com"
envFrom:
- secretRef:
name: vanna-clubpetro-secret
resources:
requests:
cpu: "200m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"
readinessProbe:
tcpSocket:
port: 8765
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
tcpSocket:
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: /root/.cache/chroma
subPath: chroma-onnx-cache
volumes:
- name: data
persistentVolumeClaim:
claimName: vanna-clubpetro-data

32
k8s/ingress.yaml Normal file
View 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: lab.clubpetro.com
http:
paths:
- path: /api/vanna(/|$)(.*)
pathType: Prefix
backend:
service:
name: vanna-clubpetro
port:
number: 80
tls:
- hosts:
- lab.clubpetro.com
secretName: lab-tls

13
k8s/pvc.yaml Normal file
View 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/service.yaml Normal file
View 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

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
# Runtime deps. O `vanna` editable é instalado via `pip install -e ./vanna` no Dockerfile.
clickhouse-connect>=0.7,<1.0
chromadb>=0.4,<1.0
openai>=1.0
python-dotenv>=1.0
fastapi>=0.110
uvicorn[standard]>=0.27
pandas>=2.0
plotly>=5.18
pydantic>=2.5