Membangun Docker Images di Pipeline CI/CD
Anda punya Dockerfile yang bekerja sempurna di laptop. Anda jalankan docker build, image terbangun, dan aplikasi berjalan. Tapi saat Anda mendorong Dockerfile yang sama ke pipeline, semuanya mulai berbeda.
Build yang hanya butuh dua menit di mesin lokal kini memakan waktu lima belas menit. Image yang kemarin bekerja hari ini gagal tanpa alasan jelas. Dan saat Anda perlu membangun ulang versi lama untuk rollback, image yang dihasilkan berperilaku berbeda dari aslinya.
Masalah-masalah ini tidak acak. Semua berasal dari kesenjangan antara membangun image di mesin developer dan membangunnya di pipeline otomatis. Memahami kesenjangan itu adalah langkah pertama untuk memperbaikinya.
Apa yang Berubah Saat Anda Pindah ke Pipeline
Saat Anda membangun image di pipeline, tiga hal berubah secara fundamental.
Pertama, build harus berjalan di mesin orang lain. Mesin itu mungkin memiliki sumber daya, kondisi jaringan, dan sistem file yang berbeda. Dockerfile Anda harus bekerja di mana pun ia dieksekusi.
Kedua, pipeline harus membangun ulang image setiap kali kode berubah. Ini tidak opsional. Jika Anda melewatkan rebuild, deployment Anda akan menjalankan kode basi. Tapi membangun ulang setiap kali berarti setiap commit memicu proses build penuh, dan proses itu harus cukup cepat agar pengembangan tetap berjalan.
Ketiga, build harus reproducible. Kode sumber yang sama harus menghasilkan image yang sama, kapan pun dan di mana pun build dijalankan. Tanpa reproducibility, Anda tidak bisa percaya bahwa rollback ke commit lama akan mengembalikan perilaku aplikasi yang persis sama.
Diagram di bawah menunjukkan tahapan tipikal pipeline Docker build, dari kode sumber hingga push ke registry.
Kendalikan Build Context Anda
Build context adalah folder yang Anda kirim ke Docker daemon saat menjalankan docker build. Di laptop Anda, ini biasanya folder proyek Anda. Di pipeline, hal yang sama terjadi, tapi konsekuensi dari context yang besar lebih buruk.
Setiap file di build context dikirim ke Docker daemon. Jika repositori Anda berisi node_modules, virtual environment Python, atau binary hasil kompilasi, file-file itu ikut ditransfer meskipun tidak diperlukan untuk build. Ini memperlambat setiap eksekusi pipeline.
Solusinya adalah file .dockerignore. Cara kerjanya seperti .gitignore tapi untuk Docker build. Daftarkan semua yang tidak diperlukan untuk image: folder dependensi, direktori cache, riwayat .git, fixture pengujian, dan dokumentasi. Build context yang ramping berarti build lebih cepat dan lalu lintas jaringan lebih sedikit.
Manfaatkan Cache Secara Maksimal
Docker membangun image dalam lapisan-lapisan (layers). Setiap instruksi di Dockerfile Anda membuat satu layer. Saat Anda membangun ulang, Docker memeriksa apakah setiap layer berubah. Jika layer tidak berubah, Docker menggunakan kembali hasil cache dari build sebelumnya.
Mekanisme caching ini adalah sekutu terbesar Anda untuk build yang cepat. Tapi ini hanya berfungsi jika cache bertahan di antara eksekusi pipeline.
Di pengembangan lokal, cache ada di mesin Anda. Di pipeline, cache menghilang saat runner selesai kecuali Anda secara eksplisit menyimpannya. Beberapa sistem CI menyediakan Docker layer caching bawaan. Jika tidak, Anda perlu mengonfigurasinya secara manual atau menerima bahwa setiap build dimulai dari awal.
Bahkan dengan caching yang berfungsi, urutan instruksi di Dockerfile Anda menentukan seberapa banyak cache yang benar-benar terpakai. Aturan emasnya adalah: salin hal-hal yang jarang berubah terlebih dahulu.
Untuk aplikasi Node.js, ini berarti menyalin package.json dan package-lock.json sebelum kode sumber lainnya. Jalankan npm install tepat setelah menyalin file-file ini. Kemudian salin kode aplikasi. Dengan urutan ini, instalasi dependensi hanya berjalan ulang saat dependensi Anda benar-benar berubah, bukan saat Anda memodifikasi satu baris kode aplikasi.
Prinsip yang sama berlaku untuk bahasa apa pun. Proyek Python harus menyalin requirements.txt atau pyproject.toml terlebih dahulu. Proyek Go harus menyalin go.mod dan go.sum terlebih dahulu. Polanya universal: pisahkan dependensi yang stabil dari kode aplikasi yang berubah.
Gunakan Build Arguments untuk Fleksibilitas
Dockerfile Anda tidak boleh meng-hardcode nilai yang berubah antar lingkungan. Versi base image, nama lingkungan, atau token akses untuk registry privat harus berasal dari luar Dockerfile.
Docker menyediakan ARG untuk tujuan ini. Anda mendefinisikan placeholder di Dockerfile, dan pipeline mengisinya saat build.
ARG BASE_IMAGE_VERSION=20.04
FROM ubuntu:${BASE_IMAGE_VERSION}
Di pipeline Anda, berikan nilai aktual:
docker build --build-arg BASE_IMAGE_VERSION=22.04 .
Ini menjaga Dockerfile Anda tetap generik dan dapat digunakan kembali. Satu Dockerfile bisa melayani build development, staging, dan production tanpa duplikasi.
Pastikan Build yang Reproducible
Reproducibility berarti membangun kode sumber yang sama di waktu yang berbeda menghasilkan image yang sama. Tanpa ini, Anda tidak bisa mempercayai strategi rollback, jejak audit, atau pemindaian keamanan Anda.
Tiga hal umum yang merusak reproducibility.
Pertama, menggunakan tag latest untuk base image. Tag latest berubah seiring waktu. latest hari ini bukan latest besok. Kunci base image Anda ke tag versi spesifik seperti ubuntu:22.04 atau node:20-alpine.
Kedua, tidak mengunci versi dependensi. package.json Anda mungkin menentukan rentang versi seperti ^4.0.0. Rentang itu akan meresolusi ke versi yang berbeda seiring waktu. Gunakan file lock seperti package-lock.json atau yarn.lock untuk membekukan versi eksak.
Ketiga, menyematkan timestamp build atau metadata build ke dalam image. Jika proses build Anda menulis tanggal saat ini ke file di dalam image, file itu akan berbeda antar build meskipun kode sumber identik. Hindari ini kecuali Anda memiliki alasan operasional spesifik.
Simpan Image di Registry
Setelah pipeline membangun image, ia membutuhkan tempat untuk disimpan di mana server dan cluster Kubernetes bisa menariknya. Tempat itu adalah container registry.
Pipeline Anda harus memberi tag image dengan identifier yang bermakna. Pola umum adalah menggunakan commit SHA sebagai tag utama, dengan tag tambahan untuk branch atau versi semantik.
docker tag myapp:${COMMIT_SHA} registry.example.com/myapp:${COMMIT_SHA}
docker push registry.example.com/myapp:${COMMIT_SHA}
Ini memberi Anda referensi permanen untuk setiap image yang pernah dibangun. Anda selalu bisa kembali ke commit mana pun dan menarik image persis yang dibangun dari commit tersebut.
Checklist Praktis
Sebelum Anda mendorong Docker build ke pipeline, periksa hal-hal ini:
.dockerignoremengecualikan folder dependensi, cache, dan.git- Dockerfile menyalin file dependensi sebelum kode aplikasi
- Tag base image dikunci ke versi spesifik, bukan
latest - Versi dependensi dikunci dengan file lock
- Build arguments digunakan untuk nilai spesifik lingkungan
- Pipeline menyimpan Docker layer cache antar eksekusi
- Image diberi tag dengan commit SHA dan didorong ke registry
Apa Artinya untuk Pipeline Anda
Membangun image di pipeline bukan sekadar menjalankan docker build di mesin yang berbeda. Ini tentang merancang Dockerfile dan pipeline Anda agar bekerja bersama. Dockerfile yang terstruktur dengan baik yang menghormati urutan layer dan build context akan membangun lebih cepat. Pipeline yang menyimpan cache dan memberi tag image dengan benar akan memberi Anda artefak yang andal dan reproducible.
Image yang Anda bangun hari ini harus sama dengan image yang bisa Anda bangun ulang enam bulan kemudian dari commit yang sama. Konsistensi itulah yang membuat deployment dapat diprediksi dan rollback aman.