Environment_Infrastructure/facts/node-recovery-failover.md
Murat ÖZDEMİR d20578c91a docs: Document SWAG fail2ban configuration for Grafana Live WebSocket 401s
Explains that SWAG's `fail2ban` configurations reside in a node-local Docker volume, not StorageBox. Details a scenario where Grafana Live WebSocket reconnects can cause 401s, leading to IP bans. Provides an Ansible mitigation strategy and highlights the need to ensure consistent `fail2ban` overrides across all potential failover nodes for smooth recovery.
2026-06-17 15:04:09 +03:00

12 KiB
Raw Permalink Blame History

Test — Docker Swarm Node Recovery

Test ortamında tek manager (iklim-app-01) ve tek worker (iklim-db-01) bulunur. Hangi node'un yeniden kurulduğuna göre recovery süreci farklılaşır.

Senaryo 1: iklim-app-01 (Manager) Yeniden Kurulur

Sorun

Yeni iklim-app-01 üzerinde docker swarm init farklı cluster ID ile yeni bir küme başlatır. iklim-db-01 hâlâ eski kümeye bağlıdır. Ansible swarm role'u iklim-db-01'de Swarm'ı active görür, join denemez. İki node iki ayrı kümede kalır.

Çözüm

# 1. iklim-db-01 üzerinde — eski kümeden çık
docker swarm leave --force

# 2. Ansible ile her iki node'u yeniden kur
cd ansible/test
ansible-playbook -i inventory/generated/test.yml test-bootstrap.yml --ask-vault-pass

# 3. DB stack'i yeniden deploy et
ansible-playbook -i inventory/generated/test.yml test-db-post-stack.yml --ask-vault-pass

DB verileri iklim-db-01'deki named volume'larda korunur, kayıp yaşanmaz.


Senaryo 2: iklim-db-01 (Worker) Yeniden Kurulur

Durum

Yeni iklim-db-01 Swarm'dan habersiz başlar (inactive). Manager (iklim-app-01) eski dead node kaydını tutar.

⚠️ Veri kaybı: iklim-db-01 yeniden kurulduğu için tüm named volume'lar silinmiştir. 3. adım öncesinde backup'tan restore yapılması zorunludur.

Çözüm

# 1. iklim-app-01 üzerinde — eski dead node kaydını temizle (bootstrap'tan ÖNCE yapılmalı)
docker node ls                   # eski node ID'yi bul
docker node rm <eski-node-id>

# 2. Ansible bootstrap — yeni node otomatik join olur
cd ansible/test
ansible-playbook -i inventory/generated/test.yml test-bootstrap.yml --ask-vault-pass

# 3. DB stack'i yeniden deploy et (backup'tan restore sonrası)
ansible-playbook -i inventory/generated/test.yml test-db-post-stack.yml --ask-vault-pass

Ansible swarm role'u inactive durumu gördüğü için token alıp join eder, role=db label'ını uygular. DB servisleri placement constraint sayesinde yeni node'a schedule edilir.


Senaryo 3: Her İki Node Yeniden Kurulur

Her şey sıfırdan kurulur, Swarm uyumsuzluğu yaşanmaz.

cd ansible/test
ansible-playbook -i inventory/generated/test.yml test-bootstrap.yml --ask-vault-pass
ansible-playbook -i inventory/generated/test.yml test-db-post-stack.yml --ask-vault-pass

Özet

