Playbook Upgrade Hybrid: npm In-Container Plus Fallback Image
Playbook upgrade dua fase: npm di dalam kontainer plus fallback image custom, diverifikasi lewat versi yang benar-benar dilayani endpoint.
Ringkasan
Gara-gara paket npm naik tapi versi yang dilayani tetap kuno, penulis sadar versi paket cuma klaim. Playbook upgradenya dirombak jadi dua fase: install npm dulu, terus cek /api/version, kalau gagal baru build image dari source. Ada health gate plus rollback otomatis, dan serial: 1 bikin blast radius cuma satu host.
Pagi itu saya menjalankan playbook upgrade untuk 9router di salah satu host fleet. Instalasi paket npm 0.5.99 [8] berjalan tanpa error, restart sukses. Lalu saya buka endpoint /api/version dan versi yang dilayani masih 0.5.95. Paket di dalam container sudah naik, aplikasinya tidak.
Tebakan pertama saya salah dua kali. Saya curiga cache npm, bersih-bersih cache, jalankan ulang. Sama saja. Setelah membedah isi paket, ketemu penyebabnya: paket npm 9router tidak menyertakan .next/custom-server.js, kode server yang dijalankan CMD berasal dari bundle di dalam image. npm i -g hanya mengganti paket global di lokasi node_modules, sementara proses yang hidup tetap mengeksekusi kode lama dari image. Versi paket naik, versi yang dilayani tidak.
Dari situ saya menulis ulang playbook upgrade jadi hybrid dua fase: Phase A cepat, Phase B tebal. Perubahan terpenting ada di definisi "sukses" yang dipakai playbook, bukan di cara install-nya.
Verifikasi Versi yang Benar-Benar Dilayani
Fase A tetap jalan dulu karena untuk mayoritas rilis, instalasi npm di dalam container jauh lebih murah daripada build image: docker exec menjalankan npm i -g, restart container, selesai. Bagian yang baru adalah verifikasinya. Playbook tidak percaya laporan manajer paket. Ia memanggil /api/version berulang kali dan hanya menyatakan sukses kalau versi yang dilayani sama dengan target. Kalau tidak, playbook tidak gagal, ia turun ke Fase B.
Definisi sukses yang benar ternyata soal filosofi. Versi paket adalah klaim; versi yang dilayani adalah fakta yang dialami pengguna. Satu endpoint kecil mengubah playbook dari "kira-kira sudah" jadi "terbukti sudah".
Fase B: Build dari Source, Bukan Nunggu Docker Hub
Fase B menyerang akar masalah lama: image di Docker Hub sering tertinggal jauh di belakang rilis npm. Daripada menunggu, playbook membangun image sendiri dari source master di control node. Build-nya dijaga tiga pintu. Source-version guard membandingkan versi di package.json source dengan target; kalau beda, build dibatalkan daripada menghasilkan image versi salah. Setelah jadi, docker image save membungkus image beserta semua parent layer dan tag-nya menjadi arsip tar [4], di-gzip, lalu dikirim ke host memakai modul copy Ansible [6]. Di host, docker image load membaca arsip ter-kompresi itu dan memulihkan image sekaligus tag-nya [5]. Kalau image atau arsip untuk versi itu sudah ada dari run sebelumnya, semua langkah ini dilewati. Playbook bisa diulang seperlunya tanpa build ulang.
Pertukaran container memakai pola rename: container lama di-stop, di-rename jadi cadangan, container baru di-run dengan flag identik, bind mount data yang sama, port yang sama, rotasi log sama, dan restart policy unless-stopped [3]. Policy itu pas untuk kasus ini: container otomatis hidup setelah daemon restart, tapi tetap mati kalau saya yang menghentikannya untuk maintenance.
Health Gate dengan Rollback Otomatis
Container baru harus lulus health gate sebelum dinyatakan selesai. Docker baru menganggap container "mulai sukses" setelah hidup sepuluh detik, sebagai penangkal restart-loop [10]. Playbook saya lebih galak lagi: probing HTTP sampai dapat status redirect atau 200 plus versi dilayani yang cocok dengan target, delapan percobaan, jeda tiga detik. Gagal? Container cadangan di-rename balik dan di-start. Rollback termasuk alur normal playbook, bukan rencana darurat.
Dua pengaman lain menutup celah klasik. Never-downgrade: kalau versi yang dilayani malah lebih tinggi dari target, playbook berhenti untuk host itu lewat meta: end_host, yang mengakhiri play untuk host tersebut tanpa menandainya gagal [7]. Host lanjut ke barisan berikutnya, tidak ada penurunan versi diam-diam. Dan karena registered variable selalu terisi untuk tiap host, termasuk yang gagal atau di-skip [2], laporan per host tetap bisa diperiksa lewat modul debug tanpa menghentikan playbook [9].
serial: 1, Blast Radius Satu Host
Seluruh proses berjalan dengan serial: 1, keyword Ansible untuk rolling update yang menyelesaikan play per kelompok host sebelum pindah ke kelompok berikutnya [1]. Dengan satu host per batch, kegagalan terburuk hanya menyentuh satu mesin, dan host berikutnya aman.
Uji end-to-end di satu host membuktikan alurnya: dari 0.5.95 ke 0.5.99, container lain tidak tersentuh. Rollout penuh ke semua host fleet kemudian berjalan dengan playbook yang sama, dan re-run --limit untuk host yang lambat menunjukkan sifat idempoten-nya. Dua resep manual yang dulu saya tulis di dua artikel terpisah kini jadi satu playbook yang bisa memutuskan sendiri. Itu bedanya playbook yang mengagumi versi paket dengan playbook yang memeriksa versi yang benar-benar dilayani.
Sumber: [1] Ansible strategies (serial) · [2] Ansible conditionals (register) · [3] docker run reference · [4] docker image save · [5] docker image load · [6] Ansible copy module · [7] Ansible meta (end_host) · [8] npm registry, 9router · [9] Ansible debug module · [10] Docker policy guide