Mert Özen Her Satırda Daha İleri
DevOps GitOps ve Argo CD — #05

Çok Environment, Çok Uygulama: App of Apps, ApplicationSet Generator'ları ve AppProject

Mert Özen 15 Kas 2026 10 dk 7 görüntülenme
Çok Environment, Çok Uygulama: App of Apps, ApplicationSet Generator'ları ve AppProject

Çok sayıda uygulama ve environment'ı Argo CD'de yönetmenin yolu, Application'ları klasör yapısından üreten bir ApplicationSet'i App of Apps root'uyla Git'ten uygulamak ve sınırları AppProject ile çizmek. Bookstore'un api ve web uygulamalarını dev, staging ve prod için tek bir ApplicationSet ile üretiyor, RollingSync ile ortamları sırayla güncelliyor, AppProject'in yasak bir namespace'i reddetmesini ve preserveResourcesOnDeletion'ın etkisini gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler kind 0.33 ile Kubernetes 1.37, Argo CD 3.6.1, Kustomize 5.8, .NET 10 ve Angular 22 ile denenmiştir.

Önce Tahmin Et

Bookstore'un iki uygulaması (api ve web) üç environment'ta (dev, staging, prod) tek bir ApplicationSet'ten üretiliyor ve ApplicationSet'in preserveResourcesOnDeletion: true ayarı var. Biri config repo'sunda apps/web/overlays/staging klasörünü silip commit atıyor. ApplicationSet'in ürettiği bookstore-web-staging Application'ına ne olur, staging'deki web pod'larına ne olur? Bu iki sorunun cevabı farklı ve yazının sonunda gerçek çıktısıyla duruyor.

Altı Application'ı Elle Yazmak

