Files
sicabe-core/deploy/docker/airflow/Dockerfile.332
T

240 lines
15 KiB
Docker
Raw Normal View History

2026-10-09 02:35:14 +07:00
# syntax=docker/dockerfile:1
# =============================================================================
# eBdesk SiCABE — Custom Airflow Image 3.3.2 / Python 3.11 (BERDAMPINGAN)
# =============================================================================
# VARIAN BARU (review R18 / BATCH B). Berdampingan dengan `Dockerfile.slim`
# (3.3.1/py3.11) — yang lama TIDAK diubah, sehingga rollback hanya soal menunjuk
# tag image. Rencana: reviews/sicabe-18-merge-airflow332-plan.md
# Asal-usul & bukti validasi: experiments/airflow332/ (H1–H9 terbukti)
#
# ⚠️ REVISI PENTING (B3 → B2 revisi): percobaan pertama memakai base
# **Python 3.13** dan DITOLAK pada uji go/no-go: `dbt debug`/`dbt compile`
# GAGAL di 3.13 —
# mashumaro → typing_extensions.Annotated.__class_getitem__
# → AttributeError: __class_getitem__ (typing.py 3.13)
# dan TIDAK ADA versi `mashumaro` yang sekaligus memenuhi batas dependensi kita
# (`collate-data-diff <3.11`, `dbt-core <3.15`) dan berfungsi di 3.13.
# Bukti lengkap: deploy/output/b3-hasil-uji.md
# ➡️ Karena itu base dikembalikan ke **Python 3.11** — tumpukan yang sudah
# terbukti di produksi. Tujuan BATCH B (Airflow 3.3.2 + OM 2.0.3 + provider
# MySQL) tetap tercapai, karena pin OM memaksa 3.3.2 terlepas dari Python.
#
# Perbedaan dari Dockerfile.slim:
# 1. base : slim-3.3.1-python3.11 -> slim-3.3.2-python3.11
# 2. provider MySQL : tidak ada -> DIPASANG (keputusan K3), dengan mysqlclient
# dibangun di STAGE BUILDER + libmariadb3 sebagai runtime
# 3. openmetadata : 2.0.2.0 -> 2.0.3.0, extra `[airflow]` DILEPAS
# 4. sqlalchemy<2.1 : PIN WAJIB (hasil A1) — lihat requirements.332.in
# 5. paket tambahan (2026-10-06): statsd (metrik StatsD), duckdb,
# pyiceberg[s3fs,pyarrow]
#
# Base: apache/airflow:slim-3.3.2-python3.11
# Debian 12 (bookworm) · Python 3.11.16 · TANPA gcc (maka stage builder wajib)
#
# Fakta yang sudah diverifikasi pada base ini:
# - Python.h ADA di /usr/python/include/python3.11/Python.h → mysqlclient BISA
# dibangun. Karena itu `python3-dev` Debian TIDAK dipasang.
# - libmariadb3 tersedia (1:10.11.19+maria~deb12); `libmariadb3t64` TIDAK ada.
# - mysqlclient terbukti terbangun di base 3.3.2 (di 3.13:
# mysqlclient-2.3.0-cp313-cp313-linux_x86_64.whl; di 3.11 ekuivalen cp311).
# - `pip install --user` DITERIMA di base 3.11 (di base 3.13 ia DITOLAK karena
# VIRTUAL_ENV=/home/airflow/.local). Berkas ini TIDAK memakai `--user`:
# hasilnya sama, karena venv-nya memang di ~/.local.
#
# ⚠️ URUTAN PEMASANGAN YANG MENENTUKAN (jangan diubah tanpa alasan):
# mysqlclient tidak punya wheel Linux → bila dibiarkan, pip akan mencoba
# membangunnya dari sdist di stage akhir yang TIDAK punya compiler → gagal.
# Karena itu: bangun wheel di stage builder, lalu pasang dari wheelhouse LEBIH
# DULU (sebelum install utama).
#
# Regenerasi lock (CARA YANG DIPAKAI — menghasilkan diff minimal):
# (1) ambil lock produksi 3.3.1 sebagai dasar constraint
# (2) BUANG dari constraint paket yang SENGAJA dinaikkan:
# grep -vE '^(apache-airflow|openmetadata-|collate-)' requirements.lock > c.lock
# (tanpa ini, constraint justru memblokir kenaikan 3.3.2 / OM 2.0.3 yang kita inginkan)
# (3) resolve:
# docker run --rm -v "$PWD/deploy/docker/airflow:/w" -v /path/c.lock:/c.lock:ro \
# apache/airflow:slim-3.3.2-python3.11 \
# bash -lc 'pip -q install uv && uv pip compile /w/requirements.332.in --constraint /c.lock'
# Hasil: hanya 44 baris paket berubah vs produksi (Airflow, provider, OM, collate,
# + MySQL) — jauh lebih kecil daripada resolusi bebas.
# ⚠️ Constraints RESMI Airflow 3.3.2 TIDAK dapat dipakai bersama OM 2.0.3.0
# (bentrok tabulate==0.10.0 vs 0.9.0 dan kubernetes==36.0.3 vs <36).
#
# Build (konteks = folder ini, karena COPY requirements.332.*):
# docker build -t sicabe/airflow:3.3.2-om203-ol \
# -f deploy/docker/airflow/Dockerfile.332 deploy/docker/airflow
# =============================================================================
# ─── STAGE 1 — bangun mysqlclient (compiler hanya ada di sini) ──────────────
FROM apache/airflow:slim-3.3.2-python3.11 AS mysqlwheel
USER root
# gcc + pkg-config + header MariaDB. SENGAJA TANPA `python3-dev` (lihat catatan atas).
# ⚠️ `/wheelhouse` harus DIBUAT dan dimiliki `airflow`; tanpa itu build gagal:
# PermissionError: [Errno 13] Permission denied: '/wheelhouse'
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gcc pkg-config default-libmysqlclient-dev \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /wheelhouse && chown airflow:0 /wheelhouse
USER airflow
RUN --mount=type=cache,target=/home/airflow/.cache/pip,uid=50000 \
pip wheel --no-cache-dir --wheel-dir /wheelhouse "mysqlclient>=2.2.5" \
&& ls -la /wheelhouse
# ─── STAGE 2 — image akhir (tanpa compiler) ────────────────────────────────
FROM apache/airflow:slim-3.3.2-python3.11
ARG SLING_VERSION=1.5.23
USER root
# Library RUNTIME untuk ekstensi mysqlclient (libmariadb.so.3). Tanpa paket ini:
# ImportError: libmariadb.so.3: cannot open shared object file
RUN apt-get update \
&& apt-get install -y --no-install-recommends libmariadb3 \
&& rm -rf /var/lib/apt/lists/* \
&& ldconfig
USER airflow
COPY --from=mysqlwheel --chown=airflow:0 /wheelhouse /wheelhouse
COPY requirements.332.in /tmp/requirements.in
COPY requirements.332.lock /tmp/requirements.lock
# ─── (1) mysqlclient dari WHEELHOUSE, lebih dulu ────────────────────────────
# `--no-index` + `--find-links` memaksa pip memakai wheel hasil stage builder,
# sehingga tidak pernah mencoba membangun sdist di stage tanpa compiler ini.
RUN --mount=type=cache,target=/home/airflow/.cache/pip,uid=50000 \
pip install --no-compile --no-index --find-links /wheelhouse \
--constraint /tmp/requirements.lock mysqlclient
# ─── (2) Backend build untuk paket sdist pure-Python ────────────────────────
RUN --mount=type=cache,target=/home/airflow/.cache/pip,uid=50000 \
pip install --no-compile \
"setuptools" "wheel" "hatchling" "hatch-vcs" "poetry-core" "flit-core" "setuptools-scm"
# ─── (3) Dependensi dari lock ───────────────────────────────────────────────
RUN --mount=type=cache,target=/home/airflow/.cache/pip,uid=50000 \
pip install --no-build-isolation --no-compile \
--constraint /tmp/requirements.lock \
-r /tmp/requirements.in
# ─── Verifikasi inti (FATAL bila gagal) ─────────────────────────────────────
RUN python -B -c "import airflow; print('airflow', airflow.__version__)"
# Kelima klien koneksi harus dapat diimpor (inti eksperimen airflow332).
RUN python -B -c "import MySQLdb; print('MySQLdb (mysqlclient) OK ->', MySQLdb.version_info)" \
&& python -B -c "import mysql.connector; print('mysql-connector-python OK ->', mysql.connector.__version__)" \
&& python -B -c "import pymysql; print('pymysql OK')" \
&& python -B -c "import psycopg2; print('psycopg2 OK ->', psycopg2.__version__)" \
&& python -B -c "import trino; print('trino client OK ->', trino.__version__)"
# Bukti mysqlclient benar-benar DIMUAT (bukan sekadar terpasang): impor modul C-nya
# sudah memuat libmariadb.so.3 — kalau library runtime tidak ada, perintah gagal.
RUN python -B -c "import MySQLdb, MySQLdb._mysql; print('modul C mysqlclient dimuat:', MySQLdb._mysql.__file__)" \
&& ( ldconfig -p 2>/dev/null | grep -i mariadb || echo '[WARN] libmariadb tidak terlihat di ldconfig' )
RUN python -B -c "import dbt, great_expectations; print('dbt + great_expectations OK')" \
&& python -B -c "import airflow.providers.mysql; print('provider mysql OK')"
# ─── Provider OpenLineage (FATAL bila gagal) ────────────────────────────────
# Ditambahkan 2026-10-02 atas permintaan pemilik proyek. Verifikasi memastikan:
# 1. paket openlineage-python terpasang
# 2. provider Airflow-nya dapat diimpor
# 3. entry point provider benar-benar terdaftar
# ⚠️ Provider ini INERT tanpa backend: hanya aktif bila transport/endpoint
# OpenLineage dikonfigurasi (OPENLINEAGE_URL + OPENLINEAGE_API_KEY, dst).
# Belum ada backend OpenLineage di stack ini — jadi ini mengaktifkan KEMAMPUAN.
# Catatan pemeriksaan: di Airflow 3, NAMA entry point provider selalu
# `provider_info` (yang membedakan adalah VALUE/module path-nya). Percobaan
# pertama saya memeriksa `e.name` -> positif palsu. Yang fatal di sini cukup
# impor keduanya; info entry point hanya DICETAK (tidak di-assert) agar tidak rapuh.
RUN python -B -c "import openlineage; print('openlineage-python OK')" \
&& python -B -c "import airflow.providers.openlineage; print('provider openlineage import OK')" \
&& python -B -c "import importlib.metadata as m; print('versi provider:', m.version('apache-airflow-providers-openlineage')); vals=[e.value for e in m.entry_points(group='apache_airflow_provider')]; print('entry point provider:', [v for v in vals if 'lineage' in v.lower()] or '(nama/value tidak memuat kata lineage — normal di Airflow 3)')"
# ─── Shim eventTime OpenLineage (FATAL bila gagal) ───────────────────────────
# Ditemukan di experiments/lineage-airflow-om: OpenMetadata 2.0.3 menolak event
# yang `eventTime`-nya berakhiran `+00:00` (bentuk yang dihasilkan
# datetime.isoformat() untuk UTC) dengan
# 400 {"code":400,"message":"Invalid request format"}
# Shim ZuluHttpTransport menormalkan `+00:00` -> `Z` sebelum event dikirim.
# Ditempatkan di direktori khusus + PYTHONPATH agar dapat diimpor sebagai
# `ol_time_shim.ZuluHttpTransport` dari proses task maupun subprosesnya.
COPY --chown=airflow:0 ol_time_shim.py /opt/airflow/ol_shim/ol_time_shim.py
# Catatan: image dasar `apache/airflow:slim-3.3.2-python3.11` TIDAK mendefinisikan
# PYTHONPATH (diverifikasi dengan `docker run --entrypoint env`), jadi tidak ada
# nilai lama yang tertimpa. Jangan menulis "${PYTHONPATH}" di sini: BuildKit
# memperingatkannya sebagai variabel tak terdefinisi.
ENV PYTHONPATH="/opt/airflow/ol_shim"
RUN python -B -c "from ol_time_shim import ZuluHttpTransport; print('shim eventTime OpenLineage DAPAT DIMUAT:', ZuluHttpTransport)"
# ─── Penjaga regresi dbt (FATAL) — pelajaran dari uji B3 ────────────────────
# `import dbt` dan `dbt --version` TIDAK CUKUP: pada percobaan Python 3.13
# keduanya LOLOS, tetapi `dbt debug`/`dbt compile` gagal dengan
# mashumaro -> typing_extensions.Annotated.__class_getitem__
# -> AttributeError: __class_getitem__ (typing.py)
# Pemeriksaan di bawah memakai PEMAKAIAN YANG SAH (`Annotated[tipe, anotasi]`),
# yang menempuh jalur kode yang sama dengan dbt saat mem-parse model — sehingga
# kerusakan jenis itu tertangkap saat BUILD.
# ⚠️ Percobaan pertama penjaga ini SALAH dan menghasilkan positif palsu:
# memanggil `Annotated.__class_getitem__((int,))` dengan SATU argumen ->
# TypeError "Annotated[...] should be used with at least two arguments".
# (Catatan: galat itu sekaligus membuktikan fungsinya ADA di py3.11.)
# Catatan: `dbt debug` penuh tidak bisa dijalankan di sini karena butuh koneksi
# Trino; pemeriksaan ini adalah proksi yang tepat dan murah.
RUN python -B -c "import typing_extensions as te; te.Annotated[int, 'meta']" \
&& python -B -c "import importlib.metadata as m; print('jalur Annotated OK -> dbt dapat berjalan; mashumaro', m.version('mashumaro'))"
# ─── Plugin OM harus benar-benar DAPAT DIMUAT (FATAL) ───────────────────────
# Pelajaran dari experiments/airflow332: `import openmetadata_managed_apis` BERHASIL,
# tetapi entry point gagal `load()` karena rantai
# flask_appbuilder -> sqlalchemy_utils -> SQLAlchemy 2.1
# (AttributeError: 'ScalarAttributeImpl')
# → plugin MATI saat Airflow start; tombol "Test Connection" hilang; log hanya
# berbunyi "Failed to import plugin openmetadata_managed_apis".
# ⚠️ Verifikasi lama (hanya mengimpor MODUL) LOLOS pada kondisi rusak itu — karena
# itu di sini entry point benar-benar dimuat, dan kegagalannya FATAL.
# Ditulis SATU BARIS: kode Python multi-baris dalam `python -c "..."` tanpa `\`
# akan dibaca Docker sebagai instruksi baru ("unknown instruction: import").
RUN python -B -c "import flask_appbuilder; print('flask_appbuilder import OK')" \
&& python -B -c "import sqlalchemy as sa; print('sqlalchemy', sa.__version__)" \
&& python -B -c "import importlib.metadata as m; eps=[e for e in m.entry_points(group='airflow.plugins') if 'openmetadata_managed_apis' in e.name]; assert eps, 'entry point openmetadata_managed_apis tidak ada'; cls=eps[0].load(); print('plugin openmetadata_managed_apis DAPAT DIMUAT:', cls)"
# ─── Paket tambahan 2026-10-06 (FATAL bila gagal) ───────────────────────────
# 1) statsd — WAJIB agar Airflow memakai StatsClient (StatsD → statsd-exporter).
# Tanpa ini Airflow jatuh ke NoStatsLogger dan TIDAK ada metrik terkirim.
RUN python -B -c "import statsd; print('statsd OK (StatsClient aktif)')"
# 2) duckdb — analitik lokal di task DAG.
RUN python -B -c "import duckdb; print('duckdb OK ->', duckdb.__version__)"
# 3) pyiceberg[s3fs,pyarrow] — S3FileIO via s3fs + tabel Arrow.
# Impor ketiganya SEKALIGUS: pyiceberg tanpa s3fs/pyarrow kehilangan extra-nya.
RUN python -B -c "import pyiceberg, pyarrow, s3fs; print('pyiceberg OK ->', pyiceberg.__version__); print('pyarrow OK ->', pyarrow.__version__); print('s3fs OK ->', s3fs.__version__)"
# ─── Verifikasi non-fatal (melaporkan saja) ─────────────────────────────────
RUN ( python -B -c "import metadata; print('openmetadata-ingestion OK')" \
|| echo '[WARN] modul `metadata` gagal di-import' ) \
&& ( python -B -c "import openmetadata_managed_apis; print('plugin openmetadata-managed-apis OK')" \
|| echo '[WARN] plugin openmetadata-managed-apis gagal di-import' ) \
&& ( python -B -c "import airflow.providers.postgres; print('provider postgres OK')" \
|| echo '[WARN] provider postgres' ) \
&& ( command -v metadata >/dev/null && echo 'CLI metadata OK' \
|| echo '[WARN] CLI metadata tidak ada' ) \
&& ( pip check || echo '[WARN] pip check melaporkan ketidaksesuaian (lihat log)' )
# ─── Sling CLI (binary Go — BUKAN paket pip `sling`) ────────────────────────
RUN --mount=type=cache,target=/tmp/sling-dl,uid=50000 \
( tar -tzf "/tmp/sling-dl/sling-${SLING_VERSION}.tar.gz" >/dev/null 2>&1 \
|| curl -fL --retry 5 --retry-delay 5 --retry-all-errors \
--max-time 2400 --connect-timeout 15 --continue-at - \
"https://github.com/slingdata-io/sling-cli/releases/download/v${SLING_VERSION}/sling_linux_amd64.tar.gz" \
-o "/tmp/sling-dl/sling-${SLING_VERSION}.tar.gz" ) \
&& mkdir -p "${HOME}/.local/bin" \
&& tar -xzf "/tmp/sling-dl/sling-${SLING_VERSION}.tar.gz" -C /tmp/ \
&& chmod +x /tmp/sling \
&& mv /tmp/sling "${HOME}/.local/bin/sling" \
&& "${HOME}/.local/bin/sling" --version
USER airflow