Kubernetes CRD ve Controller ile Kendi Kaynaklarınızı Yaratın
12 min read

Kubernetes CRD ve Controller ile Kendi Kaynaklarınızı Yaratın

Kubernetes'in sunduğu güçlü yapı taşları sayesinde Custom Resource Definition (CRD), Custom Resource (CR) ve Custom Controller yazarak Kubernetes API'sini genişletiyoruz.
Kubernetes CRD ve Controller ile Kendi Kaynaklarınızı Yaratın
Photo by Christopher Burns / Unsplash

Kubernetes, güçlü ve esnek yapısıyla modern uygulama altyapılarının temel taşı haline geldi. Ancak bazen, standart Kubernetes kaynakları ihtiyaçlarımızı tam olarak karşılamayabilir. İşte tam bu noktada devreye Kubernetes'in API extend imkanları giriyor.

Bu makalede, Kubernetes’i nasıl kendi iş ihtiyaçlarınıza göre şekillendirebileceğinizi göstereceğim. CRD (Custom Resource Definition) ve Controller kullanarak Kubernetes’e yeni kaynak türleri eklemeyi, bu kaynaklarla nasıl etkileşim kurabileceğinizi ve sisteminize nasıl özel işlevler kazandırabileceğinizi adım adım anlatacağım.

Ayrıca, CRD'lerin neden aggregation layer yöntemine göre daha esnek ve sürdürülebilir bir çözüm sunduğunu gerçek bir örnekle açıklayacağız. Kubernetes’i sadece kullanan değil, aynı zamanda genişleten tarafta yer almak istiyorsanız, doğru sayfadasınız.

1. Kubernetes Extend Yöntemleri

Kubernetes, sadece güçlü bir container orchestration platformu değil, aynı zamanda esnek bir mimariye sahip bir sistemdir. Bu esneklik, geliştiricilerin Kubernetes API'sini doğrudan genişleterek kendi özel ihtiyaçlarına göre uyarlamalarına imkan tanır.

Kubernetes API’si üzerinde genişletme yaparken, öncelikle API'nin iki ana endpoint türü içerdiğini anlamak önemlidir:

  1. Koleksiyon Bazlı Endpointler
    1. Kubernetes nesnelerinin (Pod, ConfigMap, Service gibi) yönetilmesini sağlayan ve kalıcı (persistent) varlıkları temsil eden endpointlerdir. Kubernetes API’nin büyük çoğunluğunu bu tür oluşturur.
  2. Diğer Endpointler:
    1. /metrics, /logs, /apis gibi Kubernetes nesneleriyle doğrudan ilişkili olmayan, daha sistemsel işlevler sunan endpointlerdir. Bu tür endpointler ya doğrudan Kubernetes API sunucusuna (kube-apiserver) gömülüdür ya da API Aggregation Layer kullanılarak genişletilmiştir.

Yani bir developer Kubernetes’i genişletmek istediğinde;

  • Yeni Kubernetes nesneleri (örneğin özel bir resource tipi) eklemek istiyorsa, Custom Resource Definition (CRD) kullanır ve bu nesnelerin yaşam döngüsünü yönetmek için bir Controller yazar.
  • Yeni bir API endpointi (örneğin /metrics gibi resource koleksiyonu olmayan özel bir API endpoint) eklemek istiyorsa, API Aggregation Layer üzerinden yeni bir API sunucusu entegre eder.

Bu tanımı daha da özetlersem, genel olarak Kubernetes API’sini genişletmenin iki temel yöntemi bulunur:

  • Custom Resource Definitions (CRD)
  • Aggregation Layer

Şimdi bunların detaylarını ve ne olduklarını inceleyelim.

Şekil 1: Ne yapacağımıza, nasıl genişleteceğimize karar vermek aslında çok kolay :)

1.1. Aggregation Layer

Aggregation Layer, Kubernetes API server gibi başka bir API sunucusu ekleyerek extend etme yöntemidir. Yani kendi bağımsız API sunucunuzu geliştirip Kubernetes API sistemine entegre edersiniz.

Artık, kendi endpointinize bir request geldiğinde, Aggregation Layer bu isteği direkt sizin API serverınıza yönlendirir. Böylece karmaşık logicler yazabilir ve maksimum esnekliği elde etmiş olursunuz.

Şekil 2: Aggregation Layer, kendi geliştirdiğiniz endpointe gelen istekleri sizin API Server'ınıza iletir.

Avantajları:

  • Çok özel, karmaşık, yüksek performans gerektiren API ihtiyaçlarını karşılar.
  • Kubernetes API sınırlarının ötesinde tamamen özgür bir yapı sağlar.