Senaryo Manuel Adım Ansible Yeterli mi?
Manager (iklim-app-01) ölür docker swarm leave --force (worker'da) Sonrasında evet
Worker (iklim-db-01) ölür docker node rm <id> (manager'da, bootstrap'tan önce) Hayır — backup restore gerekir
Her ikisi ölür Yok Evet

Neden Prod'da Bu Sorun Yok

Prod ortamında birden fazla manager node (en az 3) çalıştırılır. Tek manager düşse diğerleri liderliği devralır, küme sağlıklı kalmaya devam eder.


Prod — Monitoring & SWAG Failover

SWAG, cert-reloader, Prometheus ve Grafana cluster-native (replicated) değildir; her zaman tek instance çalışırlar ve varsayılan olarak iklim-app-01'e (Floating IP node) sabitlenmişlerdir. iklim-app-01 çöktüğünde bu servisler durur; DNS/HTTPS erişimi ve izleme (monitoring) kesilir. Swarm quorum 2 manager ile devam eder; mikroservisler ve Vault başka node'lara taşınır.

cert-distributor bu kuralın dışındadır: mode: global ile node.labels.type == service olan tüm node'larda çalışır; StorageBox'tan sertifikayı node-lokal /opt/iklimco/ssl'e kopyalar (Vault FUSE mount kısıtlaması nedeniyle). iklim-app-01 düştüğünde diğer node'lardaki cert-distributor instance'ları çalışmaya devam eder — failover gerektirmez.

Tüm bu servislerin verileri ve konfigürasyonları StorageBox'ta tutulur:

  • SWAG: /mnt/storagebox/swag/config
  • SSL: /mnt/storagebox/ssl
  • Prometheus: /mnt/storagebox/prometheus/data
  • Grafana: /mnt/storagebox/grafana/data

SWAG Config Kaynağı ve fail2ban Notu

Mevcut stack dosyasında SWAG ana /config dizini Docker named volume olarak bağlıdır:

  • swag-vl:/config

StorageBox üzerinden ayrıca sadece şu alt dizinler bind edilir:

  • ${SWAG_DNS_CONFIG_DIR}:/config/dns-conf
  • ${SWAG_SITE_CONFS_DIR}:/config/nginx/site-confs
  • ${SWAG_PROXY_CONFS_DIR}:/config/nginx/proxy-confs
  • swag-logs-vl:/config/log

Bu nedenle SWAG'in nginx site config'leri deploy pipeline tarafından StorageBox altına render edilirken, SWAG container içindeki fail2ban config'leri (/config/fail2ban/...) swag-vl named volume içinde kalır. Bu alan Ansible storagebox rolü tarafından yönetilmez.

Grafana açık kalan tarayıcı sekmeleri wss://grafana.iklim.co/api/live/ws için tekrar tekrar 401 döndürebilir. SWAG içindeki nginx-unauthorized fail2ban jail'i bu 401'leri sayarsa ofis IP'si banlanabilir. Bu auth login problemi değildir; /v1/auth/login 200 dönerken Grafana Live WebSocket reconnect'leri fail2ban eşiğini doldurabilir.

Kalıcı çözüm için deploy pipeline içinde runtime config yazılmamalıdır; bu Ansible ile drift yaratır. Bunun yerine Ansible tarafında SWAG persistent config'i yöneten ayrı ve küçük bir rol kullanılmalıdır. Rol iklim-app-01 üzerinde iklimco_swag-vl Docker volume mountpoint'ini bulup şu override dosyasını idempotent şekilde yazmalıdır:

/config/fail2ban/filter.d/nginx-unauthorized.local

Önerilen içerik:

[Definition]
ignoreregex = ^<HOST> - - \[[^\]]+\] "GET /api/live/ws HTTP/[0-9.]+" 401\b

SWAG container çalışıyorsa rol değişiklikten sonra sadece ilgili jail'i reload etmelidir:

docker exec $(docker ps -q -f name=iklimco_swag | head -1) \
  fail2ban-client reload nginx-unauthorized

Failover sırasında SWAG iklim-app-02 veya iklim-app-03 üzerine taşınacaksa, swag-vl Docker named volume'unun node-local olduğu unutulmamalıdır. Hedef node üzerinde aynı fail2ban override dosyasının mevcut olduğundan emin olunmalıdır. Bu nedenle swag_config Ansible rolü failover hedefi olabilecek app node'ları için de çalıştırılabilir tasarlanmalıdır; prod varsayılanında aktif SWAG node'u iklim-app-01 olduğu için ilk uygulama orada yapılır.

Prod Senaryo: iklim-app-01 Çöktü

1. Servisleri Başka Node'a Taşı

SWAG ve cert-reloader birlikte taşınmalıdır. Prometheus ve Grafana da bağımsız olarak veya aynı anda taşınabilir. cert-distributor global mode'da çalıştığından taşıma gerekmez.

# iklim-app-02 veya iklim-app-03 üzerinde (aktif manager):

# SWAG & Cert-Reloader taşıma (replicas=1 olduğundan taşıma sırasında kısa kesinti yaşanır)
docker service update --constraint-add "node.hostname == iklim-app-02" --constraint-rm "node.hostname == iklim-app-01" iklimco_swag
docker service update --constraint-add "node.hostname == iklim-app-02" --constraint-rm "node.hostname == iklim-app-01" iklimco_cert-reloader

# Prometheus & Grafana taşıma
docker service update --constraint-add "node.hostname == iklim-app-02" --constraint-rm "node.hostname == iklim-app-01" iklimco_prometheus
docker service update --constraint-add "node.hostname == iklim-app-02" --constraint-rm "node.hostname == iklim-app-01" iklimco_grafana

2. Floating IP'yi Yeni Node'a Taşı

CLI ile:

hcloud floating-ip assign <floating-ip-id> <iklim-app-02-server-id>

Hetzner Cloud Console (web) ile:

  1. console.hetzner.cloud adresine giriş yap.
  2. Sol menüden ilgili projeyi seç (iklim_prod).
  3. Sol menüden Floating IPs sekmesine gir.
  4. iklim-prod-app-fip satırının sağındaki (üç nokta) menüsünü aç → Reassign.
  5. ılan listeden iklim-app-02'yi seç → Reassign butonuna tıkla.

Not: Floating IP Hetzner panelinde yeniden atandıktan sonra iklim-app-02'nin network interface'inde de aktif olması gerekir. Ansible bootstrap bu konfigürasyonu yapıyorsa otomatiktir; emin olmak için ip addr show ile Floating IP'nin bind edildiğini doğrula.

3. Doğrula

