diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d6731ed --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,243 @@ +# CLAUDE.md — DBIZ Lakehouse trên Kubernetes (full context) + +> File ngữ cảnh cho các session sau. Đọc file này là nắm được: đang ở đâu, đã chốt gì, +> bẫy nào đã gặp, và còn phải làm gì. Cập nhật mục "TRẠNG THÁI" mỗi khi qua một mốc. +> +> **Nguyên tắc vàng của cả dự án:** rebuild có chủ đích (sửa nợ dev khi lên K8s), +> KHÔNG lift-and-shift, KHÔNG đập cái đang chạy. GitOps thuần. Helm + YAML thuần, **KHÔNG Kustomize**. +> "dev giống prod" = cùng chart, khác values theo env. +> "Stateful đứng yên, compute di": MinIO (data lake) GIỮ NGUYÊN ngoài K8s, compute di lên K8s trỏ về qua S3. + +--- + +## 0. TL;DR — đang ở đâu (cập nhật: 2026-07-08) + +Đang ở **Phase 0 (nền K8s)**, sắp xong. Cụ thể: + +- ✅ Resize `/` các node worker — xong. +- ✅ **Phép thử sinh tử**: pod K8s → MinIO ngoài (.37) — **XANH HOÀN TOÀN** (list + get + put + delete từ trong pod). Đây là rủi ro #1 của cả kế hoạch, đã gỡ. +- ✅ CPU baseline x86-64-v2 — đã kiểm hết 5 node (xem mục Bẫy #1). +- ✅ iscsi_tcp + iscsid trên cả 5 VM (prerequisite Longhorn) — xong, module đã ghim tự-load-sau-reboot. +- ✅ Sealed Secrets controller — đã có sẵn trên cụm (kube-system). +- ✅ ArgoCD — đã chạy (namespace argocd). +- ✅ Repo GitOps `dbiz-lake-k8s` — đã có, **public**, đã điền `` = `https://git.renolation.com/renolation/dbiz-lake-k8s.git`, đã commit + push. +- ⏳ **ĐANG LÀM: cài Longhorn** như hạ tầng nền cụm (cách A — tách riêng khỏi envs//apps). Chưa xong. +- ⬜ Sau Longhorn: apply root app tầng lake (`envs/dev/apps/root.yaml`). + +**Bước kế tiếp ngay:** cài Longhorn qua ArgoCD Application (tầng infra nền cụm), verify StorageClass `longhorn` + PVC survive pod delete. RỒI mới apply root lake stack. + +--- + +## 1. Bối cảnh hạ tầng + +### Cụm K8s +- **5 VM K8s trên 1 host Proxmox.** (Có Proxmox thứ hai cùng mạng → lộ trình HA thật sau này.) +- Control plane HA, đứng sau VIP/LB tại **`192.168.110.14:8443`** (kubeconfig trỏ vào đây, KHÔNG phải 127.0.0.1:6443). +- StorageClass hiện tại: `local-path` (default). Sắp thêm `longhorn`. +- Namespace convention **lakehouse**: `dbiz-lake-dev`, `dbiz-lake-prod`. + - (Phân biệt: `dbiz-dev/staging/prod` + `monitoring` là namespace của app **platform** khác, KHÔNG phải lake.) + +### Hạ tầng NGOÀI K8s (giữ nguyên, compute trỏ vào qua Service+Endpoints không selector) +- **MinIO (data lake): VM `lake` = `192.168.110.37:9000`.** Bucket `dbiz-warehouse`. Key `dbiz`/`dbiz1234` (dev — NỢ bảo mật). + - Đây là điểm neo dữ liệu xuyên suốt — KHÔNG BAO GIỜ migrate. Toàn bộ compute K8s trỏ về đây như S3 endpoint. + - Trong bucket đã có prefix: `raw_crm/`, `raw_posthog/`, `raw_test/`, `results_posthog/`, `staging_posthog/`, vài prefix UUID, và rác `staging_posthog_staging_posthog/`. +- **Postgres nguồn CRM: `172.20.109.5:5432`** — cho Debezium/Strimzi trỏ tới khi làm CDC (Phase 4). Cần xác nhận đúng là PG nguồn CRM trước khi dùng. + +### Git / CI +- Repo GitOps: **`git.renolation.com/renolation/dbiz-lake-k8s`** (public). + - Plan gốc ghi đích cuối là `git.dbiz.com` (Gitea built-in registry) — nhưng "URL là biến số". Đang dùng Renolation, đổi sau = một lệnh sed + cập nhật ArgoCD repo creds. +- CI: **Woodpecker** (đã có). File `.woodpecker/`. Build + push image spark-cdc. +- Registry: placeholder `` (đích: Gitea built-in). Image cdc-job hiện trỏ `git.dbiz.com/dbiz/spark-cdc` trong values — **cần đổi sang registry Renolation thật khi build image** (chưa làm). + +--- + +## 2. Kiến trúc đích (K8s) + +``` +┌──────────────── K8s cluster (5 VM / 1 host Proxmox) ────────────────┐ +│ namespace: dbiz-lake-dev | dbiz-lake-prod │ +│ │ +│ ArgoCD (app-of-apps) ── sync ──► mọi stack │ +│ │ +│ Strimzi(Kafka+Connect+Debezium) · Spark Operator · Trino · Polaris · │ +│ Airflow(K8sExecutor) · dbt · Superset/Metabase · OpenMetadata │ +│ Longhorn (block PV: Postgres / Kafka / ES / Spark checkpoint) │ +└──────────────────────────┬───────────────────────────────────────────┘ + │ S3 endpoint (qua mạng) + ┌──────────────────▼─────────────┐ + │ MinIO (GIỮ NGUYÊN, ngoài K8s) │ ← data lake đứng yên + │ 192.168.110.37, dbiz-warehouse │ + └────────────────────────────────┘ + Postgres CRM (172.20.109.5) ──► Debezium(Strimzi) bắt CDC +``` + +### Stack đích & mô hình chạy +| Layer | Stack | Ghi chú K8s | +|-------|-------|-------------| +| Storage | MinIO | NGOÀI K8s (giữ nguyên), Service+Endpoints `minio-lake` | +| Table format | Apache Iceberg | thư viện trong engine, không phải service | +| Catalog (L4) | **Polaris** | chart CHƯA có trong skeleton → thêm app riêng | +| Engine | Trino, Spark (Spark Operator, SparkApplication CRD) | Spark job = pod ephemeral, image bundled | +| Ingestion | Strimzi (Kafka+Connect+Debezium) dựng mới trên K8s | chấp nhận snapshot CDC lại (không migrate offset Docker) | +| Transform | dbt (KubernetesPodOperator) | compute ở Trino | +| Orchestration | Airflow (K8sExecutor, git-sync DAG) | giải nợ version-control DAG | +| Consumption | Superset + Metabase | đọc results qua Trino | +| Governance | OpenMetadata + ES + PG | LÀM SAU CÙNG — version lock mong manh nhất | + +> ⚠️ **Lưu ý catalog:** tài liệu ARCHITECTURE.md (bản cũ) ghi **Lakekeeper**, nhưng bản đang chạy thật + mọi tài liệu vận hành/K8s dùng **Apache Polaris 1.5.0**. → **Polaris là bản đúng.** + +--- + +## 3. Quyết định đã chốt (không bàn lại trừ khi có lý do mới) + +1. **1 cụm K8s, tách namespace dev/prod.** Helm + values per-env. KHÔNG Kustomize. +2. **GitOps đầy đủ từ đầu:** ArgoCD (app-of-apps) + Woodpecker CI + Sealed Secrets + cert-manager. +3. **Secret = Sealed Secrets** (mã hoá, commit an toàn vào git). Giải nợ credential plaintext. +4. **MinIO GIỮ NGUYÊN ngoài K8s.** Không migrate data lake. +5. **Longhorn** cho block PV nội cụm (Postgres/Kafka/ES/Spark checkpoint). Không ôm data lake → nhu cầu nhỏ. +6. **Kafka: Strimzi dựng mới trên K8s** (không xài ké Docker cũ). Snapshot CDC lại. +7. **Spark: Spark Operator** (SparkApplication CRD), image bundled (jar Iceberg/Kafka sẵn trong image). +8. **Longhorn cài theo "cách A": hạ tầng nền cụm tách riêng** khỏi `envs//apps` (nó là singleton toàn cụm, không thuộc riêng dev/prod). +9. **Multi-tenant — phân biệt 2 loại:** + - Tenant trong CRM (data DBIZ) → tách bằng cột `tenantId`, KHÔNG nhân hạ tầng. Lake DBIZ là MỘT. + - Khách mua nền tảng (Vietbank/Gemadept) → isolate per khách qua **ApplicationSet**. 3 mức cách ly (M1 chung compute/tách data · M2 tách namespace · M3 isolate hoàn toàn kể cả on-prem). +10. **"Lakehouse as a product", YAGNI có kỷ luật:** LÀM NGAY = DBIZ lake "khách số 0", tham số hoá sạch (chart nhận customer/source/bucket/s3.endpoint qua values). CHƯA LÀM = ApplicationSet lake, preset 3 mức, đa cụm (thêm khi có khách thứ 2). +11. **Chart nhận mọi dependency qua values** (S3/nguồn/kafka) — KHÔNG hardcode hạ tầng. Điều kiện để 1 chart phục vụ cả multi-tenant DBIZ lẫn on-prem air-gapped. + +### Quyết định cho Longhorn (chốt tại session này) +- Cài qua **ArgoCD Application** (Helm chart upstream `https://charts.longhorn.io`), GHIM version. +- **GIỮ `local-path` làm default StorageClass** — Longhorn là StorageClass tường minh, chỉ workload chỉ định `storageClass: longhorn` mới dùng (tránh vô tình đẩy mọi PVC lên Longhorn). +- **Replica count: đề xuất 2** (không 3). Lý do: 1 host vật lý → 3 replica chỉ "an toàn giả" (mất host mất hết). 2 replica đủ chống node/pod chết, nhẹ hơn; phần còn lại dựa vào backup (đúng mục E rủi ro #2). + +--- + +## 4. Cấu trúc repo `dbiz-lake-k8s` + +``` +charts/lake-cdc-job/ Chart CDC job (SparkApplication + checkpoint PVC) — THAM SỐ HOÁ theo customer +envs/dev/ + apps/ ArgoCD Applications (App-of-Apps tầng LAKE) + root.yaml Root → quét envs/dev/apps (path=envs/dev/apps, dest ns=argocd) + infra.yaml → envs/dev/infra (Service+Endpoints ngoài), dest ns=dbiz-lake-dev + strimzi-operator.yaml + spark-operator.yaml + kafka-cluster.yaml → envs/dev/kafka + cdc-job.yaml chart lake-cdc-job + values + infra/external-services.yaml MinIO ngoài .37 + Postgres CRM .5 (ĐÃ điền IP thật) + kafka/kafka.yaml Strimzi KRaft, Longhorn PV + values/cdc-job.yaml values khách số 0 = dbiz + sealed-secrets/ (.gitkeep) — SealedSecrets sẽ để đây +envs/prod/sealed-secrets/ (.gitkeep) +images/spark-cdc/Dockerfile FROM spark:3.5.6 + jar Iceberg/Kafka + cdc_crm_to_raw.py +.woodpecker/ Woodpecker CI +``` + +> **SẮP THÊM (Longhorn, cách A):** thư mục tầng nền cụm riêng, ví dụ `infra/cluster/` với `root.yaml` (app-of-apps nền) + `00-longhorn.yaml`. Tách khỏi `envs//apps`. (Chốt tên chính xác khi làm ở session repo.) + +### File values cdc-job (khách số 0 = dbiz) — điểm cần biết +- `customer: dbiz`, `targetTable: iceberg.raw_crm.cdc_events`, `topicPattern: crm\.public\..*` +- `catalog.uri: http://polaris:8181/api/catalog`, `warehouse: dbiz_warehouse` +- `s3.endpoint: http://minio-lake:9000` (Service nội cụm trỏ ra .37), `region: us-east-1` +- `checkpoint: storageClass=longhorn, size=5Gi` ← **lý do Longhorn phải xong TRƯỚC khi apply root lake** +- `image.repository: git.dbiz.com/dbiz/spark-cdc` ← **CẦN đổi sang registry Renolation thật** +- `imagePullSecret: gitea-registry` ← cần seal secret này + +--- + +## 5. Roadmap theo Phase (K8S_MIGRATION_PLAN) + +- **Phase 0 — Nền K8s** (đang ở đây): resize `/` ✅ · Longhorn ⏳ · Sealed Secrets ✅ · cert-manager + ingress ⬜ · ArgoCD app-of-apps ✅ (một phần). Verify mạng K8s→MinIO ✅. +- **Phase 1 — Image & MinIO endpoint:** Dockerfile spark-cdc + Woodpecker build/push ⬜ · SealedSecret minio-creds ⬜. (Verify pod K8s `mc`/`aws s3 ls` bucket ✅ đã làm sớm.) +- **Phase 2 — State metadata (Longhorn PV):** Postgres K8s (CloudNativePG) — dump Docker → restore K8s. (MinIO không cần.) +- **Phase 3 — Catalog + Engine:** Polaris (Deployment, chart phải TỰ THÊM) → PG K8s + MinIO ngoài · Spark Operator + Trino (Helm). Verify Trino query Iceberg (data cũ MinIO, catalog Polaris K8s). +- **Phase 4 — Ingestion CDC (cắt over):** Strimzi (Kafka+Connect+Debezium) PV Longhorn · KafkaConnector Debezium → PG CRM .5 · SparkApplication CDC → `raw_crm.cdc_events`. Verify update ở CRM chảy vào lake. RỒI tắt CDC Docker cũ. +- **Phase 5-7:** Airflow (K8sExecutor, git-sync) + dbt (KubernetesPodOperator) · Superset + Metabase · OpenMetadata (SAU CÙNG, ghim version chặt). +- **Phase 8 — Dọn dẹp:** verify toàn hệ ≥ vài ngày, giữ Docker fallback, tắt dần Docker. MinIO vẫn giữ. + +--- + +## 6. Bẫy đã gặp (cheat-sheet — đọc trước khi debug) + +1. **x86-64-v2 glibc fatal error.** Image hiện đại (minio/mc mới, aws-cli, sẽ cả Spark/Trino/ClickHouse/ES) build với glibc yêu cầu x86-64-v2. VM Proxmox để CPU type mặc định (`kvm64`/`qemu64`) CHE mất instruction v2 (thiếu sse4_2/popcnt/ssse3/sse4_1). → Sửa gốc: Proxmox `VM→Hardware→Processors→Type` = `host` (perf tối đa) hoặc `x86-64-v2-AES` (an toàn migrate 2 host), rồi **power cycle** (reboot guest không đủ). Kiểm: `lscpu | grep -oE 'sse4_2|popcnt|ssse3|sse4_1'`. **Phải đồng nhất MỌI node** (pod schedule bất kỳ node nào). Vá tạm qua phép thử mạng: pin `minio/mc:RELEASE.2023-11-20T16-30-59Z` (chạy trên v1). +2. **kubeconfig trỏ VIP `.14:8443`, không phải `127.0.0.1:6443`.** `curl 127.0.0.1:6443/healthz` = ok chỉ chứng minh apiserver LOCAL sống; nếu `kubectl` (qua .14) báo `Unable to connect to the server: EOF` thì VIP/LB đang fail-over. Xảy ra khi power-cycle VM master. +3. **etcd 3-node: chỉ đụng MỘT master tại một thời điểm.** Đổi CPU/power-cycle tuần tự, đợi node rejoin + quorum ổn rồi mới sang node kế. Tắt 2 master cùng lúc = mất quorum, cả cụm đứng. (Đã thấy restart count etcd/apiserver=5 khi power-cycle — fail-over tạm, tự hồi.) +4. **Longhorn prerequisite:** `open-iscsi` + `iscsid` active + module `iscsi_tcp` trên MỌI node. Ghim `/etc/modules-load.d/iscsi_tcp.conf` để tự load sau reboot (modprobe tay chỉ tới reboot kế). Thiếu → volume `Pending`/`faulted`. +5. **ArgoCD đọc git REMOTE, không đọc file local.** Commit mà chưa `git push` → ArgoCD vẫn thấy bản cũ. Luôn push trước khi apply/sync. +6. **MinIO advertised-endpoint:** `list` (ls) thông KHÔNG chứng minh get/put thông. Phép thử sinh tử phải là put+get+delete object thật TỪ TRONG POD (không phải từ host, không phải ping). Đã test XANH. +7. **Spark ↔ MinIO cần path-style S3** (không virtual-host style) + executor `-Daws.region=us-east-1` (system property). Nếu Spark fail đọc S3 dù mc OK → nghi path-style/region. +8. **Iceberg từ chối `timestamp(3)` → phải `timestamp(6)`.** +9. **Polaris chặn DROP-with-purge** (an toàn prod). Trino/dbt KHÔNG purge được, chỉ Spark `DROP TABLE ... PURGE`. dbt marts phải `+on_table_exists: replace` (dùng CREATE OR REPLACE), nếu không chạy lần 2 fail. +10. **OpenMetadata version lock** (OM ↔ ES ↔ Airflow-trong-ingestion). ES phải khớp client (OM 1.13.1 → ES 9.3.0). Restart `openmetadata_server` sau khi đổi ES. Scheduler UI hỏng với Airflow 3.2 → ingestion bằng CLI/YAML. → Để SAU CÙNG, ghim version, không auto-latest. +11. **1 host Proxmox = điểm chết đơn.** HA storage là ảo giác tới khi trải 2 host. Backup thật = Velero (PV/manifest) + pg_dump + mc mirror MinIO → Proxmox 2. + +--- + +## 7. NỢ mang từ bản Docker sang (giải trong lúc rebuild, không dựng lại nền) + +- **Bảo mật (nguy hiểm nhất):** creds plaintext (`dbiz1234`, POSTHOG_KEY, JWT trong YAML, `polaris-secret-dev`) → Sealed Secrets. Auth thật cho Trino/Superset/OM/MinIO. +- **Git-sync DAG:** Airflow DAG đang tạo tay → version control + git-sync (giải khi lên Airflow K8s Phase 5). +- **Backup:** MinIO + Postgres metadata chưa có backup → Velero + pg_dump + mc mirror. +- **Monitoring/alert:** Prometheus/Grafana đã dựng, chưa gắn alert DAG fail / data trễ. +- **Retention Iceberg:** expire snapshots + compaction định kỳ. +- **Checkpoint Spark:** bản Docker để local FS (không migrate được) → K8s đặt PVC Longhorn/MinIO ngay từ đầu; cắt over = snapshot lại. +- **Dọn rác:** schema ghép cũ `staging_posthog_staging_posthog`, bảng `*__dbt_backup` — dọn bằng Spark PURGE. + +--- + +## 8. Việc phải làm trước khi apply root lake stack (checklist) + +- [x] Điền IP MinIO `.37` vào `external-services.yaml` (đã có sẵn trong file). +- [x] Điền `` = Renolation, commit + push. +- [x] iscsi trên mọi node (Longhorn prereq). +- [ ] **Cài Longhorn (đang làm)** — StorageClass `longhorn` xuất hiện + PVC survive pod delete. +- [ ] Kiểm các Application còn lại (strimzi/spark/kafka) — còn placeholder hay version pin tạm nào không (kiểm bản mới nhất Strimzi/Spark Operator chart, cập nhật targetRevision). +- [ ] Seal imagePullSecret registry (tên `gitea-registry`) → `envs/dev/sealed-secrets/`. +- [ ] Đổi `image.repository` cdc-job từ `git.dbiz.com` sang registry Renolation thật. +- [ ] Dựng Polaris trên K8s (chart CHƯA có trong skeleton — thêm app riêng, Phase 3). +- [ ] cert-manager + ingress (nếu expose UI) — Phase 0 còn lại. + +--- + +## 9. Lệnh hay dùng + +```bash +# Verify cụm lành +kubectl get nodes +kubectl get pods -n kube-system | grep -E 'etcd|apiserver' + +# CPU baseline mọi node +for n in $(kubectl get nodes -o name | sed 's|node/||'); do + echo -n "$n: "; ssh k8sadmin@$n "lscpu | grep -oE 'sse4_2|popcnt|ssse3|sse4_1' | tr '\n' ' '"; echo +done + +# Phép thử sinh tử MinIO từ pod (image v1-safe) +kubectl run mc-test -n dbiz-lake-dev --rm -it --restart=Never \ + --image=minio/mc:RELEASE.2023-11-20T16-30-59Z \ + --env="MC_HOST_lake=http://dbiz:dbiz1234@192.168.110.37:9000" \ + --command -- /bin/sh +# mc ls lake/dbiz-warehouse +# echo probe > /tmp/p.txt; mc cp /tmp/p.txt lake/dbiz-warehouse/_k8s_probe/p.txt +# mc cat lake/dbiz-warehouse/_k8s_probe/p.txt; mc rm lake/dbiz-warehouse/_k8s_probe/p.txt + +# StorageClass / Longhorn +kubectl get storageclass +kubectl get pods -n longhorn-system + +# Đổi REPO_URL toàn repo (nếu chuyển registry/repo) +grep -rl '' . | xargs sed -i 's###g' + +# Bootstrap app-of-apps (ArgoCD đọc remote → push trước!) +kubectl apply -f envs/dev/apps/root.yaml +``` + +--- + +## 10. Lưu ý cho session sau + +- **Luôn push git trước khi apply/sync ArgoCD.** +- **Không đập cái đang chạy:** CDC Docker cũ giữ chạy tới khi CDC K8s verify xong (Phase 4). +- **MinIO không bao giờ đụng** — điểm neo dữ liệu. +- **Mỗi phase verify kỹ mới sang.** "Di toàn bộ" = nhiều tuần. +- **Version Helm chart (Strimzi/Spark Operator) đang pin TẠM** — kiểm bản mới khi dựng thật. +- Khi tạo file/manifest cho repo: đưa NỘI DUNG ĐẦY ĐỦ + lệnh git + lệnh verify. GitOps thuần, không helm install tay. +- Credential dev đang là `dbiz`/`dbiz1234` khắp nơi — đừng đưa vào git dạng plaintext, dùng Sealed Secrets. \ No newline at end of file diff --git a/infra/cluster/00-longhorn.yaml b/infra/cluster/00-longhorn.yaml new file mode 100644 index 0000000..ec3c614 --- /dev/null +++ b/infra/cluster/00-longhorn.yaml @@ -0,0 +1,42 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: infra-longhorn + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-10" # nền cụm -> sync sớm nhất trong tầng infra +spec: + project: default + source: + repoURL: https://charts.longhorn.io + chart: longhorn + targetRevision: 1.11.3 # pin: Longhorn v1.11.3 (2026-07-01), dòng 1.11 đã chín + helm: + releaseName: longhorn + values: | + persistence: + # KHÔNG để longhorn làm default StorageClass — giữ local-path là default của cụm. + # Workload lakehouse xin tường minh storageClassName: longhorn (checkpoint, kafka PV). + defaultClass: false + # 3 bản sao, trải trên 3/5 VM (hard anti-affinity mặc định). + # Chống 1-2 VM/pod chết. KHÔNG chống host Proxmox chết -> backup là phòng thủ thật. + # Muốn tiết kiệm đĩa dev: đổi xuống 2 (survive 1 VM). Prod-parity: giữ 3. + defaultClassReplicaCount: 3 + defaultSettings: + defaultDataPath: /var/lib/longhorn # cần đủ chỗ trên MỌI node (đã resize +50-100G) + destination: + server: https://kubernetes.default.svc + namespace: longhorn-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true # tránh lỗi "metadata.annotations: Too long" trên CRD Longhorn + retry: + limit: 5 + backoff: + duration: 15s + factor: 2 + maxDuration: 5m diff --git a/infra/cluster/root.yaml b/infra/cluster/root.yaml new file mode 100644 index 0000000..4d4faa9 --- /dev/null +++ b/infra/cluster/root.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: infra-cluster-root + namespace: argocd +spec: + project: default + source: + repoURL: https://git.renolation.com/renolation/dbiz-lake-k8s.git + targetRevision: main + path: infra/cluster # quét chính thư mục này (gồm cả root.yaml -> tự quản) + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true