391 lines
17 KiB
Markdown
391 lines
17 KiB
Markdown
# Migracja `trade` do k3s + GitOps (pull) na Gitea + CI/CD
|
||
|
||
Ten dokument opisuje plan migracji obecnego stacka (Docker Compose) do k3s z CD w modelu **pull-based** (GitOps): klaster sam synchronizuje „desired state” z repo na Gitei, a CI jedynie buduje/publikuje obrazy i aktualizuje repo deploymentu.
|
||
|
||
## Status (VPS / k3s)
|
||
|
||
Na VPS jest już uruchomione (k3s single-node):
|
||
|
||
- Ingress: Traefik (80/443)
|
||
- TLS: cert-manager + `ClusterIssuer/letsencrypt-prod`
|
||
- Gitea (Ingress `https://rv32i.pl`) + registry (`https://rv32i.pl/v2/`)
|
||
- Argo CD w namespace `argocd`
|
||
- Gitea Actions runner w namespace `gitea-actions` (act_runner + DinD)
|
||
- GitOps repo `trade/trade-deploy` podpięte do Argo:
|
||
- `Application/argocd/trade-staging` (auto-sync)
|
||
- `namespace/trade-staging`: Postgres/Timescale + Hasura + pgAdmin + Job `hasura-bootstrap`
|
||
- `namespace/trade-staging`: `trade-api` + `trade-ingestor` (obrazy z registry `rv32i.pl`)
|
||
- `namespace/trade-staging`: `trade-frontend` + Ingress `trade.rv32i.pl` (basic auth, TLS OK)
|
||
|
||
Szybka weryfikacja (VPS):
|
||
|
||
```bash
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging get deploy trade-api trade-ingestor -o wide
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging logs deploy/trade-ingestor --tail=40
|
||
```
|
||
|
||
### Tokeny `trade-api` (read/write)
|
||
|
||
Endpointy w `trade-api` są chronione tokenami z tabeli `api_tokens`:
|
||
- `GET /v1/ticks`, `GET /v1/chart` → scope `read`
|
||
- `POST /v1/ingest/tick` → scope `write`
|
||
|
||
W `staging` tokeny są przechowywane jako K8s Secrets (bez commitowania do gita):
|
||
- `trade-staging/Secret/trade-ingestor-tokens`: `alg.json` (write) + `heliusN.json` (RPC)
|
||
- `trade-staging/Secret/trade-read-token`: `read.json` (read)
|
||
- `trade-staging/Secret/trade-frontend-tokens`: `frontend.json` (basic auth) + `read.json` (proxy do API)
|
||
|
||
### Dostęp: Argo CD UI (bez konfliktu z lokalnym Hasurą na 8080)
|
||
|
||
Port-forward uruchamiasz „na żądanie” (to nie jest stały serwis). Jeśli lokalnie masz Hasurę na `8080`, użyj `8090`.
|
||
|
||
Na VPS:
|
||
|
||
```bash
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n argocd port-forward --address 127.0.0.1 svc/argocd-server 8090:443
|
||
```
|
||
|
||
Na swoim komputerze:
|
||
|
||
```bash
|
||
ssh -L 8090:127.0.0.1:8090 user@rv32i.pl
|
||
```
|
||
|
||
UI: `https://localhost:8090`
|
||
|
||
### Argo CD: username / hasło
|
||
|
||
- Username: `admin`
|
||
- Hasło startowe (pobierz na VPS; nie commituj / nie wklejaj do gita):
|
||
|
||
```bash
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo
|
||
```
|
||
|
||
Po pierwszym logowaniu ustaw własne hasło i (opcjonalnie) usuń `argocd-initial-admin-secret`.
|
||
|
||
### Runner: log (czy CI działa)
|
||
|
||
Na VPS:
|
||
|
||
```bash
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n gitea-actions logs deploy/gitea-act-runner -c runner --tail=80
|
||
```
|
||
|
||
### Portainer (Kubernetes UI): `portainer.rv32i.pl`
|
||
|
||
Portainer jest wdrażany przez Argo CD jako osobna aplikacja `portainer` (namespace `portainer`) z Ingressem i cert-managerem.
|
||
|
||
Wymagania:
|
||
- DNS: rekord A `portainer.rv32i.pl` → `77.90.8.171`
|
||
|
||
Weryfikacja na VPS:
|
||
|
||
```bash
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n argocd get application portainer -o wide
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n portainer get pods,svc,ingress,certificate -o wide
|
||
```
|
||
|
||
Jeśli cert-manager „utknie” na HTTP-01 z powodu cache NXDOMAIN w klastrze, pomocne jest zrestartowanie CoreDNS:
|
||
|
||
```bash
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n kube-system rollout restart deploy/coredns
|
||
```
|
||
|
||
Jeśli Portainer pokaże ekran `timeout.html` (setup/login timed out), zrestartuj deployment i od razu dokończ inicjalizację:
|
||
|
||
```bash
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n portainer rollout restart deploy/portainer
|
||
```
|
||
|
||
### Frontend UI: `trade.rv32i.pl`
|
||
|
||
Frontend jest wdrożony w `trade-staging` i ma Ingress na `trade.rv32i.pl` (Traefik + cert-manager).
|
||
|
||
Wymagania:
|
||
- DNS: rekord A `trade.rv32i.pl` → `77.90.8.171`
|
||
|
||
Weryfikacja na VPS:
|
||
|
||
```bash
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging get deploy trade-frontend -o wide
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging get ingress trade-frontend -o wide
|
||
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging get certificate trade-rv32i-pl-tls -o wide
|
||
```
|
||
|
||
Status na dziś:
|
||
- `Certificate/trade-rv32i-pl-tls` = `Ready=True` (Let’s Encrypt)
|
||
|
||
## 0) Stan wejściowy (co dziś jest w repo)
|
||
|
||
Projekt ma już logiczny podział „mikroserwisowy”:
|
||
|
||
- **DB stack**: `devops/db/docker-compose.yml`
|
||
- TimescaleDB/Postgres (`postgres`, port 5432)
|
||
- Hasura (`hasura`, port 8080)
|
||
- pgAdmin (`pgadmin`, port 5050) – raczej tylko dev/staging
|
||
- **App stack**: `devops/app/docker-compose.yml`
|
||
- `trade-api` (`api`, port 8787; health: `GET /healthz`)
|
||
- `trade-frontend` (`frontend`, port 8081; health: `GET /healthz`)
|
||
- `trade-ingestor` (`ingestor`, profil `ingest`) – worker ingestujący ticki
|
||
- **One-shot bootstrap**: `devops/tools/bootstrap/docker-compose.yml`
|
||
- `db-init`, `db-version`, `db-backfill` (SQL)
|
||
- `hasura-bootstrap` (track tabel + permissions)
|
||
|
||
Sekrety/konfiguracja lokalnie są dziś w `tokens/*.json` (gitignored) i są montowane jako pliki do kontenerów.
|
||
|
||
## 1) Cel migracji
|
||
|
||
- Przeniesienie usług do k3s (Kubernetes) bez zmiany logiki aplikacji.
|
||
- Wdrożenia przez GitOps (**pull**, bez „kubectl apply” z CI).
|
||
- Powtarzalność: jedna ścieżka build → deploy, jednoznaczne tagi/digests.
|
||
- Minimalny downtime: możliwość uruchamiania wersji równolegle (v1/v2…), jak w `scripts/ops/*`.
|
||
|
||
## 2) Założenia i decyzje do podjęcia (checklista)
|
||
|
||
Ustal na start (zaznacz jedną opcję, resztę możesz doprecyzować później):
|
||
|
||
1) **CD (pull)**: Argo CD czy Flux
|
||
- Argo CD + (opcjonalnie) Image Updater – popularny i „klikany”
|
||
- Flux + Image Automation – bardzo „git-first”
|
||
2) **Registry obrazów**: Gitea Registry czy osobne (Harbor / registry:2)
|
||
3) **Sekrety w GitOps**: manualne seedy (na start) czy docelowo SOPS/SealedSecrets/ExternalSecrets
|
||
4) **DB**: w klastrze (StatefulSet + PVC) czy zewnętrzny Postgres/Timescale
|
||
5) **Środowiska**: `staging` + `prod`? (opcjonalnie preview per branch/PR)
|
||
6) **Ingress/TLS**: Traefik + cert-manager + Let’s Encrypt (lub inny wariant)
|
||
|
||
## 3) Docelowa architektura na k3s (mapowanie 1:1)
|
||
|
||
Minimalny, praktyczny podział zasobów:
|
||
|
||
- `timescaledb` (Postgres/Timescale): `StatefulSet` + `PVC` + `Service`
|
||
- `hasura`: `Deployment` + `Service`
|
||
- `trade-api`: `Deployment` + `Service`
|
||
- `trade-frontend`: `Deployment` + `Service` + `Ingress`
|
||
- `trade-ingestor`: `Deployment` (zwykle `replicas: 1`) albo `CronJob` (jeśli ingest ma być okresowy)
|
||
- migracje/bootstrap:
|
||
- `Job` dla `db-init`/`db-version`/`db-backfill`
|
||
- `Job` dla `hasura-bootstrap`
|
||
|
||
Ważne:
|
||
- `trade-api` i `trade-frontend` mają gotowe endpointy `/healthz` do `readinessProbe`/`livenessProbe`.
|
||
- `trade-ingestor` potrzebuje egress do internetu (RPC do Helius/Drift).
|
||
|
||
## 4) Podział repozytoriów (rekomendowane)
|
||
|
||
Żeby GitOps było czyste i proste:
|
||
|
||
1) Repo aplikacji: **`trade`** (to repo)
|
||
- kod i Dockerfile: `devops/api/Dockerfile`, `devops/ingestor/Dockerfile`, `devops/app/frontend/Dockerfile`
|
||
2) Repo deploymentu: **`trade-deploy`** (nowe repo na Gitei)
|
||
- manifesty K8s (Helm chart albo Kustomize)
|
||
- overlays per środowisko: `staging/`, `prod/`
|
||
|
||
Zasada: CI nie wykonuje deploya do klastra; CI tylko aktualizuje `trade-deploy`, a CD w klastrze to „pulluje”.
|
||
|
||
## 5) CI na Gitei (build → push)
|
||
|
||
Do wyboru:
|
||
- **Gitea Actions** + `act_runner` (najbliżej GitHub Actions)
|
||
- alternatywnie Woodpecker/Drone, jeśli już masz w infra
|
||
|
||
Zakres CI:
|
||
- zbuduj i wypchnij obrazy:
|
||
- `trade-api`
|
||
- `trade-frontend`
|
||
- `trade-ingestor`
|
||
- taguj deterministycznie, np.:
|
||
- `sha-<shortsha>` (zawsze)
|
||
- opcjonalnie `vN` na release
|
||
- po push obrazu: zaktualizuj `trade-deploy` (tag/digest) lub zostaw to automatyzacji obrazów (patrz niżej).
|
||
|
||
## 6) CD (pull) z klastra: GitOps
|
||
|
||
Wariant A (najprostszy operacyjnie): **CI robi commit do `trade-deploy`**
|
||
- CI po zbudowaniu obrazu aktualizuje `values.yaml`/`kustomization.yaml` w `trade-deploy` (tag lub digest) i robi commit/push.
|
||
- Argo CD / Flux wykrywa zmianę i robi rollout.
|
||
|
||
Wariant B (bardziej „autopilot”): **automatyczna aktualizacja obrazów**
|
||
- Argo CD Image Updater albo Flux Image Automation obserwuje registry i sam aktualizuje repo `trade-deploy`.
|
||
- CI tylko buduje/pushuje obrazy.
|
||
|
||
## 7) Sekrety: `tokens/*.json` → Kubernetes Secret (plikowy mount)
|
||
|
||
Docelowo w K8s montujesz te same pliki co w Compose:
|
||
|
||
- `tokens/hasura.json` → dla `trade-api` (admin do Hasury)
|
||
- `tokens/api.json` → dla `trade-api` (admin secret do tokenów API)
|
||
- `tokens/read.json` → dla `trade-frontend` (proxy do API)
|
||
- `tokens/frontend.json` → dla `trade-frontend` (basic auth)
|
||
- `tokens/alg.json` → dla `trade-ingestor` (write token)
|
||
- `tokens/heliusN.json` (lub `rpcUrl`) → dla `trade-ingestor`
|
||
|
||
Nie commituj sekretów do gita. W GitOps wybierz jedną drogę:
|
||
- start: ręczne `kubectl create secret ...` na środowisko
|
||
- docelowo: SOPS (age) / SealedSecrets / ExternalSecrets (Vault itp.)
|
||
|
||
## 8) Plan wdrożenia (kolejność kroków)
|
||
|
||
### Etap 1: Klaster i narzędzia bazowe
|
||
1) Namespace’y (np. `trade`, `gitea`, `argocd`/`flux-system`).
|
||
2) StorageClass (na start `local-path`, docelowo rozważ Longhorn).
|
||
3) Ingress controller (Traefik w k3s) + cert-manager + ClusterIssuer.
|
||
4) Registry obrazów (Gitea Registry / Harbor / registry:2).
|
||
5) GitOps controller (Argo CD albo Flux) podpięty do `trade-deploy`.
|
||
|
||
### Etap 2: DB + Hasura
|
||
6) Deploy TimescaleDB (StatefulSet + PVC + Service).
|
||
7) Uruchom `Job` `db-init` (idempotent) – odpowiednik `devops/tools/bootstrap:db-init`.
|
||
8) Deploy Hasura (Deployment + Service + Secret na admin/jwt/db-url).
|
||
9) Uruchom `Job` `hasura-bootstrap` – odpowiednik `devops/tools/bootstrap:hasura-bootstrap`.
|
||
|
||
### Etap 3: App stack
|
||
10) Deploy `trade-api` (Deployment + Service, env `HASURA_GRAPHQL_URL`, `TICKS_TABLE`, `CANDLES_FUNCTION`).
|
||
11) Sprawdź `GET /healthz` po Service DNS i (opcjonalnie) z zewnątrz.
|
||
12) Wygeneruj i wgraj tokeny:
|
||
- read token dla frontendu (`tokens/read.json`)
|
||
- write token dla ingestora (`tokens/alg.json`)
|
||
13) Deploy `trade-frontend` + Ingress (TLS opcjonalnie); ustaw `API_UPSTREAM=http://trade-api:8787`.
|
||
14) Deploy `trade-ingestor` (`replicas: 1`); potwierdź, że ticki wpadają.
|
||
|
||
### Etap 4: CI/CD end-to-end
|
||
15) Skonfiguruj runnera CI (Gitea Actions / Woodpecker).
|
||
16) Workflow CI: build+push obrazów (i ewentualny commit do `trade-deploy`).
|
||
17) Zasady promocji: staging → prod (np. tylko tag/release).
|
||
|
||
## 9) Wersjonowanie v1/v2… (równoległe wdrożenia + cutover)
|
||
|
||
Repo ma już pattern wersjonowania w Compose (`scripts/ops/*` + `devops/versions/vN.env`).
|
||
|
||
Odwzorowanie na K8s:
|
||
- osobne release’y Helm (np. `trade-v1`, `trade-v2`) albo osobne namespace’y
|
||
- wspólny DB/Hasura bez zmian
|
||
- `Job` „version-init”:
|
||
- uruchamia SQL `db-version` (tworzy `drift_ticks_vN` i funkcje)
|
||
- uruchamia `hasura-bootstrap` z `TICKS_TABLE`/`CANDLES_FUNCTION` ustawionymi na vN
|
||
- cutover:
|
||
- start `trade-ingestor` vN
|
||
- stop `trade-ingestor` v1
|
||
- (opcjonalnie) `db-backfill` jako osobny job
|
||
|
||
## 10) Definition of Done (DoD)
|
||
|
||
- `trade-api` i `trade-frontend` działają na k3s (proby `Ready=True`, brak crashloopów).
|
||
- `trade-ingestor` zapisuje ticki do właściwej tabeli, a UI pokazuje wykres.
|
||
- Deploy jest sterowany przez GitOps (zmiana w `trade-deploy` → rollout bez ręcznego `kubectl apply`).
|
||
- Sekrety są poza gitem (manualnie lub przez wybraną metodę GitOps).
|
||
|
||
## 11) Referencje w repo
|
||
|
||
- Compose: `devops/db/docker-compose.yml`, `devops/app/docker-compose.yml`, `devops/tools/bootstrap/docker-compose.yml`
|
||
- Workflow end-to-end: `doc/workflow-api-ingest.md`
|
||
- Szkic migracji K8s: `doc/k8s-migracja.md`
|
||
- Gitea na k3s (Traefik/TLS): `doc/gitea-k3s-rv32i.md`
|
||
|
||
## 12) Metoda „superproject” (subrepo) – plan refaktoru repozytoriów
|
||
|
||
Cel: utrzymać osobne repo dla usług (API/ingestor/frontend) i osobne repo GitOps (`trade-deploy`), a do tego mieć jedno repo „główne” (superproject) spinające wszystko (np. jako **git submodules**).
|
||
|
||
### Wybór repo głównego
|
||
|
||
Rekomendacja: **`trade/trade-infra` jako repo główne (superproject)**:
|
||
- jest neutralne (infra/ops), nie miesza kodu aplikacji z manifestami GitOps,
|
||
- pozwala trzymać jedną „zafiksowaną” wersję całego systemu (SHAs submodułów),
|
||
- nie wymaga zmiany Argo CD (który nadal może śledzić `trade/trade-deploy`).
|
||
|
||
### Docelowy podział ról repo
|
||
|
||
- `trade/trade-api` – kod + Dockerfile dla `trade-api`
|
||
- `trade/trade-ingestor` – kod + Dockerfile dla `trade-ingestor`
|
||
- `trade/trade-frontend` – kod + Dockerfile dla `trade-frontend` (apps/visualizer + serwer)
|
||
- `trade/trade-deploy` – GitOps: Kustomize/Helm dla k3s (Argo CD pull)
|
||
- `trade/trade-doc` – dokumentacja całego projektu (`doc/`), w tym log działań
|
||
- `trade/trade-infra` – superproject + narzędzia/ops (np. makefile, skrypty, checklisty)
|
||
|
||
### Proponowany układ w superprojekcie
|
||
|
||
Przykład (ścieżki w `trade/trade-infra`):
|
||
|
||
```text
|
||
trade-infra/
|
||
api/ -> submodule: trade-api
|
||
ingestor/ -> submodule: trade-ingestor
|
||
frontend/ -> submodule: trade-frontend
|
||
deploy/ -> submodule: trade-deploy
|
||
doc/ -> submodule: trade-doc
|
||
```
|
||
|
||
### Plan wdrożenia refaktoru (po akceptacji)
|
||
|
||
1) Zainicjalizować `trade/trade-infra` jako superproject i dodać submodule do wszystkich subrepo.
|
||
2) Wyciąć obecny kod do subrepo:
|
||
- API: `services/api/*` + `devops/api/Dockerfile` → `trade-api/`
|
||
- Frontend: `apps/visualizer/*` + `services/frontend/*` + `devops/app/frontend/*` → `trade-frontend/`
|
||
- Ingestor: `scripts/ingest-drift-oracle.ts` (+ minimalny zestaw zależności i Dockerfile) → `trade-ingestor/`
|
||
3) Przenieść dokumentację do `trade-doc` (w tym log: `doc/steps.md`) i zostawić w pozostałych repo linki do docs.
|
||
4) Ujednolicić nazwy obrazów i tagowanie:
|
||
- `rv32i.pl/trade/trade-api:<tag>`
|
||
- `rv32i.pl/trade/trade-ingestor:<tag>`
|
||
- `rv32i.pl/trade/trade-frontend:<tag>`
|
||
5) CI/CD (Gitea Actions) per repo usługi:
|
||
- build → push do registry,
|
||
- aktualizacja `trade-deploy` (commit/PR z bumpem tagu/digesta),
|
||
- staging auto-sync w Argo (prod manual).
|
||
6) Walidacja end-to-end na k3s:
|
||
- rollout `trade-staging`,
|
||
- UI `https://trade.rv32i.pl`,
|
||
- ingest ticków (`trade-ingestor` → `trade-api`).
|
||
|
||
Uwaga: lokalnie repo nie ma historii commitów, więc refaktor będzie startem „od zera” w nowych repo (initial commit per subrepo).
|
||
|
||
### Status wdrożenia (superproject)
|
||
|
||
- Subrepo mają już initial import i są dostępne na Gitei:
|
||
- `trade/trade-api`
|
||
- `trade/trade-ingestor`
|
||
- `trade/trade-frontend`
|
||
- `trade/trade-doc`
|
||
- Superproject `trade/trade-infra` jest utworzony i zawiera `.gitmodules` + submodules (w tym `trade/trade-deploy`).
|
||
- Argo CD nadal śledzi `trade/trade-deploy` (bez zmian w klastrze).
|
||
|
||
Klonowanie superproject:
|
||
|
||
```bash
|
||
git clone --recurse-submodules https://rv32i.pl/trade/trade-infra.git
|
||
```
|
||
|
||
## 13) Dostęp i logowanie (admin vs użytkownicy)
|
||
|
||
Wymaganie:
|
||
- **admin** ma dostęp do wszystkich serwisów (np. Portainer, Argo CD, itp.)
|
||
- pozostali użytkownicy logują się **wyłącznie** do `trade` (`trade.rv32i.pl`)
|
||
|
||
Decyzja: na ten moment **wdrażamy logowanie tylko dla `trade`**. Pozostałe serwisy (Portainer/Argo/pgAdmin/Hasura) zostają ze swoimi, osobnymi loginami.
|
||
|
||
Rekomendowana metoda dla `trade`: **Traefik `basicAuth` na Ingress (Middleware)**.
|
||
|
||
### Plan wdrożenia (staging → prod)
|
||
|
||
1) Inwentaryzacja publicznych endpointów:
|
||
- sprawdzić które serwisy mają Ingress (hosty),
|
||
- serwisy administracyjne traktować jako „admin-only” (docelowo: ukryte za port-forward/VPN/IP allowlist).
|
||
2) `trade` (multi-user):
|
||
- przygotować `htpasswd` z wieloma wpisami (bcrypt),
|
||
- utworzyć `Secret` w namespace `trade-staging` (np. `trade-basic-auth`) z plikiem `users`,
|
||
- dodać `Middleware` `basicAuth` wskazujący na secret,
|
||
- podpiąć middleware do `Ingress/trade-frontend`.
|
||
3) Wyłączyć/dopasować auth w aplikacji `trade-frontend`:
|
||
- obecnie serwer frontendu ma wbudowany basic auth (plik `frontend.json`),
|
||
- żeby nie było „podwójnego logowania”, dodać tryb `BASIC_AUTH_MODE=off` (albo podobny) i ustawić go w `trade-deploy`.
|
||
4) Testy:
|
||
- `https://trade.rv32i.pl` → 401 bez auth, 200/304 z auth dla każdego usera,
|
||
- regresja: proxy `/api/*` w frontendzie nadal działa (token read jest dodawany po stronie serwera frontendu).
|
||
5) Utrzymanie:
|
||
- dodanie/usunięcie usera = update secreta (`htpasswd`) + rollout (bez zmian w kodzie),
|
||
- docelowo przenieść sekrety do GitOps (np. SOPS/SealedSecrets/ExternalSecrets).
|
||
|
||
### Status (trade)
|
||
|
||
- `trade-staging` ma wdrożone logowanie na Ingress (Traefik `basicAuth`) oraz wyłączony wbudowany basic auth w `trade-frontend` (`BASIC_AUTH_MODE=off`).
|
||
- UI pokazuje zalogowanego użytkownika (`GET /whoami`) i ma przycisk `Wyloguj` (`GET /logout`, best-effort dla Basic Auth).
|