From 9f58b9afa53ac1abfa66dd1670358335f1f64462 Mon Sep 17 00:00:00 2001 From: Dalton Alvarenga Date: Tue, 5 May 2026 17:56:37 -0300 Subject: [PATCH] chore: bootstrap deploy no hml2 (Dockerfile + k8s + workflow CD) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile multi-stage Node 18 (webcomponent) + Python 3.11 - vanna upstream pinned em 365d0617c1a4567ffee1b19b40c27feb4206bfcf - requirements.txt + .env.example + .dockerignore - k8s/: deployment (1 replica, PVC, Recreate), service, ingress (SSE/WS timeouts), PVC 5Gi - .gitea/workflows/cd.yml seguindo template do lab Pendência: criar Secret K8s vanna-clubpetro-secret com OPENAI_API_KEY + CLICKHOUSE_* --- .dockerignore | 17 +++++++++++ .env.example | 20 +++++++++++++ .gitea/workflows/cd.yml | 60 +++++++++++++++++++++++++++++++++++++ AI-HISTORY.md | 29 ++++++++++++++++++ Dockerfile | 60 +++++++++++++++++++++++++++++++++++++ k8s/deployment.yaml | 65 +++++++++++++++++++++++++++++++++++++++++ k8s/ingress.yaml | 32 ++++++++++++++++++++ k8s/pvc.yaml | 13 +++++++++ k8s/service.yaml | 15 ++++++++++ requirements.txt | 10 +++++++ 10 files changed, 321 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitea/workflows/cd.yml create mode 100644 AI-HISTORY.md create mode 100644 Dockerfile create mode 100644 k8s/deployment.yaml create mode 100644 k8s/ingress.yaml create mode 100644 k8s/pvc.yaml create mode 100644 k8s/service.yaml create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8502cd4 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b60f497 --- /dev/null +++ b/.env.example @@ -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= diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml new file mode 100644 index 0000000..99cb2a4 --- /dev/null +++ b/.gitea/workflows/cd.yml @@ -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 diff --git a/AI-HISTORY.md b/AI-HISTORY.md new file mode 100644 index 0000000..8f022b2 --- /dev/null +++ b/AI-HISTORY.md @@ -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-`. +- **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. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..473eae5 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..1592c6c --- /dev/null +++ b/k8s/deployment.yaml @@ -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 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..2a8797a --- /dev/null +++ b/k8s/ingress.yaml @@ -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 diff --git a/k8s/pvc.yaml b/k8s/pvc.yaml new file mode 100644 index 0000000..1fbb092 --- /dev/null +++ b/k8s/pvc.yaml @@ -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 diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..809362b --- /dev/null +++ b/k8s/service.yaml @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..30968a5 --- /dev/null +++ b/requirements.txt @@ -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