Pengembangan operator Kubernetes dengan Kerangka Operator

Gambar


Seperti yang disebutkan dalam artikel Teknologi Radar , Lamoda aktif bergerak menuju arsitektur layanan mikro. Sebagian besar layanan kami dikemas menggunakan Helm dan digunakan untuk Kubernetes. Pendekatan ini sepenuhnya memenuhi kebutuhan kita dalam 99% kasus. 1% tetap ketika fungsionalitas Kubernetes standar tidak cukup, misalnya, ketika Anda perlu mengkonfigurasi cadangan atau pembaruan layanan untuk acara tertentu. Untuk mengatasi masalah ini, kami menggunakan pola operator. Dalam seri artikel ini, I - Grigory Mikhalkin, pengembang tim R&D di Lamoda - akan berbicara tentang pelajaran yang saya pelajari dari pengalaman saya dalam mengembangkan operator K8 menggunakan Kerangka Operator .


Apa itu operator?


Salah satu cara untuk memperluas fungsionalitas Kubernetes adalah dengan membuat pengontrol Anda sendiri. Abstraksi utama dalam Kubernet adalah objek dan pengontrol. Objek menggambarkan keadaan cluster yang diinginkan. Misalnya, Pod menjelaskan wadah mana yang harus dimulai dan parameter startup, dan objek ReplicaSet memberitahu berapa banyak replika dari Pod yang diberikan perlu diluncurkan. Kontroler mengontrol keadaan gugus berdasarkan pada deskripsi objek, dalam kasus yang dijelaskan di atas, ReplicationController akan mendukung jumlah replika Pod yang ditentukan dalam ReplicaSet. Dengan bantuan pengontrol baru, Anda dapat menerapkan logika tambahan, seperti mengirim pemberitahuan acara, pulih dari kegagalan, atau mengelola sumber daya pihak ketiga .


Operator adalah aplikasi kubernet yang mencakup satu atau lebih pengontrol yang melayani sumber daya pihak ketiga. Konsep ini diciptakan oleh tim CoreOS pada tahun 2016, dan baru-baru ini, popularitas operator telah berkembang pesat. Anda dapat mencoba menemukan operator yang diinginkan dalam daftar di kubedex , (lebih dari 100 operator yang tersedia untuk publik terdaftar di sini), serta di OperatorHub . Ada 3 alat populer untuk pengembangan operator: Kubebuilder , Operator SDK dan Metacontroller . Di Lamoda kami menggunakan Operator SDK, jadi kami akan membicarakannya nanti.


Operator SDK


Gambar


Operator SDK adalah bagian dari Kerangka Operator, yang mencakup dua bagian penting: Manajer Siklus Hidup Operator dan Pengukuran Operator.


  • Operator SDK adalah pembungkus untuk runtime pengontrol , perpustakaan populer untuk mengembangkan pengontrol (yang, pada gilirannya, adalah pembungkus untuk client-go ), kerangka pembuat kode + untuk menulis tes E2E.
  • Operator Siklus Hidup Operator - kerangka kerja untuk mengelola operator yang ada; menyelesaikan situasi ketika operator memasuki mode zombie atau versi baru diluncurkan.
  • Pengukuran Operator - sesuai dengan namanya, ia mengumpulkan data pada pekerjaan operator, dan juga dapat menghasilkan laporan berdasarkan pada itu.

Buat proyek baru


Contohnya adalah operator yang memantau file dengan konfigurasi di repositori dan, ketika diperbarui, memulai kembali penyebaran layanan dengan konfigurasi baru. Kode sampel lengkap tersedia di sini .


Gambar


Buat proyek dengan operator baru:


operator-sdk new config-monitor 

Pembuat kode akan membuat kode untuk operator yang bekerja di namespace yang dialokasikan. Pendekatan ini lebih disukai untuk memberikan akses ke seluruh cluster, karena jika terjadi kesalahan, masalah akan diisolasi dalam namespace yang sama. Operator cluster-wide dapat dihasilkan dengan menambahkan --cluster-scoped . Direktori berikut akan berlokasi di dalam proyek yang dibuat:


  • cmd - berisi main package , di mana Manager diinisialisasi dan diluncurkan;
  • deploy - berisi pernyataan operator, CRD dan objek yang diperlukan untuk mengatur operator RBAC
  • pkg - ini akan menjadi kode utama kita untuk objek dan pengendali baru.

