Extending Kubernetes - Writing Custom Resources/Controllers With Operator SDK

Emre Savcı
Trendyol Tech
Published in
9 min readApr 12, 2021
https://developers.redhat.com/blog/wp-content/uploads/2020/04/Operator-SDK-logo.jpg

Kubernetes sağladığı çeşitli extension pointler sayesinde farklı yönlerden extend edilebilir bir yapıya sahiptir. Örneğin network tarafında CNI aracı olarak isterseniz Calico, isterseniz Cilium kullanabilirsiniz. Veya storage için CSI implementasyonu olan herhangi bir ürünü kullanabilirsiniz.

Ya da ihtiyaçlarınız doğrultusunda custom schedular yazabilirsiniz. Aynı anda iki farklı schedular kullanabilir veya mevcut scheduları tamamen yenisiyle değiştirebilirsiniz.

Bunların dışında Kubernetes üzerinde kendi typelarınızı yani custom resourcelarınızı oluşturabilirsiniz. Oluşturduğunuz bu CR’lar ile çeşitli işlemler yaptırabilirsiniz.

Örneğin Istio, custom resourcelar aracılığı ile Envoy Proxy konfigürasyonlarını yapıyor.

Biz de Trendyol Platform Ekibi olarak CR’lar ile çalışan uygulamalar geliştiriyoruz. Bugün de sizlere OperatorSDK kullanarak custom resource ve controller geliştirmeyi anlatacağım.

Site Reliability Engineering

Operator demişken bu konunun çıkış noktasından bahsetmezsek olmaz.

Devops yaklaşımları hayatımıza girdiğinden beri, çok önemli bir kuralı sürekli kendimize hatırlatmalıyız: “Otomatize edilebilen her şey otomatize edilmeli”.

Ayrıca Matrix filminden tanıdığımız Ajan Smith’in de bu konu hakkında bir sözü bulunuyor :)

Günümüzde çeşitli SRE rolleri bulunabiliyor. Konunun çıkış noktası ise infrastructure problemlerine software development pratikleri ile çözüm getirmek.

Bu noktada SRE hakkında bazı alıntılar yapmak istiyorum:

A Site Reliability Engineer (SRE) is a person that operates an application by writing software. They are an engineer, a developer, who knows how to develop software specifically for a particular application domain. The resulting piece of software has an application’s operational domain knowledge programmed into it.

Burada SRE, bir uygulamanın operasyonel işlemlerini yazılım çözümleriyle sağlayan kişi olarak tanımlanıyor.

Site Reliability Engineering (SRE) is a set of patterns and principles for running large systems. Originating at Google, SRE has had a pronounced influence on industry practice. Practitioners must interpret and apply SRE philosophy to particular circumstances, but a key tenet is automating systems administration by writing software to run your software. Teams freed from rote maintenance work have more time to create new features, fix bugs, and generally improve their products.

Burada da temel ilkenin yazılımsal çözümlerle otomasyon olduğu, bakım ve operasyon yükünden kurtulan ekiplerin yeni özellikler ve iyileştirmeler üzerinde çalışabileceği vurgulanıyor.

SREs create software that runs other software, keeps it running, and manages it over time. SRE is a wider set of management and engineering techniques with automation as a central principle.

Son alıntıda da vardığımız ortak bir nokta var: “automation, automation, automation”…

Operators

Yukarıda anlatılanlardan sonra kafanızda Operator hakkında bazı şeyler canlanmıştır. Genel itibariyle Operator, bir yazılımın yaşam döngüsü boyunca belirli süreçleri işleten başka bir yazılımdır. Kubernetes üzerinde çalışan Operatorler bu süreci çoğunlukla Custom Resource tanımlamaları üzerinden işletir.

Operator, bir projenin setup, config, update, rollback, backup gibi süreçlerinde rol alabilir.

Kubernetes üzerinde kurmak istediğiniz birçok projenin artık kendi Operatorü bulunuyor. İlk akla gelen Prometheus Operator olabilir. Bir uygulamayı kurmadan önce <Uygulama İsmi> Operator şeklinde aratarak işe başlayabilirsiniz. Ayrıca birçok farklı Operatorün bulunduğu operatorhub.io adresine bakmakta da fayda var.

Operator geliştirmeye başlamadan önce yaptığım araştırmalar sırasında okuduğum bazı kitap ve yazılardan birkaç alıntı yapmak istiyorum:

An Operator is the application-specific combination of CRs and a custom controller that does know all the details about starting, scaling, recovering, and managing its application. The Operator’s operand is what we call the application, service, or whatever resources an Operator manages.

The actions an Operator performs can include almost anything: scaling a complex app, application version upgrades, or even managing kernel modules for nodes in a computational cluster with specialized hardware.
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — An Operator is like an automated Site Reliability Engineer for its application. It encodes in software the skills of an expert administrator. An Operator can manage a cluster of database servers, for example. It knows the details of configuring and managing its application, and it can install a database cluster of a declared software version and number of members. An Operator continues to monitor its application as it runs, and can back up data, recover from failures, and upgrade the application over time, automatically.
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — An Operator might rectify external resources its application requires. For example, consider manipulating an external load balancer’s routing rules as replicas die and new ones replace them.