Kullanım Alanları:

  • Kubernetes API'yi çok büyük ölçekte özelleştirmek gerektiğinde kullanılır (örneğin, OpenShift gibi platformlar aggregation layer kullanır)
  • Metrics Server, Prometheus Adapter gibi özelleştirilmiş projeler aggregation layer kullanmıştır.

Aggregation Layer ile geliştirilmiş API serverları listelemek için kubectl get apiservices komutunu kullanabilirsiniz.

$ kubectl get apiservices

v1.storage.k8s.io                      Local                        
v1beta1.metallb.io                     Local                        
v1beta1.metrics.k8s.io                 kube-system/metrics-server   

Bu çıktıda,

  • Local, Kubernetes API Server'ın kendisini ifade eder
  • kube-system/metrics-server, bu API'nin kube-system namespace'inde çalışan metrics-server tarafından sunulduğunu belirtir. Bu api endpointe gelen istekler metrics-server API servera iletilir.

Daha fazla detaya ve örneğe şuradan erişebilirsiniz.

Kubernetes API Aggregation Layer
The aggregation layer allows Kubernetes to be extended with additional APIs, beyond what is offered by the core Kubernetes APIs. The additional APIs can either be ready-made solutions such as a metrics server, or APIs that you develop yourself. The aggregation layer is different from Custom Resource…
💡
Aggregation Layer ile Kubernetes'i extend etmek CRD methoduna göre çok daha kapsamlı ve daha zor bir iştir.

Bu nota dayanarak başka hangi projelerin Aggregation Layer kullandığını araştırırken şu twite denk geldim. Siz de araştırmak isterseniz yorumları okuyabilirsiniz.

2.1. Custom Resource Definitions (CRD)

Custom Resource Definition (CRD), Deployment ve Pod gibi, Kubernetes’e doğrudan yeni resource türleri eklemenin en yaygın ve önerilen yoludur. CRD, Kubernetes API server üzerinde bir kayıt oluşturur ve böylece sistem, sanki yerleşik (built-in) bir resource gibi yeni tanımlanan resource'u tanımaya başlar. Böylece kubectl kullanarak kendi resourcelarınızla etkileşime geçebilirsiniz.

API Extensions API server inside of the Kubernetes API server
Şekil 3: CRD ile tanımladığınız CR (custom resource) lar etcd de saklanır ve local API Server tarafından yönetilir.