Hanya ada satu cmd/manager/main.go file dalam cmd/manager/main.go .


Cuplikan kode
 // Become the leader before proceeding err = leader.Become(ctx, "config-monitor-lock") if err != nil { log.Error(err, "") os.Exit(1) } // Create a new Cmd to provide shared dependencies and start components mgr, err := manager.New(cfg, manager.Options{ Namespace: namespace, MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort), }) ... // Setup Scheme for all resources if err := apis.AddToScheme(mgr.GetScheme()); err != nil { log.Error(err, "") os.Exit(1) } // Setup all Controllers if err := controller.AddToManager(mgr); err != nil { log.Error(err, "") os.Exit(1) } ... // Start the Cmd if err := mgr.Start(signals.SetupSignalHandler()); err != nil { log.Error(err, "Manager exited non-zero") os.Exit(1) } 

Di baris pertama: err = leader.Become(ctx, "config-monitor-lock") - seorang pemimpin dipilih. Dalam sebagian besar skenario, hanya satu instance aktif pernyataan di namespace / cluster yang diperlukan. Secara default, Operator SDK menggunakan strategi Leader for life - instance yang diluncurkan pertama dari operator akan tetap menjadi pemimpin sampai dihapus dari cluster.


Setelah instance operator ini ditunjuk sebagai pemimpin, Manager baru diinisialisasi - mgr, err := manager.New(...) . Tanggung jawabnya meliputi:


  • err := apis.AddToScheme(mgr.GetScheme()) - pendaftaran skema sumber daya baru;
  • err := controller.AddToManager(mgr) - pendaftaran pengendali;
  • err := mgr.Start(signals.SetupSignalHandler()) - luncurkan dan kendalikan pengontrol.

Saat ini, kami tidak memiliki sumber daya baru, atau pengontrol untuk pendaftaran. Anda dapat menambahkan sumber daya baru menggunakan perintah:


 operator-sdk add api --api-version=services.example.com/v1alpha1 --kind=MonitoredService 

Perintah ini akan menambahkan definisi skema sumber daya MonitoredService ke direktori pkg/apis , serta yaml dengan definisi CRD di deploy/crds . Dari semua file yang dihasilkan, Anda hanya perlu mengubah definisi skema secara manual di monitoredservice_types.go . Tipe MonitoredServiceSpec mendefinisikan status sumber daya yang diinginkan: apa yang ditentukan pengguna dalam yaml dengan definisi sumber daya. Dalam konteks operator kami, bidang Size menentukan jumlah replika yang diinginkan, ConfigRepo menunjukkan dari mana konfigurasi saat ini dapat ditarik. MonitoredServiceStatus menentukan keadaan sumber daya yang diamati, misalnya, ia menyimpan nama-nama Pods yang dimiliki sumber daya ini dan Pods spec saat ini.


Setelah mengedit skema, Anda perlu menjalankan perintah:


 operator-sdk generate k8s 

Ini akan memperbarui definisi CRD di deploy/crds .


Sekarang mari kita buat bagian utama dari operator kami, controller:


 operator-sdk add controller --api-version=services.example.com/v1alpha1 --kind=Monitor 

File monitor_controller.go akan muncul di monitor_controller.go pkg/controller , di mana kita menambahkan logika yang kita butuhkan.


Pengembangan Kontroler


Pengontrol adalah unit kerja utama operator. Dalam kasus kami, ada dua pengontrol:


  • Kontroler monitor memantau perubahan konfigurasi layanan;
  • Pengontrol pemutakhiran memperbarui layanan dan mempertahankannya dalam kondisi yang diinginkan.

Pada intinya, controller adalah loop kontrol, memonitor antrian dengan peristiwa berlangganannya dan memprosesnya:


Gambar


Kontroler baru dibuat dan didaftarkan oleh manajer dalam metode add :


 c, err := controller.New("monitor-controller", mgr, controller.Options{Reconciler: r}) 

