Java Operator SDK (JOSDK) ile Kubernetes Operator Geliştirme
Reconciler<T>, UpdateControl ve Reconciliation (Uzlaştırma) Döngüsünün Derinlemesine İncelenmesi
Referans alınan Java Operator SDK sürümü: v5.3.x (varsayılan olarak server-side apply, kendi yazdığın olaylar için event filtreleme) Referans alınan JDK sürümü: JDK 25 (LTS, 16 Eylül 2025’te yayınlandı)
İçindekiler
- Kubernetes Operator Aslında Nedir?
- Operator Geliştirme İçin Neden JDK 25?
- Proje Kurulumu
- JOSDK’nin Anatomisi
- Reconciler Arayüzü, Baştan Sona
- Reconciliation Döngüsü Gerçekte Nasıl Çalışır?
- UpdateControl ve ErrorStatusUpdateControl Derinlemesine
- Event Source’lar, Informer’lar ve Önbellekleme
- Dependent Resources (Bağımlı Kaynaklar) Çatısı
- Finalizer’lar ve Temizleme (Cleanup)
- Hata Yönetimi ve Retry (Yeniden Deneme) Semantiği
- En İyi Pratikler Kontrol Listesi
- Uçtan Uca Çalışan Örnek
- Operator’ını Test Etmek
- Gözlemlenebilirlik, Leader Election, Dağıtım
- Kaynaklar
1. Kubernetes Operator Aslında Nedir?
Kubernetes Operator, Kubernetes kontrol düzlemini alan (domain) özelinde, özel otomasyon ile genişleten bir yazılım parçasıdır. Kubernetes’in kendi içinde kullandığı aynı deseni takip eder: bir controller (kontrolör), cluster’ın durumunu (genellikle bir Custom Resource Definition - CRD - üzerinden) izler ve gerçek durumu (actual state) kullanıcının bildirdiği istenen duruma (desired state) doğru sürekli olarak yönlendirmeye çalışır.
Deployment controller’ı, ReplicaSet controller’ı gibi Kubernetes’in kendi yerleşik controller’ları dahil, her controller’ın altında yatan temel döngüye control loop veya reconciliation loop (uzlaştırma döngüsü) denir:
Bu, buyuru (imperative) tarzda script yazmaktan temelde farklıdır. Asla “şu anda şu Pod’u oluştur” diye yazmazsın. Bunun yerine “bu custom resource verildiğinde, dünyanın nasıl görünmesi gerektiğini” yazarsın ve reconciler, gerçek durum istenen duruma yakınsayana kadar (her ilgili değişiklikte, bir resync’te, retry’lerden sonra vb.) tekrar tekrar çağrılır - ve sonrasında da sapmayı (drift) düzeltmek için çağrılmaya devam eder.
Her Operator yazarının içselleştirmesi gereken temel özellikler:
- Seviye tabanlı (level-based), olay tabanlı (edge-based) değil: Reconciler, “Pod X oluşturuldu” gibi “olayları” işlemez. Her seferinde sıfırdan yeniden hesaplanan “dünyanın şu anki durumunu” işler. Bu, mantığının idempotent olması gerektiği ve neler olduğunun tarihçesine değil, sadece şu anki gözlemlenen duruma güvenmesi gerektiği anlamına gelir.
- Nihai tutarlılık (eventually consistent): Tek bir reconciliation, istenen duruma hemen ulaşamayabilir (örneğin bir Deployment’ın Ready olmasını bekliyorsundur). Bu normaldir - erken çıkarsın ve gelecekteki bir olayın veya yeniden zamanlamanın (reschedule) yeni bir deneme tetiklemesine izin verirsin.
- En az bir kez çalıştırma (at-least-once execution):
reconcilemetodun aynı mantıksal durum için birden fazla kez çağrılacaktır. (JOSDK varsayılan olarak her kaynak için reconciliation’ları seri hale getirse de.) Asla “bu sadece bir kez çalışacak” varsayımında bulunma.
2. Operator Geliştirme İçin Neden JDK 25?
JDK 25 (16 Eylül 2025’te GA), JDK 21’in ardından gelen ve Oracle ile diğer sağlayıcılardan uzun vadeli destek garantisi olan güncel LTS sürümüdür. Uzun süre çalışan, kaynak kısıtlı, cluster-native bir iş yükü olan bir Operator için JDK 25’in birkaç özelliği doğrudan ilgilidir:
- Structured Concurrency (JEP 505, beşinci önizleme): Operator’lar sıklıkla birden fazla Kubernetes API çağrısına veya harici sistemlere paralel gitmek (örneğin 3 bağımlı kaynağın durumunu kontrol etmek) ve bir sonraki durum geçişine karar vermeden önce sonuçları birleştirmek zorundadır. Structured concurrency, alt görevleri tek bir birim olarak başlatıp beklemenin disiplinli bir yolunu sunar; böylece hatalar/iptaller doğru şekilde yayılır - bu, “N alt kaynağın hazır olmasını bekleyen” reconciliation mantığına çok doğal biçimde oturur.
- Scoped Values (JEP 506, kesinleşti):
ThreadLocal‘a göre daha güvenli, değiştirilemez (immutable) bir alternatif. Reconciliation başına bağlam bilgisini (örneğin istek kapsamlı bir logger/trace ID) farklı kaynakların eşzamanlı reconciliation’ları arasında durum sızıntısı olmadan çağrı yığını boyunca taşımak için kullanışlıdır. - Virtual Thread’ler (JDK 21’den itibaren, 25’te olgunlaştı): Reconciler’ların bloklayan G/Ç (Kubernetes API çağrıları, harici sistemlere HTTP çağrıları) yaptığı durumlarda, JOSDK’nin executor modeli hafif virtual thread’lerden fayda sağlar. Bu sayede küçük bir platform-thread havuzunu tüketmeden çok sayıda kaynağı eşzamanlı olarak reconcile edebilirsin.
- Primitive’ler için Pattern Matching (Switch, JEP 507) ve Compact Source Files (JEP 512): Çoğunlukla ergonomik iyileştirmelerdir, ancak status nesneleri ve spec/status karşılaştırması için yazacağın küçük yardımcı sınıflarda/record’larda tekrar eden kodu (boilerplate) azaltırlar.
- Record’lar ve sealed interface’ler (JDK 17’den beri stabil, 25’te de merkezi konumda): CRD’inin
Spec/StatusPOJO’larını temsil etmek ve reconciliation sonuçlarını sealed hiyerarşiler olarak modellemek için idealdir (örn.sealed interface ReconcileDecision permits Requeue, Done, Waiting).
Bunların hiçbiri iyi mimarinin yerini tutmaz - ama Operator’ını JDK 25 ile yazmak, CompletableFuture zincirlerini elle örmek yerine, structured concurrency ve virtual thread’lere yaslanarak reconciliation mantığını hem okunabilir hem de bloklamayan (non-blocking) tutabileceğin anlamına gelir.
3. Proje Kurulumu
Minimal Maven kurulumu (Gradle karşılığı aynı şekilde çalışır):
<properties>
<maven.compiler.release>25</maven.compiler.release>
<josdk.version>5.3.0</josdk.version> <!-- en son patch sürümünü kontrol et -->
</properties>
<dependencies>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-core</artifactId>
<version>${josdk.version}</version>
</dependency>
<!-- Opsiyonel ama yaygın: fabric8 kubernetes-client + core'u birlikte paketler -->
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework</artifactId>
<version>${josdk.version}</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit-5</artifactId>
<version>${josdk.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
JOSDK, fabric8 Kubernetes client üzerine inşa edilmiştir; bu sayede çekirdek Kubernetes kaynaklarına (Deployment, ConfigMap, Service, …) ve model sınıflarını üretip/yazdıktan sonra kendi CRD’lerine tipli (typed) erişim elde edersin.
Bir Custom Resource’u sade bir Java sınıfı olarak tanımlayabilirsin:
@Group("example.com")
@Version("v1")
@ShortNames("wp")
public class WebPage extends CustomResource<WebPageSpec, WebPageStatus> implements Namespaced {}
public record WebPageSpec(String html, String cssStyleUrl, String javaScriptUrl) {}
public class WebPageStatus {
private String url;
private String observedGeneration; // konvansiyon: işlenmiş generation'ı takip et
// getter/setter'lar
}
Quarkus kullanıyorsan, Quarkus Operator SDK extension‘ı JOSDK’yi CDI injection, native-image desteği ve otomatik CRD üretimi ile sarmalar - production Operator’lar için şiddetle tavsiye edilir, ama altındaki reconciliation modeli burada anlatılanla birebir aynıdır.
4. JOSDK’nin Anatomisi
JOSDK’nin çalışma zamanı (runtime), birbiriyle işbirliği yapan az sayıda bileşenden oluşur:
| Bileşen | Sorumluluk |
|---|---|
| Operator | En üst seviye başlatma nesnesi; reconciler’ları kaydeder, informer’ları başlatır, yaşam döngüsünü yönetir. |
| Reconciler | İş mantığın - implemente etmen gereken tek arayüz. |
| Controller (iç bileşen) | Bir Reconciler’ı sarmalar; o kaynak türü için olay işleme kuyruğunun sahibidir. |
| EventSource | Bir kaynak türünü (primary veya secondary) izler ve işleme kuyruğuna olay yayınlar. |
| Informer / Cache | Kubernetes Watch API’sine dayanan, izlenen kaynakların yerel, bellek içi, nihai olarak tutarlı (eventually-consistent) bir aynası. |
| Workflow / DependentResource | Primary kaynağının “sahip olduğu” ikincil kaynakları (ConfigMap’ler, Deployment’lar vb.) yönetmek için opsiyonel bir bildirimsel (declarative) katman. |
| RetryManager / RateLimiter | Reconciliation hata fırlattığında backoff ve yeniden deneme sayısını yönetir. |
“Kaynak X için bir olay geldi” noktasından sonraki her şey, reconciler’ının kaynak başına, seri hale getirilmiş bir çağrısına akar - JOSDK, aynı custom resource örneğinin asla iki thread tarafından eşzamanlı olarak reconcile edilmeyeceğini garanti eder; bu da kodundan tüm bir yarış durumu (race condition) sınıfını ortadan kaldırır.
5. Reconciler<T> Arayüzü, Baştan Sona
Özünde:
public interface Reconciler<P extends HasMetadata> {
UpdateControl<P> reconcile(P resource, Context<P> context) throws Exception;
}
Hepsi bu kadar - tek bir metot. Operator’ının davranışının tamamı şunlar üzerinden ifade edilir:
resource‘tan (şu an bilinen haliyle primary kaynak - spec + status + metadata) ne okuduğun.- Ne yaptığın (bağımlı kaynakları oluşturmak/güncellemek için Kubernetes API’sini çağırmak, harici sistemleri çağırmak vb.).
- Ne döndürdüğün - JOSDK’ye neyin kalıcı hale getirileceğini ve bir sonraki reconciliation’ın nasıl zamanlanacağını söyleyen bir
UpdateControl<P>.
Minimal, deyimsel (idiomatic) bir reconciler:
@ControllerConfiguration
public class WebPageReconciler implements Reconciler<WebPage> {
private static final Logger log = LoggerFactory.getLogger(WebPageReconciler.class);
@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {
log.info("WebPage reconcile ediliyor: {}/{}",
webPage.getMetadata().getNamespace(), webPage.getMetadata().getName());
// 1. webPage.getSpec()'ten bağımlı kaynakların istenen durumunu hesapla
ConfigMap desiredConfigMap = buildConfigMap(webPage);
// 2. Bildirimsel olarak uygula (server-side apply)
context.resourceOperations().serverSideApply(desiredConfigMap);
// 3. Context'in cache'inden güncel secondary durumu al (canlı GET değil)
Deployment deployment = context.getSecondaryResource(Deployment.class)
.orElseGet(() -> createDeployment(webPage));
context.resourceOperations().serverSideApply(deployment);
// 4. "Bitti mi" yoksa hâlâ yakınsıyor mu, karar ver
boolean ready = deployment.getStatus() != null
&& Objects.equals(deployment.getStatus().getReadyReplicas(),
deployment.getSpec().getReplicas());
// 5. Status'u güncelle (asla spec'i değil!) ve döndür
webPage.getStatus().setReady(ready);
webPage.getStatus().setObservedGeneration(webPage.getMetadata().getGeneration());
return UpdateControl.patchStatus(webPage);
}
}
reconcile() metodunun sözleşme (contract) detayları
resource, canlı bir handle değil, bir anlık görüntüdür (snapshot). JOSDK’nin dağıtım anında cache’inde bulunan sürümdür. Üzerinde yaptığın herhangi bir mutasyon (genellikle.getStatus()üzerinde), daha sonraUpdateControltarafından bir API isteğine dönüştürülür.reconcileiçinde primary kaynak üzerinde,UpdateControl‘ü atlamak için özel bir sebebin olmadıkça asla doğrudanclient.resources(...).update(...)çağırma - bunu yapmak JOSDK’nin optimistic concurrency ve olay bastırma (event-suppression) mantığını bozar.Context<P>, diğer her şeye açılan kapındır: secondary kaynak cache’leri (context.getSecondaryResource(...)), Kubernetes client’ı, controller konfigürasyonu, retry bilgisi (context.isLastAttempt()) ve - v5’ten itibaren - cache-farkında (cache-aware) server-side apply çağrıları içincontext.resourceOperations().- İstisnalar (exception) retry mekanizmasına yayılır. Fırlatmak, “bu başarısız oldu, lütfen tekrar dene” demenin meşru ve teşvik edilen bir yoludur - istisnaları yutup bir sentinel
UpdateControldöndürmene gerek yok. - Dönüş değeri zorunlu ve anlamlıdır. Atlama yoluyla oluşan bir “no-op” yoktur;
patchStatus,patchResource,patchResourceAndStatusveyanoUpdate‘i açıkça seçersin.
6. Reconciliation Döngüsü Gerçekte Nasıl Çalışır?
Yeni başlayanların kavramsal olarak en çok yanlış anladığı kısım burasıdır, o yüzden yavaş gidelim.
6.1 Başlangıç (startup) aşaması
Operator süreci başladığında:
- JOSDK, kayıtlı her primary kaynak türü için (ve yapılandırılmış her secondary/dependent kaynak türü için) bir Informer başlatır. Informer’lar önce yerel cache’i doldurmak için bir LIST işlemi yapar, ardından sonraki değişiklikleri akış (stream) olarak almak için bir WATCH açar.
- En iyi pratik olarak - ve bu JOSDK’nin varsayılan davranışıdır - var olan her kaynak, başlangıçta bir kez reconcile edilir, çünkü Operator süreci kapalıyken istenen durum sapmış olabilir (örneğin biri bağımlı bir ConfigMap’i elle silmiş olabilir ya da bir node güncelleme sırasında çökmüş olabilir).
- Bir kaynak için ilk tam reconciliation tamamlandıktan sonra, sonraki reconciliation’lar olay tabanlı (event-driven) hale gelir.
6.2 Gerçekte bir reconciliation’ı ne tetikler?
Belirli bir custom resource örneğinin reconciliation’ı, aşağıdakilerden herhangi biri gerçekleştiğinde kuyruğa alınır:
- Primary kaynağın
.specalanı değişir - teknik olarak JOSDK,.metadata.generation‘ı karşılaştırır; bu değer,.specher değiştiğinde (status veya sadece metadata değişikliklerinde değil) Kubernetes API sunucusu tarafından otomatik olarak artırılır. Bu yüzden.statusiçindeobservedGeneration‘ı takip etmek konvansiyoneldir - bu, hem senin kendi mantığının hem de dışarıdaki gözlemcilerin “bu spec değişikliği gerçekten reconcile edildi mi?” sorusunu yanıtlamasını sağlar. - İzlediğin bir secondary/bağımlı kaynak değişir (örneğin sahip olduğun bir Deployment
NotReady‘denReady‘ye geçer - o Deployment için olan JOSDK event source’u, sahip olan WebPage’in reconciliation’ını kuyruğa alır). - Önceki bir reconciliation’dan gelen
UpdateControl.rescheduleAfter(...)veyareschedule()metodu (v5.3+) ile manuel bir yeniden zamanlama (reschedule) talep edilmiştir. - Önceki bir
reconcile()çağrısı istisna fırlattığı için bir yeniden deneme (retry) zamanlanmıştır. - Kaynak silinmek üzere işaretlenmiştir ve bir finalizer’a sahiptir; bu da (eğer
Cleaner<P>implemente ettiysen)cleanup()yolunu tetikler.
Kritik nokta: JOSDK varsayılan olarak her tek watch olayında reconcile etmez. Primary kaynak üzerinde sadece .metadata.labels‘ı değiştirirsen (.spec‘i değil), hiçbir reconciliation tetiklenmez, çünkü .metadata.generation değişmemiştir. Bu bilinçli bir optimizasyondur - çoğu operator sadece spec değişikliklerini önemser - ama mantığın gerçekten her metadata değişikliğine de tepki vermesi gerekiyorsa bunu @ControllerConfiguration(triggerReconcilerOnAllEvents = true) ile geçersiz kılabilirsin.
6.3 Dağıtım (dispatch) ve kuyruğa alma modeli
Dahili olarak, her Controller, Kubernetes’in kendi client-go workqueue’suna kavramsal olarak benzer bir olay işleme yapısı tutar:
- (Herhangi bir event source’tan gelen) bir olay, reconcile edilecek bir
ResourceID‘ye (namespace + name) dönüştürülür. - Bu ID bir kuyruğa yerleştirilir. Aynı ID zaten kuyrukta veya şu anda işleniyorsa, JOSDK birleştirir (coalesce) - aynı kaynak üzerindeki N tane hızlı ardışık olay için N tane gereksiz reconciliation almazsın; en güncel cache’lenmiş durumu gözlemleyen tek bir reconciliation alırsın.
- Bir worker thread,
ResourceID‘yi çeker, o kaynağın en güncel sürümünü yerel informer cache’inden (canlı bir API çağrısı değil - cache doğruluğunun bu kadar önemli olmasının sebebi budur) alır vereconcile()metodunu çağırır. - Farklı kaynaklar için reconciliation’lar eşzamanlı (concurrent) çalışır (yapılandırılabilir bir thread havuzu ile sınırlıdır - reconcile mantığın G/Ç üzerinde bloklanıyorsa JDK 25 virtual thread’lerinin en çok fayda sağladığı yer burasıdır). Aynı kaynak için reconciliation’lar kesinlikle seri hale getirilir.
6.4 “Kendi yazdığın” geri besleme döngüsü ve event filtreleme
Birçok operator yazarını şaşırtan bir incelik: reconciler’ın bağımlı bir kaynak üzerinde context.resourceOperations().serverSideApply(...) çağırdığında, bu yazma işleminin kendisi bir Kubernetes watch olayı üretir. Tarihsel olarak, bu, aslında “az önce yaptığım değişikliği” gözlemleyen bir reconciliation’ı yeniden tetikleyebilir; bu da bir döngüyü boşa harcar (ya da dikkatlice ele alınmazsa daha kötüsü, “hot loop” - sıcak döngü - oluşmasına yol açar).
JOSDK v5.3 itibarıyla, framework, Operator’ın context.resourceOperations() üzerinden yaptığı kendi yazmalarından üretilen olayları otomatik olarak filtreler - böylece UpdateControl ve ErrorStatusUpdateControl, kendi status patch’lerinden kaynaklanan gereksiz bir reconciliation’ı artık tetiklemez. Framework’ün yazma yolunu atlarsan (örneğin ham fabric8 client çağrıları), bu korumayı kaybedersin ve bunu kendin hesaba katman gerekir.
6.5 Reconciliation sonrası: UpdateControl‘ün uygulanması
reconcile() döndükten sonra JOSDK:
- Neyin kalıcı hale getirileceğini belirlemek için
UpdateControl‘ü inceler (status patch, resource patch, ikisi birden, ya da hiçbiri). - İlgili Kubernetes API isteğini/isteklerini, genellikle bir server-side apply PATCH olarak gönderir.
- Mümkün olduğunda yerel cache’ini patch’lenmiş nesneyle iyimser (optimistic) şekilde günceller; böylece hızlı bir sonraki reconciliation, watch round-trip’ini beklemeden güncel veriyi görür.
- Bir
rescheduleAfterveyareschedule()belirtilmişse, herhangi bir watch olayından bağımsız olarak bu kaynağı belirtilen gecikmeden sonra yeniden kuyruğa almak için bir zamanlayıcı kurar. - Reconciler istisna fırlattıysa, RetryManager, yapılandırılan retry politikasına (varsayılan: doğrusal/üstel backoff, üst limitli deneme sayısı, yapılandırılabilir) göre yeniden kuyruğa alıp almayacağına ve ne zaman alacağına karar verir.
7. UpdateControl ve ErrorStatusUpdateControl Derinlemesine
UpdateControl<P>, her reconciliation için tam olarak bir sonucu kodlayan küçük, değiştirilemez (immutable) bir değer nesnesidir (JOSDK’nin implementasyonu record’ların yaygınlaşmasından önce geldiği için tam olarak record olarak yazılmasa da, kavramsal olarak JDK 25 record’larına çok uygundur):
// Kalıcı hale getirilecek hiçbir değişiklik yok
UpdateControl.noUpdate();
// SADECE status subresource'unu kalıcı hale getir
UpdateControl.patchStatus(resource);
// SADECE ana kaynağı (spec/metadata) kalıcı hale getir, status'u değil
UpdateControl.patchResource(resource);
// İKİSİNİ BİRDEN kalıcı hale getir - iki ayrı API isteği olarak, önce resource sonra status
UpdateControl.patchResourceAndStatus(resource);
Bunların her biri zamanlama direktifleriyle zincirlenebilir:
UpdateControl.patchStatus(webPage)
.rescheduleAfter(Duration.ofSeconds(30)); // olaylardan bağımsız olarak daha sonra yeniden reconcile ettir
7.1 Neden tam bir update değil de patchStatus?
Kubernetes, status subresource‘unu özellikle controller’ların, istenen durum (.spec) üzerinde yazma erişimine ihtiyaç duymadan - ya da kazara mutasyona uğratma riskine girmeden - ve .metadata.generation artışlarını tetiklemeden (status güncellemeleri asla generation’ı artırmaz) gözlemlenen/türetilmiş durumu (.status) güncelleyebilmesi için sunar. UpdateControl.patchStatus(resource):
- v5’ten itibaren varsayılan olarak server-side apply kullanarak
/status‘a kapsamlı bir PATCH gönderir. - CRD’inin gerçekten bir
statussubresource’u tanımlamasını gerektirir (CRD içindesubresources: { status: {} }, ya da@Kepcodegen / Quarkus kullanıyorsan eşdeğer annotation tabanlı üretim). - Operator’ının her reconciliation’da yaptığı işin ezici çoğunluğunu oluşturmalıdır. Kendini sık sık
patchResourceçağırırken bulursan, spec’i, controller’ın sahip olması gereken değiştirilebilir bir durum (mutable state) gibi kazara ele alıp almadığını sorgula (yaygın bir anti-pattern - spec, kullanıcının niyetidir, controller’ın karalama defteri değildir).
7.2 Server-side apply ve reconciler kodunu neden değiştirdi
JOSDK v5’ten itibaren, server-side apply (SSA), eski “resourceVersion tabanlı optimistic locking ile oku-değiştir-yaz” yaklaşımının yerini alan varsayılan güncelleme stratejisidir. SSA, patchStatus‘a geçirdiğin nesneyi nasıl oluşturman gerektiğini değiştirir:
- SSA ile, nesnenin geri kalanını olduğu gibi yeniden belirterek tam bir kopyasını değil, sadece sahip olmak/iddia etmek istediğin alanları göndermelisin. Cache’lenmiş
resourcenesnesini doğrudan mutasyona uğratıp tamamını geri gönderirsen, aynı status nesnesinin farklı kısımlarına meşru olarak yazan başka aktörlerin (örneğin Kubernetes API sunucusunun kendisi ya da başka bir controller’ın) sahip olduğu alanların üzerine yazma riskine girersin. - Birçok ekibin benimsediği güvenli desen, sadece
metadata.name/namespace/resourceVersionve gerçekten ayarladığınstatusalanlarını taşıyan minimal bir patch nesnesi oluşturmaktır:
WebPage statusPatch = new WebPage();
statusPatch.setMetadata(new ObjectMetaBuilder()
.withName(webPage.getMetadata().getName())
.withNamespace(webPage.getMetadata().getNamespace())
.build());
statusPatch.setStatus(computeStatus(webPage));
return UpdateControl.patchStatus(statusPatch);
- Pratikte,
.statusnesnesinin tek sahibi olan basit operator’lar için, getirilenresource‘u doğrudan mutasyona uğratıp geri döndürmek gayet iyidir ve çoğu eğitim materyalinin (JOSDK’nin kendi dokümantasyonu dahil) gösterdiği yöntem budur. Birden fazla yazarın status’un farklı alt alanlarına dokunabileceği durumlarda, ya da paylaşılan sahiplikle bağımlı kaynaklarda SSA yaparken minimal-patch desenine geçmek daha doğrudur.
7.3 “patchStatus sonrası bayat cache” tuzağı
İyi belgelenmiş bir sorun: UpdateControl.patchStatus(...) çalıştıktan sonra, yeni patch’lenmiş kaynağın bir sonraki reconciliation’a hemen görünür olacağı garanti edilmez, çünkü yerel informer cache’i, watch olayı API sunucusundan geri döndükçe asenkron olarak güncellenir. İki çözüm vardır:
- Reconciler’ının kendi içinde, son bilinen iyi durumdaki status’u bellek içinde önbelleğe alması; böylece mantığın hafif bayat bir cache’lenmiş nesneyi tekrar okursa geriye gitmez.
- JOSDK v5.1’den itibaren, güncellenmiş status’un bir sonraki reconciliation için kullanılabilir olmasını garanti etmek için özel olarak sunulan bir yardımcı araç var - bu garantiye ihtiyaç duyduğunda (örneğin sonraki bir reconciliation adımının doğru okumaya bağımlı olduğu bir status verisi yazarken),
context.resourceOperations()tabanlı yardımcılar içinUpdateControl/Context‘i kontrol et.
7.4 ErrorStatusUpdateControl<P>
Reconciliation başarısız olsa bile (ve yeniden denenecek olsa bile) .status üzerinde bir hata durumu kaydetmek istediğinde, şunu geçersiz kıl (override):
@Override
public ErrorStatusUpdateControl<WebPage> updateErrorStatus(
WebPage resource, Context<WebPage> context, Exception e) {
resource.getStatus().setErrorMessage(e.getMessage());
return ErrorStatusUpdateControl.patchStatus(resource);
}
Bu, kubectl describe kullanan kullanıcıların, sadece Operator’ın loglarındaki retry’leri görmek yerine, operator’ın neden takılı kaldığını görmesini sağlar. Belirli bir hata status güncellemesini .withNoRetry() ile retry sayacının dışında da tutabilirsin; ancak JOSDK’nin dokümantasyonu, çok dar ve bilinçli durumlar dışında (örneğin kullanıcı müdahalesi olmadan asla başarılı olmayacak, kalıcı olarak geçersiz bir spec - bu durumda retry’lerin sorunu çözeceğini ummaktansa API’yi dövmeyi durdurmak tercih edilir) retry’leri kapatmayı açıkça caydırır.
8. Event Source’lar, Informer’lar ve Önbellekleme
Event Source’lar (Olay Kaynakları), herhangi bir şeyin - primary kaynaklar, secondary/bağımlı Kubernetes kaynakları, hatta Kubernetes dışı sistemler (bir mesaj kuyruğu, bir webhook, bir REST API’ye karşı çalışan bir polling işi) - bir reconciliation’ı tetikleyebilmesinin mekanizmasıdır.
- Primary Event Source: Reconciler’ının kaynak türü için otomatik olarak kaydedilir; bir Informer tarafından desteklenir.
- Secondary Event Source’lar: Reconciler’ının bağımlı olduğu ya da yönettiği kaynaklar için bunları kaydedersin - en yaygın olarak
EventSourceInitializerarayüzü üzerinden, ya da daha basitçe, bunu senin için otomatik olarak bağlayan Dependent Resource soyutlamasını (bölüm 9) kullanarak. - Özel/Harici Event Source’lar: Kubernetes dışından reconciliation tetiklemek için (örneğin harici bir sistemin webhook’u) kendi
EventSource‘unu implemente edip elle kaydedersin.
Polling/zamanlayıcı yerine event source’ları neden tercih etmeli: Polling, API sunucusu yükünü boşa harcar ve gecikme ekler (poll aralığınla sınırlısındır). Informer destekli bir event source, gerçek bir değişikliğin milisaniyeler içinde farkına varır ve ağır işi Kubernetes’in Watch API’sine yaptırır. JOSDK’nin kendi en iyi pratik rehberliği açıktır: rescheduleAfter‘ı seyrek kullan, ve sadece beklenen koşulu Kubernetes-native izlenebilir bir olay olarak gerçekten gözlemlemenin bir yolu yoksa (örneğin duvar saati zamanına bağlı bir bekleme, mesela 30 gün içinde süresi dolan bir sertifika gibi) kullan.
Cache doğruluğu önemlidir çünkü her reconciliation, canlı bir GET’ten değil, yerel cache’ten okur. Bu, ölçeklenebilirlik için bilinçli bir tasarım tercihidir (her reconciliation’da API sunucusunu dövmezsin), ama şu anlama gelir:
- Reconciler’ının aldığı nesneyi, çağrıldığı anda %100 güncel olduğu garantisiyle asla ele alma - “son watch olayı ya da LIST’ten itibaren, yeterince güncel” olarak ele al.
- İdempotent, yakınsayan mantık (bölüm 12), bunu güvenli kılan şeydir: hafif bayat verilerle hareket edersen, bir sonraki reconciliation (kendi yazman, drift tespiti, ya da bir reschedule tarafından tetiklenen) bunu düzeltir.
9. Dependent Resources (Bağımlı Kaynaklar) Çatısı
“Custom resource’um, hesaplanan durumu yansıtması gereken bir ConfigMap/Deployment/Service’e sahip” şeklindeki son derece yaygın durum için JOSDK, her secondary kaynak için oluştur-veya-güncelle-ve-izle tekrar eden kodunu elle yazmaman için bildirimsel bir Dependent Resource soyutlaması sunar:
public class ConfigMapDependentResource
extends CRUDKubernetesDependentResource<ConfigMap, WebPage> {
@Override
protected ConfigMap desired(WebPage webPage, Context<WebPage> context) {
return new ConfigMapBuilder()
.withNewMetadata()
.withName(webPage.getMetadata().getName())
.withNamespace(webPage.getMetadata().getNamespace())
.endMetadata()
.addToData("index.html", webPage.getSpec().html())
.build();
}
}
@ControllerConfiguration(dependents = {
@Dependent(type = ConfigMapDependentResource.class),
@Dependent(type = DeploymentDependentResource.class)
})
public class WebPageReconciler implements Reconciler<WebPage> {
// reconcile() artık context.getSecondaryResource(ConfigMap.class)'i çağırabilir
// ve framework, senin reconcile() gövden çalışmadan *önce* onu senin için zaten
// reconcile etmiştir (workflow tabanlı dependent'lar bir ön adım olarak reconcile edilir).
}
Bu sana şunları sağlar:
- Bu kaynak türü için otomatik secondary event source kaydı ve önbellekleme.
- Her reconciliation’da istenen ve gerçek durumu karşılaştırarak otomatik oluşturma/güncelleme (“apply”) mantığı.
- “Sadece X ise bu dependent’ı yönet” ya da “bu dependent Ready raporlamadan parent Ready değildir” ifade etmek için koşullar (
ReconcilePrecondition,ReadyCondition,ActivationCondition) desteği. - v5.2‘den itibaren, klasik bir yarış durumundan kaçınmak için yerleşik bir Expectations deseni (
io.javaoperatorsdk.operator.processing.expectation): kendi son yazman watch üzerinden geri dönmeden önce bayat cache’lenmiş duruma göre hareket etmek.
Tam Dependent Resources kullanmak mı, yoksa reconcile() içinde doğrudan buyuru (imperative) mantık yazmak mı - bu bir yargı meselesidir: Dependent Resources, oldukça standart bir CRUD-ve-izle yaşam döngüsüne sahip birden fazla secondary kaynağın olduğu durumlarda parlar; tek seferlik ya da yüksek düzeyde koşullu mantık için düz buyuru kodu genelde daha net olur.
10. Finalizer’lar ve Temizleme (Cleanup)
Bir custom resource silindiğinde Operator’ının harici temizleme yapması gerekiyorsa (örneğin bir bulut kaynağını provizyondan kaldırmak, bir sertifikayı iptal etmek, harici bir sistemden bir kaydı kaldırmak - Kubernetes’in owner reference tabanlı garbage collection’ının senin için yapamayacağı her şey), Cleaner<P>‘ı implemente et:
public class WebPageReconciler implements Reconciler<WebPage>, Cleaner<WebPage> {
@Override
public UpdateControl<WebPage> reconcile(WebPage resource, Context<WebPage> context) { ... }
@Override
public DeleteControl cleanup(WebPage resource, Context<WebPage> context) {
externalSystem.deprovision(resource.getStatus().getExternalId());
return DeleteControl.defaultDelete(); // finalizer'ı kaldırır, gerçek silmeye izin verir
}
}
Cleaner<P>implemente etmek, JOSDK’nin ilk reconciliation’da custom resource’una otomatik olarak bir finalizer eklemesine yol açar (bir isim belirtmediğin sürece otomatik üretilen bir isimle). Bu, senin finalizer’ın kaldırılana kadar Kubernetes’in nesneyi gerçekten silmesini engeller (.metadata.deletionTimestampayarlamak, silmekle aynı şey değildir).- Temizleme henüz tamamlanamıyorsa (örneğin asenkron bir provizyondan kaldırma işinin bitmesini bekliyorsan),
DeleteControl.noFinalizerRemoval()döndür ve yeniden zamanla - silinmek üzere işaretlenmiş kaynak yeniden reconcile edilecektir. - Harici temizlemeye ihtiyacın yoksa - örneğin tüm secondary kaynakların uygun
ownerReferences‘a sahip Kubernetes nesneleriyse ve Kubernetes’in yerleşik garbage collector’ı onları otomatik olarak silecekse -Cleaner<P>implemente etme. Gereksiz bir finalizer eklemek sadece operasyonel risk ekler (bozuk bir Operator örneğindeki takılı kalmış bir finalizer, silmeyi tamamen engelleyebilir).
11. Hata Yönetimi ve Retry (Yeniden Deneme) Semantiği
reconcile()‘dan fırlatılan herhangi bir istisna, JOSDK tarafından yakalanır, loglanır ve varsayılan olarak üstel backoff ile maksimum deneme sayısı kullanan yapılandırılmış RetryManager‘a devredilir.context.isLastAttempt(), son denemeyi özel olarak ele almanı sağlar - örneğin geçici (transient) bir status yerine nihai bir hata status’u yazmak için:
@Override
public UpdateControl<MyResource> reconcile(MyResource resource, Context<MyResource> context) {
if (context.isLastAttempt()) {
resource.getStatus().setErrorMessage("Tüm yeniden deneme denemelerinden sonra başarısız oldu");
return UpdateControl.patchStatus(resource);
}
// normal mantık
}
- Retry tükenmesi “sonsuza kadar vazgeç” anlamına gelmez. Belirli bir hata serisi için retry bütçesi tükendiğinde, JOSDK otomatik retry’leri durdurur, ama yeni bir watch olayı (spec değişikliği, bağımlı kaynak değişikliği ya da manuel
kubectl annotate/dürtme) reconciliation’ı yeniden başlatır ve bir sonraki başarıda retry sayacını sıfırlar. - Framework’ün kendi rehberliği, retry’leri tamamen devre dışı bırakmayı şiddetle caydırır -
.withNoRetry()‘ı dar, bilinçli durumlar için sakla. - Geçici hataları (ağ dalgalanmaları, API sunucusu kısıtlaması (throttling) - retry’lerin hallettmesine izin ver, belki backoff ayarıyla) kalıcı hatalardan (geçersiz, kurtarılamaz spec - status’ta açıkça göster, muhtemelen bir Kubernetes
Eventaracılığıyla, ve retry’lerin bunu düzelteceği yanılgısını sürdürme) ayırt et.
12. En İyi Pratikler Kontrol Listesi
- Her şeyden önce idempotency. Aynı gözlemlenen durum her zaman aynı sonucu üretmelidir. Asla “bunu ikinci kez görüyorum” varsayımına güvenme - çağrı sayılarını aynı şekilde görme şansın yok; sadece şu anki durumu görürsün.
- Yönettiğin her şeyi, her seferinde reconcile et. “Sadece değişeni akıllıca özel olarak ele almaya” çalışma. İstenen durumu tüm dependent’lar için spec’ten her çağrıda yeniden hesapla; gerçek bir API yazması gerekip gerekmediğine JOSDK’nin karşılaştırma mantığının (ya da kendi eşitlik kontrollerinin) karar vermesine izin ver. Kısmi reconciliation mantığı, drift hatalarının yaygın bir kaynağıdır.
rescheduleAfter/polling yerine event source’ları tercih et. Yeniden zamanlamayı, gerçekten zamana dayalı koşullar (TTL’ler, sertifika süresi dolumu, izleyemediğin sistemlere karşı periyodik sağlık kontrolleri) için sakla.reconcile()içinde asla süresiz olarak bloklama. Asenkron bir koşulu bekliyorsan (bir Pod’un Ready olması, harici bir provizyon işinin bitmesi),UpdateControl.noUpdate()ile erken çık (ilgili event source’un seni yeniden tetiklemesine güvenerek) ya da sınırlı birrescheduleAfterkullan. Metot içinde döngüyle bekleme (spin-wait) yapma - paylaşılan reconciliation thread havuzunu aç bırakırsın. Sınırlı asenkron bekleme yapman gerektiğinde JDK 25’in structured concurrency’si ve virtual thread’lerinin tam olarak yardımcı olduğu yer burasıdır.- Status’ta
observedGeneration‘ı takip et. Bu, hem kendi mantığının hem de harici araçların (dashboard’lar,kubectl wait, GitOps araçları) en son spec’in gerçekten işlenip işlenmediğini bilmesinin deyimsel yoludur. - Status subresource’unu doğru kullan.
speckullanıcının niyetidir;statuscontroller tarafından gözlemlenen/türetilmiş gerçektir. Reconciler’ındanspec‘e anlamlı “kontrol” verisi asla yazma. - Server-side apply için tasarla. Sahip olduğun alanlar için minimal, kasıtlı patch’ler gönder; özellikle paylaşılan nesneler için yönetmediğin alanları körü körüne round-trip yapıp yeniden yazma.
- Otomatik retry’ler dostundur - açık bırak, kapatmak yerine backoff/limitleri ayarla.
- Finalizer’ları sadece gerçekten harici temizlemeye ihtiyacın olduğunda kullan. Owner reference’lar + Kubernetes GC, cluster içi kaynak temizliğini bedavaya halleder.
- Başlangıçta her şeyi reconcile et. Bu, JOSDK’nin varsayılan davranışıdır; bunu “optimize etmeye” çalışma - bütün mesele, Operator kapalıyken biriken sapmayı düzeltmektir.
- Reconciler’ları test edilebilir ve yan etkilerden izole tut. Harici sistem çağrılarını mock’layabileceğin arayüzlerin arkasına it; “bu spec+status+secondary durumu verildiğinde, şu UpdateControl’ü üret” mantığını izole olarak test et.
- Her log satırında kaynak kimliği (namespace/name/UID) ile logla - eşzamanlı reconcile edilen birçok kaynak arasında logları ilişkilendirmen gerekecek.
- Kullanıcıya yönelik, insan tarafından okunabilir durum geçişleri için Kubernetes
Events‘i yayınla (sadece status alanları değil) -kubectl describe‘da görünen ve operator’lar için deyimsel kullanıcı deneyimi budur. - Şema değişeceğini bekliyorsan CRD’ini versiyonla ve conversion webhook’larını erkenden planla -
v1alpha1 → v1dönüşümlerini canlı bir filoya sonradan eklemek, önceden buna göre tasarlamaktan çok daha zordur.
13. Uçtan Uca Çalışan Örnek
@Group("example.com")
@Version("v1")
public class WebPage extends CustomResource<WebPageSpec, WebPageStatus> implements Namespaced {}
public record WebPageSpec(String html) {}
public class WebPageStatus {
private boolean ready;
private Long observedGeneration;
private String errorMessage;
// getter & setter'lar kısalık için atlandı
}
@ControllerConfiguration
public class WebPageReconciler implements Reconciler<WebPage>, Cleaner<WebPage> {
private static final Logger log = LoggerFactory.getLogger(WebPageReconciler.class);
@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {
var name = webPage.getMetadata().getName();
var ns = webPage.getMetadata().getNamespace();
log.info("{}/{} reconcile ediliyor", ns, name);
ConfigMap desired = new ConfigMapBuilder()
.withNewMetadata().withName(name).withNamespace(ns).endMetadata()
.addToData("index.html", webPage.getSpec().html())
.build();
context.resourceOperations().serverSideApply(desired);
webPage.getStatus().setReady(true);
webPage.getStatus().setObservedGeneration(webPage.getMetadata().getGeneration());
webPage.getStatus().setErrorMessage(null);
return UpdateControl.patchStatus(webPage);
}
@Override
public ErrorStatusUpdateControl<WebPage> updateErrorStatus(
WebPage resource, Context<WebPage> context, Exception e) {
resource.getStatus().setErrorMessage(e.getMessage());
return ErrorStatusUpdateControl.patchStatus(resource);
}
@Override
public DeleteControl cleanup(WebPage resource, Context<WebPage> context) {
log.info("{}/{} temizleniyor", resource.getMetadata().getNamespace(),
resource.getMetadata().getName());
return DeleteControl.defaultDelete();
}
}
public class Main {
public static void main(String[] args) {
Operator operator = new Operator();
operator.register(new WebPageReconciler());
operator.start();
}
}
14. Operator’ını Test Etmek
JOSDK, operator-framework-junit-5 paketiyle LocalizedOperatorExtension / OperatorExtension‘ı sunar; bu, entegrasyon testleri için reconciler’ını gerçek (ya da envtest tarzı) bir Kubernetes API sunucusuna karşı çalıştırır:
@RegisterExtension
static LocalizedOperatorExtension operator = LocalizedOperatorExtension.builder()
.withReconciler(new WebPageReconciler())
.build();
@Test
void webPagedenConfigMapOlusturur() {
WebPage wp = new WebPage();
wp.setMetadata(new ObjectMetaBuilder().withName("test").build());
wp.setSpec(new WebPageSpec("<h1>Merhaba</h1>"));
operator.create(wp);
await().untilAsserted(() -> {
WebPage updated = operator.get(WebPage.class, "test");
assertThat(updated.getStatus().isReady()).isTrue();
});
}
reconcile()‘ın saf karar mantığını da ayrı olarak birim (unit) test et: “istenen durumu hesapla” ve “gözlemlenen durumdan status’u hesapla” kısımlarını, hiçbir Kubernetes API’sine ihtiyaç duymadan test edebileceğin sade fonksiyonlara çıkararak.
15. Gözlemlenebilirlik, Leader Election, Dağıtım
- Leader election: Yüksek erişilebilirlik (high-availability) dağıtımları için (birden fazla Operator replikası), JOSDK leader election’ı destekler; böylece herhangi bir anda sadece bir örnek aktif olarak reconcile eder, tekrar eden işleri ve çakışan yazmaları önler.
LeaderElectionConfigurationüzerinden yapılandırılır. - Metrikler: JOSDK, Micrometer tabanlı metrikler (reconciliation sayıları, süreleri, kuyruk boyutları) sunar - bunları Prometheus’a bağlayarak reconciliation hata oranları ve gecikme için dashboard/alarm kurabilirsin.
- RBAC: Operator’ının ServiceAccount’unun, izlediği/okuduğu/yazdığı her kaynak türü için - hem primary CRD hem de her dependent tür (ConfigMap’ler, Deployment’lar vb.) için, ayrıca ilgili olduğunda
statusvefinalizerssubresource izinleri için - RBAC kurallarına ihtiyacı vardır. - Container imajı: JDK 25 kullandığın için, hızlı başlayan, düşük bellek ayak izine sahip Operator pod’ları için (Quarkus Operator SDK üzerinden iyi desteklenen) GraalVM native-image build’lerini değerlendir - ölçekte cluster kaynak bütçeleri için anlamlı bir kazanım.
16. Kaynaklar
- Java Operator SDK dokümantasyonu - https://javaoperatorsdk.io/docs/
- JOSDK “Implementing a reconciler” - https://javaoperatorsdk.io/docs/documentation/reconciler/
- JOSDK “Patterns and best practices” - https://javaoperatorsdk.io/docs/getting-started/patterns-best-practices/
- JOSDK “Error handling and retries” - https://javaoperatorsdk.io/docs/documentation/error-handling-retries/
- JOSDK v5.2 sürüm notları (Expectations deseni) - https://javaoperatorsdk.io/blog/2025/11/25/version-5.2-released/
- JOSDK v5.3 sürüm notları (kendi yazma event filtreleme,
reschedule()) - https://javaoperatorsdk.io/blog/2026/03/13/version-5.3-released/ - JOSDK “From legacy approach to server-side apply” - https://javaoperatorsdk.io/blog/2025/02/25/from-legacy-approach-to-server-side-apply/
- JOSDK GitHub deposu - https://github.com/operator-framework/java-operator-sdk
- OpenJDK JDK 25 proje sayfası - https://openjdk.org/projects/jdk/25/
Bu rehber, 2026 yılının ilk yarısı itibarıyla JOSDK ve JDK’nin durumunu yansıtır. Framework hızlı geliştiği için (örneğin server-side apply varsayılanları ve event filtreleme davranışı görece yakın zamanlı major/minor sürümlerde tanıtıldı), bağımlı olduğun tam sürüm için her zaman resmi dokümantasyonu kontrol et.