Özellikleri:

  • Kubernetes API üzerinden kubectl, client-go gibi araçlarla doğal şekilde erişilebilir.
  • API server, CRD tanımına göre doğrulama (validation) ve şema kontrolü (schema enforcement) yapabilir.
  • Mevcut Kubernetes ekosistemiyle (RBAC, Audit Logs, Admission Controller'lar vb.) tam uyumludur.
  • Controller yazarak CRD kaynakları üzerinde özel business logicler çalıştırılabilir.

Avantajları:

  • Yönetimi kolaydır, native Kubernetes objeleri gibi davranır.
  • API sunucusunu değiştirmeden genişleme yapılabilir.
  • Güncellemeler ve sürümlemeler (versions, conversionWebhook) desteklenir.

Kullanım Alanları:

  • Özel iş akışları yönetmek (örneğin, bir “Backup” kaynağı oluşturmak)
  • Kubernetes üstünde SaaS platformları geliştirmek
  • Operator pattern uygulamaları yazmak

CRD ile bir resource oluşturduğunuzda, örneğin bu resource'un adı backup ise, kubectl get backups komutu ile listeleyebilirsiniz.

$ kubectl get backups

Server       Last Backup Date
xrepo01      20250410
xrepo02      20250409

Konuya genel bir giriş yaptıktan sonra artık nasıl yapacağımız kısmına geçebiliriz.

2. CRD İle Kubernetes'i Extend Etmek

Kubernetes'i extend etmenin en yaygın ve en kolay yolu CRD dir. Peki CRD yazmak tek başına yeterli midir? Cevap, hayır.

Önce bazı kavramlara hakim olmamız gerekiyor:

  • CRD (Custom Resource Defination), custom resourceları oluştururken hazırlayacağımız manifestlerin tanımlayan objemizdir. Hangi fieldlar hangi formatta olmalı, oluşturulan resource hangi status'lara sahip olacak gibi tanımları bu objede yapıyoruz.
  • CR (Custom Resource), CRD tanımımıza uygun olarak oluşturduğumuz ve API Server tarafından yönetilen, kubectl ile ulaşabildiğimiz, etcd'de barındırılan Pod, Deployment gibi resource'un kendisidir.
  • Operator veya Custom Controller, CR'leri izleyerek ve yöneterek, bu özel kaynakların istenen duruma ulaşmasını otomatikleştiren yazılımdır.

Bu üç tanım arasındaki ilişkiyi hızlıca özetlersek; CRD ile oluşturacağımız CR'ların tanımını yaparız, Operator ile de bu CR'ların state'ini güncelleriz.

Şekil 4: CRD, CR ve operator ayrılmaz bir bütün olarak işlerler.

Şimdi her birine daha detaylı göz atalım ve nasıl oluşturulabileceklerini inceleyelim.

2.1. CRD Nedir ve Nasıl Oluşturulur?

CRD, Kubernetes API'ye yeni bir resource tipi eklememizi sağlayan tanımdır. CRD ile Kubernetes'e, tıpkı Pod veya Service gibi, kendi özel nesne türlerimizi (örneğin Database, BackupJob) tanıtabiliriz.

Bir Custom Resource Definition (CRD) oluşturmak için temel yöntem, bir YAML manifest dosyası yazıp Kubernetes clustera apply etmektir.

Örnek bir CRD manifestini inceleyelim.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: backups.example.com
spec:
  group: kerteriz.net
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                host:
                  type: string
                ip:
                  type: string
            status:
              type: object
              properties:
                currentBackup:
                  type: string
      additionalPrinterColumns:
        - name: Last Backup Date
          description: Last Backup Date of the host
          type: string
          jsonPath: .status.currentBackup
  scope: Namespaced
  names:
    plural: backups
    singular: backup
    kind: Backup
    shortNames:
      - bk

Bu manifestteki önemli alanları açıklayalım:

  • apiVersion: Kubernetes 1.16+ sürümleri için modern ve stable CRD API'sidir.
  • kind: Kubernetes'e bir CRD tanımladığımızı belirtir.
  • name: CRD nesnesinin adıdır. Format: {plural}.{group} olmalıdır.
  • group: Bu CRD'nin ait olduğu API grubu. API endpointleri apis/kerteriz.net/ altında açılır.
  • versions:
    • name: CRD'nin versiyonudur (örneğin v1).
    • served: true: Bu versiyon kullanıcı isteklerine açık olacak demektir. false durumunda kubectl komutları ilgili CR'lar için çalışmaz. Örneğin kubectl get backups hata döner.
    • storage: true: Bu versiyonun, etcd üzerinde saklanacağını belirler. Birden fazla versiyon olduğunda sadece bir versiyon storaged olarak belirlenebilir.
  • openAPIV3Schema: CRD'nin veri yapısının nasıl olacağını (spec ve status alanlarını) OpenAPI v3 formatında tanımlar. Kubernetes API sunucusu, istekleri bu şemaya göre doğrular.
  • properties: spec altında, host ve ip alanlarının bulunacağını ve her ikisinin de string türünde olacağını belirtir.
  • status: resource'un güncel state bilgilerini taşır.
  • additionalPrinterColumns: kubectl get backups çıktısında gösterilecek sütunları belirler. Burada Last Backup Date adında ekstra bir kolon gösterir.
  • scope: Bu kaynak Namespace seviyesinde yönetilecektir. Yani her namespace içinde ayrı ayrı Backup nesneleri oluşturabiliriz.
  • names: CRD için kullanılacak isimler:
    • plural: Çoğul adı (backups)
    • singular: Tekil adı (backup)
    • kind: Kaynağın tipi (Backup)
    • shortNames: Kısa adlar (bk ile hızlı erişim)

Artık bu manifesti kubectl apply -f crd.yaml ile apply edebilirsiniz.

Şekil 5: kubectl get crds

Son olarak CRD yazmanı kolaylaştırmak için kubebuilder gibi birçok açık kaynak araç ve framework bulunmaktadır. Fakat en güzeli doğal olanı :)

2.2. CR Nedir ve Nasıl Oluşturulur?

CRD tarafından tanımlanan yeni kaynak türüne ait gerçek nesnelerdir. Kullanıcılar veya sistemler tarafından oluşturulan ve Kubernetes üzerinde yönetilen bu özel nesneler, uygulamanın istenen durumunu (desired state) tarif eder.

Bir Custom Resource (CR) oluşturmak için yine basit ve bu sefer daha anlaşılır bir manifest hazırlayacağız.

apiVersion: kerteriz.net/v1alpha1
kind: Backup
metadata:
  name: xharbor01
  namespace: default
