rubis/.claude/deploy-memory.md
ordinarthur cc44ed8ce8
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 23s
chore(ci): rename deploy.yml → deploy-landing.yml
Cohérence avec deploy-web.yml / deploy-api.yml — un fichier par cible.
Met à jour la self-reference dans le path filter + la doc deploy-memory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:30:56 +02:00

14 KiB
Raw Permalink Blame History

Deploy memory — rubis

Ce repo contient deux déploiements distincts sur la même infra K3s, namespace rubis :

  1. Landing statique (rubis.pro) — image nginx-alpine.
  2. App SaaS (app.rubis.pro) — image AdonisJS + React (V1).

Chacun a sa propre image, son propre Dockerfile, son propre workflow CI.


1. Landing (rubis.pro)

Infra

  • Deployment : rubis · Container : rubis · NodePort : 30109
  • Image : git.arthurbarre.fr/ordinarthur/rubis
  • Domaine : https://rubis.pro (sous-domaine temporaire — domaine définitif rubis.pro pas encore acheté)
  • Manifests : k3s/{namespace,deployment,service}.yml
  • Route Traefik : ~/dev/perso/proxmox/ansible/roles/traefik/templates/rubis.yml.j2
  • Workflow CI : .gitea/workflows/deploy-landing.yml
  • Source : landing/ (servi par nginx)

Mise à jour