SWAG başlama ve sertifika kontrolü birkaç saniye sürebilir; servis Running görünse de ilk curl başarısız dönebilir. Birkaç saniye bekleyip tekrar dene.

docker service ls | grep -E 'swag|cert-reloader|prometheus|grafana'
curl -si https://api.iklim.co/health

iklim-app-01 Geri Döndüğünde

Node Swarm'a yeniden katıldıktan sonra tüm servisleri tekrar iklim-app-01'e taşıyıp Floating IP'yi geri aktarabilirsiniz.

# Önce node'un Swarm'a gerçekten katıldığını doğrula (STATUS = Ready olmalı)
docker node ls

# Servisleri geri taşı
for svc in iklimco_swag iklimco_cert-reloader iklimco_prometheus iklimco_grafana; do
  docker service update --constraint-add "node.hostname == iklim-app-01" --constraint-rm "node.hostname == iklim-app-02" $svc
done

# Floating IP'yi iklim-app-01'e geri ata
hcloud floating-ip assign <floating-ip-id> <iklim-app-01-server-id>

Özet

Bileşen Failover davranışı
Swarm quorum Otomatik — 2 manager yeterli
Vault, mikroservisler Otomatik — node.labels.type == service constraint ile başka node'a schedule edilir
SWAG, cert-reloader Manuel — docker service update --constraint-* + Floating IP taşıma
cert-distributor Otomatik — mode: global, tüm servis node'larında zaten çalışır
Prometheus, Grafana Manuel — docker service update --constraint-*
Veriler & Konfig StorageBox'ta; failover node hemen erişir, veri kaybı yaşanmaz

Prod — DB Node Recovery

Her DB node'u (iklim-db-01, iklim-db-02, iklim-db-03) aynı servis üçlüsünü barındırır:

Node Servisler
iklim-db-01 etcd-01, patroni-01, mongodb-01
iklim-db-02 etcd-02, patroni-02, mongodb-02
iklim-db-03 etcd-03, patroni-03, mongodb-03

Senaryo A: Node Geçici Olarak Çöker (Volume'lar Korunur)

etcd, Patroni ve MongoDB'nin tamamı 3 üyeli HA cluster'lardır; quorum için 2 node yeterlidir.

Servis Etki Otomatik İyileşme
etcd 2/3 node ile quorum devam eder Node geri dönünce cluster'a otomatik katılır
Patroni Replica düşerse primary devam eder; primary düşerse etcd üzerinden yeni primary seçilir Node geri dönünce replica olarak otomatik katılır
MongoDB 2/3 node ile quorum devam eder; gerekirse yeni primary seçilir Node geri dönünce primary'den initial sync ile güncellenir

Manuel adım gerekmez. Docker Swarm restart_policy: on-failure servisleri otomatik başlatır.

Senaryo B: Node Yeniden Kurulur (Volume'lar Silinir)

etcd named volume'ları node-lokal olduğundan node yeniden kurulunca kaybolur. Patroni ve MongoDB kendi kendine iyileşir; etcd manuel müdahale gerektirir.

# Aktif bir etcd container'ından — eski üyeyi cluster'dan çıkar
docker exec -it $(docker ps -q -f name=iklimco_etcd-01) \
  etcdctl member list --endpoints=http://etcd-01:2379,http://etcd-02:2379,http://etcd-03:2379
# Çıktıdan yeniden kurulan node'un <member-id>'sini al:
docker exec -it $(docker ps -q -f name=iklimco_etcd-01) \
  etcdctl member remove <member-id> --endpoints=http://etcd-01:2379,http://etcd-02:2379,http://etcd-03:2379

# Servisleri yeniden başlat (etcd boş volume ile existing cluster'a katılır;
# Patroni primary'den pg_basebackup ile otomatik clone alır;
# MongoDB hostname değişmediyse primary'den otomatik initial sync yapar)
docker service update --force iklimco_etcd-0N
docker service update --force iklimco_patroni-0N
docker service update --force iklimco_mongodb-0N

MongoDB hostname değişirse: Replica set konfigürasyonu eski hostname'i tutar. mongosh ile rs.remove("<eski-host>:27017") ardından rs.add("<yeni-host>:27017") çalıştır.

etcd ETCD_INITIAL_CLUSTER_STATE: Stack dosyasında new olarak tanımlıdır (ilk kurulum için). Yeniden kurulum senaryosunda Swarm servisi --force ile güncellenince etcd boş volume ile başlar ve mevcut cluster'a existing modunda katılmaya çalışır. Bitnami etcd image'ı bunu otomatik algılar; sorun yaşanırsa stack dosyasında ilgili node'un ETCD_INITIAL_CLUSTER_STATE değerini geçici olarak existing yapıp redeploy et, ardından geri al.

Özet

Servis Geçici çöküş Yeniden kurulum
etcd Otomatik Manuel: member removeservice update --force
Patroni Otomatik Otomatik: boş dir'den primary'yi clone alır
MongoDB Otomatik Otomatik (aynı hostname); hostname değişirse rs.remove + rs.add