Menggunakan metode Watch , kami berlangganan ke acara terkait penciptaan sumber daya baru atau pembaruan Spec dari sumber daya MonitoredService ada:


 err = c.Watch(&source.Kind{Type: &servicesv1alpha1.MonitoredService{}}, &handler.EnqueueRequestForObject{}, common.CreateOrUpdateSpecPredicate) 

Jenis acara dapat dikonfigurasi menggunakan parameter src dan predicates . src menerima objek bertipe Source .


  • Informer - secara berkala polling apiserver untuk peristiwa yang cocok dengan filter, jika ada peristiwa seperti itu, masukkan ke dalam antrian pengontrol. Di controller-runtime ini adalah pembungkus atas SharedIndexInformer dari client-go .
  • Kind juga merupakan pembungkus lebih dari SharedIndexInformer , tetapi, tidak seperti Informer , itu secara independen membuat contoh informan berdasarkan parameter yang dikirimkan (skema sumber daya yang dipantau).
  • Channel - menerima acara chan event.GenericEvent sebagai parameter, peristiwa yang datang melaluinya ditempatkan dalam antrian pengontrol.

redicates mengharapkan objek yang memenuhi antarmuka Predicate . Bahkan, ini adalah filter tambahan untuk acara, misalnya, saat memfilter UpdateEvent Anda bisa melihat perubahan apa yang dibuat dalam spec sumber daya.


Ketika suatu peristiwa tiba, seorang EventHandler menerimanya - argumen kedua dari metode Watch - yang membungkus peristiwa tersebut dalam format permintaan yang diharapkan oleh Reconciler :


  • EnqueueRequestForObject - membuat permintaan dengan nama dan namespace objek yang menyebabkan peristiwa;
  • EnqueueRequestForOwner - membuat permintaan dengan data induk objek. Ini diperlukan, misalnya, jika Pod dikontrol sumber daya telah dihapus, dan Anda perlu memulai penggantinya;
  • EnqueueRequestsFromMapFunc - sebagai parameter fungsi map yang menerima acara (dibungkus dengan MapObject ) dan mengembalikan daftar permintaan. Contoh ketika pawang ini diperlukan - ada timer, untuk setiap centang yang Anda butuhkan untuk mengeluarkan konfigurasi baru untuk semua layanan yang tersedia.

Permintaan ditempatkan di antrian pengontrol, dan salah satu pekerja (secara default pengontrol memiliki satu) menarik peristiwa keluar dari antrian dan meneruskannya ke Reconciler .


Reconciler mengimplementasikan hanya satu metode - Reconcile , yang berisi logika dasar pemrosesan peristiwa:


metode rekonsiliasi
 func (r *ReconcileMonitor) Reconcile(request reconcile.Request) (reconcile.Result, error) { reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) reqLogger.Info("Checking updates in repo for MonitoredService") // fetch the Monitor instance instance := &servicesv1alpha1.MonitoredService{} err := r.client.Get(context.Background(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue return reconcile.Result{}, nil } // Error reading the object - requeue the request. return reconcile.Result{}, err } // check if service's config was updated // if it was, send event to upgrade controller if podSpec, ok := r.isServiceConfigUpdated(instance); ok { // Update instance Spec instance.Status.PodSpec = *podSpec instance.Status.ConfigChanged = true err = r.client.Status().Update(context.Background(), instance) if err != nil { reqLogger.Error(err, "Failed to update service status", "Service.Namespace", instance.Namespace, "Service.Name", instance.Name) return reconcile.Result{}, err } r.eventsChan <- event.GenericEvent{Meta: &servicesv1alpha1.MonitoredService{}, Object: instance} } return reconcile.Result{}, nil } 

Metode menerima objek Request dengan bidang NamespacedName , di mana sumber daya dapat ditarik dari cache: r.client.Get(context.TODO(), request.NamespacedName, instance) . Dalam contoh ini, permintaan dibuat ke file dengan konfigurasi layanan yang dirujuk oleh bidang ConfigRepo dalam spec sumber daya. Jika konfigurasi diperbarui, acara baru dari tipe GenericEvent dan dikirim ke saluran yang didengarkan pengontrol pemutakhiran.


