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.
12 KiB
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-01yeniden 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-confsswag-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:
- console.hetzner.cloud adresine giriş yap.
- Sol menüden ilgili projeyi seç (
iklim_prod). - Sol menüden Floating IPs sekmesine gir.
iklim-prod-app-fipsatırının sağındaki ⋮ (üç nokta) menüsünü aç → Reassign.- Açı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çinip addr showile 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.
mongoshilers.remove("<eski-host>:27017")ardındanrs.add("<yeni-host>:27017")çalıştır.
etcd
ETCD_INITIAL_CLUSTER_STATE: Stack dosyasındanewolarak tanımlıdır (ilk kurulum için). Yeniden kurulum senaryosunda Swarm servisi--forceile güncellenince etcd boş volume ile başlar ve mevcut cluster'aexistingmodunda katılmaya çalışır. Bitnami etcd image'ı bunu otomatik algılar; sorun yaşanırsa stack dosyasında ilgili node'unETCD_INITIAL_CLUSTER_STATEdeğerini geçici olarakexistingyapıp redeploy et, ardından geri al.
Özet
| Servis | Geçici çöküş | Yeniden kurulum |
|---|---|---|
| etcd | Otomatik | Manuel: member remove → service 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 |