Java Operator SDK (JOSDK) ile Kubernetes Operator Geliştirme

30 Temmuz 2026 · netologist · 21 dakika, 4382 kelime ·

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

  1. Kubernetes Operator Aslında Nedir?
  2. Operator Geliştirme İçin Neden JDK 25?
  3. Proje Kurulumu
  4. JOSDK’nin Anatomisi
  5. Reconciler Arayüzü, Baştan Sona
  6. Reconciliation Döngüsü Gerçekte Nasıl Çalışır?
  7. UpdateControl ve ErrorStatusUpdateControl Derinlemesine
  8. Event Source’lar, Informer’lar ve Önbellekleme
  9. Dependent Resources (Bağımlı Kaynaklar) Çatısı
  10. Finalizer’lar ve Temizleme (Cleanup)
  11. Hata Yönetimi ve Retry (Yeniden Deneme) Semantiği
  12. En İyi Pratikler Kontrol Listesi
  13. Uçtan Uca Çalışan Örnek
  14. Operator’ını Test Etmek
  15. Gözlemlenebilirlik, Leader Election, Dağıtım
  16. 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:

flowchart LR A["gözlemle"] --> B["karşılaştır (istenen vs gerçek)"] --> C["eyleme geç"] --> D["tekrarla"] D -.-> A

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:

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:

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şenSorumluluk
OperatorEn ü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.
EventSourceBir kaynak türünü (primary veya secondary) izler ve işleme kuyruğuna olay yayınlar.
Informer / CacheKubernetes Watch API’sine dayanan, izlenen kaynakların yerel, bellek içi, nihai olarak tutarlı (eventually-consistent) bir aynası.
Workflow / DependentResourcePrimary kaynağının “sahip olduğu” ikincil kaynakları (ConfigMap’ler, Deployment’lar vb.) yönetmek için opsiyonel bir bildirimsel (declarative) katman.
RetryManager / RateLimiterReconciliation 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:

  1. resource‘tan (şu an bilinen haliyle primary kaynak - spec + status + metadata) ne okuduğun.
  2. Ne yaptığın (bağımlı kaynakları oluşturmak/güncellemek için Kubernetes API’sini çağırmak, harici sistemleri çağırmak vb.).
  3. 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ı

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:

  1. 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.
  2. 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).
  3. 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:

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:

  1. (Herhangi bir event source’tan gelen) bir olay, reconcile edilecek bir ResourceID‘ye (namespace + name) dönüştürülür.
  2. 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.
  3. 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 ve reconcile() metodunu çağırır.
  4. 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:

  1. Neyin kalıcı hale getirileceğini belirlemek için UpdateControl‘ü inceler (status patch, resource patch, ikisi birden, ya da hiçbiri).
  2. İlgili Kubernetes API isteğini/isteklerini, genellikle bir server-side apply PATCH olarak gönderir.
  3. 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.
  4. Bir rescheduleAfter veya reschedule() 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.
  5. 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):

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:

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);

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:

  1. 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.
  2. 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çin UpdateControl/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.

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:

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:

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

11. Hata Yönetimi ve Retry (Yeniden Deneme) Semantiği

@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
}

12. En İyi Pratikler Kontrol Listesi

  1. 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.
  2. 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.
  3. 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.
  4. 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ı bir rescheduleAfter kullan. 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.
  5. 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.
  6. Status subresource’unu doğru kullan. spec kullanıcının niyetidir; status controller tarafından gözlemlenen/türetilmiş gerçektir. Reconciler’ından spec‘e anlamlı “kontrol” verisi asla yazma.
  7. 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.
  8. Otomatik retry’ler dostundur - açık bırak, kapatmak yerine backoff/limitleri ayarla.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. Şema değişeceğini bekliyorsan CRD’ini versiyonla ve conversion webhook’larını erkenden planla - v1alpha1 → v1 dö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

16. Kaynaklar


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.