Files
sicabe-core/deploy/README.md
T
2026-10-09 02:35:14 +07:00

698 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# eBdesk SiCABE — `deploy` (Installer Hasil Merge Eksperimen)
**SiCABE** (Solusi Integrasi Cerdas Adaptif Big Data Enterprise) adalah platform
data modern 5-lapisan (lakehouse) untuk analisis harga dan logistik kebutuhan
pokok. Folder `deploy/` adalah **installer hasil merge** dari:
- `deploy-legacy/` — base installer lama (yang sudah berjalan baik),
- `experiments/nessie-dremio/` — **Nessie** (ganti Polaris) + **OPA** + **Dremio**,
- `experiments/starrocks-minio/` — StarRocks **shared-data** (FE + Compute Node),
- `experiments/starrocks-oidc/` — **LDAP group provider**.
> `deploy-legacy/` (base lama) **tidak diubah** — tetap menjadi referensi/rollback.
> Dokumen rencana & mapping: [`../reviews/sicabe-07-merge-experiments-plan.md`](../reviews/sicabe-07-merge-experiments-plan.md).
## Arsitektur
```
L1 INGESTION Airflow (base) ── dbt/Sling ──► Trino
L2 STORAGE MinIO (base) + Nessie (JDBC2/PostgreSQL) + OpenLDAP (identitas)
L3 PROCESSING Trino 483 ──► Nessie catalog; authz Trino ──► OPA
L4 SERVING StarRocks shared-data (FE + CN) | Dremio OSS 25.2.0
L5 GOVERNANCE Keycloak (user lokal) · OpenMetadata · Grafana
IDENTITY OpenLDAP (sumber grup untuk Trino & StarRocks)
DATABASE PostgreSQL shared: keycloak | nessie | airflow | openmetadata_db
```
### Dependency Graph (urutan deploy)
```
Phase 1: Keycloak + OpenLDAP ──────── (tidak ada dependensi)
Phase 2: MinIO ◄──── Nessie ───────── (MinIO dulu, baru Nessie)
Phase 3: Trino ◄──── Nessie + OPA ─── (katalog Nessie, authz OPA)
Phase 4: StarRocks FE + CN (+ Dremio opt-in) ── (shared-data; engine paralel)
Phase 5: Airflow ◄──── Postgres ───── (PostgreSQL sebagai backend DB)
Phase 6: OpenMetadata ◄──── Grafana ─
```
### Matriks Install (Fase × Layer)
**A — Service yang baru diinstall per fase:**
| Fase | Database | Identity | L1 Ingestion | L2 Storage & Catalog | L3 Processing | L4 Serving | L5 Governance |
|---|---|---|---|---|---|---|---|
| **Phase 1** | `postgres` | `openldap` | — | — | — | — | `keycloak` |
| **Phase 2** | — | — | — | `minio`, `minio-init`*, `nessie` | — | — | — |
| **Phase 3** | — | — | — | — | `opa`, `trino` | — | — |
| **Phase 4** | — | — | — | — | — | `starrocks-fe`, `starrocks-cn` | — |
| **Phase 5** | — | — | `airflow-init`, `airflow-webserver`, `airflow-dag-processor`, `airflow-scheduler` | — | — | — | — |
| **Phase 6** | — | — | — | — | — | — | `openmetadata-opensearch`, `openmetadata-migrate`*, `openmetadata-server`, `grafana` |
`*` one-shot (selesai lalu exit): `minio-init` (buat bucket), `openmetadata-migrate` (migrasi schema OM).
**B — Kondisi aktif kumulatif setelah fase N (full deploy `./install.sh`):**
| Fase | Database | Identity | L1 Ingestion | L2 Storage & Catalog | L3 Processing | L4 Serving | L5 Governance |
|---|---|---|---|---|---|---|---|
| **1** | `postgres` | `openldap` | — | — | — | — | `keycloak` |
| **2** | `postgres` | `openldap` | — | `minio`, `nessie` | — | — | `keycloak` |
| **3** | `postgres` | `openldap` | — | `minio`, `nessie` | `opa`, `trino` | — | `keycloak` |
| **4** | `postgres` | `openldap` | — | `minio`, `nessie` | `opa`, `trino` | `starrocks-fe`, `starrocks-cn` | `keycloak` |
| **5** | `postgres` | `openldap` | `airflow-*` | `minio`, `nessie` | `opa`, `trino` | `starrocks-*` | `keycloak` |
| **6** | `postgres` | `openldap` | `airflow-*` | `minio`, `nessie` | `opa`, `trino` | `starrocks-*` | `keycloak`, `openmetadata-*`, `grafana` |
> `./install.sh --phase N` menjalankan profil `phaseN`. Phase 1–4 kumulatif; phase 5–6 lebih sempit
> (disarankan full deploy berurutan). `bootstrap.sh` (9 step) dijalankan **setelah** install.
>
> **Dremio bersifat opt-in**: tidak ikut `--profile all` / `phaseN` (installer-nya
> menolak heap < 4 GB sehingga boros RAM di dev). Aktifkan dengan
> `docker compose --profile dremio up -d dremio`. `bootstrap.sh` step 9 akan
> dilewati bila container Dremio tidak berjalan.
## Stack
| Layer | Komponen | Versi | Catatan |
|-------|----------|-------|---------|
| Database | PostgreSQL | 15 | 1 instance, DB terpisah: keycloak/nessie/airflow/openmetadata_db |
| L2 Catalog | **Project Nessie** | 0.108.4-java | JDBC2 di PostgreSQL; Iceberg REST `/iceberg/` |
| L2 Storage | MinIO | RELEASE.2025-09-07 | Bucket `iceberg-data` (warehouse + StarRocks) |
| L3 Processing | Trino | 483 | Katalog `iceberg` via Nessie |
| L3 Authz | **OPA** | latest | Deny-by-default; policy Rego + ACL `config/opa/data.json` |
| L4 Serving | StarRocks | 4.0.13 | **shared-data** (FE + CN) |
| L4 Serving | **Dremio OSS** *(opt-in)* | 25.2.0 | Engine paralel; auth lokal |
| L5 IAM | Keycloak | 26.7 | OIDC; user lokal |
| Identity | **OpenLDAP** | 1.5.0 | Sumber grup (`data-admin`, dst.) |
| L1 Ingestion | Airflow | **3.3.2** | Sling → Nessie Iceberg REST; **image ber-plugin OpenMetadata** (`/pluginsv2/`); **provider MySQL** (koneksi ke server MySQL eksternal) |
| L5 Metadata | OpenMetadata | **2.0.3** | Konektor Trino + Airflow; jalur DQ = Great Expectations (§8.9) |
| L5 Search | OpenSearch | **3.8.0** | Backend pencarian OM 2.0.x (security plugin dimatikan) |
| L5 Observability | Grafana | 13.1 | Provisioning file |
### Kebutuhan CPU (AVX2)
Hanya **satu** komponen di stack ini yang mensyaratkan **AVX2**: **StarRocks CN (BE)**. Build
resmi StarRocks memakai `USE_AVX2=ON`, dan CPU tanpa AVX2 membuat container-nya **gagal start**
dengan `SIGILL` (illegal instruction) — bukan sekadar lambat, dan saat ini **tidak ada paket
resmi non-AVX2**. StarRocks **FE** tidak terpengaruh (berbasis JVM). **AVX-512 tidak wajib** di
komponen mana pun.
Komponen lain berjalan di x86-64 biasa: Trino (hanya butuh 64-bit Linux + Java 25), OpenSearch
(kita hanya memakai pencarian teks/BM25 — AVX2/AVX-512 hanya relevan untuk plugin k-NN yang
tidak dipakai), OpenMetadata/Keycloak/Nessie (JDK 21), Airflow (CPython 3.11), serta
MinIO/PostgreSQL/Grafana/Prometheus. Dremio opt-in tidak mendokumentasikan syarat ISA.
Periksa CPU calon server **sebelum** deploy:
```bash
grep -o -m1 avx2 /proc/cpuinfo # satu-satunya gate nyata di stack ini
lscpu | grep -o -E 'avx2|avx512[a-z]*|sse4_2|bmi2|popcnt' | sort -u
```
Tanpa AVX2, opsinya: lewati Fase 4 (StarRocks) dan pakai Dremio sebagai engine serving, atau
kompilasi StarRocks sendiri dengan `build.sh --without-avx2`. Catatan arsitektur: **ppc64le**
hanya didukung Trino, PostgreSQL, Prometheus, dan Airflow — komponen lain butuh x86-64 atau ARM64.
## Peta Port (prefix 12xxx)
Aturan: **host port = `12` + 3 digit port kanonik**. Port internal container tidak berubah.
| Service | Internal | Host |
|---|---|---|
| Grafana | 3000 | **12300** |
| Dremio Flight | 32010 | **12011** |
| Dremio JDBC | 31010 | **12010** |
| StarRocks FE HTTP | 8030 | **12030** |
| Keycloak | 8080 | **12080** |
| Airflow | 8080 | **12082** |
| OpenMetadata | 8585 | **12085** |
| Nessie | 19120 | **12120** |
| OPA | 8181 | **12181** |
| Trino HTTPS | 8443 | **12443** |
| OpenSearch | 9200 | **12920** |
| MinIO API | 9000 | **12900** |
| MinIO Console | 9001 | **12901** |
| StarRocks FE MySQL | 9030 | **12930** |
| Dremio UI | 9047 | **12947** |
| OpenLDAP | 389 | **12389** |
## Prasyarat
| Tool | Minimal | Catatan |
|------|---------|---------|
| Docker Engine | 24.x+ | `docker --version` |
| Docker Compose | v2.x+ | `docker compose version` |
| OpenSSL | 1.1+ | generate password `.env` |
| curl | 7.x+ | bootstrap & test |
| python3 | 3.8+ | parsing JSON |
| MinIO Client (mc) | latest | opsional |
| MySQL Client | 8.x+ | opsional (StarRocks) |
## Quick Start
```bash
cd deploy
chmod +x install.sh bootstrap.sh test-verification.sh
# 1. Deploy 6 fase (~15 menit)
./install.sh
# 2. Bootstrap & integrasi (Keycloak, OpenLDAP, MinIO, StarRocks, Airflow, OM)
./bootstrap.sh
# 3. Verifikasi
./scripts/verify-phase1-iam.sh # gate kontrak IAM (Phase 1) — sebelum lanjut fase berikutnya
./scripts/verify-phase2-storage.sh # gate kontrak storage & catalog (Phase 2)
./scripts/verify-phase3-processing.sh # gate kontrak processing: Trino + OPA (Phase 3)
./scripts/verify-phase4-serving.sh # gate kontrak serving: StarRocks shared-data (Phase 4)
./scripts/verify-phase5-ingestion.sh # gate kontrak ingestion: Airflow + connections (Phase 5)
./scripts/verify-phase6-governance.sh # gate kontrak governance: OpenMetadata + Grafana (Phase 6)
# 3b. Setelah restart StarRocks FE — pulihkan dynamic config (auth chain OIDC)
./scripts/starrocks-reapply-dynamic-config.sh
# 3c. Isi katalog OpenMetadata (agent ingestion) — tanpa ini halaman Explore kosong
./scripts/ingest-openmetadata.sh
./test-verification.sh
./test-verification.sh --layer L2 # MinIO + Nessie
./test-verification.sh --layer E2E # End-to-end smoke test
# 3d. ALUR DATA (urutan ini penting)
# Ingest DULU, baru DAG:
./scripts/ingest-openmetadata.sh --all # buat entitas tabel di katalog OM
docker exec sicabe-airflow-webserver airflow dags test sicabe_batch_daily
#
# Mengapa urutannya begitu: action GE (OpenMetadataValidationAction) menempelkan
# test suite/test case ke ENTITAS TABEL di OM. Sebelum ingest, entitas itu belum ada,
# sehingga hasil Great Expectations tidak muncul di OM walau tugas DAG-nya sukses.
# Catatan: bagian LINEAGE dari --all membaca artefak dbt (target/manifest.json) yang
# baru ada SETELAH DAG berjalan — untuk lineage utuh jalankan `--dbt-only` sekali lagi
# setelah DAG, atau ulangi `--all`.
#
# 4. (Opsional) Seed tabel contoh Iceberg — namespace bronze + bronze.orders
./scripts/seed-sample-data.sh
# 5. (Opsional) Seed SUMBER PostgreSQL untuk DAG — sicabe_data.public.orders
# WAJIB bila ingin menjalankan DAG 'sicabe_batch_daily' (task ingest_to_bronze)
./scripts/seed-source-postgres.sh
```
> Memakai komponen yang **sudah ada** (Keycloak/LDAP/MinIO/Nessie/Trino/OPA, atau Postgres
> milik DBA)? Arahkan endpoint-nya di `.env` — `KEYCLOAK_URL`, `LDAP_URL`, `S3_ENDPOINT`,
> `NESSIE_URL`, `TRINO_URL`, `OPA_URL` (atau `EXTERNAL_SERVICES="postgres ..."`). `install.sh`
**Menempatkan komponen kita di host lain (multi-host).** Beda dari komponen EXISTING:
di sini komponen tetap milik klaster ini, hanya dipindah host. Override variabel
`*_HOST` di `.env` dengan alamat host pemiliknya — default-nya nama container, sehingga
**single-host tidak berubah**:
| Variabel | Default | Untuk |
|---|---|---|
| `POSTGRES_HOST` | `postgres` | Keycloak, Nessie, Airflow |
| `MINIO_HOST` | `minio` | Nessie, minio-init |
| `KEYCLOAK_HOST` | `keycloak` | Keycloak backend URL, Grafana OAuth |
| `OPENMETADATA_HOST` | `openmetadata-server` | `OM_API_URL`, `SERVER_HOST_API_URL` |
| `AIRFLOW_WEBSERVER_HOST` | `airflow-webserver` | `AIRFLOW__WEBSERVER__BASE_URL`, pipeline client |
| `STARROCKS_FE_HOST` / `STARROCKS_CN_HOST` | `starrocks-fe` / `starrocks-cn` | registrasi CN, healthcheck |
| `OPENSEARCH_HOST` | `openmetadata-opensearch` | `ELASTICSEARCH_HOST` OpenMetadata |
> File `config/**` yang dibaca **container** (`.properties`/`.yaml`/`.cfg`/`.json`/`.rego`)
> tidak bisa ekspansi env. Untuk multi-host jalankan `scripts/render-config.sh` yang
> menyalinnya ke `generated/host-<x>/config/` sambil mengganti hostname terdaftar:
>
> ```bash
> ./scripts/render-config.sh --out generated/host-compute/config --dry-run \
> --map postgres=10.20.0.12 --map nessie=10.20.0.10
> ```
>
> Penggantian hanya pada konteks jaringan (setelah `//`, setelah `@`, atau `host:port`)
> sehingga path filesystem seperti `/etc/trino/` dan baris komentar tidak ikut berubah.
> Rencana lengkap: `reviews/sicabe-10-multi-host-installer-plan.md`.
> ⚠️ **Pilot multi-host di host yang SAMA tidak boleh dua stack berjalan bersamaan.**
> Memisah network/port saja **tidak cukup** — RAM tidak akan cukup (host pilot 15 GB,
> satu stack penuh ±10 GB). **Matikan stack lama lebih dulu** sebelum menyalakan hasil
> `generated/host-<x>/`.
**Trino dipisah coordinator/worker.** Sejak 2026-09-28 Trino bukan lagi satu service:
`trino-coordinator` (port host `12443`, HTTPS + OAuth2/Basic, client-facing) dan
`trino-worker` (tanpa port host; mengeksekusi task, `discovery.uri` ke coordinator).
Klien **tidak berubah** (`TRINO_URL` tetap `https://localhost:12443`). Untuk restart:
`./install.sh --restart trino-coordinator` (atau `trino-worker`). Perubahan **nama
service** mewajibkan regenerasi sertifikat: `./scripts/generate-trino-keystore.sh`
(SAN memuat `trino-coordinator`), lalu restart coordinator.
### Menyiapkan server baru (Ubuntu 24.04)
Dua script, dijalankan **di tiap server** sebelum instalasi:
```bash
sudo ./scripts/prepare-host.sh --dry-run # lihat rencananya dulu (tidak mengubah apa pun)
sudo ./scripts/prepare-host.sh # paket apt + Docker + sysctl + precheck
./scripts/prefetch-images.sh # tarik semua image lebih dulu
```
**`prepare-host.sh`** memasang paket yang benar-benar dipakai script deploy
(`htpasswd`/apache2-utils, `ldapsearch`/ldap-utils, klien `mysql`, `openssl`, `curl`,
`python3`+PyYAML), memasang Docker Engine + compose v2 (lewati dengan `--no-docker`),
menetapkan `vm.max_map_count=262144` (**syarat keras OpenSearch**), dan memeriksa
**AVX2** (wajib `starrocks-cn`), jam/NTP, RAM, disk, serta port `12xxx` yang akan dipakai.
Flag `--swap-off` (opsional) mematikan swap sesuai rekomendasi OpenSearch.
**`prefetch-images.sh`** mengambil daftar image dari `docker compose config --images`
(**tidak** di-hardcode, jadi selalu sinkron dengan compose). Default: **14 image ≈ 6,2 GB**
(terukur). Pilihan: `--all` (default) · `--host <nama>` (hanya image host itu, dari
`generated/host-<x>/`) · `--list` (lihat saja).
Tiga image **dikecualikan secara default** dan ditampilkan sebagai catatan
("dikecualikan: …") supaya tidak terkesan terlupa:
| Dikecualikan | Alasan | Mengaktifkan |
|---|---|---|
| `dremio/dremio-oss:25.2.0` | Dremio **opt-in** di compose (default nonaktif) | `--with-dremio` |
| `apache/airflow:slim-3.3.2-python3.11` | hanya base untuk build lokal Airflow | `--with-base` |
| `sicabe/airflow:3.3.2-om203-ol` | **dibangun lokal**; sejak 2026-10-02 **juga tersedia di registry** | `docker pull` dari registry (di bawah), atau build-sekali-salin |
> 📌 **Sejak 2026-10-02 image ini tersedia di registry internal** — jadi host lain bisa
> **menariknya**, tidak harus membangun:
> ```bash
> docker pull registry.sicabe.com/airflow:3.3.2-om203-ol
> # opsi A (disarankan): arahkan compose ke nama registry — tanpa retag
> export AIRFLOW_IMAGE=registry.sicabe.com/airflow:3.3.2-om203-ol
> # opsi B: beri nama yang diharapkan compose
> docker tag registry.sicabe.com/airflow:3.3.2-om203-ol sicabe/airflow:3.3.2-om203-ol
> ```
> ⚠️ `docker-compose.yml` memakai `${AIRFLOW_IMAGE:-sicabe/airflow:3.3.2-om203-ol}` —
> jadi setelah `pull`, **pilih salah satu** dari dua opsi di atas; kalau tidak, compose
> akan mencari tag `sicabe/...` yang belum ada di host itu.
> ⚠️ **`sicabe/airflow:3.3.2-om203-ol` dibangun lokal** (base `apache/airflow:slim-3.3.2-python3.11`),
> jadi **tidak tersedia di Docker Hub**. ✅ Sejak 2026-10-02 fetch bisa lewat registry
> internal (lihat blok di atas); alur build-sekali-salin di bawah tetap tersedia untuk
> host yang **tidak punya akses registry**:
> ```bash
> # di SATU host
> docker build -t sicabe/airflow:3.3.2-om203-ol -f docker/airflow/Dockerfile.332 docker/airflow
> ```
> 📌 Sejak BATCH B (review R18, 2026-10-01) varian image **berdampingan**:
> `Dockerfile.332` (+ `requirements.332.{in,lock}`) = Airflow 3.3.2 / Python 3.11 —
> **yang dipakai sekarang**; `Dockerfile.slim` (+ `requirements.{in,lock}`) tetap
> disimpan sebagai jalur rollback ke Airflow 3.3.1.
> 📌 **2026-10-02 — adopsi opsi C**: tag **`-ol`** = image di atas **+ provider
> OpenLineage + shim `eventTime`**. OpenLineage **AKTIF** mengirim lineage task
> Airflow ke OpenMetadata (rute langsung, tanpa Marquez).
>
> - **Transport dibaca dari KONEKSI Airflow** `openlineage_config` (bukan `.env`),
> sehingga token **tidak** tersebar di berkas. `./bootstrap.sh` membuatnya
> bersama `openmetadata_lineage_api` (password = token bot `lineage-bot`,
> `JWTTokenExpiry=Unlimited`).
> - Aktif/mati: `AIRFLOW_OPENLINEAGE_CONFIG_CONN_ID` (default `openlineage_config`;
> kosongkan untuk mematikan). ⚠️ Koneksinya **harus sudah ada** — pemuatan
> konfigurasi provider tidak dibungkus try/except (bisa menggagalkan task).
> - ⚠️ `AIRFLOW__OPENLINEAGE__EXECUTE_IN_THREAD=true` **wajib**; tanpa itu proses
> emisi HANG lalu dimatikan dan tidak ada event yang keluar.
> - ⚠️ Shim `ol_time_shim.ZuluHttpTransport` **wajib** untuk OM 2.0.3: provider
> memakai `eventTime` berakhiran `+00:00`, sedangkan OM hanya menerima `Z`
> (tanpa shim → `400 Invalid request format`).
> - Lineage **tabel + kolom** muncul untuk task yang memakai **operator SQL**
> (mis. `SQLExecuteQueryOperator`). Task dbt/Sling via `BashOperator` tidak
> menghasilkan lineage dari jalur ini — dbt tetap dari ingestion dbt.
> - ⚠️ OM 2.0.3 mencatat error berulang `null value in column "updatedat"`
> saat mencoba auto-create entitas pipeline (bug OM; tidak berbahaya).
> - Rincian: `experiments/lineage-airflow-om/README.md`.
> Rollback: `sicabe/airflow:3.3.2-om203` (tanpa OpenLineage).
> ./scripts/prefetch-images.sh --save-airflow /tmp/airflow-image.tar # ±835 MB
> # di host lain
> scp /tmp/airflow-image.tar <host>:/tmp/
> ./scripts/prefetch-images.sh --load-airflow /tmp/airflow-image.tar
> ```
### Menempatkan komponen ke beberapa host (Fase 1)
```bash
cp cluster.example.yml cluster.yml # sesuaikan address tiap host
./install.sh --generate # → generated/host-<x>/ (gitignored)
./install.sh --generate --blueprint b5 # preset 5 host (blueprint B)
./install.sh --sync # salin deploy/ (termasuk .env) ke host lain (dari control)
./install.sh --sync --host state --dry-run # lihat saja (tanpa menyalin)
./install.sh --host compute --dry-run # lihat perintahnya tanpa menjalankan
```
**Distribusi ke host lain (`--sync`).** Dijalankan di **host control** (master installer).
Menyalin seluruh `deploy/` — **termasuk `.env` dan `generated/`** — ke tiap host lain di
`cluster.yml` via `rsync` over SSH:
```bash
./install.sh --sync # ke SEMUA host lain (host ini dideteksi dari alamat lokal)
./install.sh --sync --host state # hanya satu host
./install.sh --sync --user admin --dest '~/sicabe-doc/deploy' # SSH user & tujuan
./install.sh --sync --delete # hapus di tujuan berkas yang tak ada di sumber
```
Host yang sedang dijalankan **dikecualikan otomatis** (kecocokan `hostname -I` dengan
`address` di inventory); bila tak cocok, ia menyinkron ke semua host. `.env` **wajib ikut**
(rahasia bersama — OIDC client secret harus identik lintas host). Setelah sync, di tiap host
jalankan `./scripts/prefetch-images.sh --host <nama>` lalu `./install.sh --host <nama>`.
Tanpa `cluster.yml`, installer berperilaku **persis** seperti single-host (tidak ada
yang di-generate). Hasil generate berisi per host: `docker-compose.yml` (hanya service
milik host itu), `.env` (endpoint → alamat host), dan `config/` ter-render.
Uji: `./scripts/test-inventory.sh` (validasi inventory) · `./scripts/test-generate.sh`
(semantik hasil generate vs single-host). Rencana lengkap:
`reviews/sicabe-10-multi-host-installer-plan.md`.
**Endpoint per host di skrip (multi-host).** Skrip gate/verifikasi memakai helper
`sicabe_load_env`/`sicabe_local_url`/`sicabe_local_host`, jadi jalankan dengan env host agar
endpoint menunjuk **ALAMAT HOST PEMILIK** (bukan `localhost`):
```bash
./scripts/verify-phase3-processing.sh --env-file generated/host-compute/.env
./scripts/verify-phase6-governance.sh --env-file generated/host-control/.env
./install.sh --status --host compute # status + endpoint host compute
```
`--env-file` juga dihormati lewat env `SICABE_ENV_FILE` — dipakai otomatis oleh
`bootstrap.sh --host`, sehingga sub-script (`seed-*`/`ingest-*`/`reapply`) mewarisi env host
dan tidak lagi menimpanya dengan `deploy/.env`. **Single-host tidak berubah** (tetap
`localhost`) tanpa opsi ini.
> **Skrip host-scoped (multi-host).** Skrip yang `docker exec` container harus dijalankan **DI
> host pemilik container** — mis. `seed-source-postgres.sh` di host **`state`**,
> `seed-starrocks.sh` di **`compute`**, `seed-native-dq.sh` di **`control`**. Bila dijalankan
> di host lain, skrip keluar rapi (kode **3**) + instruksi, bukan error; `bootstrap.sh`
> menampilkannya sebagai WARN. Gate (`verify-phase*`) otomatis **SKIP** cek container milik
> host lain (bukan FAIL).
>
> ```bash
> # di host state:
> ./scripts/seed-source-postgres.sh --env-file generated/host-state/.env
> # di host compute:
> ./scripts/seed-starrocks.sh --env-file generated/host-compute/.env
> ```
> **TLS Trino lintas-host.** Saat `--generate`, keystore + cert Trino dibangkitkan **sekali**
> dengan **SAN alamat IP tiap host**, lalu **disalin identik ke tiap bundle** (keypair harus
> sama lintas host). Jadi klien yang terhubung via IP (mis. ingestion **OpenMetadata**) tetap
> **memverifikasi TLS** — tak perlu `verify: false`. **`DBT_TRINO_HOST`** juga di-set ke IP
> pemilik Trino sehingga dbt (di Airflow, host lain) menyambung lintas-host. Ubah alamat host
> → `./install.sh --generate` → `--sync` → recreate `trino-coordinator` **dan** service Airflow.
> ⚠️ **Setelah mengubah file yang di-MOUNT (cert/config), container WAJIB di-restart manual.**
> `./install.sh --host <x>` menjalankan `docker compose up -d`, yang **hanya** recreate bila
> *definisi* compose berubah — **bukan** bila isi file bind-mount berubah. Contoh: regenerasi
> cert Trino → `docker restart sicabe-trino-coordinator` (di host Trino) dan service Airflow
> (di host control). Cek cepat: fingerprint cert yang **disajikan** vs yang **dipakai**
> container harus sama:
> ```bash
> # disajikan (dari host lain): echo | openssl s_client -connect <ip-trino>:8443 -servername trino 2>/dev/null | openssl x509 -noout -fingerprint -sha256
> # dipakai (di container airflow):
> docker exec sicabe-airflow-webserver openssl x509 -in /etc/ssl/trino/trino.crt -noout -fingerprint -sha256
> ```
> **Endpoint komponen SE-HOST memakai nama service.** `.env` bundle kini menulis `*_HOST`
> komponen **se-host** sebagai **nama service** (mis. `OPENMETADATA_HOST=openmetadata-server`)
> dan komponen **beda host** sebagai **alamat IP**. Sebab port container (mis. OM 8585) tidak
> diterbitkan di host; memakai IP host justru "connection refused" (regresi OpenLineage).
> ⚠️ **`./install.sh --host <nama>` dijalankan DI host itu**, dan hanya setelah stack
> lama dimatikan bila host-nya sama. `depends_on` lintas host sudah dibuang generator —
> penggantinya `preflight` yang **belum ada** (Fase 2), jadi urutan start antar host
> masih manual.
> dan `test-verification.sh` akan **melewati container** komponen tersebut, dan `bootstrap.sh`
> beserta gate fase memakai endpoint yang sama. Detail: `docs/sicabe-install-phases-detail.md`
> §2.8–§2.12.
>
> `scripts/seed-sample-data.sh` (idempoten) membuat namespace + tabel contoh
> `iceberg.bronze.orders` (6 baris) agar catalog tidak kosong dan langsung
> terlihat di Trino, Dremio, dan StarRocks.
>
> `scripts/seed-source-postgres.sh` (idempoten) menyiapkan **sumber** di
> PostgreSQL: database `sicabe_data` + tabel `public.orders` (6 baris). Ini yang
> dibaca task `ingest_to_bronze` (Sling: `POSTGRES_DB.public.orders` →
> `ICEBERG bronze.orders`). Tanpa ini DAG `sicabe_batch_daily` gagal dengan
> pesan database `sicabe_data` tidak ditemukan.
> **Diagram alur instalasi** — `install.sh` (6 fase) → `bootstrap.sh` (9 step) → stack siap:
> [Mermaid `sicabe-install-sequence.mmd`](../docs/diagrams/sicabe-install-sequence.mmd) ·
> [versi interaktif `.drawio`](../docs/diagrams/sicabe-install-sequence.drawio).
> Regenerasi `.drawio` dari `.mmd`:
> `bash docs/diagrams/generate-drawio-from-mermaid.sh sicabe-install-sequence.mmd`
> **Detail per fase & bootstrap step** (prasyarat/dependency, container yang
> dinyalakan, aksi **configure/seed/verify** tiap step):
> [`docs/sicabe-install-phases-detail.md`](../docs/sicabe-install-phases-detail.md)
>
> **Memakai layanan existing?** Variabel `.env` yang perlu ditambahkan:
> Keycloak/LDAP → §2.8 · MinIO/S3 & Nessie → §2.9 pada dokumen yang sama.
> Untuk **IAM existing** (dikelola pihak lain/tanpa akses admin): daftar permintaan ke
> admin → Keycloak [`docs/keycloak-existing-requirements.md`](../docs/keycloak-existing-requirements.md) ·
> OpenLDAP [`docs/openldap-existing-requirements.md`](../docs/openldap-existing-requirements.md).
Deploy per phase:
```bash
./install.sh --phase 1 # Keycloak + OpenLDAP + PostgreSQL
./install.sh --phase 2-4 # MinIO + Nessie + Trino + OPA + StarRocks FE+CN
./install.sh --status
./install.sh --destroy
# Dremio opt-in — tidak ikut phase mana pun (butuh RAM ~4 GB lebih)
docker compose --profile dremio up -d dremio
```
### URL browser-facing (issuer OIDC, redirect, UI)
Enam variabel berisi URL yang dilihat **browser**, bukan lalu lintas container:
`KEYCLOAK_PUBLIC_URL`, `GRAFANA_PUBLIC_URL`, `OPENMETADATA_PUBLIC_URL`,
`AIRFLOW_PUBLIC_URL`, `TRINO_PUBLIC_URL`, `MINIO_CONSOLE_PUBLIC_URL`.
| Kondisi | Nilai `*_PUBLIC_URL` |
|---|---|
| `tls.mode: none` (default) | `http://<alamat host yang menjalankan komponen>:<port>` — diisi otomatis oleh `install.sh --generate`, **sama di semua bundle** |
| `tls.mode: edge` / `full` | `https://<label>.<domain>` dari `cluster.domain` |
| tanpa `cluster.yml` (single-host) | `http://localhost:<port>` |
**Keycloak 26.x (hostname v2):** yang menentukan issuer, discovery, dan redirect adalah
**`KC_HOSTNAME`**, dan isinya **URL lengkap** (`http://host:port`) — diambil dari
`KEYCLOAK_PUBLIC_URL`. Jangan pakai `KC_HOSTNAME_URL`: opsi hostname v1 itu **dihapus di
Keycloak 26.0.0** sehingga tidak berefek; `KC_HOSTNAME_BACKEND_URL` bahkan bukan opsi
Keycloak. `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true` membuat container lain tetap bisa
memakai nama internal `keycloak:8080` untuk token/JWKS.
**Akses HTTP dari alamat non-lokal (mis. Tailscale).** `sslRequired` adalah atribut
**realm** (default Keycloak `external`) dan dinilai dari **alamat sumber koneksi**, bukan
header Host. Menurut Keycloak, "lokal" = loopback + RFC1918 (`10/8`, `172.16/12`,
`192.168/16`) + link-local + IPv6 `fc00::/7` — sehingga **`100.64.0.0/10` (CGNAT/Tailscale)
dianggap eksternal** dan admin console gagal dengan `HTTPS required`, padahal akses via
`localhost` normal. Keycloak 26 **tidak punya** opsi server-level untuk ini (tidak ada
`KC_HTTPS_REQUIRED`), jadi harus per realm: `bootstrap.sh --step 1` menyetelnya untuk
**`master` dan realm Sicabe** dari `KEYCLOAK_SSL_REQUIRED` (`none` saat `tls.mode: none`,
`external` saat ada TLS/Traefik).
**Mengubah alamat publik berarti mengubah `iss`** → semua konsumen harus ikut dan
**semua pengguna login ulang**:
- Trino: `issuer` + `auth-url` memakai `${ENV:KEYCLOAK_PUBLIC_URL}` (token/JWKS tetap
ke `keycloak:8080`) — lihat `config/trino/config-coordinator.properties`;
- redirect URI + web origin klien: disinkronkan otomatis oleh `bootstrap.sh step 1`
(digabung, idempoten — termasuk client lama yang masih localhost-only);
- Grafana (`GF_SERVER_ROOT_URL`) dan Airflow (`AIRFLOW__WEBSERVER__BASE_URL`) mengikuti
`*_PUBLIC_URL` masing-masing.
Gejala bila salah: browser dibuang ke `localhost`, atau login ditolak dengan
`Invalid parameter: redirect_uri`.
### Ingress + TLS (langkah 2 / T-20)
Traefik berjalan **hanya pada profile `edge`**, dan hanya ikut ke bundle host saat
`tls.mode` != `none`. Setiap host menjalankan Traefik-nya sendiri, dengan rute diambil dari
label container (provider docker) — jadi daftar rute = label pada layanan.
```bash
# 1. inventory: tls.mode: edge (domain: sicabe.com)
# 2. install.sh --generate && install.sh --sync -> distribusi bundle ke host lain
# 3. isi kredensial DNS di .env tiap host (contoh Cloudflare: token Zone:DNS:Edit)
# CF_DNS_API_TOKEN=...
# 4. jalankan ingress
docker compose --profile all --profile edge up -d
```
| Variabel | Arti |
|---|---|
| `CLUSTER_DOMAIN` | domain rute `Host()` (label dari `PUBLIC_URL_SERVICES`: kc/grafana/om/airflow/trino/minio) — hanya di host ingress |
| `tls.ingress_host` | (inventory, opsional) host yang menjalankan Traefik; default pemilik Keycloak |
| `TRAEFIK_BIND_ADDR` | alamat bind port 80/443 — diisi generator dengan alamat host (bukan semua antarmuka) |
| `TRAEFIK_ACME_CASERVER` | **staging** secara default; produksi: `https://acme-v02.api.letsencrypt.org/directory` |
| `CF_DNS_API_TOKEN` | kredensial DNS-01 Cloudflare |
Catatan penting:
- **Tanpa kredensial DNS, Traefik tetap start** dan memakai sertifikat self-signed bawaan,
jadi routing masih bisa diuji: `curl -k -H 'Host: kc.<domain>' https://<host>/`.
Log ACME akan menyebut variabel yang kurang secara eksplisit.
- `acme.json` **wajib mode 0600** (Traefik menolak start bila terlalu terbuka). Compose
memakai named volume, sehingga Traefik membuatnya sendiri dengan mode benar.
- **Ingress dijalankan SATU host saja** (default: host yang menjalankan Keycloak —
`control`; bisa dipatok dengan `tls.ingress_host`). Alasannya bukan hanya kesederhanaan:
apex (`sicabe.com`) dan wildcard (`*.sicabe.com`) memakai **nama TXT DNS-01 yang sama**
(`_acme-challenge.<domain>`), sehingga beberapa Traefik yang menerbitkan wildcard yang
sama akan **saling bertabrakan** — gejalanya `403 Incorrect TXT record ... (and 1 more)`.
Batas Let's Encrypt juga hanya **5 sertifikat per himpunan nama per minggu**.
- Host ingress mengambil rute komponen **lokal** dari label container, dan rute komponen di
**host lain** dari `config/traefik/dynamic.yml` (provider file) yang dibangkitkan generator —
mis. `trino` → `https://<alamat-compute>:12443`, `minio` → `http://<alamat-state>:12901`.
Traefik hanya melihat container di host-nya sendiri, jadi rute lintas host **wajib** lewat
berkas dinamis.
- Karena ingress terpusat, **A-record cukup satu wildcard** `*.sicabe.com` → alamat host
ingress (tidak perlu per label). Port 80/443 hanya perlu dibuka di host ingress.
- Label `traefik.*` tetap tertulis di semua bundle (inert tanpa Traefik) — tidak berbahaya,
tapi jangan menjalankan `--profile edge` di host non-ingress karena akan menggandakan rute.
- Trino internal memakai HTTPS (sertifikat self-signed) -> Traefik memakai `serversTransport`
dari `config/traefik/dynamic.yml` dengan `insecureSkipVerify` **hanya untuk Trino**.
`serversTransports` **tidak bisa** didefinisikan lewat label Docker (menghasilkan error
runtime `servers transport not found`).
- `tls.mode: edge` membuat generator mengisi `KC_PROXY_HEADERS=xforwarded`,
`TRINO_PROCESS_FORWARDED=true`, dan `KEYCLOAK_SSL_REQUIRED=external`. Sebaliknya pada
`none` semuanya kosong/`false` — Trino dan Keycloak **menolak** header `X-Forwarded-*`
dari klien, jadi host bisa dipalsukan tidak.
## Detail per Komponen
### Phase 1 — Keycloak + OpenLDAP
- Realm `sicabe`; OIDC clients: `trino`, `starrocks`, `airflow`, `openmetadata`, `grafana`.
- Users lokal: `admin`, `engineer`, `analyst`, `viewer`.
- **OpenLDAP** = sumber kebenaran **grup** (`data-admin`, `data-engineer`,
`data-analyst`, `data-viewer`). User tidak difederasikan — autentikasi via
Keycloak, resolusi grup via LDAP (opsi 1).
- Akses: `http://localhost:12080/admin/sicabe/console`.
### Phase 2 — Storage: MinIO + Nessie
- MinIO bucket `iceberg-data` (dipakai Nessie warehouse + StarRocks shared-data).
- **Nessie** menyimpan version store di **PostgreSQL** (`nessie` DB, schema `nessie`,
`NESSIE_VERSION_STORE_TYPE=JDBC2`). Tabel Nessie di-auto-create.
- Warehouse: `s3://iceberg-data/warehouse`; S3 MinIO path-style, kredensial server-side.
- Akses: `http://localhost:12120` (`/api/v2`, `/iceberg/`).
### Phase 3 — Processing: Trino + OPA
- Katalog `iceberg` via Nessie (`iceberg.catalog.type=nessie`).
- **Authz OPA** (`access-control.name=opa`, deny-by-default). Policy:
`config/opa/policies/trino.rego`; ACL: `config/opa/data.json`.
- **Group provider LDAP** (`group-provider.name=ldap`) → grup `data-*`.
- **Auth hybrid** (`http-server.authentication.type=oauth2,password`):
- **End user**: OAuth2 SSO Keycloak (Web UI + JDBC) di `https://localhost:12443`.
- **Service/pipeline**: **HTTP Basic** service account (`svc_dbt`, `svc_seed`,
`svc_airflow`, `svc_om`) via `password-authenticator` file
(`config/trino/password.db`, bcrypt; di-generate `bootstrap.sh`/`install.sh`,
pra-syarat `htpasswd`). Otorisasi via OPA `data.users` (user→role).
- **HTTPS-only**: HTTP polos **dimatikan** (`allow-insecure-over-http=false`) —
`http://…:12081` tidak lagi di-expose. Semua akses via `https://localhost:12443`
(self-signed → `curl -k` / terima peringatan browser).
- ⚠️ **Web UI OAuth2 wajib HTTPS** — Trino mengalihkan `/ui/` ke
`/ui/static/disabled.html` bila request tidak secure.
### Phase 4 — Serving: StarRocks (shared-data) + Dremio
**StarRocks shared-data:**
- FE `run_mode=shared_data`, `cloud_native_storage_type=S3`; data tabel internal
disimpan di MinIO via **storage volume** `s3_volume`.
- Cluster = **FE + CN** (Compute Node, `ALTER SYSTEM ADD COMPUTE NODE`).
- OIDC: `CREATE SECURITY INTEGRATION sr_oidc`; **LDAP group provider** `sr_ldap`;
`authentication_chain = native,sr_oidc`; `GRANT role TO EXTERNAL GROUP`.
- External catalog `iceberg_catalog` → **Nessie** (`http://nessie:19120/iceberg/`).
- Akses: `mysql -h localhost -P 12930 -u root -p`.
**Dremio OSS:**
- Engine paralel di atas Nessie yang sama; auth **lokal** (first-run admin via UI).
- Source Nessie: endpoint `http://nessie:19120/api/v2`, MinIO path-style
(`fs.s3a.path.style.access=true`, `dremio.s3.compat=true`).
- Akses: `http://localhost:12947`. Detail: `config/dremio/README.md`.
### Phase 5 — Ingestion: Airflow
- Sling menarik PostgreSQL → Bronze (Iceberg via **Nessie Iceberg REST**).
- dbt via Trino: Bronze → Silver → Gold. Great Expectations validasi.
- Akses: `http://localhost:12082`.
### Phase 6 — Governance: OpenMetadata + Grafana
- OpenMetadata: DB `openmetadata_db`; konektor Trino + Airflow.
- Grafana: datasource provisioning file. Akses `http://localhost:12300`.
## RBAC
| User | Password | Role | Akses |
|------|----------|------|-------|
| `admin` | `KEYCLOAK_ADMIN_PASSWORD` | admin | Penuh (OPA superuser, StarRocks `sr_admin`) |
| `engineer` | `engineer123` | engineer | OPA write+ddl, StarRocks `sr_engineer` |
| `analyst` | `analyst123` | analyst | OPA select (per-kolom), StarRocks `sr_analyst` |
| `viewer` | `viewer123` | viewer | Terbatas (StarRocks tanpa grant → fail-closed) |
Pemetaan grup → akses:
- **Trino/OPA**: grup LDAP `data-*` → `config/opa/data.json` `roles.<grup>.tables`.
- **StarRocks**: external group `data-*` → role `sr_*` (`GRANT ... TO EXTERNAL GROUP`).
## Perbedaan dari `deploy-legacy/`
| Aspek | `deploy-legacy/` (base lama) | `deploy/` (merge) |
|---|---|---|
| Iceberg catalog | Polaris | **Nessie** (JDBC2/Postgres) |
| Trino authz | `rules.json` | **OPA** |
| Trino group provider | plugin Keycloak | **LDAP** |
| StarRocks mode | shared-nothing (FE+BE) | **shared-data (FE+CN)** |
| StarRocks storage | disk lokal | **S3/MinIO (storage volume)** |
| Engine paralel | — | **Dremio OSS** |
| Identitas | user+grup di Keycloak | user di Keycloak, **grup di OpenLDAP** |
| Project/network | `sicabe` / `172.28.0.0/16` | `sicabe` / `172.28.0.0/16` (sama) |
| Host port | 3xxx/8xxx/9xxx | **12xxx** |
## Catatan Penting
- **`run_mode=shared_data` bersifat final** — tidak bisa diubah setelah first
start; `deploy/` mulai dari volume bersih.
- **Nessie tidak mendukung Iceberg spec v3** → selalu `format-version=2`.
- **Dremio OSS** memakai auth lokal & penyimpanan volume (bukan PostgreSQL).
- **Tulis lintas engine** (Trino/StarRocks/Dremio) di Nessie sebaiknya bergantian,
bukan simultan.
- **`.env`** di-generate `install.sh` (JANGAN commit).
- Container & project memakai **`sicabe-*`** (sama dengan `deploy-legacy/`).
Karena nama container/project sama, **jangan jalankan `deploy/` dan
`deploy-legacy/` bersamaan** — `deploy-legacy/` hanya arsip/referensi.
- Host port tetap **12xxx**.
## Troubleshooting (ringkas)
| Gejala | Solusi |
|---|---|
| Nessie unhealthy | Cek DB `nessie` + schema `nessie` ada; cek `NESSIE_DB_PASSWORD` |
| Trino query ditolak semua | Cek OPA reachable (`curl localhost:12181/v1/data/trino/allow`); cek `config/opa/data.json` |
| StarRocks CN tidak terdaftar | `docker logs sicabe-starrocks-cn`; `SHOW COMPUTE NODES` di FE |
| StarRocks S3 400/301 | Pastikan path-style di storage volume + `cn.conf`/env S3 |
| Login OIDC StarRocks gagal | `required_issuer` harus host yang dipakai login (`http://localhost:12080/...`); redirect URI client `starrocks` harus memuat `http://starrocks-fe:8030/api/oauth2` |
| Dremio tidak melihat Nessie | Buat source Nessie manual (first-run); MinIO path-style + `dremio.s3.compat=true` |
| Sling gagal ke katalog | Sling tidak mendukung `catalog_type=nessie`; gunakan `catalog_type: rest` + `rest_uri: http://nessie:19120/iceberg/` |
Detail tambahan: `experiments/*/README.md` dan `config/*/README.md`.
## Referensi
> **Catatan versi & perilaku terverifikasi** — [`docs/sicabe-catatan-versi-dan-perilaku.md`](../docs/sicabe-catatan-versi-dan-perilaku.md):
> hal-hal yang pernah menggigit dan nilai yang benar (Keycloak 26 hostname/sslRequired/cluster
> health, Traefik v3 label/file-provider/ACME, token DNS-01 Cloudflare, env OM & MinIO),
> termasuk **Traefik membuang container yang unhealthy** yang membuat komponen hilang dari
> ingress tanpa error.
- Rencana merge: `../reviews/sicabe-07-merge-experiments-plan.md`
- [Project Nessie](https://projectnessie.org/guides/iceberg-rest/)
- [Trino OPA access control](https://trino.io/docs/current/security/opa-access-control.html)
- [StarRocks shared-data](https://docs.starrocks.io/docs/deployment/deploy_shared_data_manually/)
- [Dremio OSS](https://docs.dremio.com/)
- [Sling Iceberg](https://docs.slingdata.io/connections/datalake-connections/iceberg)
---
*deploy — hasil merge `deploy-legacy/` + 3 eksperimen (2026-09-10).*