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

261 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```bash
# 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
```bash
# 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.
```bash
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:
```text
/config/fail2ban/filter.d/nginx-unauthorized.local
```
Önerilen içerik:
```ini
[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:
```bash
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.
```bash
# 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:**
```bash
hcloud floating-ip assign <floating-ip-id> <iklim-app-02-server-id>
```
**Hetzner Cloud Console (web) ile:**
1. [console.hetzner.cloud](https://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.
```bash
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.
```bash
# Ö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.
```bash
# 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 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` |