Debug build backend yang tidak reproducible di CI dan produksi biasanya berujung pada pola yang sama: test lulus, image atau artifact berhasil dibangun, tetapi service gagal saat runtime di produksi. Penyebabnya sering bukan bug logika aplikasi murni, melainkan perbedaan binary, library native, path, environment variable, atau cache build yang membuat artifact di satu tempat tidak identik dengan artifact di tempat lain.
Masalah ini sulit direproduksi karena gejalanya tampak acak. Di lokal semuanya normal, di CI semua hijau, tetapi di produksi muncul error seperti shared library not found, perilaku parsing berbeda, TLS handshake gagal, atau proses crash hanya pada host tertentu. Kunci debugging-nya adalah berhenti mengasumsikan bahwa “kode yang sama” berarti “environment yang sama”, lalu membandingkan dependency graph dan runtime secara sistematis.
Studi kasus: test lolos di CI, service gagal di produksi
Bayangkan sebuah backend service ditulis dalam bahasa yang sebagian dependency-nya bergantung pada library native, misalnya OpenSSL, libc, zlib, atau driver database. CI menjalankan unit test dan integration test dengan sukses. Artifact kemudian di-deploy ke produksi. Saat startup, service gagal dengan gejala seperti ini:
error while loading shared libraries: libssl.so: cannot open shared object file: No such file or directoryAtau lebih sulit lagi, service tetap hidup tetapi sebagian request gagal:
TLS handshake failed
certificate verification error
unexpected EOFDalam kasus lain, worker queue berjalan normal di staging tetapi crash di produksi karena executable tambahan ditemukan dari PATH yang berbeda:
spawned helper binary exited with status 127Di atas kertas, tim merasa sudah aman karena:
- commit yang sama dipakai di lokal, CI, dan produksi,
- test lulus di CI,
- pipeline deploy tidak melaporkan error,
- lockfile dependency aplikasi sudah dikomit.
Namun lockfile aplikasi saja tidak cukup jika runtime masih bergantung pada paket sistem, binary eksternal, dynamic linker, shell initialization, atau variabel environment yang tidak dikontrol ketat.
Gejala nyata build yang tidak reproducible
1. Versi binary atau library berbeda
Ini gejala paling umum. Misalnya aplikasi dikompilasi atau diuji terhadap satu versi OpenSSL di CI, tetapi saat runtime memuat versi lain di produksi. Pada bahasa yang memakai ekstensi native atau FFI, perbedaan kecil di ABI bisa memicu error yang sulit dibaca.
2. Path berubah antar environment
Perintah yang tersedia di lokal belum tentu tersedia di CI atau produksi dengan path yang sama. Aplikasi yang memanggil sh, ffmpeg, convert, psql, atau helper binary lain sering diam-diam bergantung pada isi PATH.
3. Environment variable tidak identik
Beberapa framework membaca konfigurasi dari environment variable saat build, bukan hanya saat runtime. Akibatnya artifact yang dihasilkan di CI bisa sudah “terkontaminasi” nilai tertentu, sedangkan produksi memakai nilai lain.
4. Cache build menyesatkan
Cache di CI bisa membuat pipeline terlihat sehat padahal sebenarnya memakai hasil build dari dependency lama. Ini berbahaya jika perubahan environment tidak ikut menjadi bagian dari kunci cache. Anda merasa sedang menguji dependency baru, padahal CI masih menjalankan artifact hasil kombinasi lama.
5. Ketergantungan implisit ke host
Contohnya adalah locale, timezone, CA bundle, shell profile, glibc vs musl, direktori kerja, izin file, atau file konfigurasi default di host. Ketika dependency semacam ini tidak dideklarasikan, hasil build dan runtime menjadi bergantung pada mesin tempat proses dijalankan.
Root cause: kode sama, environment berbeda
Akar masalahnya hampir selalu salah satu dari dua kategori berikut:
- Build tidak benar-benar hermetik: proses build masih membaca dependency dari host, internet, atau toolchain yang tidak dipin.
- Runtime contract tidak eksplisit: artifact mengasumsikan keberadaan binary, library, file, path, atau env var yang tidak dijamin tersedia identik di target deploy.
Di sinilah konteks Nix Flakes vs Guix relevan. Bukan karena keduanya harus dibahas sebagai ulasan tool, tetapi karena keduanya memaksa kita melihat environment sebagai deklarasi yang dapat dibangun ulang. Guix dikenal menekankan kemurnian environment secara ketat, sedangkan Nix Flakes memberi cara praktis untuk mem-pin input dan menyusun dev/build shell secara konsisten. Dalam debugging, sudut pandang ini penting: jika environment tidak bisa dideklarasikan ulang secara deterministik, maka bug runtime lintas environment akan terus muncul kembali.
Langkah investigasi yang efektif
1. Bekukan gejala dengan bukti konkret
Jangan mulai dari asumsi. Kumpulkan bukti dari tiga tempat: lokal, CI, dan produksi.
- hash artifact atau image,
- versi runtime dan toolchain,
- daftar shared library yang dimuat,
- nilai env var penting,
- isi
PATH, - working directory dan user yang menjalankan service,
- log startup lengkap, bukan hanya error ringkas.
Contoh perintah yang berguna di host Linux:
which my-service
my-service --version
printenv | sort
printf '%s
' "$PATH"
ldd /path/to/my-service
sha256sum /path/to/my-serviceJika service memanggil helper binary:
which ffmpeg
which sh
which psql
ffmpeg -versionTujuannya bukan sekadar melihat “ada atau tidak”, tetapi memastikan path dan versi yang benar-benar dipakai.
2. Bandingkan dependency graph, bukan hanya lockfile aplikasi
Banyak tim berhenti di package-lock.json, poetry.lock, go.sum, atau sejenisnya. Itu penting, tetapi belum cukup. Anda juga perlu membandingkan:
- toolchain compiler/interpreter,
- library native,
- base image atau paket sistem,
- helper executable,
- CA certificates dan locale data bila relevan.
Pada aplikasi yang menghasilkan binary native, gunakan tool sistem untuk melihat dependency runtime. Pada container, inspeksi layer dan paket yang terpasang. Pada deploy non-container, catat dari mana paket host berasal dan kapan terakhir diperbarui.
Pertanyaan yang perlu dijawab:
- Apakah artifact benar-benar sama antara CI dan produksi?
- Jika sama, apakah runtime dependency di host berbeda?
- Jika artifact berbeda, apa yang berubah: source, input build, atau cache?
3. Ulangi build tanpa cache
Cache sering menjadi pelaku diam-diam. Jalankan build bersih di CI dan lokal dengan cache dimatikan sementara. Jika hasil berubah, berarti proses build Anda belum stabil atau cache key tidak memasukkan seluruh input yang relevan.
Catatan: build yang tidak stabil tidak selalu berarti output file berbeda byte-per-byte. Kadang output sama, tetapi sumber dependency yang dipakai selama build berbeda, dan baru terlihat saat runtime.
4. Isolasi environment sampai bug muncul atau hilang
Cara tercepat menemukan perbedaan adalah mempersempit surface area environment. Jalankan service dalam shell atau container minimal, lalu tambahkan dependency satu per satu. Jika punya deklarasi environment dengan Nix Flakes atau Guix, gunakan itu untuk membangun shell yang sama di lokal dan CI.
Misalnya, untuk investigasi Anda bisa mendefinisikan environment yang secara eksplisit memasukkan runtime dan helper binary yang dibutuhkan. Tujuannya bukan memamerkan tool, tetapi memastikan developer tidak lagi mengambil dependency dari mesin host tanpa sadar.
# contoh ide deklaratif, disederhanakan
# fokusnya: runtime, helper binary, dan env yang eksplisit
packages = [
runtime
openssl
ca-certificates
helper-binary
];
env = {
APP_ENV = "debug-repro";
PATH = "... hanya path yang dideklarasikan ...";
};Jika bug hanya muncul di host produksi tetapi hilang dalam environment terisolasi yang identik, kemungkinan besar sumber masalah ada di host: package drift, env var injeksi, file sistem, atau konfigurasi service manager.
5. Periksa dynamic linking dan binary resolution
Untuk service yang memuat shared library atau memanggil executable lain, dua hal ini sangat sering terlewat:
- Dynamic linker memilih library yang berbeda karena path pencarian berubah.
- Shell atau proses supervisor memberi PATH berbeda dari yang Anda gunakan saat login manual ke server.
Service yang jalan di bawah systemd, init process dalam container, atau runner tertentu bisa memiliki environment yang jauh lebih sempit dibanding sesi interaktif. Jangan menyimpulkan “di server aman” hanya karena perintah berhasil saat Anda SSH lalu menjalankannya manual.
Contoh alur debugging yang realistis
Berikut contoh pola investigasi yang sering berhasil:
- Ambil satu instance produksi yang gagal dan satu job CI yang lolos.
- Catat hash artifact, runtime version, path executable, dan output
ldd. - Bandingkan env var penting:
PATH,HOME,SSL_CERT_FILE,LANG,TZ, dan variabel aplikasi. - Bangun ulang artifact tanpa cache.
- Jalankan artifact hasil CI di environment terisolasi yang menyerupai produksi.
- Jika bug muncul, bandingkan dependency runtime. Jika tidak muncul, inspeksi host produksi: package drift, volume mount, injected secret, atau konfigurasi supervisor.
Contoh diagnosis yang sering ditemukan:
- CI membangun terhadap satu versi library SSL, tetapi image produksi menarik base image yang lebih baru saat deploy berikutnya.
- Lockfile aplikasi sudah benar, tetapi helper binary di produksi berasal dari paket host yang berbeda patch level-nya.
- Cache layer Docker atau cache artifact CI tidak invalid ketika file environment build berubah.
- Service manager di produksi mengosongkan sebagian env var, sehingga fallback path aplikasi berubah.
Perbaikan jangka pendek
Pin seluruh input yang relevan
Jangan hanya mem-pin dependency level aplikasi. Pin juga:
- base image,
- toolchain build,
- paket sistem penting,
- helper binary eksternal,
- sumber download jika build masih mengambil artifact dari luar.
Jika memungkinkan, deploy artifact yang sama persis dari CI ke produksi, bukan membangun ulang di tahap deploy dengan host atau image yang berbeda.
Hilangkan dependency implisit ke host
Jika aplikasi memanggil binary eksternal, referensikan path yang eksplisit atau pastikan binary tersebut menjadi bagian dari paket runtime. Jangan berharap PATH host selalu berisi tool yang sama.
Matikan cache saat insiden aktif
Untuk debugging, lebih baik build lebih lambat tetapi jujur. Setelah root cause ditemukan, baru desain ulang strategi cache agar semua input environment ikut masuk ke kunci cache.
Tambahkan logging environment yang aman
Saat startup, log informasi diagnostik non-rahasia seperti versi runtime, hash build, path executable, dan mode konfigurasi. Hindari mencetak secret, tetapi jangan terlalu hemat informasi hingga investigasi jadi buta.
Pencegahan jangka panjang dengan environment deklaratif
Masalah reproducibility jarang selesai hanya dengan disiplin manual. Pencegahan jangka panjang biasanya membutuhkan environment deklaratif, yaitu definisi eksplisit tentang apa yang dibutuhkan untuk develop, build, test, dan run.
Mengapa Nix Flakes dan Guix relevan
Dalam konteks ini, Nix Flakes dan Guix berguna bukan sebagai topik “tool mana yang terbaik”, tetapi sebagai cara berpikir dan mengoperasionalkan reproducible environment.
- Nix Flakes cocok ketika tim ingin mem-pin input dan menyatukan dev shell, build, serta CI dengan bentuk deklarasi yang relatif praktis untuk adopsi bertahap.
- Guix menarik ketika Anda ingin model purity yang lebih ketat sehingga dependency host lebih sulit bocor ke build atau runtime.
Trade-off pentingnya adalah: semakin ketat isolasi dan deklarasi, semakin kecil ruang untuk “berhasil karena kebetulan”. Sebaliknya, learning curve, waktu setup, dan integrasi ke pipeline lama bisa meningkat.
Pola adopsi yang masuk akal
- Mulai dari dev shell yang identik untuk lokal dan CI.
- Pindahkan helper binary dan library native ke deklarasi environment.
- Pastikan pipeline test dan build memakai deklarasi yang sama, bukan image berbeda yang kebetulan mirip.
- Simpan metadata build: commit, input dependency, dan hash artifact.
- Jika memungkinkan, jalankan smoke test pada artifact final, bukan hanya pada source sebelum packaging.
Checklist diagnosis build backend yang tidak reproducible
- Apakah artifact yang diuji di CI sama persis dengan yang dijalankan di produksi?
- Apakah ada build ulang di tahap deploy?
- Apakah base image atau paket sistem dipin?
- Apakah helper binary eksternal dideklarasikan eksplisit?
- Apakah
PATHdi CI dan produksi identik secara fungsional? - Apakah env var build dan runtime terdokumentasi dan dapat dibandingkan?
- Apakah cache build invalid ketika dependency environment berubah?
- Apakah Anda sudah membandingkan shared library yang dimuat saat runtime?
- Apakah service manager di produksi menambahkan atau menghapus env tertentu?
- Apakah smoke test dijalankan terhadap artifact final di environment yang menyerupai produksi?
Trade-off adopsi tooling reproducible build
Kelebihan
- Bug lintas lokal, CI, dan produksi jauh lebih mudah direproduksi.
- Onboarding developer lebih konsisten.
- Drift dependency berkurang.
- Insiden akibat perubahan host yang tidak terdokumentasi lebih jarang.
Kekurangan
- Learning curve untuk tim bisa nyata.
- Integrasi dengan pipeline lama, image lama, atau paket proprietary mungkin perlu usaha tambahan.
- Debugging awal terasa lebih ketat karena dependency implisit tidak lagi “ditoleransi”.
- Cache dan binary substituter perlu dipahami agar performa build tetap masuk akal.
Trade-off ini biasanya layak jika Anda sering mengalami insiden “works on CI, fails in production”, terutama pada backend yang bergantung pada library native, binary tambahan, atau runtime host yang kompleks.
Penutup
Saat menghadapi build backend yang tidak reproducible di CI dan produksi, jangan terpaku pada source code saja. Fokuskan investigasi pada perbedaan environment: binary, library, path, env var, dan cache. Build yang tampak sukses di CI tidak menjamin artifact akan berjalan di produksi jika runtime contract-nya tidak eksplisit.
Pendekatan paling efektif adalah membandingkan dependency graph secara menyeluruh, mengisolasi environment sampai perbedaan terlihat, lalu memindahkan dependency penting ke deklarasi yang dapat dibangun ulang. Dalam praktiknya, Nix Flakes atau Guix membantu bukan karena keduanya sekadar tool baru, tetapi karena keduanya memaksa environment menjadi bagian dari kode yang bisa diaudit, dipin, dan direproduksi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!