spec:
    host: xharbor01
    ip: 10.10.50.1

Fazla bir açıklama yapmaya gerek yok. Yazdığımız CRD deki kurallara uyarak oldukça basit bir resource manifesti oluşturduk. Artık kubectl apply -f cr.yaml ile bunu da oluşturabiliriz.

Şekil 6: kubectl get backups

Süper, artık kendimize ait resourceları görebiliyoruz.

Fakat, bir dk...

Neden sütunda herhangi bir değer yok?

Kim burada bir dizi komut çalıştırıp ilgili datayı güncelleyecek?

Hmm..

Gerçekten hmm... Hadi sıradaki başlığa geçelim.

2.3. Operator (Custom Controller) Nedir ve Nasıl Oluşturulur?

Operator, CR'leri izleyerek ve yöneterek, gerektiğinde cluster içindeki ve dışındaki hedeflerle etkileşime geçerek bu özel resourceların statelerini kontrol eden ve güncelleyen yazılımdır.

Bir önceki başlıktaki custom resource'umuzu düşünürsek; ilgili resource'un spec altındaki host ve ip bilgilerini kullanıp, o sunucuya erişip, snapshot tarihini ilgili komutu çalıştırarak kontrol edip, o tarihi yine aynı resource'un status altındaki currentBackup alanına yazacak programın ta kendisidir.

Şekil 7: Her controller, kendi sorumlu olduğu resourceları izler ve günceller.

Zaten Kubernetes'in Control Plane'nindeki bileşenlere baktığımızda kube-controller-manager'a denk geliriz. Bu Controller Manager bileşeni, altında birçok controller barındırır. Bunlardan bazıları şunlardır:

  • Node controller
  • Job controller
  • EndpointSlice controller
  • ServiceAccount controller
  • .....

Özetle her bir controller, yukarıda anlattığım senaryo benzeri kendi logicini yürütür ve sorumlu olduğu resourceların statelerini kontrol edip günceller.

💡
Tüm bu k8s ile gelen built-in controllerı ayrı ayrı bulamazsınız. Tüm controller'lar kube-controller-manager binary si içinde bundle olarak yer alır.

İşte bizde kendi controller'ımızı yazarak resourcelarımızı güncelleyeceğiz. Fakat son bir diyagramla controller kavramını tam anlamanızı istiyorum.

Şekil 8: Bir request'in akışında controller'ın yeri

Hadi öyleyse biz de kendi controller'ımızı yazalım.

2.3.1. Custom Controller İçin Karar Verme Süreci

Bir custom controller yazmak için önce bazı şeylerin kararını vermeliyiz. Hadi karar alalım:

  • Dil seçimi: Dil seçimi yaparken Python ve Go ön plana çıkan iki dil. Fakat içim dışım Python olduğu ve artık sevmediğim için ve Kubernetes'de zaten Go ile yazıldığından seçimimi Go'dan yana yapıyorum.
  • Framework seçimi: Kubebuilder ve Operator SDK gibi frameworkler mevcut. Fakat hiçbirine bulaşmadan doğrudan native Go yazacağım. Zaten business logic oldukça basit.

Öyleyse projemizi açtık ve başlıyoruz.

Şekil 9: İlk kurşunu sıktık

2.3.2. Reconciler Yazmak

CRD ve CR lerimizi apply ettikten sonra clusterda hangi resourceları izleyeceğimizi artık biliyoruz. Bu andan itibaren;

  • Yeni bir resource eklendiğinde,
  • Mevcut bir resource güncellendiğinde,
  • Mevcut bir resource silindiğinde,
  • Belirli periyotlarda,

ilgili resourceları alıp kontrol etmek ve statelerini güncellememiz gerekiyor. Bunun içinde bir reconciler mekanizmasına ihtiyacımız var.

Hadi main.go içini dolduralım:

package main

import (
	"fmt"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/cache"
	"log"
	"tb-controller/internal"
)

func main() {
	resources := []schema.GroupVersionResource{
		{Group: "kerteriz.net", Version: "v1", Resource: "backups"}
	}

	// load kubeconfig
	kubeconfig, err := internal.LoadKubeConfig(err)
	if err != nil {
		log.Fatal(err)
	}

	dynamicClient, err := dynamic.NewForConfig(kubeconfig)
	if err != nil {
		panic(err.Error())
	}

	stop := make(chan struct{})
	defer close(stop)

	for _, gvr := range resources {
		informer := internal.CreateInformer(dynamicClient, gvr)
		if informer == nil {
			fmt.Printf("Failed to create informer for %s\n", gvr.Resource)
			continue
		}

		internal.RegisterEventHandlers(informer)
		go informer.Run(stop)

		if !cache.WaitForCacheSync(stop, informer.HasSynced) {
			fmt.Printf("Failed to sync cache for %s\n", gvr.Resource)
			return
		}
	}

	// Start the cron job
	internal.Cron(dynamicClient, resources[0])

	fmt.Println("Custom Resource Controller started successfully")

	<-stop
}