Operator, belirli bir uygulamayı, servisi veya bir resource’u yöneten ve bunu kubernetes üzerinde çeşitli custom resource’lar aracılığı ile yapan bir uygulamadır. Operator ile uygulamanın yaşam döngüsü yönetimi, scaling, upgrading gibi ihtiyaçlarını karşılayabiliriz. Hatta harici bir kaynağı konfigüre etmek gibi işlemler de gerçekleştirebiliriz.

7 Keys Of Operators

Bir Operatorün karşılaması gereken bazı anahtar değerler vardır. Birlikte bunlara göz atalım.

1- An Operator should run as a single Kubernetes deployment.
2- Operators should define new custom resource types on the cluster.
3- Operators should use appropriate Kubernetes abstractions whenever possible.
4- Operator termination should not affect the operand.
5- An Operator should understand the resource types created by any previous versions.
6- An Operator should coordinate application upgrades.
7- Operators should be thoroughly tested, including chaos testing.

Yazdığımız operator kubernetes üzerinde tek bir deployment olarak çalışabilmeli, yeni custom resource tipleri tanımlamalı, son kullanıcıya bazı abstractionlar sunabilmeli, operatorün sonlandırılması oluşturulan/yönetilen uygulama(lar)ı etkilememeli, backward compatible olmalı ve son olarak iyi test edilmiş olmalı.

Operator Capability Levels

Operatorlerin kabiliyetlerine göre farklı seviyeleri bulunuyor. Aşağıdaki görselde seviyelerin altında Helm, Ansible ve Go görüyoruz. Bunun sebebi OperatorSDK ile bu üç araçtan herhangi birisini kullanarak Operator geliştirebiliriz. Fakat Helm ile sadece seviye 2 kabiliyetinde Operator geliştirebilirken Ansible veya Go ile Auto Pilot seviyesinde geliştirme yapabiliriz.

Biz örneklerimizde Go ile geliştirmeye göz atacağız.

https://www.openshift.com/blog/top-kubernetes-operators-advancing-across-the-operator-capability-model

OperatorSDK

OperatorSDK Red Hat tarafından geliştirilen custom controller geliştirmek için ihtiyacımız olan işlemleri arkaplanda controller-runtime üzerine çeşitli soyutlamalar yaparak bizlerin kullanımına sunan bir projedir.

Şimdi sizlerle beraber bir Operator geliştirelim. Bu Operator bir Custom Resource yönetsin ve her bir Custom Resource karşılığında bizim belirttiğimiz sayıda Pod ayağa kaldırsın. Custom Resource silindiğinde oluşturulan Podlar da otomatik olarak silinsin.

Not: Yazıda anlatılan operator-sdk son version değil. Yeni versionlarda bazı değişiklikler yapıldı. Kullanılan version:

operator-sdk version: “v0.18.2”, commit: “f059b5e17447b0bbcef50846859519340c17ffad”, kubernetes version: “v1.18.2”, go version: “go1.14.4 darwin/amd64”

Initializing Project

Operatorümüzü geliştirmeye başlayalım. İlk yapacağımız şey operator-sdk ile bir proje başlatmak:

operator-sdk new hello-operator

Projemizi açtığımızda şöyle bir yapı ile karşılaşıyoruz:

Creating Custom Resource

Sonrasında custom resource oluşturmaya başlayabiliriz.

operator-sdk add api --api-version=trendyol.com/v1 --kind=Hello

Proje yapımıza tekrar baktığımızda bazı değişiklikler olduğunu, pkg/apis altına custom resource tanımlarımızın oluşturulduğunu görüyoruz.

Burada bizim ilgileneceğimiz dosya hello_types.go. Custom Resource’umuz için ilgili field tanımlamalarını burada yapacağız.

Şimdi buraya en basit haliyle kaç adet pod çalıştıracağımızı yazabileceğimiz bir field oluşturalım.

Creating Custom Controller

Şimdi ilgili custom resource için bir custom controller oluşturalım:

operator-sdk add controller --api-version=trendyol.com/v1 --kind=Hello

Controllerımızı ekledikten sonra proje yapımıza baktığımızda hello_controller.go dosyasının oluştuğunu görüyoruz:

Oluşturulan controller bizim için boilerplate kodu bize hazır bir şekilde getiriyor ve içerisinde bir pod oluşturma örneği de bulunuyor:

Bizim için burada en önemli kısım Reconcile() fonksiyonunun içerisi. Şimdi sizlere bununla ilgili bazı bilgiler vermek istiyorum.

Reconciliation Loop

Building your own kubernetes CRDs | LaptrinhX
https://laptrinhx.com/building-your-own-kubernetes-crds-3561698093/

Reconcile fonksiyonu aynı zamanda reconciliation loop olarak da bilinir, Operator logicimiz bu fonksiyon içerisinde bulunur. Bu fonksiyonun amacı actual state ile desired state arasındaki senkronizasyonu yapmaktır.

