Kubernetes CRD ve Controller ile Kendi Kaynaklarınızı Yaratın
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:
- Koleksiyon Bazlı Endpointler
- 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.
- Diğer Endpointler:
/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.

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.
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 ederkube-system/metrics-server
, bu API'nin kube-system namespace'inde çalışanmetrics-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.

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.
Are there some prominent examples when the Kubernetes API was extended using the Aggregation Layer?
— Ivan Velichko (@iximiuz) February 6, 2022
Or it's always Custom Resources paired with admission hooks and controllers?
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.

Ö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.

Ş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ğinv1
).served: true
: Bu versiyon kullanıcı isteklerine açık olacak demektir. false durumunda kubectl komutları ilgili CR'lar için çalışmaz. Örneğinkubectl 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
veip
alanlarının bulunacağını ve her ikisinin destring
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. BuradaLast 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.

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.

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?

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.

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.
İşte bizde kendi controller'ımızı yazarak resourcelarımızı güncelleyeceğiz. Fakat son bir diyagramla controller kavramını tam anlamanızı istiyorum.

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.

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
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.

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.
Bir sonraki yazıda görüşmek üzere.