# 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.