add longhorn

This commit is contained in:
2026-07-08 15:48:18 +07:00
parent 0052e152f2
commit 7d18a1c6ce
3 changed files with 305 additions and 0 deletions

243
CLAUDE.md Normal file
View File

@@ -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 `<REPO_URL>` = `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/<env>/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 `<IMAGE_REGISTRY>` (đí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/<env>/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/<env>/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 `<REPO_URL>` = 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 '<REPO_URL>' . | xargs sed -i 's#<REPO_URL>#<url-mới>#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.