Optimasi cache pada Dockerfile

All the cool kids use Docker!

Siapa yang tidak kenal Docker. Tools yang sedang populer di dunia persilatan ini telah membantu kita semua mengatasi masalah “it works on my machine!”.

Lah, kalau “it works” kok dibilang masalah? Ya itu jadi masalah kalau di mesin lokalnya engineer jalan, tapi begitu di-deploy ke server ternyata beda dan bikin runyam. Betul? Padahal hal tersebut seringkali disebabkan hal sesepele lupa install dependency baru, PHP-nya beda versi, dll. Dengan Docker, masalah distribusi software tersebut teratasi secara elegan. You define how to package the software, its dependencies, how to run, and that’s it. It runs anywhere!

Di sini saya menggarisbawahi bagian “how to package the software”. Dalam dunia per-Docker-an, ada file yang namanya “Dockerfile”. File ini mendeskripsikan langkah-langkah mengemas kode kita ke menjadi Docker image.

Satu contoh sederhana:

Cukup intuitif kan? Gunakan Python versi 2.7.12 sebagai base image, kemudian install dependency dari “requirements.txt”, salin codebase ke direktori “app”, lalu gunakan “python app.py” sebagai command untuk menjalankan container. Selesai. Simpel.

Iya, semuanya terlihat baik-baik saja sampai saatnya build image. Khususnya di local environment yang membuat kita secara sadar tak sadar melakukan build yang sama berulang-ulang untuk memastikan everything works well.

Coba jalankan “docker build -t anu .” berulang-ulang tanpa mengubah apapun. Dalam kondisi normal, build yang sebenarnya hanya dilakukan sekali yang pada build pertama. Build kedua dan seterusnya akan berjalan sangat cepat berkat layer caching, selama cache masih valid (dengan kata lain tidak ada perubahan kode).

Sekarang coba ubah satu baris saja di file “app.py”. Perubahan apapun, tambah satu baris komentar juga boleh. Lalu lakukan build ulang. What’ll happens?

Boom!

Seperti bisa dilihat, perubahan sesepele menambah komentar pada codebase menyebabkan seluruh proses build image harus diulang dari awal. Proses unduh dependency yang makan waktu pun harus ikut diulang. Jelas ini bukan kondisi yang ideal. Eh, tapi kenapa ini bisa terjadi? What’s wrong?

Setiap step pada Dockerfile akan membuat satu layer pada Docker image. Layer tersebut akan disimpan sebagai cache sehingga step yang sama dengan data yang sama tidak perlu dieksekusi ulang. Ketika ada satu layer yang cache-nya tidak valid lagi, seluruh layer di bawahnya akan turut menjadi invalid.

Nah! Cache pada step “COPY . /app” menjadi invalid ketika pada direktori sumber tersebut terjadi perubahan konten (dalam hal ini, file app.py). Karena itu, seluruh cache pada step di bawahnya, terutama “pip install -r requirements.txt” pun turut invalid dan menjadi harus dieksekusi ulang.


Then, how can we overcome this?

Kuncinya adalah memisahkan instalasi dependency dari proses menyalin codebase pada Dockerfile.

Nah, dengan cara ini perubahan pada codebase tidak akan mengganggu cache proses yang memakan waktu. Instalasi dependensi hanya akan dieksekusi ulang ketika terjadi perubahan pada file “requirements.txt”.

Rule of thumb
Posisikan step yang kemungkinan cache invalidate-nya relatif lebih kecil di bagian atas Dockerfile. Dan sebaliknya, step yang datanya sering berubah (e.g. codebase) letakkan di bagian bawah.

Secara garis besar, urutan ideal dalam Dockerfile adalah sebagai berikut:

  1. Install system dependencies (apt-get install, apk add, etc)
  2. Install app dependencies (pip, composer, gem, npm, etc)
  3. Copy code base

Ngomong-ngomong, why should I care about this? Toh build image sudah dilakukan oleh mesin CI (Continuous Integration) kan? Lagipula, kalau mengandalkan cache, berarti build tidak bermula dari clean state kan? Nanti buildnya tidak konsisten dong?

Jadi begini…

  1. Cache yang benar dan efektif dapat mempercepat proses build secara signifikan. Ada trade-off di sini.
  2. Docker cache lumayan menghemat space harddisk ketika menyimpan banyak docker image yang memiliki kesamaan layer.
  3. Build step yang melalui TCP/IP (dalam hal ini, install dependency) sangat bergantung dengan kondisi jaringan.
  4. Kalau takut dengan build yang tidak konsisten, there’s probably something wrong with your dependency management. Jangan gunakan wildcard version. Mari kita percayakan pada package maintainer dan package manager bahwa setiap versi yang sama pasti menghasilkan output yang sama.

By the way, masalah clean state dan build consistency ini mungkin bisa jadi bahan tulisan saya selanjutnya. Doakan saja bisa terlaksana 🙈