PushizoDocs
Operasyon

Helm ile üretim dağıtımı (Kubernetes)

v2 Faz 16 madde 4 (docs/plan/10). Chart: infra/k8s/pushizo. Değer örnekleri: values-production.yaml, values-staging.yaml. Doğrulama: values.schema.json (tek alan) + templates/validate.yaml (alanlar arası kurallar,…

v2 Faz 16 madde 4 (docs/plan/10). Chart: infra/k8s/pushizo. Değer örnekleri: values-production.yaml, values-staging.yaml. Doğrulama: values.schema.json (tek alan) + templates/validate.yaml (alanlar arası kurallar, anlamlı fail mesajları).

1. Ön koşullar

BileşenNedenNot
Kubernetes ≥ 1.27matchLabelKeys (topology spread), policy/v1 PDB, autoscaling/v2 HPAChart.yaml kubeVersion
ingress-nginx + cert-managerIngress, TLS (ingress.clusterIssuer)/metrics ve /readyz için server-snippet gerekir (snippet'ler açık olmalı) ya da ingress.blockInternalPaths=false + başka bir engelleme
metrics-serverAPI HPA (CPU/bellek)
External Secrets OperatorexternalSecrets.enabledexternal-secrets.io/v1 (ESO ≥ 0.17); eski kurulumda externalSecrets.apiVersion: external-secrets.io/v1beta1
KEDA ≥ 2.12keda.enabled (worker ölçekleme)fallback ve advanced.horizontalPodAutoscalerConfig kullanılır
Prometheus OperatorpodMonitor / serviceMonitor, KEDA sorgusukube-prometheus-stack önerilir
NetworkPolicy uygulayan CNInetworkPolicy.enabledCalico, Cilium, AWS VPC CNI (network policy açık) vb.
Yönetilen Postgres 16, Redis 7, S3veri katmanı chart dışındauygulama rolü NOSUPERUSER NOBYPASSRLS (docs/runbooks/postgres-roles.md)

Ad alanı Pod Security Admission restricted profiliyle uyumludur (runAsNonRoot, seccomp RuntimeDefault, drop: [ALL], allowPrivilegeEscalation: false, salt okunur kök dosya sistemi; yazılabilir dizinler yalnızca emptyDir):

kubectl create ns pushizo
kubectl label ns pushizo pod-security.kubernetes.io/enforce=restricted pod-security.kubernetes.io/enforce-version=latest
```text

## 2. İlk kurulum

```bash
NS=pushizo
# (a) Yönetilen Postgres/Redis CA paketi (ör. AWS RDS: global-bundle.pem). Migration hook'u da bağladığı için kurulumdan önce.
kubectl -n $NS create configmap pushizo-ca-bundle --from-file=ca.pem=./global-bundle.pem

# (b) Gizli bilgiler: ExternalSecrets (§3) ya da elle Secret (anahtarlar packages/config adlarıyla)
kubectl -n $NS create secret generic pushizo-secrets \
  --from-literal=DATABASE_URL="$DATABASE_URL" --from-literal=REDIS_URL="$REDIS_URL" \
  --from-literal=MASTER_KEY="$MASTER_KEY" --from-literal=SESSION_SECRET="$SESSION_SECRET" \
  --from-literal=S3_ACCESS_KEY="$S3_ACCESS_KEY" --from-literal=S3_SECRET_KEY="$S3_SECRET_KEY" --from-literal=SMTP_URL="$SMTP_URL"

# (c) Ortam değerleri: values-production.yaml'ı kopyalayıp alan adlarını, CIDR'ları, mağaza adını düzenleyin (gizli değer yazmayın)
helm lint infra/k8s/pushizo -f infra/k8s/pushizo/values-production.yaml -f prod.values.yaml
helm template pushizo infra/k8s/pushizo -n $NS -f infra/k8s/pushizo/values-production.yaml -f prod.values.yaml \
  --set image.tag=0.1.0 | kubectl apply --dry-run=server -f -
helm upgrade --install pushizo infra/k8s/pushizo -n $NS \
  -f infra/k8s/pushizo/values-production.yaml -f prod.values.yaml --set image.tag=0.1.0 --wait --timeout 15m
```text

Kurulum sırası (Helm hook'ları): ExternalSecret/Secret hook'u (ağırlık −10) → migrate Job (ağırlık 0, `pre-install,pre-upgrade`)
→ release kaynakları. Migrate Job yalnızca kendinden önce var olan kaynaklara dayanır: Secret, CA ConfigMap'i ve ad alanının
`default` ServiceAccount'u (token bağlanmaz). Job ConfigMap okumaz; migration CLI yalnızca `DATABASE_URL` kullanır.

Kontroller:

```bash
kubectl -n $NS get pods,pdb,hpa,scaledobject,networkpolicy -l app.kubernetes.io/instance=pushizo
kubectl -n $NS logs job/pushizo-migrate        # hook-succeeded ile silinmeden önce; hata olursa Job kalır
kubectl -n $NS port-forward svc/pushizo-api 4000:4000 & curl -fsS localhost:4000/readyz
```text

## 3. ExternalSecrets

- Chart SecretStore/ClusterSecretStore oluşturmaz; platform ekibi kimliği (IRSA, GKE Workload Identity, Vault Kubernetes auth)
  ile mağazayı tanımlar, chart `externalSecrets.secretStoreRef` ile başvurur.
- Kasadaki belge packages/config anahtar adlarını kullanır: zorunlu `DATABASE_URL`, `REDIS_URL`, `MASTER_KEY`, `SESSION_SECRET`,
  `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `SMTP_URL`; isteğe bağlı `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `SENTRY_DSN`,
  `GOOGLE_OAUTH_CLIENT_SECRET`, `GITHUB_OAUTH_CLIENT_SECRET`, `IYZICO_*`; PgBouncer açıksa `DATABASE_POOLED_URL`.
- Toplu eşleme `dataFrom: [{ extract: { key: pushizo/production } }]`; tek tek eşleme `data: [{ secretKey, remoteRef }]`.
- ExternalSecret `pre-install,pre-upgrade` hook'udur (migrate Job'dan önce Secret oluşsun). `creationPolicy: Orphan` sayesinde
  hook her yükseltmede yeniden oluşturulurken hedef Secret silinmez; `helm uninstall` Secret'ı bırakır (bilinçli: veri
  katmanı kimlik bilgileri release'den uzun yaşar). Elle silmek: `kubectl -n $NS delete secret pushizo-secrets`.
- Chart gizli değer içeren anahtarları `config` (ConfigMap) içinde reddeder (`config.MASTER_KEY gizli bir değerdir ...`).
- Rotasyon: kasada değeri değiştirin → ESO `refreshInterval` (varsayılan 1 sa) içinde Secret'ı günceller → pod'lar ortam
  değişkenlerini yeniden okumak için `kubectl -n $NS rollout restart deploy -l app.kubernetes.io/instance=pushizo`.
  `MASTER_KEY` rotasyonu ayrı prosedürdür: docs/runbooks/key-rotation.md.
- `secrets.create=true` yalnızca geliştirme içindir (değerler `--set` ile); `externalSecrets.enabled` ile birlikte açılamaz.

## 4. Blue/green API dağıtımı

Açıkken iki Deployment vardır: `pushizo-api-blue`, `pushizo-api-green`. `pushizo-api` Service'i (ve dashboard'un
kullandığı `api` takma adı) yalnızca `api.blueGreen.activeColor` rengini seçer; `pushizo-api-preview` pasif rengi gösterir.
HPA yalnızca aktif renge bağlanır. Worker ve dashboard kayan güncellemeyle `image.tag` etiketini kullanır.

**Kurallar**
- Blue/green modunda iki rengin etiketini de her zaman açıkça verin (`api.blueGreen.blue.imageTag`, `green.imageTag`). Boş etiket
  `image.tag`'i izler; `image.tag` değişince pasif renk de yeni sürüme döner ve geri dönüş seçeneği kaybolur.
- Renk `replicas` alanı `null` ise kümedeki mevcut replika korunur (`lookup`); ilk kurulumda aktif renk `api.replicas`, pasif renk 0.
  `lookup` çalışmayan araçlarda (`helm template`, `--dry-run=client`, Argo CD) iki rengin `replicas` değerini açıkça yazın;
  aksi halde pasif renk 0'a, aktif renk `api.replicas`'a çekilir.
- Migration'lar her zaman geriye dönük uyumlu olmalı: hazırlık adımında migrate hook yeni sürümün imajıyla (`migrate.imageTag`)
  çalışır, eski renk yeni şemayla çalışmaya devam eder (docs/plan/08 §8.8: önce ekle, sonra kullan, sonra düşür). Geri almada da
  `migrate.imageTag` yeni sürümde kalır (eski migrator bilmediği uygulanmış sürümleri yok sayar, şemayı geri almaz).

**Geçiş (blue aktif; eski sürüm `$OLD`, yeni sürüm `$NEW`)**

```bash
OLD=0.1.0 NEW=0.2.0
H="helm upgrade pushizo infra/k8s/pushizo -n $NS -f infra/k8s/pushizo/values-production.yaml -f prod.values.yaml --wait --timeout 15m"
BLUE_N=$(kubectl -n $NS get deploy pushizo-api-blue -o jsonpath='{.spec.replicas}')

# 1) Hazırlık: green'i $NEW ile aktif kapasitede ayağa kaldır (migrate hook bu adımda çalışır). Trafik hâlâ blue'da.
$H --set image.tag=$OLD --set api.blueGreen.activeColor=blue \
   --set api.blueGreen.blue.imageTag=$OLD --set api.blueGreen.green.imageTag=$NEW --set migrate.imageTag=$NEW --set api.blueGreen.green.replicas=$BLUE_N
kubectl -n $NS rollout status deploy/pushizo-api-green

# 2) Duman testi pasif renge (port-forward NetworkPolicy'den etkilenmez)
kubectl -n $NS port-forward svc/pushizo-api-preview 4100:4000 &
curl -fsS localhost:4100/healthz && curl -fsS localhost:4100/readyz
curl -fsS -H "Authorization: Bearer $SMOKE_API_KEY" "localhost:4100/v1/apps/$SMOKE_APP_ID/channels"

# 3) Geçiş: Service selector green'e döner (anlık); blue replikaları korunur (replicas=null → lookup). Worker/dashboard $NEW sürümüne geçer.
$H --set image.tag=$NEW --set api.blueGreen.activeColor=green \
   --set api.blueGreen.blue.imageTag=$OLD --set api.blueGreen.green.imageTag=$NEW --set migrate.imageTag=$NEW
kubectl -n $NS get svc pushizo-api -o jsonpath='{.spec.selector}'; echo

# 4) Gözlem (en az 30 dk): 5xx oranı, p95, kuyruk derinliği (Grafana API/Kuyruklar panoları), Sentry.

# 5a) Geri alma (gerekirse): trafik anında blue'ya döner; blue hâlâ ayakta ve $OLD sürümünde.
$H --set image.tag=$OLD --set api.blueGreen.activeColor=blue \
   --set api.blueGreen.blue.imageTag=$OLD --set api.blueGreen.green.imageTag=$NEW --set migrate.imageTag=$NEW

# 5b) Kalıcılaştırma: gözlem temizse eski rengi küçült (bir sonraki sürüm blue'ya hazırlanır).
$H --set image.tag=$NEW --set api.blueGreen.activeColor=green \
   --set api.blueGreen.blue.imageTag=$OLD --set api.blueGreen.green.imageTag=$NEW --set migrate.imageTag=$NEW --set api.blueGreen.blue.replicas=0
```text

Geri almada şema geri alınmaz (yeni şema eski kodla uyumludur). Migration'ın kendisi hatalıysa önce docs/runbooks/restore.md'deki
yedek kontrolü, sonra son migration geri alınır (bağlantı dizesi Secret'tan okunur, komut satırına yazılmaz):

```bash
kubectl -n $NS run migrate-down --rm -i --restart=Never --image=ghcr.io/exec0de/pushizo-migrate:$NEW --overrides='{
  "spec": {
    "securityContext": { "runAsNonRoot": true, "runAsUser": 1001, "seccompProfile": { "type": "RuntimeDefault" } },
    "containers": [{
      "name": "migrate-down", "image": "ghcr.io/exec0de/pushizo-migrate:'"$NEW"'", "args": ["down", "--steps=1"],
      "env": [{ "name": "DATABASE_URL", "valueFrom": { "secretKeyRef": { "name": "pushizo-secrets", "key": "DATABASE_URL" } } }],
      "securityContext": { "allowPrivilegeEscalation": false, "capabilities": { "drop": ["ALL"] } }
    }]
  }
}'
```text

**Kayan güncellemeden blue/green'e kesintisiz geçiş**
1. `api.blueGreen.enabled=true`, `transitionFromRolling=true`, `activeColor=blue`, `blue.imageTag=<mevcut>`,
   `blue.replicas=<mevcut replika>`: eski `pushizo-api` Deployment'ı kalır, `pushizo-api-blue` eklenir; Service tüm API
   pod'larını seçer. `kubectl rollout status deploy/pushizo-api-blue`.
2. `transitionFromRolling=false`: Service yalnızca blue'yu seçer; eski Deployment, PDB ve HPA silinir.
Ters yön (blue/green'den kayan güncellemeye) aynı iki adımla: önce `transitionFromRolling=true`, sonra `enabled=false`.

Not: `dashboard.apiHostAlias` (varsayılan `api`) ad alanı başına tek Service adıdır; aynı ad alanında iki release çalışacaksa
birinde boş bırakın.

## 5. KEDA (worker ölçekleme)

- Grup başına `ScaledObject`: `sum(max by (queue, state) (queue_depth{queue=~"<regex>",state=~"waiting|prioritized",namespace="<ns>"}))`.
  `queue_depth{queue,state}` worker tarafından 15 sn'de bir yazılır (apps/worker/src/registry.ts); öncelikli işler BullMQ'da
  `prioritized` kümesinde olduğundan iki durum toplanır. `max by` aynı kuyruğu yayımlayan birden çok pod'un tekrarını giderir.
- `threshold` replika başına hedef bekleyen iş sayısıdır: istenen replika = ⌈derinlik / threshold⌉ (min/max sınırları içinde).
  Başlangıç değeri: dispatch için `WORKER_CONCURRENCY × iş süresi` ile 1 dakikalık işlenebilir iş; ölçümden sonra ayarlayın.
- `minReplicaCount ≥ 1` zorunludur (şema): metrik yalnızca kuyruğu tüketen worker'dan gelir, 0 replikada metrik kaybolur.
- `keda.namespaceLabel`: Prometheus Operator `namespace` etiketini ekler. Pod keşfini elle yapılandırılmış Prometheus'ta etiket
  farklıysa (ör. `kubernetes_namespace`) değiştirin; boş değer sınırlamayı kaldırır.
- `fallback`: Prometheus 3 ardışık sorguda yanıt vermezse grup sabit replikaya (`keda.fallbackReplicas` ya da `hpa.minReplicas`)
  düşer. `keda.cpuTrigger.enabled=true` iken fallback üretilmez.
- KEDA açıkken chart worker HPA'sı üretmez; KEDA'nın ürettiği HPA Deployment ile aynı adı taşır. HPA → KEDA geçişinde KEDA,
  Helm eski HPA'yı silene kadar bir kez çakışma hatası loglayıp yeniden dener (geçici).
- Kontrol:

```bash
kubectl -n $NS get scaledobject
kubectl -n $NS describe scaledobject pushizo-worker-dispatch | sed -n '/Conditions/,$p'
kubectl -n $NS get hpa pushizo-worker-dispatch -o jsonpath='{.status.currentMetrics}'; echo
```text

API ölçekleme CPU + bellek HPA'sıdır (`api.hpa.behavior`: hızlı büyüme — 60 sn'de %100 veya 4 pod; yavaş küçülme — 300 sn
pencere, dakikada en fazla %20).

## 6. Veri katmanı bağlantı profilleri

### Postgres (yönetilen)
- Bağlantı dizesi Secret'ta `DATABASE_URL`. Uygulama rolü süper kullanıcı veya `BYPASSRLS` olamaz; worker üretimde bunu
  başlangıçta denetler (docs/runbooks/postgres-roles.md).
- `database.sslMode` → `PGSSLMODE` (node-postgres yalnızca bağlantı dizesinde `sslmode` yoksa okur):
  - `verify-full`: TLS + sertifika ve ana makine doğrulaması (üretim). node-postgres `require`/`verify-ca` değerlerini de
    `verify-full` gibi işlediği için şema yalnızca `""`, `disable`, `verify-full` kabul eder.
  - Sağlayıcı CA'sı Node güven deposunda değilse (RDS, Cloud SQL sunucu CA'sı, Azure) `caBundle.configMapName`/`secretName`
    ile bağlanır; `NODE_EXTRA_CA_CERTS` API, worker ve migrate Job'una verilir. Aynı paket `rediss://` için de geçerlidir.
- Havuz: pod başına `DATABASE_POOL_MAX` (worker en fazla 5 kullanır). Toplam bağlantı ≈ API replikası × havuz + worker
  replikası × 5 + migrate 2; HPA/KEDA üst sınırlarıyla hesaplayıp DB `max_connections` değerinin %80'inde tutun.

### PgBouncer (`database.pgbouncer.enabled`)
API ve worker `DATABASE_URL`'i Secret'taki `database.pgbouncer.urlSecretKey` (varsayılan `DATABASE_POOLED_URL`) anahtarından
alır; migrate Job doğrudan `DATABASE_URL` kullanır (migrator oturum düzeyi `pg_advisory_lock` alır, transaction havuzunda
kilit bağlantıyla birlikte kaybolur).

Uygulamanın havuzlayıcıdan beklediği:
1. `pool_mode = transaction` uyumludur: kiracı RLS bağlamı `set_config(..., true)` ile transaction'a yereldir
   (apps/api/src/common/tenant-rls.interceptor.ts), journey kilidi `pg_advisory_xact_lock`, oturum düzeyi `SET`/`LISTEN` yoktur,
   node-postgres adsız hazır ifadeler kullanır.
2. Uygulama bağlantıları `options=-c app.rls_bypass=on` başlangıç parametresiyle açılır (packages/db/src/client.ts). PgBouncer bu
   parametreyi sunucuya iletmez; `ignore_startup_parameters = options` ayarlanmalı ve aynı varsayılan rol düzeyinde verilmelidir:

```sql
-- Doğrudan bağlantıyla (yönetici rolü). Davranış değişmez: bağlantılar yine bypass açık başlar, API isteği SET LOCAL ile kapatır.
alter role pushizo set app.rls_bypass = 'on';
```text

```ini
; pgbouncer.ini
pool_mode = transaction
ignore_startup_parameters = options
```text

Doğrulama: `psql "$DATABASE_POOLED_URL" -c "show app.rls_bypass"` → `on`; API'de bir kiracı isteği (RLS kapalı yol) ve worker
`/readyz` 200. Rol ayarı olmadan worker sorguları RLS'e takılır (boş sonuç).

### Redis
- Desteklenen tek profil `redis.mode: standalone`: `REDIS_URL` (`redis://` ya da TLS için `rediss://`). API ve worker ioredis'i
  `new Redis(REDIS_URL)` ile kurar; Sentinel/Cluster istemcisi ve BullMQ Cluster için `{hash}` etiketli önek yoktur. `sentinel` veya
  `cluster` seçilirse chart açıklamalı hatayla durur.
- Yüksek erişilebilirlik: tek birincil uç nokta sunan yönetilen hizmet (ElastiCache replication group primary endpoint +
  Multi-AZ otomatik yük devretme, Memorystore Standard, Azure Cache Standard/Premium). Yük devretmede ioredis yeniden bağlanır;
  BullMQ işleri Redis kalıcılığıyla korunur, kayıp durumunda `queue:recover` (docs/runbooks/restore.md).
- Zorunlu ayar: `maxmemory-policy noeviction` (BullMQ anahtarları tahliye edilmemeli). AOF/RDB kalıcılığı açık.

## 7. NetworkPolicy uyarlama

Açıkken: release pod'larına varsayılan reddet (giriş + çıkış), sonra yalnızca gerekli akışlar.

| Akış | Değer |
|---|---|
| ingress denetleyicisi → api, dashboard | `networkPolicy.ingressController.{namespaceSelector,podSelector}` |
| Prometheus → api `:4000/metrics`, worker `:4001/metrics` | `networkPolicy.monitoring.*` |
| dashboard → api | otomatik |
| tüm pod'lar → DNS | `networkPolicy.dns` (kube-dns; NodeLocal DNSCache kullanılıyorsa `ipBlock` olarak `egress.extra`'ya 169.254.20.10/32:53 ekleyin) |
| api/worker → Postgres, Redis | `egress.postgres` / `egress.redis`: `cidrs` (yönetilen servisin özel alt ağları) veya `to` (küme içi seçici) — açıkken zorunlu |
| api/worker → S3, SMTP | `egress.s3` / `egress.smtp` (VPC uç noktası CIDR'ı); boşsa internet kuralı kapsar |
| api/worker → sağlayıcılar, webhook hedefleri, IdP'ler | `egress.internet`: `0.0.0.0/0` eksi özel ağlar ve `169.254.0.0/16` (bulut metadata — SSRF ikinci hattı), portlar 443, 2197 (APNs), 465/587 |
| migrate → Postgres | otomatik (`egress.postgres`) |
| ek akışlar | `egress.extra` (ör. OTel collector), `api.extraIngress` (ör. yük testi ad alanı), `dashboard.allowInternetEgress` (Sentry) |

- Postgres/Redis CIDR'ını bulma: AWS `aws rds describe-db-subnet-groups`, `aws elasticache describe-cache-subnet-groups`; GCP
  özel hizmet erişimi aralığı; Azure delege alt ağ. Doğrudan IP yerine alt ağ CIDR'ı verin (yük devretmede IP değişir).
- Kubelet probları düğümden gelir ve çoğu CNI'da politikadan etkilenmez; CNI düğüm trafiğini de politikaya tabi tutuyorsa
  (ör. Cilium `allow-localhost=policy`) düğüm kaynağına giriş izni ekleyin.
- IPv6 çift yığın: `egress.internet.blocks` listesine `{ cidr: "::/0", except: ["fc00::/7", "fe80::/10", "::1/128"] }` ekleyin.
- Doğrulama:

```bash
kubectl -n $NS exec deploy/pushizo-worker-general -- wget -qO- -T 5 http://169.254.169.254/ || echo "metadata engelli (beklenen)"
kubectl -n $NS exec deploy/pushizo-worker-general -- wget -qO- -T 5 https://fcm.googleapis.com/ >/dev/null; echo "internet çıkışı: $?"
kubectl -n $NS run np-probe --rm -i --restart=Never --image=busybox:1.36 --overrides='{"spec":{"securityContext":{"runAsNonRoot":true,"runAsUser":65534,"seccompProfile":{"type":"RuntimeDefault"}},"containers":[{"name":"np-probe","image":"busybox:1.36","command":["wget","-qO-","-T","5","http://pushizo-api:4000/healthz"],"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}}}]}}'   || echo "ad alanı içinden etiketsiz pod API'ye ulaşamıyor (beklenen)"
```text

## 8. Pod güvenliği, kaynaklar ve problar

- `podSecurityContext`: `runAsNonRoot`, UID/GID 1001 (imajlardaki `pushizo` kullanıcısı), `seccompProfile: RuntimeDefault`.
  `containerSecurityContext`: `readOnlyRootFilesystem`, `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`; şema bu
  güvencelerin gevşetilmesini reddeder. Yazılabilir dizinler: `/tmp` (tümü) ve dashboard `.next/cache` (`emptyDir`, sınırlı).
- ServiceAccount token'ı bağlanmaz (uygulama Kubernetes API'sine erişmez).
- Problar gerçek uçlardır: API `/healthz` (süreç) ve `/readyz` (Postgres + Redis) — apps/api/src/health/health.controller.ts;
  worker `/healthz` ve `/readyz` (Postgres + Redis) `:4001` — apps/worker/src/http.ts; dashboard ayrı sağlık ucu olmadığından
  oturumsuz `/login` (imaj HEALTHCHECK'iyle aynı). `startupProbe` 2 dk'ya kadar başlangıç süresi tanır.
- Kapanış: API ve dashboard `preStop` 5 sn bekler (uç nokta listesinden düşme ingress'e yayılır), sonra SIGTERM; worker SIGTERM
  sonrası aktif işleri 30 sn içinde bitirir, `terminationGracePeriodSeconds` 45.
- Kaynak önerileri `values-production.yaml` içindedir; ilk yük testinden sonra (docs/runbooks/load-testing.md) istek/limit
  değerlerini gözlenen p95 CPU/belleğe göre güncelleyin.
- Dağılım: `topologySpread` her bileşen için bölge (`topology.kubernetes.io/zone`) ve düğüm (`kubernetes.io/hostname`) kısıtı
  üretir (`matchLabelKeys: [pod-template-hash]` ile yalnızca aynı sürümün pod'ları sayılır). Varsayılan `ScheduleAnyway`;
  en az 3 bölgeli kümede bölge kısıtını `DoNotSchedule` yapmak bölge kaybında yeni pod'ların beklemesine yol açar.

## 9. Gözlemlenebilirlik

- `podMonitor.enabled` (önerilen): API (iki renk) ve worker pod'larını `pushizo.com/metrics=true` etiketiyle kazır.
- `serviceMonitor.enabled`: API Service'i (yalnızca aktif renk) ve worker headless Service'leri. İkisi birlikte açılamaz.
- Etiket `labels.release` kube-prometheus-stack'in seçicisiyle eşleşmelidir. Alarm kuralları: `infra/monitoring/prometheus/alerts.yml`.

## 10. Sıfır kesinti dağıtım kontrol listesi

- [ ] `helm lint` ve `helm template … | kubectl apply --dry-run=server -f -` temiz (şema + `validate.yaml`).
- [ ] Migration'lar geriye dönük uyumlu (eski kod yeni şemayla çalışır); `down` dosyası var; büyük tablo değişikliği ayrı bakım işi.
- [ ] Secret anahtarları mevcut (`kubectl get externalsecret` → `SecretSynced`); CA ConfigMap'i var.
- [ ] API: `maxUnavailable: 0`, `minReadySeconds`, `preStop`, readiness `/readyz`; PDB `minAvailable` < HPA `minReplicas`
      (eşitse düğüm boşaltma kilitlenir).
- [ ] Worker: `terminationGracePeriodSeconds` (45) > uygulama kapanış süresi (30); PDB `maxUnavailable: 1`.
- [ ] Blue/green: iki rengin `imageTag`'i açık; pasif renk `rollout status` hazır; önizleme duman testi geçti; geçişten sonra
      eski renk gözlem süresince ayakta.
- [ ] HPA/KEDA min replika en az 2 (API, dispatch) ve bölgelere yayılmış (`kubectl get pods -o wide`).
- [ ] Dağıtım sırasında `ApiHigh5xxRate`, `QueueDepthHigh`, `PostgresConnectionsHigh` alarmları tetiklenmedi.
- [ ] Staging'de aynı adımlar yük altında (docs/runbooks/load-testing.md §5) prova edildi: k6 hata oranı < %0,1.

## 11. Chart doğrulaması (geliştirici)

```bash
helm lint infra/k8s/pushizo
for f in "" "-f infra/k8s/pushizo/values-production.yaml" "-f infra/k8s/pushizo/values-staging.yaml" \
  "-f infra/k8s/pushizo/values-production.yaml -f infra/k8s/pushizo/values-cell-eu.yaml" \
  "-f infra/k8s/pushizo/values-production.yaml -f infra/k8s/pushizo/values-cell-tr.yaml"; do
  helm template hs infra/k8s/pushizo -n pushizo $f | kubeconform -strict -summary -kubernetes-version 1.30.0 \
    -schema-location default \
    -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'
done
helm template hs infra/k8s/pushizo --set redis.mode=sentinel   # beklenen: açıklamalı hata
```text

Kombinasyonlar (2026-10-15'te helm v3.17.3 + kubeconform v0.6.7 ile doğrulandı): varsayılan, production, staging,
ExternalSecrets, blue/green, blue/green geçiş modu, KEDA (± CPU tetiği), NetworkPolicy, PgBouncer + CA paketi, `secrets.create`
+ ServiceMonitor, HPA'sız, hepsi birlikte; ayrıca beklenen 13 hata senaryosu (Redis sentinel/cluster, config'te gizli anahtar,
eksik mağaza, iki monitor, NetworkPolicy'de DB çıkışı yok, geçersiz renk/kuyruk/sslmode, root kullanıcı, bilinmeyen anahtar).
Canlı kümede kurulum (ESO/KEDA/CNI ile uçtan uca) bu doğrulamaya dahil değildir; ilk staging kurulumunda §2 ve §4 adımlarıyla
yapılır.

Bu sayfada