Kubernetes bir resource lifecycle’ı boyunca Reconcile fonksiyonunu birden fazla kez çağırabilir, bu sebeple duplicate child resource’lar oluşturmamak amacıyla Reconcile fonksiyonunun idempotent olması önemlidir.

Bu noktada son kullanıcı CR ismini bize belirtirken, oluşturacağımız child resource’ların isimlerini oluşturmak Operator’ün sorumluluğundadır.

Bizim Reconcile fonksiyonunda yapacağımız ekleme, bir for döngüsü ile CR’da belirtilen sayıda pod oluşturmak olacak.

Reconcile fonksiyonunun yeni halini bakalım:

Fonksiyonun içerisinde yapılan işlemleri açıklamak gerekirse:

  • 6–17 satırları arasında ilgili CR elde ediliyor
  • 19. satırda CR’da belirtilen adet kadar for döngüsü oluşturuluyor
  • 21. satırda yeni bir pod resource’u oluşturuyoruz
  • 24–26 satırlarında oluşturduğumuz child resource için reference veriyoruz, böylelikle ilgili CR silindiğinde child resource’lar otomatik olarak garbage collector tarafından silinecek
  • 29–40 satırlarında podun daha önce oluşturulup oluşturulmadığı control ediliyor, eğer pod bulunmazsa yeni bir pod oluşturuyoruz
  • 40–43 arasında pod zaten oluşturulduysa bir sonraki adıma geçiyoruz
  • 43–45 arasında eğer farklı bir hata ile karşılaştıysak retry edilmesi için hatayı return ediyoruz

Reconcile fonksiyonunda geriye döndüğümüz değere göre farklı işlemler yapılıyor.

Burada ne döneceğimizi anlamak için bu fonksiyonun kim tarafından çağrıldığına ve dönüş tiplerinin nasıl değerlendirildiğine göz atmakta fayda var. Reconcile fonksiyonumuz operator sdk içerisinde yer alan controller.go içerisindeki reconcileHandler fonksiyonu tarafından çağrılıyor:

Dönüş tiplerini ve karşılığında ne olacağını kısaca özetlemek gerekirse:

1-) return reconcile.Result{}, nil

Herhangi bir hata almadık, başarılı bir şekilde return ediyoruz

2-) return reconcile.Result{}, err

Bir hata ile karşılaştık ve loopun tekrar tetiklenmesini istiyoruz.

3-) return reconcile.Result{ RequeueAfter: 5 * time.Second }, nil

Bir sebepten ötürü reconciliation loopu belirli bir süre sonra tekrar ettirmek istiyoruz

4-) return reconcile.Result{ Requeue: true }, nil

Bir sebepten ötürü reconciliation loopu tekrar ettirmek istiyoruz

Deploy & Run Operator

Controller logicimizi implement ettik ve Operatorümüzü test etmek istiyoruz. Bunun için ikinci adımda oluşturduğumuz custom resourceların kubernetes tarafından tanınmasını sağlayacak Custom Resource Definitionı oluşturmalıyız.

operator-sdk generate crds
operator-sdk generate k8s

Oluşturduğumuz kubernetes resourcelarımızı deploy edelim:

kubectl apply -f deploy/crds/*_crd.yaml
kubectl apply -f deploy/crds/*_cr.yaml

Şimdi Operatorümüzün çalışması için tek yapmamız gereken aşağıdaki komutu çalıştırmak:

operator-sdk run local

Şöyle bir çıktı ile karşılaşacağız:

Okuması kolay olması açısından bazı logları silerek ihtiyacımız olan çıktıyı buraya ekledim. Loglarda görebileceğimiz üzere Operatorümüzü çalıştırdığımızda, önceden oluşturmuş olduğumuz CR’ı yakalayıp bizim için gerekli podları oluşturdu.

Oluşturulan podları kubectl ile görüntüleyebiliriz:

Örnek olarak geliştirdiğimiz Operator production ready olmamakla beraber sadece belirli adımları implement ediyor. CR’ın update edildiği podCount değerinin değiştiği durumları, CR için status updateleri gibi işlemleri karşılamıyor.

Build Operator Image

Operatorümüzü build etmek için

operator-sdk build image-name:tag

komutunu kullanıyoruz. Default olarak docker ile build alıyor fakat podman veya buildah ile build almak istersek

operator-sdk build image-name:tag --image-builder podman

komutu ile builder belirtebiliyoruz.

OLM (Operator Lifecycle Manager)

Kısaca OLM’e de atıfta bulunup yazıyı tamamlamak istiyorum. OLM bizim için Operatorleri yöneten Operatordür desek yanlış olmaz. Bir Operatorün kubernetes clusterına deploy edilebilmesi için gerekli işlemleri sağlayan bir araçtır.

--

--

Emre Savcı
Trendyol Tech

Tech. Lead @Trendyol & Couchbase Ambassador | Go Türkiye, Kubernetes, Istio, CNCF, Scalability. Open Source Contributor.