Şimdiye kadar sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) yalnız API'sini, yalnız dev'de ve tek bir Application YAML (manifest'lerin yazıldığı metin formatı) dosyasıyla yönettik. Bu yazıda Angular istemcisi bookstore-web da geliyor ve ortamlar üçe çıkıyor. Config repo'su artık şöyle:

apps/
  api/
    base/
    overlays/
      dev/
      staging/
      prod/
  web/
    base/
    overlays/
      dev/
      staging/
      prod/
argocd/
  root.yaml
  bootstrap/
    project.yaml
    applicationset.yaml

Web image'ı nginx üzerinde Angular build'ini sunuyor ve /api/ isteklerini aynı environment'taki api Service'ine iletiyor. Hangi environment'ta olduğunu pod kendi namespace'inden öğreniyor; manifest'te ortam adı yazmıyor:

          env:
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            # nginx /api/ isteklerini aynı environment'taki api Service'ine iletir.
            - name: API_UPSTREAM
              value: http://api.$(POD_NAMESPACE).svc.cluster.local

Prod overlay'leri ikişer replica istiyor, staging ve dev base'i olduğu gibi kullanıyor. İki uygulama ve üç ortam altı Application demek. Bunları ikinci yazıdaki gibi altı YAML dosyası olarak yazmak mümkün; ama dördüncü ortamda dosya sayısı sekiz, üçüncü uygulamada on iki olur ve hepsi birbirinin kopyası. Argo CD bunun için iki yol sunuyor: App of Apps ve ApplicationSet.

App of Apps: Application'ları Yöneten Application

App of Apps'te bir "root" Application, içinde başka Application YAML'ları olan bir klasörü izler. Argo CD Application'ın kendisi de bir Kubernetes nesnesi olduğu için root onları diğer manifest'ler gibi uygular. Bizim root'umuz elle uyguladığımız tek nesne:

# App of Apps root'u: elle yaratılan tek Application (kubectl apply -f argocd/root.yaml).
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: bookstore-root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: http://git-server.git-server.svc.cluster.local/bookstore-config
    targetRevision: main
    path: argocd/bootstrap
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Bu, ikinci yazıda kubectl apply ile uyguladığımız Application YAML'ının sorununu çözüyor: artık Argo CD'nin tanımlarını da Argo CD uyguluyor, biri UI'dan bir Application'ı değiştirirse root onu Git'teki hâline geri çekiyor. Cluster'ı sıfırdan kurarken (felaket sonrası ya da yeni bir bölgede) tek komut yetiyor. Sınırları da var. Root'un altındaki her Application yine elle yazılmış bir dosya; App of Apps tekrar eden yapıyı üretmez, yalnız bir araya toplar. Root, alt Application'ların health'ini varsayılan olarak beklemez; Application health check'i 1.8'de kaldırıldığı için alt uygulamalar arasında wave ile sıra kurmak isterseniz bu check'i argocd-cm'de geri eklemek gerekir. Bir de root'u silmek, finalizer'ı varsa bütün alt Application'ları ve onların kaynaklarını silmek demek. Biz root'un altına tek tek Application yerine bir AppProject ve bir ApplicationSet koyduk.

ApplicationSet: Klasörlerden Application Üretmek

ApplicationSet controller'ı bir ya da daha fazla generator'dan parametre listesi üretir ve her parametre seti için bir template'ten bir Application yaratır. Generator'lar:

GeneratorParametreleri nereden alırNe zaman
listApplicationSet'e elle yazılmış eleman listesiAz sayıda, nadiren değişen ortam
git (directories)Repo'daki klasörlerin path'leriKlasör-per-environment düzeni
git (files)Repo'daki JSON/YAML dosyalarının içeriğiOrtama özel değerler (cluster adı, replica, domain)
clusterArgo CD'ye kayıtlı cluster'lar ve label'larıAynı uygulamayı her cluster'a dağıtmak
matrix / mergeİki generator'ın çarpımı ya da birleşimiHer cluster × her uygulama gibi kombinasyonlar

Klasör yapımız zaten bilgiyi taşıdığı için git directory generator'ı yetiyor. Bookstore'un ApplicationSet'i:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: bookstore
  namespace: argocd
spec:
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    # Her klasör için bir Application: apps/<component>/overlays/<env>
    - git:
        repoURL: http://git-server.git-server.svc.cluster.local/bookstore-config
        revision: main
        directories:
          - path: apps/*/overlays/*
  strategy:
    type: RollingSync
    rollingSync:
      steps:
        - matchExpressions:
            - key: env
              operator: In
              values: [dev]
        - matchExpressions:
            - key: env
              operator: In
              values: [staging]
        - matchExpressions:
            - key: env
              operator: In
              values: [prod]
          maxUpdate: 1
  syncPolicy:
    # ApplicationSet'i (ya da bir klasörü) silmek çalışan workload'ları silmesin.
    preserveResourcesOnDeletion: true
  template:
    metadata:
      name: 'bookstore-{{index .path.segments 1}}-{{.path.basename}}'
      labels:
        component: '{{index .path.segments 1}}'
        env: '{{.path.basename}}'
    spec:
      project: bookstore
      source:
        repoURL: http://git-server.git-server.svc.cluster.local/bookstore-config
        targetRevision: main
        path: '{{.path.path}}'
      destination:
        server: https://kubernetes.default.svc
        namespace: 'bookstore-{{.path.basename}}'
      syncPolicy:
        syncOptions:
          - CreateNamespace=true
          - PruneLast=true

apps/api/overlays/staging gibi bir path için .path.segments [apps, api, overlays, staging], .path.basename staging olur; template'ten bookstore-api-staging adında, bookstore-staging namespace'ine giden bir Application çıkar. goTemplate: true Go template'lerini (index, if, printf) açar; missingkey=error ise yanlış yazılmış bir parametre adının sessizce boş string'e dönüşmesini engeller. Ortama göre farklı bir Application ayarı gerekirse (ör. yalnız dev'de automated sync) template'e templatePatch ile Go template koşulu yazılabilir.

Geçiş: Var Olan Application'ı Devretmek

bookstore-api-dev Application'ı ikinci yazıdan beri elle uygulanmış bir nesneydi; ApplicationSet aynı adla bir Application üretecekti. Eskisini, kaynaklarına dokunmadan silmek için önce finalizer'ını kaldırdık. Lab tarafında staging ve prod için veritabanı Secret'larını yarattık ve progressive sync'i açtık; bu özellik 3.3'ten beri beta ve hâlâ açıkça açılması gerekiyor:

kubectl -n argocd patch configmap argocd-cmd-params-cm --type merge \
    -p '{"data":{"applicationsetcontroller.enable.progressive.syncs":"true"}}'
kubectl -n argocd rollout restart deployment argocd-applicationset-controller
kubectl -n argocd patch application bookstore-api-dev --type merge -p '{"metadata":{"finalizers":null}}'
kubectl -n argocd delete application bookstore-api-dev
kubectl apply -f argocd/root.yaml
NAME                          CLUSTER                         NAMESPACE          PROJECT    STATUS     HEALTH       SYNCPOLICY
argocd/bookstore-api-dev      https://kubernetes.default.svc  bookstore-dev      bookstore  Synced     Healthy      Manual
argocd/bookstore-api-prod     https://kubernetes.default.svc  bookstore-prod     bookstore  OutOfSync  Missing      Manual
argocd/bookstore-api-staging  https://kubernetes.default.svc  bookstore-staging  bookstore  OutOfSync  Missing      Manual
argocd/bookstore-root         https://kubernetes.default.svc  argocd             default    Synced     Progressing  Auto-Prune
argocd/bookstore-web-dev      https://kubernetes.default.svc  bookstore-dev      bookstore  OutOfSync  Missing      Manual
argocd/bookstore-web-prod     https://kubernetes.default.svc  bookstore-prod     bookstore  OutOfSync  Missing      Manual
argocd/bookstore-web-staging  https://kubernetes.default.svc  bookstore-staging  bookstore  OutOfSync  Missing      Manual

bookstore-api-dev ilk anda Synced ve Healthy: yeni Application aynı adı taşıdığı için kaynaklardaki tracking annotation'ı (bookstore-api-dev:apps/Deployment:bookstore-dev/api) birebir uydu ve hiçbir pod yeniden başlamadı. Ad değiştirmiş olsaydık bütün kaynaklar OutOfSync görünür ve yeniden etiketlenirdi.

RollingSync: Environment'ları Sırayla Güncellemek

SYNCPOLICY sütununda bütün üretilen uygulamaların Manual olduğuna dikkat et. RollingSync, ürettiği Application'larda automated sync'i zorla kapatır; sync'i kendisi, adım adım başlatır. Adımlar Application label'larıyla seçilir: önce env=dev, sonra env=staging, sonra env=prod ve prod'da aynı anda en fazla bir Application (maxUpdate: 1). Bir adımdaki bütün Application'lar Healthy olmadan sonraki adım başlamaz. İlk kurulumda Application'ların sync başlama ve bitiş zamanları:

bookstore-web-dev     2026-11-15T03:28:02Z 2026-11-15T03:28:02Z {"automated":true,"username":"applicationset-controller"}
bookstore-web-staging 2026-11-15T03:28:04Z 2026-11-15T03:28:04Z {"automated":true,"username":"applicationset-controller"}
bookstore-api-staging 2026-11-15T03:28:32Z 2026-11-15T03:28:52Z {"automated":true,"username":"applicationset-controller"}
bookstore-api-prod    2026-11-15T03:29:00Z 2026-11-15T03:29:22Z {"automated":true,"username":"applicationset-controller"}
bookstore-web-prod    2026-11-15T03:29:01Z 2026-11-15T03:29:01Z {"automated":true,"username":"applicationset-controller"}

Prod adımı, staging'in API'si (veritabanı, migration Job'ı ve API'nin wave'leriyle 20 saniye süren sync) Healthy olana kadar bekledi. Sync'leri başlatan da applicationset-controller. Daha ilginç olan sonraki değişiklik: base'deki API image tag'ini 1.1.0'a çeken tek bir commit. Dev zaten 1.1.0'daydı; staging ve prod'u etkiliyor. ApplicationSet'in status'unu beş saniyede bir okuduk:

06:30:15 api-dev=Healthy api-prod=Waiting api-staging=Waiting web-dev=Healthy web-prod=Healthy web-staging=Healthy
06:30:46 api-dev=Healthy api-prod=Waiting api-staging=Pending web-dev=Healthy web-prod=Healthy web-staging=Healthy
06:31:16 api-dev=Healthy api-prod=Pending api-staging=Healthy web-dev=Healthy web-prod=Healthy web-staging=Healthy
06:31:50 api-dev=Healthy api-prod=Healthy api-staging=Healthy web-dev=Healthy web-prod=Healthy web-staging=Healthy
{"version":"1.1.0","environment":"Production","pod":"api-85d44bd785-42tkd","database":"Postgres"}
{"version":"1.1.0","environment":"Production","pod":"api-85d44bd785-6kmwk","database":"Postgres"}

Waiting, sırası gelmemiş bir Application; Pending, sync'i başlatılmış ve Progressing'e geçmesi beklenen bir Application. Staging bittikten sonra prod başladı. Adımlar arasındaki on saniyeler mertebesindeki bekleme normal: 3.6'dan beri progressive sync bir değişiklik gördüğünde bütün Application'lara refresh annotation'ı ekliyor ve hepsi reconcile edilmeden adımlara başlamıyor; eski bilgiyle karar vermemek için. Sürüm notu, çok sayıda Application yöneten kurulumlarda adımların biraz gecikebileceğini söylüyor. 3.6 ayrıca progressive sync için yeni metrikler ve ApplicationSet condition'larında açıklayıcı reason ve mesajlar ekledi.

RollingSync'in bedeli açık: dev'de de automated sync ve self-heal yok. Dev'e her commit'in hemen gitmesini istiyorsan iki ApplicationSet kurmak (dev için automated sync'li ayrı bir set) ya da promotion'ı RollingSync yerine Git'teki commit'lerle yapmak (altıncı yazı) daha uygun olabilir.

05-argocd-ui-apps-tr.png

AppProject: Kim, Neyi, Nereye Deploy Edebilir?

Üretilen Application'lar bookstore project'inde. AppProject bir Application'ın sınırlarını çizer: hangi repo'lardan okuyabileceği, hangi cluster ve namespace'lere yazabileceği, hangi cluster-scoped kind'ları yaratabileceği:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: bookstore
  namespace: argocd
spec:
  description: Bookstore api and web, every environment
  sourceRepos:
    - http://git-server.git-server.svc.cluster.local/bookstore-config
  destinations:
    - server: https://kubernetes.default.svc
      namespace: bookstore-*
  # Project'in yaratabileceği cluster-scoped kind'lar: yalnız namespace (CreateNamespace=true).
  clusterResourceWhitelist:
    - group: ""
      kind: Namespace
  roles:
    - name: deployer
      description: CI or on-call may sync, but not create or delete applications
      policies:
        - p, proj:bookstore:deployer, applications, get, bookstore/*, allow
        - p, proj:bookstore:deployer, applications, sync, bookstore/*, allow

Sınırı denemek için aynı repo'dan kube-system'e deploy etmek isteyen bir Application yarattık:

CONDITION         MESSAGE
InvalidSpecError  application destination server 'https://kubernetes.default.svc' and namespace 'kube-system' do not match any of the allowed destinations in project 'bookstore'

Argo CD Application'ı reddetmedi ama hiçbir şey uygulamadı. default project'te bu Application sorunsuz çalışırdı; default'u herkese açık bırakmak, Argo CD'nin cluster-admin yetkisini Application yaratabilen herkese vermek demek. clusterResourceWhitelist boşsa project hiçbir cluster-scoped nesne yaratamaz; biz yalnız Namespace'e izin verdik, böylece bir overlay'e yanlışlıkla eklenen bir ClusterRole ya da CRD (Custom Resource Definition; Kubernetes'e eklenen özel nesne tipi) reddedilir. roles project'e özel roller tanımlar: deployer rolü bookstore project'indeki Application'ları görebilir ve sync edebilir, yaratamaz ve silemez. Argo CD rol için bir JWT (JSON Web Token; imzalı erişim token'ı) üretebilir ya da rolü bir SSO (Single Sign-On; şirket hesabıyla tek seferde giriş) grubuna bağlayabilirsin; ekip başına bir project ve rol en temiz düzen. argocd proj role get bookstore deployer rolün policy'lerini gösteriyor; Argo CD project'i görme iznini kendisi ekliyor:

Role Name:     deployer
Description:   CI or on-call may sync, but not create or delete applications
Policies:
p, proj:bookstore:deployer, projects, get, bookstore, allow
p, proj:bookstore:deployer, applications, get, bookstore/*, allow
p, proj:bookstore:deployer, applications, sync, bookstore/*, allow

Global RBAC (argocd-rbac-cm), SSO grupları ve 3.0'daki ayrıntılı update/delete izinleri serinin son yazısında.

Üretimde Dikkat

ApplicationSet controller Git'i varsayılan olarak üç dakikada bir yoklar; yeni bir ortam klasörü o kadar sürede görünür. Hızlandırmak için webhook ya da argocd.argoproj.io/application-set-refresh: "true" annotation'ı kullanılır. RollingSync'te bir adıma uymayan Application hiç sync edilmez: yarın overlays/qa eklersen ve step'lere qa yazmazsan o ortam elle sync bekler. 3.0'dan beri nested generator'lardaki selector'lar her zaman uygulanıyor (applyNestedSelectors alanı yok sayılıyor); 2.x'ten yükseltirken matrix içinde selector kullanan ApplicationSet'ler daha az Application üretebilir. ApplicationSet bir Application'ın sahibi olduğunda o Application'ı UI'dan değiştirmek kalıcı olmaz; ApplicationSet controller onu template'e geri çeker. Elle müdahale gerekiyorsa applicationsSync: create-update ya da create-only policy'leri controller'ın neyi değiştirip neyi silebileceğini sınırlar; controller'a --policy verilmişse o kazanır, ApplicationSet'teki değerin onu ezebilmesi için applicationsetcontroller.enable.policy.override açık olmalı.

Sık Düşülen Tuzaklar

Önce Tahmin Et'in cevabı: apps/web/overlays/staging klasörünü silip ApplicationSet'i yeniledik.

argocd/bookstore-web-dev
argocd/bookstore-web-prod
deployment.apps/web   1/1   1     1     5m37s
service/web   ClusterIP   <cluster-ip>   <none>   80/TCP   5m37s

bookstore-web-staging Application'ı silindi, staging'deki web Deployment'ı ve Service'i çalışmaya devam ediyor. preserveResourcesOnDeletion: true, üretilen Application'lara finalizer eklenmesini engelliyor; Application gider, kaynaklar kalır. Bu ayar olmasaydı tek bir klasör silmek o ortamın uygulamasını da silerdi; ApplicationSet'in kendisini silmek de altı ortamın hepsini. Bedeli şu: kaynaklar artık hiçbir Application'a ait değil, sahipsiz kaldılar ve temizlemek senin işin. Klasörü git revert ile geri getirince ApplicationSet Application'ı yeniden üretti ve aynı tracking ID'si sayesinde kaynakları Synced olarak devraldı.

Ürettiği Application'ı UI'dan değiştirip değişikliğin kalmasını beklemek. ApplicationSet template'i her zaman kazanır.

RollingSync açıkken dev'de self-heal olduğunu sanmak. Automated sync zorla kapalı.

Project kullanmadan her şeyi default'ta çalıştırmak. Application yaratabilen herkes cluster'ın her yerine deploy edebilir.

App of Apps root'una finalizer koyup root'u "temizlik için" silmek. Bütün ortamlar gider.

Küçük Bir Deneme

★☆☆ apps/api/overlays/qa klasörünü ekle (namespace bookstore-qa, Secret'ı lab script'iyle yarat). ApplicationSet Application'ı üretiyor mu, RollingSync onu sync ediyor mu? Step'lere qa'yı ekleyip tekrar dene.

★★☆ Git directory generator yerine bir matrix generator kur: bir list generator'ı ortamları (dev, staging, prod) ve replica sayılarını, git directory generator'ı uygulamaları versin. Prod'un replica sayısını overlay yerine generator'dan gelen bir parametreyle ver ve iki yaklaşımı okunurluk açısından karşılaştır.

★★★ deployer rolü için bir JWT token üret (argocd proj role create-token), bu token ile argocd app sync bookstore-web-dev çalıştır, sonra aynı token ile argocd app delete dene. Hata mesajını oku ve sonra rolü bir SSO grubuna bağlamanın neden token'dan daha iyi olduğunu düşün.

Altı Application tek bir ApplicationSet'ten geliyor ve ortamlar sırayla güncelleniyor. Ama image tag'ini hâlâ biz elle değiştiriyoruz. Bir sonraki yazıda CI'ı GitOps'a bağlıyoruz: image build edildikten sonra config repo'suna commit ya da pull request açmak, environment promotion ve Argo CD Image Updater.