Setelah memproses permintaan, Reconcile mengembalikan objek bertipe Result dan error . Jika bidang Result adalah Requeue: true atau error != nil , controller akan mengembalikan permintaan kembali ke antrian menggunakan metode queue.AddRateLimited . Permintaan akan dikembalikan ke antrian dengan penundaan, yang ditentukan oleh RateLimiter . Secara default, ItemExponentialFailureRateLimiter digunakan, yang meningkatkan waktu tunda secara eksponensial dengan peningkatan jumlah permintaan "pengembalian". Jika bidang Requeue tidak disetel, dan tidak ada kesalahan terjadi selama pemrosesan permintaan, controller akan memanggil metode Queue.Forget , yang akan menghapus permintaan dari cache RateLimiter (dengan demikian mengatur ulang jumlah pengembalian). Pada akhir pemrosesan permintaan, pengontrol menghapusnya dari antrian menggunakan metode Queue.Done .


Peluncuran operator


Komponen operator dijelaskan di atas, dan satu pertanyaan tetap: bagaimana memulainya. Pertama, Anda perlu memastikan bahwa semua sumber daya yang diperlukan sudah diinstal (untuk pengujian lokal, saya sarankan menyiapkan minikube ):


 # Setup Service Account kubectl create -f deploy/service_account.yaml # Setup RBAC kubectl create -f deploy/role.yaml kubectl create -f deploy/role_binding.yaml # Setup the CRD kubectl create -f deploy/crds/services_v1alpha1_monitoredservice_crd.yaml # Setup custom resource kubectl create -f deploy/crds/services_v1alpha1_monitoredservice_cr.yaml 

Setelah prasyarat dipenuhi, ada dua cara mudah untuk menjalankan pernyataan untuk pengujian. Cara termudah adalah memulainya di luar cluster menggunakan perintah:


 operator-sdk up local --namespace=default 

Cara kedua adalah menggunakan operator di cluster. Pertama, Anda perlu membuat gambar Docker dengan operator:


 operator-sdk build config-monitor-operator:latest 

Dalam file deploy/operator.yaml , ganti REPLACE_IMAGE dengan config-monitor-operator:latest :


 sed -i "" 's|REPLACE_IMAGE|config-monitor-operator:latest|g' deploy/operator.yaml 

Buat Penempatan dengan pernyataan:


 kubectl create -f deploy/operator.yaml 

Sekarang dalam daftar Pod pada cluster akan muncul Pod dengan layanan tes, dan dalam kasus kedua - yang lain dengan operator.


Alih-alih kesimpulan atau praktik terbaik


Masalah utama pengembangan operator saat ini adalah dokumentasi alat yang lemah dan kurangnya praktik terbaik yang ada. Ketika pengembang baru mulai mengembangkan operator, ia tidak punya tempat untuk melihat contoh penerapan persyaratan tertentu, sehingga kesalahan tidak dapat dihindari. Berikut adalah beberapa pelajaran yang kami pelajari dari kesalahan kami:


  • Jika ada dua aplikasi terkait, Anda harus menghindari keinginan untuk menggabungkannya dengan satu operator. Jika tidak, prinsip layanan kopling longgar dilanggar.
  • Anda harus ingat tentang pemisahan masalah: Anda tidak harus mencoba menerapkan semua logika dalam satu pengontrol. Sebagai contoh, ada baiknya menyebarkan fungsi pemantauan konfigurasi dan membuat / memperbarui sumber daya.
  • Memblokir panggilan harus dihindari dalam metode Reconcile . Misalnya, Anda dapat menarik konfigurasi dari sumber eksternal, tetapi jika operasinya lebih lama, buat goroutine untuk ini, dan kirim permintaan kembali ke antrian, yang menunjukkan dalam respons Requeue: true .

Dalam komentar, akan menarik untuk mendengar tentang pengalaman Anda dalam mengembangkan operator. Dan di bagian selanjutnya kita akan berbicara tentang pengujian operator.

Source: https://habr.com/ru/post/id446648/


All Articles