Menerapkan GitOps Di Kubernetes Dengan kpt
GitOps adalah sebuah konsep dimana aplikasi yang di-deploy diwakili oleh sebuah repository Git. Perubahan pada infrastruktur harus dilakukan dengan menambahkan commit baru ke repository Git tersebut yang nantinya akan diaplikasikan ke server (infrastructure as code). Dengan demikian, seluruh riwayat perubahan pada infrastruktur dapat dilihat dari riwayat commit di repository. Salah satu hal penting di GitOps adalah tidak melakukan perubahan secara langsung ke server atau perubahan manual lainnya yang tidak terdokumentasikan di repository. Pada tulisan ini, saya akan menggunakan kpt untuk mempermudah menerapkan GitOps.
Saya sudah memiliki folder kubernetes yang mewakili infrastruktur Kubernetes. Saat ini saya mengelolanya secara manual dengan kubectl apply
dan hanya ada sebuah script init.sh untuk membantu instalasi chart Helm. Pada tulisan ini, saya akan menggunakan kpt untuk memudahkan saya menerapkan GitOps. Langkah pertama yang perlu saya lakukan adalah mengubah folder ini menjadi sebuah package kpt dengan memberikan perintah seperti berikut ini (saat berada di folder tersebut):
Perintah ini akan membuat sebuah file baru dengan nama Kptfile
di folder yang sama. Ini adalah file konfigurasi kpt dimana saya dapat memberikan informasi seperti nama dan informasi package. Saya juga bisa mendeklarasikan pemanggilan functions bawaan kpt yang akan dikerjakan secara deklaratif di file ini.
Helm & Kpt
Salah satu cara paling sederhana untuk memakai Helm bersamaan dengan kpt adalah dengan memberikan perintah helm template
. Perintah ini akan menghasilkan file manifest dari chart yang dibutuhkan sebagai bagian dari proyek saya. Namun ini juga berarti kini file tersebut, sama seperti file manifest lainnya, akan dikelola oleh kpt. File manifest yang dihasilkan perintah helm template
tidak akan dikenali oleh helm
lagi (misalnya saya tidak bisa memperintah perintah seperti helm upgrade
dan sebagainya). Dengan demikian, bila ingin memperbaharui dependency, saya harus melakukan perubahan secara manual di file manifest yang ada. Tapi secara tidak langsung, ini juga memberikan lebih banyak fleksibilitas (misalnya saya dapat melakukan enkripsi pada file manifest Secret yang dipakai oleh dependency tersebut).
Berikut ini adalah contoh perintah helm template
yang saya berikan:
Setelah perintah di atas selesai diberikan, saya akan menemukan folder elasticsearch
, mongodb
, keycloak
, dan rabbitmq
yang berisi file manifest Kubernetes berdasarkan definisi chart mereka. Bila saya memberikan perintah kpt pkg tree
, saya dapat melihat bahwa setiap folder yang ada merupakan bagian dari package utama saya seperti pada:
File baru yang dihasilkan perintah helm template
di atas adalah file manifest biasa sehingga mereka dapat di-deploy dengan menggunakan perintah seperti kubectl apply -f --rescursive
atau kpt live apply
sama seperti file manifest pada umumnya.
Pada kpt, functions adalah kumpulan perintah yang dapat dipakai untuk melakukan perubahan pada file konfigurasi Kubernetes. Implementasi functions berada dalam bentuk image Docker yang dapat dipanggil secara deklaratif atau imperatif. Pemanggilan secara imperatif lebih tepat dipakai untuk operasi yang hanya dibutuhkan sekali-kali, sementara pemanggilan secara deklaratif lebih tepat dipakai untuk perubahan yang perlu dilakukan berulang kali (setiap kali melalukan deployment).
Konfigurasi Deklaratif
Sebagai contoh, saya ingin menambahkan label tertentu ke seluruh resources yang ada. Untuk itu, saya bisa memanggil functions set-labels
secara deklaratif dengan menambahkan baris berikut ini pada file Kptfile
:
Pada deklarasi di atas, saat package ini di-render, kpt akan menjalankan function set-labels
yang menambahkan label app.kubernetes.io/version
dan env
ke seluruh file manifest yang ada di folder ini (termasuk sub folder-nya). Proses pengerjaan functions deklaratif di kpt disebut sebagai operasi render, yang dapat dilakukan dengan memberikan perintah berikut ini:
Saat men-deploy package ini di server lain, bila saya ingin memakai nilai env
berbeda, saya cukup mengubah nilai label yang ada di file Kptfile
dan me-render ulang package. Ini lebih praktis daripada harus mengubah file satu per satu. Walaupun demikian, kpt tidak mewajibkan harus melalui proses render. Bila saya ingin melakukan perubahan secara manual dengan mengubah file manifest, hal tersebut tetap diperbolehkan.
Konfigurasi Imperatif
Selain label, saya juga dapat mengubah versi (tag) image Docker yang dipakai di package. kpt
memiliki functions universal untuk substitusi nilai: create-setters
dan apply-setters
. Function create-setters
dipakai untuk membuat placeholder nilai yang hendak di-substitusi yang disebut sebagai setters. Karena ia hanya dipanggil sekali saja, saya dapat mengeksekusinya secara imperatif dengan menggunakan perintah seperti berikut ini:
Untuk memastikan bahwa setters sudah dibuat, saya dapat menjalan functions list-setters
seperti yang terlihat pada perintah berikut ini:
Disini terlihat bahwa ada 7 setters yang saya ubah nilainya secara cepat melalui functions apply-setters
. Sebagai latihan, saya akan menambahkan pemanggilan apply-setters
secara deklaratif di Kptfile
sehingga isinya terlihat seperti berikut ini:
Sekarang, setiap kali ingin melakukan perubahan versi tag, saya hanya perlu mengubah nilai configMap
untuk functions apply-setters
di file Kptfile
di atas. Begitu saya me-render package, setters di file manifest yang bersangkutan akan berisi nilai yang saya tentukan.
Menyimpan File Sensitif Di Repository
Kubernetes memiliki object yang disebut Secret yang dapat dipakai untuk menampung data sensitif seperti password dan sertifikat. Walaupun demikian, bagi Kubernetes, Secret pada dasarnya hanya sebuah ConfigMap yang spesial yang isi-nya tidak mudah dilihat. Untuk mendeklarasikan Secret, saya tetap perlu membuat file manifest yang isinya tidak dilindungi. Sementara itu, bila mengikuti filosofi GitOps, semua yang dibutuhkan untuk membangun infrastruktur harus diletakkan ke dalam repository Git, termasuk file manifest Secret. Kalau begitu, bukankah siapa saja bisa melihat isi password atau sertifikat dengan men-download file manifest Secret dari repository Git? Ini tentu saja akan menimbulkan masalah keamanan.
Salah satu solusinya adalah dengan menggunakan tool seperti Bitnami Sealed Secrets dan Mozilla SOPS: Secrets Operations. Dengan tool tersebut, saya dapat meng-enkripsi Secret dan men-commit file manifest yang sudah di-enkripsi ke repository Git sehingga setiap perubahannya tetap tercatat oleh riwayat Git. Bitnami Sealed Secrets dirancang khusus untuk dipakai di Kubernetes, sehingga lebih mudah dipakai dibandingkan dengan Mozilla SOPS. Walaupun demikian, karena kpt memiliki functions sops yang akan memanggil Mozilla SOPS (tanpa perlu di-install di komputer lokal), saya akan menggunakannya.
Mozilla SOPS mendukung tool Pretty Good Privacy (PGP) dan age. Karena sistem operasi saya sudah dilengkapi dengan GNU Privacy Guard (GPG) yang merupakan implementasi OpenPGP, saya akan menggunakan pendekatan PGP. Untuk itu, saya akan mulai dengan membuat sebuah key baru dengan memberikan perintah berikut ini:
Saya perlu menjawab beberapa pertanyaan mengenai identitas dan masa berlaku key. Pada pertanyaan terakhir, saya perlu memastikan bahwa key tersebut tidak dilindungi oleh password dengan cara mengosongkan nilai password saat diminta. Hal ini perlu dilakukan karena key tersebut akan dipakai secara programatis sehingga tidak akan ada kesempatan untuk memasukkan password lagi. Ini juga berarti siapa saja yang mendapatkan key tersebut dapat langsung memakainya.
Untuk memastikan key berhasil dibuat, saya dapat memberikan perintah berikut ini:
Saya perlu meyalin nilai long key (16 karakter) dari hasil perintah di atas. Selain itu, saya juga perlu mendapatkan public key yang dihasilkan dengan memberikan perintah berikut ini:
Saya kemudian membuat file encrypt.yaml
dengan isi seperti berikut ini:
File konfigurasi di atas akan dipakai sebagai parameter saat memanggil functions sops
. Daftar parameter selengkap dapat dilihat di https://catalog.kpt.dev/contrib/sops/v0.3/. Sebagai contoh, saya mengisi nilai cmd-json-path-filter
dengan ekspresi yang hanya menyertakan file manifest Secret sehingga hanya file Secret saja yang akan di-enkripsi. Selain parameter untuk functions sops
, saya juga dapat memasukkan parameter untuk Mozilla SOPS seperti unencrypted-suffix
, encrypted-suffix
, encrypted-regex
, unencrypted-regex
, dan sebagainya.
Proses enkripsi dapat dilakukan cukup melalui public key tanpa membutuhkan informasi private key. Saya sudah menyertakan public key saya di cmd-import-gpg
pada file encrypt.yaml
di atas. Sekarang, saatnya untuk melakukan proses enkripsi:
Setelah perintah selesai dikerjakan, bila saya membuka salah satu file manifest Secret, saya akan menemukan isi seperti:
Mozilla SOPS akan mengubah nilai password ke dalam bentuk nilai seperti ENC[AES256_GCM,data:...,type:str]
. Selain itu, ia juga menambahkan key baru dengan nama sops
yang berisi informasi enkripsi termasuk message authentication code (MAC). Bila ada perubahan di data yang ter-enkripsi, termasuk perubahan urutan key, proses dekripsi akan gagal dengan pesan kesalahan seperti “MAC mismatch”. Ini menunjukkan kemungkinan file tersebut tidak aman lagi.
Lalu, bagaimana dengan proses dekripsi? Saya tetap perlu memanggil functions sops
, hanya saja kali ini menggunakan nilai decrypt
untuk cmd
. Selain itu, saya perlu menyertakan private key karena ia dibutuhkan untuk proses dekripsi. Sebagai contoh, saya akan membuat file baru dengan nama decrypt.yaml
yang isinya seperti berikut ini:
Setelah ini, saya bisa memanggil functions sops
seperti berikut ini:
Pada perintah di atas, saya mengirim private key hasil perintah gpg --export-secret-keys
langsung ke variabel SOPS_IMPORT_PGP
. Functions sops
akan meng-import private key tersebut secara otomatis saat ia bekerja di dalam Docker. Berbeda dengan public key, private key bersifat rahasia dan tidak boleh di-commit ke repository Git. Bila mengerjakan perintah ini dari platform CI/CD, saya bisa meletakkannya ke fitur penyimpanan rahasia yang disediakan oleh platform tersebut. Sebagai contoh, di GitHub Actions, saya dapat menambahkan secret dengan memilih halaman repository, Settings, Secrets, dan men-klik tombol New repository secret.
Salah satu efek samping dari penggunakan enkripsi adalah saya harus selalu melakukan dekripsi sebelum me-render package. Saat ini, saya tidak menemukan cara untuk melewatkan environment variable yang berisi private key ke functions sops
untuk dipanggil secara deklaratif (dengan didefinisikan di Kptfile
). Oleh sebab itu, saya tetap perlu mengerjakan functions sops
secara imperatif saat melakukan deployment. Efek samping lainnya adalah beberapa tool yang melakukan merging melalui Git akan bingung karena harus membandingkan upstream yang ter-enkripsi dengan repository lokal yang sudah di-dekripsi.
Deployment Di Server Berbeda
Anggap saja saya berada di server dengan akses ke cluster Kubernetes. Karena latihan-k8s sekarang sudah menerapkan GitOps, saya bisa menciptakan infrastruktur aplikasi tersebut berdasarkan informasi yang ada di repository GitHub tersebut. Saya akan menyebut repository yang ada di GitHub ini sebagai upstream, sementara itu, saya juga akan membuat sebuah repository di server yang akan saya sebut sebagai lokal. Untuk membuat package lokal, saya memberikan perintah berikut ini:
Perintah kpt live init
akan menambahkan key inventory
di Kptfile
. Informasi ini tidak boleh dihapus dari file Kptfile
di lokal karena nantinya akan dipakai untuk menghubungkan repository lokal ke cluster Kubernetes tujuan. Saya juga bisa menyimpan dan men-push repository ini sebagai repository baru yang berbeda di server GitHub (sebagai backup dan juga dokumentasi perubahan).
Sekarang, saatnya untuk memakai upstream dengan memberikan perintah berikut ini:
Perintah di atas akan mengambil file manifest dari folder kubernetes
di upstream untuk commit dengan tag kubernetes/v0.0.1
. Selain menggunakan tag, saya juga bisa menggunakan nama branch seperti pada contoh berikut ini:
Perintah di atas akan membuat sebuah folder kubernetes
dengan isi sesuai dengan yang ada di repository GitHub. Folder ini akan dianggap sebagai subpackage seperti yang diperlihatkan oleh hasil perintah berikut ini:
Karena saya memiliki file yang ter-enkripsi, saya perlu melakukan dekripsi terlebih dahulu dengan menggunakan perintah seperti berikut ini:
Pada perintah di atas, saya mengasumsikan bahwa isi private key sudah disimpan ke sebuah environment variable dengan nama PRIVATE_KEY
. Setelah itu, saya men-render file manifest dengan memberikan perintah:
Dan sebagai langkah terakhir yang paling penting, saya akan menerapkan perubahan ke cluster Kubernetes dengan memberikan perintah berikut ini:
Saya dapat menggunakan kpt live status
untuk melihat status package. Dan seperti biasanya, saya juga dapat menggunakan perintah kubectl
untuk berinteraksi dengan resources Kubernetes (seperti pod, services, ingress dan sebagainya) yang dibuat. Proyek ini belum sepenuhnya mengikuti GitOps karena ada beberapa hal yang masih harus dilakukan secara manual di Kubernetes. Sebagai contoh, saya harus menginstall ingress controller karena sebelumnya saya menggunakan minikube addons enable ingress
di minikube. Sebagai alternatif yang lebih baik, saya sebaiknya men-install ingress controller melalui Helm. Selain itu, saya juga perlu mendaftarkan realm baru di Keycloak dengan nama Latihan
, membuat client dengan nama latihan-k8s
, dan membuat user baru sehingga pengguna nantinya bisa login di aplikasi web. Namun, ini sepertinya lebih ke arah aplikasi dan bukan lagi infrastruktur.
Bagaimana bila ada perubahan di repository Git? Sebagai contoh, anggap saja saya memutuskan untuk menghilangkan dependency ke Keycloak dengan menghapus folder keycloak
, men-push perubahannya di upstream dan memberikan tag kubernetes/v0.0.2
ke commit tersebut. Untuk mengaplikasikan perubahan tersebut di lokal, saya akan memberikan perintah seperti berikut ini:
Saya menggunakan nilai force-delete-replace
karena saya tidak akan pernah melakukan perubahan di lokal. Semua perubahan harus datang dari upstream (yang sudah disetujui terlebih dahulu sebelum di-merge ke repository).
Saya kemudian melakukan proses dekripsi file Secret, men-render file manifest dan akhirnya melakukan live apply
untuk mengaplikasikan perubahan ke cluster Kubernetes, seperti yang ditunjukkan pada perintah berikut ini:
Pada saat kpt live apply
dikerjakan, terlihat bahwa resource yang sudah dihapus dari upstream juga secara otomatis akan di-hapus di cluster Kubernetes.
Untuk menghapus seluruh resource yang ada, saya dapat memberikan perintah seperti:
Sampai disini, bila saya mengulangi langkah-langkah di atas pada server lain (misalnya server untuk testing dan server untuk production), saya tetap akan memperoleh hasil yang konsisten asalkan mereka dijalankan berdasarkan repository Git yang sama.