GitOps Nedir? Push Tabanlı CD'den Pull Tabanlı Reconciliation'a ve Ne Zaman Gerekmez
GitOps, istenen durumun Git'te declarative olarak tutulduğu ve cluster içindeki bir agent'ın bu durumu sürekli çekip uyguladığı bir deploy modeli. Push tabanlı bir pipeline'ın drift'i neden göremediğini gerçek çıktılarla, OpenGitOps'un dört prensibini, oyuncak bir reconcile döngüsünü, repo düzeni kararlarını, Argo CD ile Flux farkını ve GitOps'un ne zaman gereksiz olduğunu görüyoruz.
Sürüm notu: Bu yazıdaki örnekler kind 0.33 ile Kubernetes 1.37, kubectl 1.37, Kustomize 5.8, PostgreSQL 18 ve .NET 10 ile denenmiştir.
Önce Tahmin Et
Bookstore API'si bir CI pipeline'ının çalıştırdığı kubectl apply -k ile cluster'a gidiyor. Gece bir olay oluyor ve nöbetçi arkadaş iki şey yapıyor: Deployment'ı kubectl scale ile 1'den 3 replica'ya çıkarıyor, bir de kubectl set env ile log seviyesini Debug yapıyor. Ertesi sabah pipeline Git'teki manifest'lerle tekrar çalışıyor. Sence hangisi geri alınır: replica sayısı mı, environment variable mı, ikisi birden mi? Cevap bu yazının başlangıç noktası ve aşağıda gerçek çıktısıyla duruyor.
Push Tabanlı Deploy: Pipeline'ın Elinde Kubeconfig
Serinin örneği, sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı). Kubernetes serisinin on dördüncü yazısında Bookstore manifest'lerini Kustomize ile base ve overlay olarak düzenlemiştik; bu seride aynı yapıyı ayrı bir config repo'suna taşıyoruz. Veritabanı, Kubernetes serisinde olduğu gibi cluster içinde çalışan bir PostgreSQL. Bookstore'un varsayılanı SQL Server; API, Database__Provider=Postgres ile PostgreSQL'e geçiyor ve PostgreSQL migration'ları ayrı bir projede duruyor. Config repo'sunun ilk hâli:
apps/
api/
base/
kustomization.yaml
deployment.yaml
service.yaml
postgres.yaml
overlays/
dev/
kustomization.yaml
Bugün birçok ekibin deploy'u şöyle görünür: pipeline image'ı build eder, registry'ye push eder, sonra cluster'a bağlanıp manifest'leri uygular. Bizim lab'deki karşılığı tek bir script:
#!/usr/bin/env bash
# Push tabanlı deploy: CI job'unun kubectl'i kendisinin çalıştırdığı model.
# Cluster'a yazma yetkisi olan bir kubeconfig gerekir.
set -euo pipefail
overlay="${1:-apps/api/overlays/dev}"
kubectl apply -k "$overlay"
kubectl -n bookstore-dev rollout status deployment/api --timeout=120s
service/api created
service/postgres created
deployment.apps/api created
statefulset.apps/postgres created
Waiting for deployment "api" rollout to finish: 0 of 1 updated replicas are available...
deployment "api" successfully rolled out
Çalışıyor ve 16 saniye sürdü. Asıl soru şu: bu script'i çalıştıran job'un elinde ne var? Cluster'a yazabilen bir kubeconfig. GitHub Actions serisinin deploy yazılarında bu credential'ı secret olarak saklamayı ve OIDC (OpenID Connect; kısa ömürlü token ile kimlik doğrulama) ile kısaltmayı gördük, ama ilke değişmiyor: cluster'ın anahtarı cluster'ın dışında, CI sisteminde duruyor. Pipeline'ın her adımı, her üçüncü parti action ve her runner bu anahtara yakın. Bir de akış tek yönlü: pipeline yalnızca çalıştığı anda bir şey yapıyor, arada cluster'da ne olduğunu bilmiyor.
Drift: Cluster'ın Git'ten Ayrılması
Önce Tahmin Et'teki iki değişikliği yapalım ve Git'teki hâliyle farkına bakalım:
kubectl -n bookstore-dev scale deployment/api --replicas=3
kubectl -n bookstore-dev set env deployment/api Logging__LogLevel__Default=Debug
kubectl diff -k apps/api/overlays/dev
@@ -16,7 +16,7 @@
spec:
progressDeadlineSeconds: 600
- replicas: 3
+ replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
Cluster'ın Git'te yazandan farklı hâle gelmesine drift deniyor. kubectl diff bir drift görüyor: replica sayısı. Environment variable ise diff'te yok. Pipeline'ı tekrar çalıştırıp sonucu soralım:
deployment.apps/api configured
statefulset.apps/postgres configured
deployment "api" successfully rolled out
1
ASPNETCORE_ENVIRONMENT
Database__Provider
ConnectionStrings__BookstoreDb
Logging__LogLevel__Default
Önce Tahmin Et'in cevabı: replica 1'e döndü, Logging__LogLevel__Default yerinde kaldı. Sebep client-side apply'ın çalışma şekli. kubectl apply son uyguladığı hâli kubectl.kubernetes.io/last-applied-configuration annotation'ında saklar ve üç şeyi karşılaştırır: son uygulanan, şimdi uygulanacak ve canlı nesne. replicas alanı son uygulananın içinde olduğu için geri yazılır. Elle eklenen env girişi ise hiçbir zaman apply edilmediği için kubectl'in sorumluluğunda değil; başkasının (bir controller'ın, bir mutating webhook'un) eklediği alan gibi dokunulmadan bırakılır. Server-side apply'da da sonuç aynı: o alanın sahibi kubectl set env, apply değil.
Bu yüzden push modelinde iki ayrı sorun var. Birincisi, drift bir sonraki pipeline koşusuna kadar fark edilmez; pipeline haftada bir çalışıyorsa bir hafta. İkincisi, bir sonraki koşu bile drift'in bir kısmını görmez. Git'te olmayan ama cluster'da çalışan ayarlar zamanla birikir ve bir gün "staging'de çalışıyordu" sorusunun cevabı olur.
OpenGitOps'un Dört Prensibi
GitOps kelimesi 2017'de Weaveworks'ün yazılarıyla yaygınlaştı; bugün tanımı CNCF (Cloud Native Computing Foundation; Kubernetes'in de bağlı olduğu vakıf) bünyesindeki OpenGitOps çalışma grubunun yayımladığı prensipler (v1.0.0). Dört prensibi kendi cümlelerimizle ve her birinin Bookstore'daki karşılığıyla:
| Prensip | Ne istiyor | Bookstore'da karşılığı |
|---|---|---|
| Declarative | Sistemin istenen hâli adım listesi değil, varılacak hâl olarak tarif edilir | Kustomize base ve overlay'leri; script değil, manifest |
| Versioned and immutable | İstenen hâl değiştirilemeyen, sürümlenen ve geçmişi tutulan bir yerde saklanır | Config repo'sunda her değişiklik bir commit; geçmiş silinmez |
| Pulled automatically | Bir agent istenen hâli kaynaktan kendisi çeker | Cluster içindeki agent Git'i okur; CI cluster'a bağlanmaz |
| Continuously reconciled | Agent gerçek hâli sürekli izler ve istenen hâle getirmeye çalışır | Drift birkaç saniye içinde fark edilir ve kapatılır |
Dikkat edersen prensiplerde "Git" kelimesi geçmiyor; tarif edilen şey bir state store. Pratikte bu neredeyse her zaman Git, çünkü sürümleme, değiştirilemeyen geçmiş, pull request ile review ve imzalı commit'ler hazır geliyor. Kubernetes serisinin ilk yazısında gördüğümüz desired state ve reconciliation loop fikrinin bir kat yukarı taşınmış hâli bu: Kubernetes, Deployment nesnesini pod'larla eşitliyor; GitOps agent'ı da Git'teki manifest'leri Kubernetes nesneleriyle eşitliyor.
Pull Modeli: Döngüyü Cluster'ın İçine Almak

Pull modelinde pipeline cluster'a hiç dokunmaz. Pipeline'ın işi image'ı build edip config repo'suna bir commit (ya da pull request) açmakla biter. Cluster içinde çalışan agent Git'i izler, değişikliği görünce uygular, aradaki zamanlarda da canlı hâli Git'le karşılaştırır. Credential da yer değiştirir: agent bir ServiceAccount ve RBAC (Role-Based Access Control; Kubernetes'in rol tabanlı yetkilendirmesi) ile çalışır, cluster'ın anahtarı cluster'dan çıkmaz. Dışarıya açılan tek şey Git'e okuma yetkisidir.
Fikri görmek için Argo CD'ye geçmeden önce oyuncak bir döngü yazdık. Bu script production için değil; bir GitOps agent'ının özünü birkaç satırda gösteriyor:
#!/usr/bin/env bash
# Oyuncak bir pull döngüsü: GitOps agent'ının temel fikri. Production için değil.
# kullanım: ./reconcile-loop.sh <git-clone> <kustomize-path> [interval-seconds]
set -uo pipefail
clone="$1"; path="$2"; interval="${3:-30}"
while true; do
git -C "$clone" pull --ff-only -q
rev="$(git -C "$clone" rev-parse --short HEAD)"
kubectl diff --server-side --force-conflicts -k "$clone/$path" > /dev/null 2>&1
case $? in
0) echo "$(date +%T) $rev in sync" ;;
1) echo "$(date +%T) $rev drift detected, applying"
kubectl apply --server-side --force-conflicts -k "$clone/$path" | grep -v unchanged ;;
*) echo "$(date +%T) $rev diff failed" >&2 ;;
esac
sleep "$interval"
done
kubectl diff fark yoksa 0, fark varsa 1, hata olursa daha büyük bir kodla çıkar; döngü buna göre karar veriyor. İlk denemede --force-conflicts'u yalnız apply'a yazmıştık ve diff her turda "conflict with kubectl with subresource scale" hatası verdi: server-side diff de alan sahipliğine bakıyor. Döngüyü 10 saniyelik aralıkla bir clone üzerinde başlattık, sonra üç şey yaptık: replica'yı elle 3'e çıkardık, Git'e "dev'de iki replica" commit'i attık ve bu commit'i git revert ile geri aldık. Döngünün log'u, her turda tekrar eden apply satırları kısaltılarak:
06:04:36 224edf2 in sync
06:04:46 224edf2 in sync
06:04:56 224edf2 drift detected, applying
deployment.apps/api serverside-applied
06:05:07 729d324 drift detected, applying
deployment.apps/api serverside-applied
06:05:17 df25766 drift detected, applying
deployment.apps/api serverside-applied
06:05:27 df25766 in sync
06:04:47'de yapılan elle scale bir sonraki turda geri alındı. 729d324 commit'i bir tur sonra cluster'daydı ve deployment 2 replica'ya çıktı; revert commit'i df25766 de bir tur sonra uygulandı ve replica tekrar 1 oldu. Rollback için kimse cluster'a bağlanmadı: rollback bir git revert. Git & GitHub serisindeki "Git ile Değişiklikleri Geri Almak 2: git revert" yazısında anlattığımız komut burada bir deploy aracına dönüşüyor ve geçmiş de kendiliğinden bir audit trail (kimin neyi ne zaman değiştirdiğinin kaydı) oluyor: her değişikliğin yazarı, zamanı, gerekçesi ve onaylayan reviewer'ı Git'te.
Oyuncak döngü bile Önce Tahmin Et'teki env değişkenini silmedi; server-side apply da sahibi olmadığı alana dokunmuyor. Bu sınır Argo CD'de de var ve serinin üçüncü yazısında aynı deneyi Argo CD'nin self-heal özelliğiyle tekrarlayacağız. Döngünün eksikleri de çok: health bilgisi yok, silinen manifest'leri temizlemiyor (prune), hangi commit'in canlıda olduğunu bir yere yazmıyor, birden çok uygulama ve cluster yönetemiyor. Argo CD bu eksiklerin her birini dolduran bir controller.
Repo Düzeni: Uygulama Repo'su, Config Repo'su ve Klasör-per-Environment
GitOps'a geçerken ilk karar manifest'lerin nerede duracağı. Yaygın seçenek iki repo: uygulama repo'su (Bookstore'un kaynak kodu, Dockerfile'lar, testler) ve config repo'su (Kubernetes manifest'leri, environment overlay'leri, Argo CD tanımları). Ayrı repo'nun üç faydası var. Uygulama repo'suna her commit'te deploy tetiklenmez; yalnız config repo'sundaki değişiklik deploy'dur. Yetki ayrılır: geliştirici uygulama kodunu merge edebilir ama prod overlay'ine yazmak için ayrı bir onay gerekebilir. Geçmiş de okunur kalır: config repo'sunun log'u doğrudan deploy geçmişidir. Bedeli, CI'ın build sonrası config repo'suna commit atması; bunu serinin altıncı yazısında kuracağız.
Küçük bir ekip için monorepo (kod ve deploy/ klasörü aynı repo'da) da işe yarar. O durumda Argo CD'nin yalnız deploy/ altını izlemesi ve CI'ın kendi açtığı tag commit'iyle sonsuz döngüye girmemesi gerekir. Serinin lab'inde config repo'su ayrı; snapshot'larda ise okur tek repo'dan takip edebilsin diye gitops/ klasöründe duruyor.
Environment'ları nasıl ayıracağın ikinci karar. Eski ve hâlâ sık görülen yöntem branch-per-environment: dev, staging, prod branch'leri ve aralarında merge ile promotion. Pratikte bu anti-pattern'e döner: branch'ler arasında yalnız sürüm değil, ortama özel farklar da taşınır; her merge'de çakışmalar çıkar, bir hotfix bir branch'e girip diğerine girmez ve "prod'da ne var?" sorusunun cevabı iki branch'in diff'ine gömülür. Git & GitHub serisinin branch stratejileri yazısında kısa ömürlü branch'leri neden önerdiğimizi görmüştük; environment'lar uzun ömürlü branch olmamalı. Önerilen yol klasör-per-environment: tek main branch'i, overlays/dev, overlays/staging, overlays/prod klasörleri ve promotion = bir overlay'deki image tag'ini değiştiren commit. Bütün environment'lar tek bakışta görünür, farkları da Kustomize'ın overlay'lerinde açıkça yazılı durur.
Argo CD mi Flux mı?
Kubernetes için iki olgun GitOps aracı var ve ikisi de CNCF'de graduated (vakfın en olgun seviyesi) projeler: Flux Kasım 2022'de, Argo Aralık 2022'de bu seviyeye geçti. Yazım günündeki son sürümler Argo CD 3.6.1 (4 Kasım 2026) ve Flux 2.10.0 (26 Ekim 2026).
Farkları kabaca şöyle. Argo CD merkezi bir uygulama: web UI, CLI, kendi API server'ı, SSO (Single Sign-On; şirket hesabıyla tek seferde giriş), kendi RBAC'ı ve Application adlı bir CRD (Custom Resource Definition; Kubernetes'e eklenen özel nesne tipi) var. Bir platform ekibinin onlarca ekibe "uygulamalarını buradan izle ve sync et" diye sunduğu bir ürün gibi davranır. Flux ise bir toolkit: GitRepository, Kustomization, HelmRelease gibi küçük controller'lar ve CRD'lerden oluşur, varsayılan bir UI'ı yoktur, her şey Kubernetes API'si ve CLI üzerinden yönetilir. Flux daha "Kubernetes-native", Argo CD ise ekipler arası görünürlükte daha güçlü.
Bu seri Argo CD ile ilerliyor. Sebebi Bookstore ekibinin ihtiyacı: birden çok environment'ı tek ekranda görmek, sync ve health durumunu geliştiricilere göstermek, ApplicationSet ile environment'ları üretmek, Argo Rollouts ile progressive delivery'e geçmek. Kavramlar (reconcile, drift, prune, health) iki araçta da aynı; Flux'a ne zaman bakılacağını serinin son yazısında tartışacağız.
Ne Zaman Gerekmez
GitOps bir maliyet. Cluster'da çalışan bir agent, onun upgrade'i, ayrı bir config repo'su, commit atan bir CI ve yeni bir hata ayıklama alışkanlığı demek. Şu durumlarda gerekmeyebilir:
- Tek bir VM ve Docker Compose:
docker compose upçalıştıran bir pipeline yeterli, reconcile edilecek bir control plane yok. - Azure App Service, Container Apps gibi PaaS hedefleri: platform zaten deploy geçmişi, slot ve rollback veriyor; GitHub Actions'tan push tabanlı deploy sade ve yeterli.
- Tek cluster, tek uygulama, küçük ekip: kimse elle değişiklik yapmıyorsa drift sorunu da küçüktür; önce pipeline'ı düzgün kur.
GitOps fikri Kubernetes'e bağlı değil; prensipler bir state store ve reconcile eden bir agent istiyor. Ama araçlar (Argo CD, Flux) Kubernetes API'si üzerine kurulu. Kubernetes dışında Terraform kodunu pull request ile plan/apply eden akışlar bu fikre yaklaşır, ancak sürekli reconcile kısmı genelde eksik kalır. Infrastructure as Code: Terraform serisinde drift tespitini terraform plan ile nasıl yaptığımızı görmüştük.
Üretimde Dikkat
GitOps'a geçiş çoğu zaman var olan bir cluster üzerinde olur. Agent mevcut kaynakları devraldığında ilk karşılaştırma bütün elle yapılmış farkları ortaya çıkarır; prune'u (Git'te olmayanı silme) ilk günden açma, önce farkları gör. Config repo'su artık production'ın kendisi: main'e doğrudan push'u kapat, prod overlay'ine CODEOWNERS ile onay zorunluluğu koy, commit imzalamayı düşün. Git servisi de kritik hâle gelir: Git erişilemezse çalışan uygulama etkilenmez ama yeni deploy yapılamaz. Acil durumlar için yazılı bir "break glass" (cam kırma; kuralların bilinçli olarak askıya alındığı acil durum yolu) prosedürü olsun: kim, hangi yetkiyle, cluster'a elle müdahale eder ve müdahale sonra Git'e nasıl yazılır.
Sık Düşülen Tuzaklar
"GitOps = Git'te YAML (manifest'lerin yazıldığı metin formatı) tutmak" sanmak. Manifest'ler Git'te ama pipeline hâlâ kubectl apply çalıştırıyorsa model push'tur; pull ve reconcile yoksa drift de görünmez.
Bir sonraki apply'ın her şeyi düzelteceğini varsaymak. Yukarıda gördük: elle eklenen alanlar apply'dan sağ çıkar. Düzeltme Git'te yapılmalı.
Environment'ları branch'lerle ayırmak ve promotion'ı merge'le yapmak. Klasörler ve overlay'ler hem okunur hem de diff'i anlamlı.
Secret'ları "her şey Git'te olmalı" diye düz metin olarak commit'lemek. Bu yazıda veritabanı şifresini lab script'i Git'in dışında üretiyor; doğru yöntemler serinin yedinci yazısında.
Bir uygulama ve bir VM için Argo CD kurmak. Araç, çözdüğü problemden büyük olmamalı.
Küçük Bir Deneme
★☆☆ Push script'ini çalıştırdıktan sonra Deployment'a kubectl annotate ile bir annotation ekle. kubectl diff -k bunu gösteriyor mu? Sonucu last-applied-configuration'ın içeriğiyle açıkla.
★★☆ Oyuncak döngüyü çalıştırırken base'den postgres.yaml'ı kaldıran bir commit at. Döngü StatefulSet'i siliyor mu? Gerçek bir agent'ta bu davranışı neyin belirlediğini (prune) düşün.
★★★ Kendi ekibinin deploy akışını çiz: credential nerede duruyor, drift'i kim ve ne kadar sürede fark ediyor, rollback kaç adım? Aynı akışı pull modeline çevirdiğinde hangi adımların ortadan kalktığını ve hangi yeni bileşenin işletilmesi gerektiğini listele.
Fikir ve oyuncak döngü tamam. Bir sonraki yazıda Argo CD'yi kind cluster'ına kuruyor, component'lerine bakıyor ve ilk Application ile Bookstore'u Git'ten deploy ediyoruz.