main.go

💡
Makaleyi kod kalabalığına dönüştürmemek için tüm projedeki kodları buraya yazmayacağım. Makale sonundaki Github linkinden tüm projeye ulaşabilirsiniz.

Burada yazdığımız kod, kısaca:

  • hangi custom resourceları takibe alacağımızı belirtiyoruz
  • önce CreateInformer, ardından RegisterEventHandlers methodu ile bu resourceların eventlerini izlemeye başlıyoruz.
  • Cron methodu ile belirlediğimiz periyotlarla tüm resourceları alarak belirlediğimiz logici her biri için çalıştırıyoruz.
func RegisterEventHandlers(informer cache.SharedIndexInformer) {
	_, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		// called when new resource is created
		AddFunc: func(obj interface{}) {
			resource, _ := obj.(*unstructured.Unstructured)
			println("Added:", resource.GetName(), "in", resource.GetNamespace())
		},
		// called when resource is updated
		UpdateFunc: func(oldObj, newObj interface{}) {
			oldResource, _ := oldObj.(*unstructured.Unstructured)
			println("Updated:", oldResource.GetName(), "in", oldResource.GetNamespace())
		},
		// called when resource is deleted
		DeleteFunc: func(obj interface{}) {
			resource, _ := obj.(*unstructured.Unstructured)
			println("Deleted:", resource.GetName(), "in", resource.GetNamespace())
		},
	})

	if err != nil {
		panic(err.Error())
	}
}

informer.go

En başta yazdığım ilk üç madde için (resource eklendiğinde, güncellendiğinde ve silindiğinde) yürütülecek kod parçacıklarını görüyorsunuz. Ben şimdilik sadece log bastım ama siziz yürütmeniz gereken özel bir logic varsa ekleyebilirsiniz.

func Cron(dynamicClient dynamic.Interface, resource schema.GroupVersionResource) {
	ticker := time.NewTicker(time.Duration(GetAppConfig().Cron.Minute) * time.Minute)
	defer ticker.Stop()
	done := make(chan bool)

	for {
		select {
		case <-done:
			fmt.Println("Done!")
			return
		case t := <-ticker.C:
			fmt.Println("Cron running time: ", t)

			resources, _ := dynamicClient.Resource(resource).Namespace("").List(context.TODO(), metav1.ListOptions{})
			for _, item := range resources.Items {
				fmt.Println("Item: ", item.GetName())

				// Run your logic here
			}
		}
	}
}

cronjob.go

Son olarak cron içerisinde, her periyotta tüm resourceları listeliyorum ve ardından:

  • ilk olarak resource spec altından ip adresini alıyorum
  • bu ip adresine ssh atarak bash komut çalıştırıyorum
  • dönen sonuçtan backup tarihini öğreniyorum
  • bu tarihi ilgili resource'un status alanındaki currentBackup fieldına yazıyorum.

Böylece tüm resourcelarımı istediğim periyorlarda reconcile ederek statelerini güncel tutmuş oluyorum.

2.3.3. Kontrol

Yazdığım go kodunu build edip clusterda Pod olarak çalıştırmaya başladıktan sonra resourcelarımın stateleri sürekli olarak güncellenmeye başladı. Ve ben kubectl get backups komutunu çalıştırdığımda artık istediğim veriyi görebiliyorum.

Şekil 10: Artık listedeki veriler geliyor

3. Son Sözler

Kubernetes'i extend edebilmek ve kendi resourcelarımızı, logiclerimizi yazabilmek için önce konsepte hakim olmak gerekiyordu. Bu yazıda kullanabileceğimiz tüm methodlara değindim ve canlı bir CRD, CR ve custom controller yazma örneğiyle bu işin aslında zor olmadığını göstermek istedim.

Tüm projeye aşağıdaki repodan ulaşabilirsiniz. Sormak istedikleriniz olursa da yorum kısmını özgürce kullanabilirsiniz.

GitHub - ibalat/k8s-crd-example
Contribute to ibalat/k8s-crd-example development by creating an account on GitHub.

Bir sonraki yazıda görüşmek üzere.