# 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-` (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:` - `rv32i.pl/trade/trade-ingestor:` - `rv32i.pl/trade/trade-frontend:` 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).