Memakai Persistent Volume Di Kubernetes
Saat sebuah pod di-restart, seluruh perubahan di dalam container-nya akan hilang. Ini tidak menjadi masalah untuk service yang stateless seperti stock-item-service yang hanya mengerjakan aksi seperti validasi dan membaca/menyimpan data di database MongoDB dan Elasticsearch. Setelah service tersebut di-restart, ia tetap akan bekerja dengan baik (dengan memory yang ‘segar’). Namun, bagaimana dengan service lain seperti database atau service yang menangani file yang di-upload oleh pengguna? File-file yang sudah ditulis tentu saja tidak boleh hilang saat pod di-restart.
Sebagai latihan, saya akan membuat service baru dengan nama file-upload-service yang berfungsi untuk mendukung operasi seperti upload dan download file. Khusus untuk proses upload, service ini akan menggunakan bahasa Python dengan framework Flask. Sebagai contoh, saya membuat file app.py dengan isi seperti berikut ini:
Pada kode program di atas, saya mendefinisikan sebuah @app.route()
dengan method POST
untuk menangani proses upload. Saya akan multiplart/form-data
yang umum dipakai untuk upload dimana nama elemen dengan nama file
yang bisa dibuat dengan menggunakan <input type='file' name='file'>
. Karena nama file adalan input dari pengguna sehingga tidak dapat dipercaya, saya kemudian melakukan sanitasi nama file dengan menggunakan secure_filename()
(untuk menghindari serangan yang menggunakan nama yang mengandung navigasi relatif seperti ../../../etc
). Selain itu, saya juga menambahkan sebuah uuid.uuid4()
sebagai awalan nama file supaya nama file sulit ditebak sekaligus juga menghindari masalah duplikasi bila file dengan nama yang sama di-upload lagi. Terakhir, kode program di atas akan menyalin file ke folder /uploads
dengan menggunakan file.save()
. Ia juga akan mengembalikan sebuah JSON yang mengandung informasi nama file yang ditulis.
Saya kemudian membuat sebuah Dockerfile untuk menjalankan kode program di atas di dalam container dengan isi seperti berikut ini:
Walaupun Flask sudah dilengkapi dengan development server yang dapat melayani HTTP langsung dengan perintah flask run
, server tersebut tidak disarankan untuk produksi seperti yang dituliskan di https://flask.palletsprojects.com/en/2.0.x/server/. Sebagai gantinya, Python memiliki standar Web Server Gateway Interface (WSGI) untuk web server. Flask termasuk framework yang kompatibel dengan WSGI, sehingga saya bisa menggunakan server WSGI seperti uwsgi
. Pada Dockerfile
di atas, saya menggunakan --protocol uwsgi
dan --socket
sehingga uwsgi
tidak dapat diakses secara langsung sebagai web server, namun harus melalui server lain yang mendukung protokol uwsgi seperti NGINX.
Untuk men-build Dockerfile
tersebut di minikube, saya segera memberikan perintah berikut ini:
Untuk proses baca (download), saya tidak menggunakan Flask. Sebagai gantinya, saya akan langsung mempublikasikan folder /uploads
melalui NGINX tanpa melalui Python untuk mendapatkan kinerja terbaik. Dengan demikian, pod untuk service ini akan terdiri atas dua container: satu untuk aplikasi Flask (Python) dan satu lagi untuk NGINX. Dalam hal ini, NGINX berperan sebagai sidecar, hampir sama seperti Envoy yang disuntikkan oleh Istio ke dalam pod untuk mengelola container utama di pod tersebut.
Kenapa saya memakai NGINX dan bukan Envoy? Hal ini karena Envoy tidak mendukung fitur web server seperti melayani file statis, FastCGI, protokol uwsgi dan sebagainya. Envoy memang dibuat murni untuk keperluan service mesh seperti routing, rate limiting, authentication, dan sejenisnya. Hal tersebut justru kadang malah susah didapatkan di NGINX. Sebagai contoh, untuk mendapatkan fasilitas validasi JWT di NGINX, saya harus memakai NGINX Plus sementara fitur ini sudah ada di Envoy tanpa biaya tambahan.
Saya kemudian membuat sebuah manifest Kubernetes dengan nama file-upload-service.yaml dengan isi seperti berikut ini:
Pada konfigurasi di atas, tidak seperti biasanya yang menggunakan Deployment
, kali ini saya menggunakan StatefulSet
. Salah satu perbedaan utama antara StatefulSet
dan Deployment
adalah bila pod mengalami masalah dan harus dibuat ulang, pod di StatefulSet
tetap akan memiliki asosiasi terhadap persistent volume yang sama. Selain itu, di StatefulSet
, saya juga perlu menambahkan serviceName
yang akan membuat sebuah headless service. Kubernetes akan menambahkan nama seperti <nama_pod>.<nama servicename>
di DNS untuk mengakses pod secara langsung dari luar. Nama pod selalu diakhiri angka berurut seperti file-upload-service-0
, file-upload-service-1
, dan seterusnya. Dengan demikian, pod dapat diakses dengan nama seperti file-upload-service-0.file-upload-service-headless
, file-upload-service-1.file-upload-service-headless
, dan seterusnya.
Walaupun kedua container ini berada dalam pod yang sama, mereka memiliki isi “harddisk” yang berbeda, tergantung dari image yang dipakai. Agar bisa saling berkomunikasi, salah satu pola yang umum dipakai adalah dengan memakai volumeMounts
yang sama. Pada konfigurasi saya, folder /uploads
akan selalu memiliki isi yang sama baik di container NGINX maupun di container Flask. Dengan demikian, apa yang ditulis oleh container Flask dapat dilihat juga oleh container NGINX.
Untuk mempermudah pengaturan, saya meletakkan konfigurasi NGINX di dalam sebuah ConfigMap yang kemudian dirujuk melalui configMap
di volumes
. Untuk mengisi nilai konfigurasi NGINX tersebut, saya menambahkan deklarasi berikut ini ke file manifest Kubernetes:
Pada konfigurasi NGINX di atas, mendeklarasikan dua rute berbeda:
- Rute
/upload
akan diteruskan ke aplikasi Flask melaluiuwsgi_pass
. Karena berada dalam pod yang sama, saya dapat menggunakan127.0.0.1
untuk mengatasi aplikasi Flask. Port 7070 yang dipakai oleh container tersebut tidak perlu di-ekspos diluar pod karena tidak akan diakses secara langsung. - Rute
/
untuk membaca file yang ada di folder/uploads
. Saya menggunakansendfile
,tcp_nopush
, danaio
untuk mengoptimalkan proses download file statis tersebut. Selain itu, saya juga menggunakanlimit_rate
supaya sebuah koneksi dari pengguna maksimal hanya 1 MB/s. Batasan ini tidak berlaku bila pengguna membuka koneksi lain misalnya dengan membuka tab baru di browser. Untuk membatasi jumlah koneksi berdasarkan alamat IP, saya bisa menggunakanlimit_conn addr
. Saya juga dapat menggunakanlimit_rate_after
untuk membatasi kecepatan hanya bila pengguna sudah men-download lebih dari batas yang saya tentukan.
Dan sebagai langkah terakhir, saya akan mempublikasikan service dengan menambahkan deklarasi berikut ini ke file manifest Kubernetes:
Bila memakai ingress controller, saya perlu menambahkan rule baru di ingress-api.yaml seperti yang terlihat pada cuplikan berikut ini:
Saya bisa menerapkan perubahan dengan memberikan perintah berikut ini:
Sekarang, saya bisa menyimpan file baru ke backend, misalnya dengan perintah berikut ini:
Terlihat bahwa file berhasil ditulis di folder uploads
. Bila saya membuka URL https://api.latihan.jocki.me/files/c44eb6e0-2c3e-4e22-b379-a3565216fc3b-gambar.png di browser, saya akan menemukan gambar yang barusan saya upload tersebut. Dengan demikian fitur upload dan download sudah bekerja dengan baik. Namun masih ada satu masalah: penyimpanannya tidak permanen! Untuk membuktikannya, saya akan menghapus pod dengan memberikan perintah berikut ini:
Karena pod dikelola oleh StatefulSet, tidak lama kemudian pod baru dengan nama yang sama akan dibuat ulang. Bila saya mencoba membuka https://api.latihan.jocki.me/files/c44eb6e0-2c3e-4e22-b379-a3565216fc3b-gambar.png di browser, kali ini saya akan menemukan pesan kesalahan 404 Not Found
. Ini menunjukkan bahwa penyimpanan di pod bersifat sementara dan file tersebut kini sudah hilang.
Untuk memakai penyimpanan permanen, langkah pertama yang harus saya lakukan adalah mendeklarasikan sebuah Persistent Volume. Kubernetes mendukung cukup banyak jenis Persistent Volume seperti AWS Elastic Block Store, Azure Disk, GCE Persistent Disk, Network File Storage (NFS), dan sebagainya. Pada latihan kali ini, saya akan memakai yang paling sederhana, yaitu local
yang akan menggunakan storage yang tersedia di node Kubernetes secara langsung. Untuk hasil yang handal dengan kinerja penyimpanan yang lebih baik, administrator dapat menggabungkan beberapa komputer yang memiliki penyimpanan optimal (misalnya PC dengan SSD yang terpasang di slot M.2 dengan bus PCIe 4.0) menjadi sebuah storage server yang berdiri sendiri (bukan bagian cluster Kubernetes). Storage server ini nantinya dapat diakses melalui NFS di Kubernetes.
Namun karena menggunakan local
, saya hanya perlu membuat sebuah folder baru di salah satu node yang merupakan bagian dari cluster Kubernetes. minikube sendiri sudah menyediakan sebuah folder di /data
yang isi-nya tidak akan hilang setelah komputer di-restart. Ini adalah lokasi yang tepat untuk dipakai sebagai Persistent Volume local
.
Karena folder /data
sudah ada di node yang dibuat oleh minikube, saya hanya perlu menambahkan konfigurasi baru seperti berikut ini:
Konfigurasi di atas akan membuat sebuah PersistentVolume dengan ukuran 100MB (berdasarkan isi nilai capacity
) yang disimpan di node minikube
(berdasarkan isi nilai nodeAffinity
) di folder /data
(berdasarkan isi nilai path
). Saya kemudian menerapkan perubahan baru tersebut dengan memberikan perintah:
Status Persistent Volume saat ini masih Available
karena belum ada yang memakainya. Untuk memakai Persistent Volume, saya perlu membuat sebuah Persistent Volume Claim. Sebagai contoh, saya dapat mendefinisikannya dengan konfigurasi seperti berikut ini:
Saya kemudian mengaplikasikannya dengan memberikan perintah:
Karena satu-satunya Persistent Volume yang tersedia saat ini hanya file-upload-storage
, Persistent Volume Claim yang barusan saya buat akan mendapatkan Persistent Volume tersebut. Bila ada beberapa Persistent Volume lain yang memenuhi kriteria penyimpanan Persistent Volume Claim tersebut, Kubernetes akan memilih salah satu yang terbaik. Selain itu, untuk jenis Persistent Volume tertentu, terdapat fitur dynamic provisioning yang dapat secara otomatis membuat Persistent Volume baru sesuai dengan yang ukuran yang diminta oleh Persistent Volume Claim.
Untuk memakai PersistentVolumeClaim tersebut, saya akan mengubah definisi volumes
menjadi seperti:
Saya perlu menggunakan securityContext
agar Persistent Volume memiliki hak akses sesuai dengan user id yang menjalankan aplikasi. Bila tidak, Persistent Volume akan di-mount sebagai milik root
sehingga aplikasi yang tidak dijalankan oleh user root
tidak akan bisa membaca dan menulis ke folder tersebut.
Untuk mengaplikasikan perubahan, saya memberikan perintah berikut ini
Sekarang, bila saya menghapus pod, file yang telah di-upload tetap akan ada karena mereka kini berada di sebuah Persistent Volume. Bahkan, karena nilai persistentVolumeReclaimPolicy
secara default adalah Retain
, setelah saat Persistent Volume Claim dan StatefulSet dihapus, Persistent Volume tidak akan ikut dihapus. Sebagai gantinya, status dari Persistent Volume tersebut berubah menjadi Retain
. Saat berada di status ini, ia tidak akan bisa dipakai oleh Persistent Volume Claim manapun. Dengan demikian, bila menemukan Persistent Volume dengan status Retain
, saya perlu meninjau ulang isi file, melakukan backup, atau operasi lainnya (misalnya dengan kubectl cp
). Setelah selesai, saya dapat menghapusnya secara manual dengan memberikan perintah seperti kubectl delete pv file-upload-storage
.