Push git, le CI build + rollout auto (filter sur landing/**, Dockerfile, k3s/{namespace,deployment,service}.yml).


2. App SaaS (app.rubis.pro)

Architecture en 2 services : nginx en frontal (web) + API Node interne.

Traefik :443 → app.rubis.pro:30110 → rubis-web (nginx)
                                                    ├─ /        → SPA static (try_files)
                                                    └─ /api/*   → rubis-api (ClusterIP :3333)

rubis-web — frontend (NodePort 30110, exposé)

  • Image : git.arthurbarre.fr/ordinarthur/rubis-web
  • Container : web (nginx-alpine + SPA Vite dist)
  • Sert /assets/* (cache 1y immutable), / (SPA fallback try_files)
  • Reverse-proxy /api/* → rubis-api.rubis.svc.cluster.local:3333
  • Manifest : k3s/app/web.yml
  • Workflow CI : .gitea/workflows/deploy-web.yml
  • Source : apps/web, packages/shared + apps/web/nginx.conf

rubis-api — backend (ClusterIP, interne uniquement)

  • Image : git.arthurbarre.fr/ordinarthur/rubis-api
  • Container : api (Node 22 + AdonisJS V7)
  • Workers BullMQ dans le même process (cf. start/queue.ts)
  • Init container migrate : node ace.js migration:run --force depuis /app/apps/api/build (idempotent)
  • Probes K3s sur /api/v1/health
  • Manifest : k3s/app/api.yml (Deployment + Service ClusterIP + ConfigMap)
  • Workflow CI : .gitea/workflows/deploy-api.yml
  • Source : apps/api, packages/shared

rubis-redis — backend BullMQ + cache (ClusterIP)

  • Image : redis:7.4-alpine
  • PVC 1Gi local-path, AOF on, maxmemory 256mb allkeys-lru
  • Manifest : k3s/app/redis.yml
  • Re-déployé par le workflow API (path filter inclut redis.yml)

Infra commune

  • Domaine : https://app.rubis.pro (DNS A 5413305619)
  • Route Traefik : ~/dev/perso/proxmox/ansible/roles/traefik/templates/rubis-app.yml.j2

Dépendances externes (déjà déployées)

  • Postgres : 10.10.10.3:5432, base rubis_prod, user rubis
  • MinIO : minio.minio.svc.cluster.local:9000, bucket rubis-prod-invoices, creds = root MinIO
  • Redis : interne au namespace rubis, pas de dépendance externe

Secrets K3s (rubis-app-secrets)

Posés une fois manuellement via kubectl create secret generic (jamais dans les manifests). Re-pose si rotation ou ajout :

kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis create secret generic rubis-app-secrets \
  --from-literal=APP_KEY=... \
  --from-literal=PG_PASSWORD=... \
  --from-literal=S3_ACCESS_KEY=... \
  --from-literal=S3_SECRET_KEY=... \
  --from-literal=RESEND_API_KEY=... \
  --from-literal=MISTRAL_API_KEY=... \
  --from-literal=REDIS_PASSWORD="" \
  --from-literal=GOOGLE_CLIENT_ID=... \
  --from-literal=GOOGLE_CLIENT_SECRET=... \
  --from-literal=MICROSOFT_CLIENT_ID=... \
  --from-literal=MICROSOFT_CLIENT_SECRET=... \
  --from-literal=STRIPE_SECRET_KEY=... \
  --from-literal=STRIPE_WEBHOOK_SECRET=... \
  --from-literal=SENTRY_DSN_API=... \
  --from-literal=POWENS_CLIENT_ID=... \
  --from-literal=POWENS_CLIENT_SECRET=... \
  --from-literal=POWENS_WEBHOOK_SECRET=... \
  --dry-run=client -o yaml | kubectl apply -f -

⚠️ Si tu rotates UN secret, repose la commande complète avec toutes les vars (sinon apply supprime celles omises). Garde la liste à jour dans un coffre-fort perso (1Password, Bitwarden…).

Alternative pour ajouter UNE/quelques clés sans toucher au reste :

kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis patch secret rubis-app-secrets --type=strategic --patch '{
  "stringData": { "NOUVELLE_VAR": "valeur" }
}'

stringData accepte du clair — Kubernetes encode en base64 automatiquement.

Google SSO — setup Google Cloud Console

Si la clé OAuth est perdue ou qu'on doit la régénérer :

  1. https://console.cloud.google.com/apis/credentials → projet courant
  2. Create CredentialsOAuth client ID → type Web application
  3. Authorized JavaScript origins :
    • https://app.rubis.pro
    • http://localhost:5173 (dev SPA)
  4. Authorized redirect URIs :
    • https://app.rubis.pro/api/v1/auth/google/callback
    • http://localhost:3333/api/v1/auth/google/callback (dev API)
  5. Copier Client ID + Client secret → mettre dans apps/api/.env (dev) et rubis-app-secrets (prod, snippet ci-dessus).

L'écran de consentement OAuth doit être configuré au moins en mode "Testing" avec l'email du user courant ajouté en testeur. Pour la prod (n'importe qui peut se connecter), il faut passer en "In production" (vérification Google si scopes sensibles ; les nôtres userinfo.email + userinfo.profile sont non-sensibles, validation auto).

Microsoft SSO — setup Azure / Entra ID

  1. https://portal.azure.comMicrosoft Entra IDApp registrationsNew registration
  2. Name : Rubis Sur l'Ongle ; Supported account types :
    • "Accounts in any organizational directory and personal Microsoft accounts" (tenant=common, recommandé)
    • ou "Accounts in any organizational directory" (tenant=organizations, M365 strict)
  3. Redirect URI type Web :
    • https://app.rubis.pro/api/v1/auth/microsoft/callback
  4. Après création : ajouter en plus le redirect dev via Authentication → Add a platform → Web :
    • http://localhost:3333/api/v1/auth/microsoft/callback
  5. Certificates & secrets → New client secret : créer, copier la Value (visible une seule fois) → MICROSOFT_CLIENT_SECRET
  6. Page Overview → copier Application (client) IDMICROSOFT_CLIENT_ID
  7. API permissions : User.Read (déjà délégué par défaut), pas besoin d'admin consent pour les comptes individuels
  8. Mettre les valeurs dans apps/api/.env (dev) et rubis-app-secrets (prod).

Le client secret expire (Azure force 6 ou 12 mois max) — penser à le renouveler avant échéance ; sinon les nouvelles connexions échoueront en silence après expiration.

Banking (Powens) — activation prod

La feature banking est déployée mais désactivée par défaut en prod via le flag BANKING_ENABLED: 'false' dans le ConfigMap rubis-api-config. La section banque dans /parametres reste invisible, et /api/v1/banking/* renvoie 503 banking_disabled. Procédure d'activation une fois le KYC Powens prod validé :

  1. Powens — créer un domaine prod chez Powens (KYC requis : Kbis, contrat AISP/DSP2). Récupérer :

    • Slug du domaine (ex : rubis → URL rubis.biapi.pro).
    • client_id + client_secret prod (différents du sandbox).
    • Webhook secret (à générer dans la console Powens prod).
  2. Whitelist côté console Powens prod :

    • Allowed redirect URIs : https://app.rubis.pro/api/v1/banking/powens/callback
    • Webhook URL : https://app.rubis.pro/api/v1/webhooks/powens
  3. Mettre à jour les secrets K3s (re-pose le snippet kubectl create secret generic ci-dessus en remplissant les 3 vars POWENS_CLIENT_ID / POWENS_CLIENT_SECRET / POWENS_WEBHOOK_SECRET avec les valeurs prod).

  4. Mettre à jour le ConfigMap dans k3s/app/api.yml si le slug diffère de rubis (changer POWENS_DOMAIN et POWENS_API_BASE_URL). Puis BANKING_ENABLED: 'true'. Commit + push → CI redéploie.

  5. Smoke test prod : se connecter avec un compte Pro/Business, aller sur /parametres → la section "Connecter votre banque" doit apparaître. Cliquer "Connecter une banque" → la webview Powens prod doit s'ouvrir. Tester avec un vrai compte bancaire perso d'abord pour valider toute la chaîne (sync + reconcile + emails).

  6. Backup avant flip : la 1re connexion crée un user Powens prod et stocke powens_user_id + token chiffré (clé APP_KEY) sur l'org. Si on doit un jour faire rotate APP_KEY, prévoir une migration des tokens (déchiffrer avec ancienne clé → re-chiffrer avec nouvelle).

Le webhook Powens (POST /api/v1/webhooks/powens) attend la signature HMAC SHA-256 dans le header BI-Signature (ou X-Webhook-Signature). Vérification automatique dans le controller avec POWENS_WEBHOOK_SECRET. Si Powens reçoit autre chose qu'un 200 en réponse il retry agressivement → garder un œil sur les logs au démarrage.

Mise à jour

Push git → un (ou les deux) workflow(s) CI se déclenchent selon les paths modifiés. Build+rollout indépendants.

Path modifié Workflow déclenché
apps/web/**, Dockerfile.web, k3s/app/web.yml deploy-web.yml
apps/api/**, Dockerfile.api, k3s/app/api.yml, k3s/app/redis.yml deploy-api.yml
packages/shared/**, pnpm-lock.yaml, tsconfig.base.json, … les deux

Particularités du Dockerfile.api

  • node ace build plante avec ERR_UNKNOWN_FILE_EXTENSION car @poppinss/ts-exec ne s'enregistre pas à temps avant l'import de bin/console.ts. Solution : pnpm exec tsx ace.js build --ignore-ts-errors (tsx = esbuild, gère .ts nativement).
  • --ignore-ts-errors car tests/bootstrap.ts référence un type généré tardivement (.adonisjs/client/registry/schema.d.ts). Le typecheck strict est exécuté en CI séparée (pnpm typecheck).

Particularités du Dockerfile.web

  • Vite build appelé directement (pnpm exec vite build) au lieu du script tsc -b && vite build qui plante sans cache .tsbuildinfo à cause de @tanstack/router-core.
  • nginx.conf inclut le upstream rubis-api.rubis.svc.cluster.local:3333. Si on renomme le service API, c'est ici qu'il faut mettre à jour (en plus du manifest).

Voie manuelle (debug, hors CI)

TAG=$(git rev-parse --short HEAD)
# Landing (rubis.pro)
docker build --platform linux/amd64 -t git.arthurbarre.fr/ordinarthur/rubis:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis set image deploy/rubis rubis=git.arthurbarre.fr/ordinarthur/rubis:$TAG

# App — web (frontend)
docker build --platform linux/amd64 -f Dockerfile.web \
  -t git.arthurbarre.fr/ordinarthur/rubis-web:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis-web:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis set image deploy/rubis-web web=git.arthurbarre.fr/ordinarthur/rubis-web:$TAG

# App — api (backend)
docker build --platform linux/amd64 -f Dockerfile.api \
  -t git.arthurbarre.fr/ordinarthur/rubis-api:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis-api:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis set image deploy/rubis-api api=git.arthurbarre.fr/ordinarthur/rubis-api:$TAG \
  && kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
       -n rubis patch deploy/rubis-api --type=json \
       -p="[{\"op\":\"replace\",\"path\":\"/spec/template/spec/initContainers/0/image\",\"value\":\"git.arthurbarre.fr/ordinarthur/rubis-api:$TAG\"}]"

⚠️ Cross-compile ARM Mac → linux/amd64 plante sur @swc/core au build de l'app. Préférer pousser et laisser le CI Gitea (linux/amd64 natif).

Si changement route/domaine : cd ~/dev/perso/proxmox/ansible && ansible-playbook playbooks/gateway.yml. Penser à sudo systemctl restart traefik sur la gateway si un nouveau domaine ne récupère pas son cert ACME tout seul (le hot-reload de la dynamic config ne déclenche pas toujours le challenge LE).


Domaine actuel — rubis.pro migré (2026-05-07)

  • Landing : https://rubis.pro
  • App SaaS : https://app.rubis.pro
  • Compat legacy : rubis.arthurbarre.fr / app.rubis.arthurbarre.fr → 301 vers rubis.pro / app.rubis.pro (Traefik dynamic config dans ~/dev/perso/proxmox/ansible/roles/traefik/templates/rubis*.yml.j2)

Email rubis.pro

Flux Provider Setup Adresses
Sortant (transactionnel) Resend DKIM + SPF + DMARC sur send.rubis.pro (verified dashboard Resend) relances@rubis.pro (via MAIL_FROM_ADDRESS secret K3s)
Entrant (humain) OVH MX Plan (gratuit) MX @ → OVH (auto-configuré via Espace Client) contact@rubis.pro, dev@rubis.pro

⚠️ Resend Inbound (toggle "Enable Receiving") doit rester OFF : il veut le MX @, qui est pris par OVH MX Plan.

Décommissionnement progressif arthurbarre.fr (à faire dans 30-90 jours)

Quand confiance acquise et plus aucune référence vivante :

  1. Vire les blocs rubis-legacy-redirect / rubis-app-legacy-redirect (et middlewares) dans rubis*.yml.j2
  2. ansible-playbook playbooks/gateway.yml
  3. Supprime A records rubis (id 5413044152) + app.rubis (id 5413305619) dans la zone DNS OVH d'arthurbarre.fr

Déjà fait — NE PAS refaire

  • Dockerfile (landing) + Dockerfile.web + Dockerfile.api (app split)
  • nginx.conf (apps/web/nginx.conf) avec upstream rubis-api
  • Manifests k3s/ (landing) + k3s/app/{api,web,redis}.yml
  • Routes Traefik rubis.yml.j2 + rubis-app.yml.j2
  • DNS OVH : A records rubis (id 5413044152) + app.rubis (id 5413305619)
  • Repo Gitea + secrets CI (KUBECONFIG, REGISTRY_PASSWORD)
  • Namespace + secret registry K3s (gitea-registry)
  • Postgres : base rubis_prod + user rubis (10.10.10.3)
  • MinIO : bucket rubis-prod-invoices
  • Secret K3s rubis-app-secrets (APP_KEY, DB pwd, MinIO, Resend, Mistral, Google/Microsoft SSO, Stripe ; Powens à poser au moment de l'activation KYC)
  • ConfigMap rubis-api-config (env non-sensibles incl. banking flags désactivés par défaut)

Les prochains /deploy font uniquement build + rollout via push git.