Implement Hetzner sizing report recommendations and detailed DB setups

- Add `hetzner-sizing-report.md` defining data-driven server type recommendations for test and prod environments.
- Update Terraform configurations to align with the recommended `CPX` server types and refine firewall rules for Docker Swarm and database interactions.
- Introduce comprehensive documentation and stack files for:
    - Single-node PostgreSQL/MongoDB deployment on a test DB worker node.
    - High-availability 3-node MongoDB replica set and Patroni+etcd PostgreSQL cluster for production.
- Enhance Ansible bootstrap roles with SELinux disabling, fail2ban configuration, and StorageBox SSH key management for CI/CD.
- Reorganize and rename setup documentation files for improved structure and clarity.
This commit is contained in:
Murat ÖZDEMİR 2026-05-11 14:54:09 +03:00
parent 76f87aa2f9
commit b115a4cbdf
12 changed files with 1701 additions and 65 deletions

2
.gitignore vendored
View File

@ -46,3 +46,5 @@ runner-config.secret.yaml
.DS_Store
*.swp
*.swo
*.pdf

270
hetzner-sizing-report.md Normal file
View File

@ -0,0 +1,270 @@
# iklim.co Hetzner Yeni Test ve Prod Sunucu Raporu
Tarih: 2026-05-11
## Yönetici Özeti
Hetzner Cloud cost-optimized `CX` modellerinde "limited availability" problemi olduğu için yeni test ve prod kurulumlarında `CPX` regular performance modellerine geçilmesi önerilir.
Mevcut elle kurulmuş test ortamı verileri, test app sunucusunda RAM'in ana sınırlayıcı kaynak olduğunu gösteriyor. Test app node üzerinde 10 mikroservis ve altyapı servisleri birlikte çalıştığı için `CPX32` yani 8 GB RAM risklidir. Bu nedenle yeni test app sunucusu için `CPX42` önerilir.
Test DB sunucusunda disk ve network kullanımı düşük, disk doluluğu da çok düşüktür. Ancak CPU grafiğinde 8 vCPU seviyesine yaklaşan kısa süreli yükler görüldüğü için test DB için de `CPX42` ile başlamak daha doğru ve daha az riskli seçenektir.
Prod ortamında app ve DB katmanları 3'er node olarak kurulacağı için tek node başına en yüksek kaynakla başlamak gerekli değildir. Başlangıç için app node'larda `CPX42`, DB node'larda `CPX32` önerilir. Bu tercih, app tarafındaki Java mikroservis RAM baskısını azaltırken DB tarafında ekonomik bir cluster başlangıcı sağlar. Büyüme görüldüğünde DB node'lar `CPX42` veya daha yüksek modellere rescale edilebilir.
Önerilen nihai başlangıç:
| Ortam | Rol | Önerilen Tip | Adet | Aylık Toplam |
| --- | --- | --- | ---: | ---: |
| Test | App / Swarm / runner | `CPX42` | 1 | $29.99 |
| Test | DB | `CPX42` | 1 | $29.99 |
| Prod | App / Swarm | `CPX42` | 3 | $89.97 |
| Prod | DB cluster | `CPX32` | 3 | $49.47 |
Toplam başlangıç maliyeti:
| Kapsam | Aylık Maliyet |
| --- | ---: |
| Test | $59.98 / mo |
| Prod | $139.44 / mo |
| Test + Prod | $199.42 / mo |
## Kapsam ve Varsayımlar
Bu rapor şu veriler esas alınarak hazırlanmıştır:
- Mevcut elle kurulmuş test app ve test DB sunucularının CPU, disk, network, RAM ve disk doluluk verileri.
- Test app sunucusunda çalışan 10 mikroservis ve altyapı servisleri.
- Test DB sunucusunda çalışan PostgreSQL/PostGIS ve MongoDB container'ları.
- Hetzner tarafında cost-optimized `CX` modellerinin alınabilir olmaması.
- Kullanıcı tarafından paylaşılan `CPX` ve `CCX` model kaynak/fiyat bilgileri.
- Prod ortamında 3 app node ve 3 DB node olacak şekilde cluster topolojisi.
Fiyatlar kullanıcı tarafından paylaşılan Hetzner fiyat tablosuna göre hesaplanmıştır. Vergi, backup, snapshot, floating IP, volume ve trafik aşım maliyetleri dahil edilmemiştir.
## Kullanılabilir Hetzner Modelleri
### Regular Performance CPX Modelleri
| Tip | CPU | RAM | SSD | Trafik | Saatlik | Aylık |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `CPX22` | 2 AMD | 4 GB | 80 GB | 20 TB | $0.015 / h | $9.49 / mo |
| `CPX32` | 4 AMD | 8 GB | 160 GB | 20 TB | $0.026 / h | $16.49 / mo |
| `CPX42` | 8 AMD | 16 GB | 320 GB | 20 TB | $0.048 / h | $29.99 / mo |
| `CPX52` | 12 AMD | 24 GB | 480 GB | 20 TB | $0.069 / h | $42.99 / mo |
| `CPX62` | 16 AMD | 32 GB | 640 GB | 20 TB | $0.095 / h | $59.49 / mo |
### General Purpose CCX Modelleri
| Tip | CPU | RAM | SSD | Trafik | Saatlik | Aylık |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `CCX13` | 2 AMD | 8 GB | 80 GB | 20 TB | $0.030 / h | $18.49 / mo |
| `CCX23` | 4 AMD | 16 GB | 160 GB | 20 TB | $0.059 / h | $36.99 / mo |
| `CCX33` | 8 AMD | 32 GB | 240 GB | 30 TB | $0.119 / h | $73.99 / mo |
### CPX ve CCX Seçim Notu
`CCX` modelleri dedicated CPU davranışı nedeniyle CPU tutarlılığı gereken senaryolarda avantajlıdır. Ancak bu proje için başlangıç aşamasında belirleyici risk app tarafında RAM, DB tarafında ise ekonomik cluster başlangıcıdır. Bu nedenle ilk tercih `CPX` modelleridir.
DB tarafında `CCX33` 32 GB RAM sunsa da 8 vCPU ve 240 GB disk ile `CPX42` veya `CPX62` kadar dengeli değildir. Başlangıçta dedicated CPU gereksinimi net olmadığı için prod DB'de `CCX` ile başlamak önerilmez.
## Mevcut Test Ortamı Bulguları
### Test App Sunucusu
Mevcut test app sunucusu üzerinde 10 mikroservis ve altyapı servisleri aynı node üzerinde çalışmaktadır. Çalışan servisler:
| Grup | Servisler |
| --- | --- |
| Mikroservisler | auth, account, lightning, thunderstorm, precipitation, nowcast-point-alarm, nowcast-geo-alarm, forecast, forecast-point-alarm, enroute |
| Altyapı servisleri | RabbitMQ, Redis, Vault, Prometheus, Grafana, APISIX, APISIX Dashboard, etcd |
Kaynak özeti:
| Metrik | Mevcut Değer | Yorum |
| -------------- | --------------------: | ----------------------------------- |
| RAM toplam | 14 GiB | Yaklaşık 16 GB sınıfı sunucu |
| RAM used | 10 GiB | App node için yüksek ve belirleyici |
| RAM available | 4.0 GiB | Kalan alan sınırlı |
| Swap | 0 B | OOM riskini artırır |
| Root disk | 226 GB | Yaklaşık 240 GB sınıfı disk |
| Disk used | 81 GB | %38 kullanım |
| Disk available | 136 GB | Test için yeterli boşluk |
| CPU | Düşük baz, kısa spike | CPU kritik sınır değil |
| Disk I/O | Düşük | IOPS/throughput sınırlayıcı değil |
| Network | Düşük | Trafik sınırlayıcı değil |
![[test-app-graphs.png]]
Değerlendirme:
- App sunucusunda ana risk RAM'dir.
- `CPX32` 8 GB RAM sunduğu için mevcut 10 GiB used seviyesini karşılamaz.
- JVM/container memory limitleri uygulanmadan `CPX32` OOM veya yoğun GC riski taşır.
- Disk kullanımı 81 GB olduğu için 160 GB disk teorik olarak yeterli görünse de Docker image/layer birikimi, loglar ve deploy sırasındaki geçici alan ihtiyacı nedeniyle 320 GB daha güvenlidir.
Sonuç: yeni test app sunucusu için `CPX42` önerilir.
### Test DB Sunucusu
Mevcut test DB sunucusunda PostgreSQL/PostGIS ve MongoDB çalışmaktadır.
Kaynak özeti:
| Metrik | Mevcut Değer | Yorum |
| -------------- | -----------------------: | --------------------------------------------- |
| RAM toplam | 14 GiB | Yaklaşık 16 GB sınıfı sunucu |
| RAM used | 7.6 GiB | Linux cache nedeniyle tek başına riskli değil |
| RAM available | 7.4 GiB | RAM açısından rahat |
| Swap | 0 B | Ani bellek baskısında risk |
| Root disk | 226 GB | Yaklaşık 240 GB sınıfı disk |
| Disk used | 16 GB | %8 kullanım, çok düşük |
| Disk available | 201 GB | Fazlasıyla yeterli |
| CPU | Kısa süreli yüksek spike | DB için asıl izlenmesi gereken metrik |
| Disk I/O | Düşük | Disk throughput/IOPS sınırlayıcı görünmüyor |
| Network | Düşük | Trafik sınırlayıcı değil |
![[test-db-graphs.png]]
Değerlendirme:
- RAM ve disk açısından `CPX32` yeterli görünebilir.
- Ancak CPU grafiğinde 8 vCPU seviyesine yaklaşan kısa süreli yükler var.
- Bu yükler PostGIS query, Mongo indexleme, import veya batch işlemlerinden kaynaklanıyorsa `CPX32`'ye düşmek testte yavaşlamaya neden olabilir.
- Test DB tek node olduğu için prod DB cluster'ına göre daha az toleranslıdır.
Sonuç: yeni test DB sunucusu için de `CPX42` önerilir.
## Yeni Test Ortamı Gereksinimleri
Yeni test ortamında iki node hedeflenir:
| Sunucu | Rol | Önerilen Tip | CPU | RAM | SSD | Trafik | Aylık |
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |
| `iklim-app-01` | Swarm manager, app, runner, altyapı servisleri | `CPX42` | 8 AMD | 16 GB | 320 GB | 20 TB | $29.99 |
| `iklim-db-01` | PostgreSQL/PostGIS, MongoDB | `CPX42` | 8 AMD | 16 GB | 320 GB | 20 TB | $29.99 |
| **Toplam** | 2 sunucu | | **16 vCPU** | **32 GB** | **640 GB** | **40 TB** | **$59.98** |
### Test İçin Reddedilen Daha Ekonomik Alternatif
| Sunucu | Alternatif Tip | Neden Reddedildi |
| --- | --- | --- |
| `iklim-app-01` | `CPX32` | Mevcut app node 10 GiB used RAM seviyesinde; 8 GB RAM riskli |
| `iklim-db-01` | `CPX32` | RAM/disk yeterli olabilir, ancak CPU spike'ları nedeniyle tek node DB'de riskli |
### Test Ortamı Operasyon Notları
- App node'da mikroservisler için JVM/container memory limitleri tanımlanmalıdır.
- Swap varsayılan olarak yoksa düşük öncelikli acil durum swap'i değerlendirilebilir; bu performans çözümünden çok OOM etkisini yumuşatma aracıdır.
- Docker image prune/log rotation politikası uygulanmalıdır.
- Prometheus retention test ortamında sınırlandırılmalıdır.
- DB ve app aynı node'a alınmamalıdır; mevcut veri ayrı DB node ihtiyacını desteklemektedir.
## Prod Ortamı Önerisi
Prod ortamında 3 app node ve 3 DB node olacak şekilde cluster topolojisi hedeflenir. Bu nedenle testteki tek node baskısı prod'da daha kontrollü dağıtılabilir.
### Önerilen Prod Başlangıç
| Sunucu | Rol | Önerilen Tip | CPU | RAM | SSD | Trafik | Aylık |
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |
| `iklim-app-01` | Swarm app node | `CPX42` | 8 AMD | 16 GB | 320 GB | 20 TB | $29.99 |
| `iklim-app-02` | Swarm app node | `CPX42` | 8 AMD | 16 GB | 320 GB | 20 TB | $29.99 |
| `iklim-app-03` | Swarm app node | `CPX42` | 8 AMD | 16 GB | 320 GB | 20 TB | $29.99 |
| `iklim-db-01` | DB cluster node | `CPX32` | 4 AMD | 8 GB | 160 GB | 20 TB | $16.49 |
| `iklim-db-02` | DB cluster node | `CPX32` | 4 AMD | 8 GB | 160 GB | 20 TB | $16.49 |
| `iklim-db-03` | DB cluster node | `CPX32` | 4 AMD | 8 GB | 160 GB | 20 TB | $16.49 |
| **Toplam** | 6 sunucu | | **36 vCPU** | **72 GB** | **1,440 GB** | **120 TB** | **$139.44** |
### Prod Gerekçesi
App katmanı:
- Test app verisi 16 GB sınıfı node'da 10 GiB used RAM göstermektedir.
- Prod'da trafik ve deploy frekansı artacağı için app node'larda `CPX32` yerine `CPX42` ile başlamak daha güvenlidir.
- 3 node sayesinde servisler dağıtılabilir; ancak Java mikroservislerin bellek karakteristiği nedeniyle node başına 16 GB RAM daha doğru başlangıçtır.
DB katmanı:
- Prod DB 3 node cluster olacağı için test DB'deki tek node riskinin aynısı birebir geçerli değildir.
- Başlangıç veri hacmi ve trafik düşük/orta ise `CPX32` ekonomik ve savunulabilir başlangıçtır.
- Büyüme görüldüğünde DB node'lar sırasıyla `CPX42`, `CPX52` veya `CPX62` modellerine rescale edilebilir.
- Lokal NVMe disk DB ana veri dizini için varsayılan tercih olarak korunmalıdır.
### Prod Büyüme Yolu
| Tetikleyici | İlk Aksiyon | İkinci Aksiyon |
| --- | --- | --- |
| App RAM baskısı | Servis memory limitlerini düzenle | App node'ları `CPX52` veya yeni app node'a genişlet |
| App CPU baskısı | Replica dağılımını düzenle | App node ekle veya `CPX52`'ye rescale et |
| DB CPU baskısı | Query/index analizi yap | DB node'ları `CPX42` veya `CPX52`'ye rescale et |
| DB RAM baskısı | Cache/config optimizasyonu yap | DB node'ları `CPX42` veya `CPX52`'ye rescale et |
| DB disk kapasitesi | Retention/cleanup uygula | Önce rescale, sonra ölçümlü ek volume |
| Disk IOPS baskısı | Query ve storage analizi yap | Lokal NVMe üzerinde daha büyük modele geç |
## Ek Volume Değerlendirmesi
DB ana veri dizinini başlangıçta Hetzner Volume üzerine taşımak önerilmez. Başlangıç için lokal NVMe disk daha güvenli varsayımdır.
Ek volume şu işler için daha uygundur:
- Backup dump dosyaları.
- Arşiv verisi.
- Düşük IOPS isteyen cold data.
- Geçici export/import alanları.
- Uygulama tarafında object-like dosya depolama ihtiyaçları.
DB ana data dizini için ek volume ancak aşağıdaki koşullarda değerlendirilmelidir:
- Volume performansı hedef DB workload'u ile ölçülmüş olmalı.
- IOPS, latency ve throughput metrikleri lokal disk ile karşılaştırılmalı.
- Backup/restore ve failover senaryoları netleştirilmeli.
- DB engine tarafında tablespace/partition yaklaşımı bilinçli tasarlanmalı.
Kapasite yetmezliği durumunda ilk tercih genellikle ek volume değil, sunucuyu daha büyük `CPX` modele rescale etmek olmalıdır.
## Terraform Hedef Değerleri
Terraform'da Hetzner server type değerleri küçük harfle kullanılmalıdır.
### Test
| Terraform Değişkeni | Hedef Değer | Gerekçe |
| --- | --- | --- |
| `server_type_swarm` | `cpx42` | Test app node için 16 GB RAM gerekli |
| `server_type_db` | `cpx42` | Tek node DB'de CPU spike riskini azaltır |
### Prod
| Terraform Değişkeni | Hedef Değer | Gerekçe |
| --- | --- | --- |
| `server_type_swarm` | `cpx42` | Java mikroservis yoğun app node'ları için dengeli başlangıç |
| `server_type_db` | `cpx32` | 3 node DB cluster için ekonomik başlangıç |
## Maliyet Özeti
### Test Maliyeti
| Kalem | Tip | Adet | Birim Aylık | Aylık Toplam |
| --- | --- | ---: | ---: | ---: |
| App | `CPX42` | 1 | $29.99 | $29.99 |
| DB | `CPX42` | 1 | $29.99 | $29.99 |
| **Toplam** | | **2** | | **$59.98** |
### Prod Maliyeti
| Kalem | Tip | Adet | Birim Aylık | Aylık Toplam |
| --- | --- | ---: | ---: | ---: |
| App | `CPX42` | 3 | $29.99 | $89.97 |
| DB | `CPX32` | 3 | $16.49 | $49.47 |
| **Toplam** | | **6** | | **$139.44** |
### Genel Toplam
| Ortam | Sunucu Adedi | Aylık Toplam |
| --- | ---: | ---: |
| Test | 2 | $59.98 |
| Prod | 6 | $139.44 |
| **Genel Toplam** | **8** | **$199.42** |
## Sonuç
Yeni test ortamında `CPX42 + CPX42` seçimi mevcut test verileriyle uyumludur. App node için `CPX32` RAM nedeniyle risklidir; DB node için `CPX32` ise tek node CPU spike'ları nedeniyle gereksiz risk taşır.
Prod ortamında `3 x CPX42 app` ve `3 x CPX32 DB` ekonomik ama teknik olarak savunulabilir bir başlangıçtır. Bu seçim, app katmanındaki bellek baskısını azaltır ve DB katmanında cluster yapısının sağladığı yatay kapasiteyi kullanır. Büyüme görüldüğünde önce metriklerle doğrulama yapılmalı, ardından rescale veya node ekleme tercih edilmelidir.

View File

@ -72,7 +72,7 @@ Mevcut uygulama stack dosyalarinda bazi servisler host port publish ediyor olabi
## Private Network Politikasi
Private network icinde acilmasi gereken portlarin ayrintili matrisi `07-private-network-port-matrisi.md` dosyasindadir. Ajanlar firewall veya Ansible UFW kurali yazarken bu dosyayi kaynak kabul etmelidir.
Private network icinde acilmasi gereken portlarin ayrintili matrisi `01-private-network-port-matrisi.md` dosyasindadir. Ajanlar firewall veya Ansible UFW kurali yazarken bu dosyayi kaynak kabul etmelidir.
## Gitea Actions Runner Karari
@ -97,12 +97,9 @@ Test icin tek runner yeterlidir:
## Deploy Lock Karari
Prod ortaminda 3 runner HA icin gereklidir; ancak ayni anda birden fazla deploy job'u
calistirabilir. Bu nedenle prod deploy islemleri StorageBox uzerinde otomatik lock ile
tekillestirilmelidir.
Prod ortaminda 3 runner HA icin gereklidir; ancak ayni anda birden fazla deploy job'u calistirabilir. Bu nedenle prod deploy islemleri StorageBox uzerinde otomatik lock ile tekillestirilmelidir.
Lock dosyalari/klasorleri manuel olusturulmayacak. Workflow basinda atomik `mkdir`
ile olusturulacak, deploy bitince `rmdir` ile silinecek.
Lock dosyalari/klasorleri manuel olusturulmayacak. Workflow basinda atomik `mkdir` ile olusturulacak, deploy bitince `rmdir` ile silinecek.
Onerilen StorageBox path'leri:
@ -118,8 +115,7 @@ Baslangic icin en sade ve guvenli model tek global prod deploy lock'tur:
prod/locks/prod-deploy.lock
```
Bu model tum prod deploy'lari siraya sokar. Daha sonra ihtiyac olursa servis bazli
lock modeline gecilebilir.
Bu model tum prod deploy'lari siraya sokar. Daha sonra ihtiyac olursa servis bazli lock modeline gecilebilir.
Ornek akış:
@ -129,10 +125,7 @@ ssh storagebox 'mkdir -p prod/locks && mkdir prod/locks/prod-deploy.lock'
ssh storagebox 'rmdir prod/locks/prod-deploy.lock'
```
`mkdir` atomik oldugu icin lock zaten varsa komut fail olur; bu durumda job beklemeli
veya temiz bir hata ile cikmalidir. Workflow fail olsa bile cleanup adimi lock'u silmeye
calismalidir. Eski kalmis lock'lari tespit etmek icin lock klasoru icine timestamp,
runner adi ve workflow bilgisi yazilabilir.
`mkdir` atomik oldugu icin lock zaten varsa komut fail olur; bu durumda job beklemeli veya temiz bir hata ile cikmalidir. Workflow fail olsa bile cleanup adimi lock'u silmeye calismalidir. Eski kalmis lock'lari tespit etmek icin lock klasoru icine timestamp, runner adi ve workflow bilgisi yazilabilir.
## Hetzner Fiziksel Host Ayrimi

View File

@ -85,22 +85,17 @@ Mevcut `docker-stack-infra.yml` bazi servisleri host mode ile publish ediyor ola
DB altyapisi manuel kurulacagi icin kesin cluster teknolojisi bu dokumanin disindadir. Yine de firewall icin varsayilan portlar asagidadir.
### PostgreSQL / PostGIS
### PostgreSQL / PostGIS (Patroni + etcd)
Prod ortami Patroni + etcd ile yonetilen PostgreSQL kullanir. Test ortaminda tek node oldugu icin replication ve HA portlari gerekmez.
| Port | Protocol | Kaynak | Hedef | Not |
| --- | --- | --- | --- | --- |
| `5432` | TCP | App/Swarm subnet | PostgreSQL node/cluster endpoint | Uygulama DB baglantisi |
| `5432` | TCP | DB subnet | PostgreSQL node'lari | Streaming replication ayni portu kullanabilir |
Eger Patroni kullanilirsa ek portlar daha sonra DB runbook'unda netlestirilmelidir:
| Port | Protocol | Amac |
| --- | --- | --- |
| `8008` | TCP | Patroni REST API |
| `2379-2380` | TCP | Patroni icin etcd kullanilirsa etcd client/peer |
| `5000-5001` | TCP | HAProxy veya benzeri DB endpoint kullanilirsa |
Bu ek portlar ancak ilgili teknoloji secildiginde acilmalidir.
| `5432` | TCP | App/Swarm subnet | PostgreSQL node'lari (Patroni yonetimli) | Uygulama JDBC — tum node'lara baglanir, driver primary'i bulur |
| `5432` | TCP | DB subnet | PostgreSQL node'lari | Patroni replication (pg_basebackup ve wal streaming) |
| `8008` | TCP | DB subnet | PostgreSQL node'lari | Patroni REST API — leader election, saglik kontrolu |
| `2379` | TCP | DB subnet | etcd node'lari | etcd client — Patroni → etcd erisimi |
| `2380` | TCP | DB subnet | etcd node'lari | etcd peer — etcd cluster icindeki raft protokolu |
### MongoDB
@ -126,17 +121,36 @@ Testte DB node tek oldugu icin DB subnet icindeki PostgreSQL/MongoDB replication
## Prod Private Kurallari
Prod ortaminda minimum:
Prod ortaminda minimum (Patroni + etcd dahil):
App subnet (swarm firewall) — kendi icindeki trafik:
| Kaynak | Hedef | Portlar |
| --- | --- | --- |
| `10.20.10.0/24` | `10.20.10.0/24` | `2377/tcp`, `7946/tcp`, `7946/udp`, `4789/udp` |
| `10.20.10.0/24` | `10.20.20.0/24` | `5432/tcp`, `27017/tcp` |
| `10.20.20.0/24` | `10.20.20.0/24` | `5432/tcp`, `27017/tcp` |
| `10.20.10.0/24` | `10.20.10.0/24` | `8200/tcp`, `6379/tcp`, `5672/tcp`, `61613/tcp`, `15674/tcp`, `2379/tcp` |
| `10.20.10.0/24` | `10.20.10.0/24` | `2377/tcp`, `7946/tcp`, `7946/udp`, `4789/udp` (Swarm) |
| `10.20.10.0/24` | `10.20.10.0/24` | `8200/tcp`, `6379/tcp`, `5672/tcp`, `61613/tcp`, `15674/tcp`, `2379/tcp` (uygulama servisleri) |
| Admin CIDR veya VPN | `10.20.10.0/24` | `15672/tcp`, `9180/tcp`, `9090/tcp`, `3000/tcp` |
Patroni, HAProxy, Mongo sharding veya ayri monitoring agent mimarisi secilirse bu matrise ek portlar kontrollu sekilde eklenmelidir.
App → DB trafigi (swarm firewall'da ilgili kural bulunmaz; db firewall'da izin verilir):
| Kaynak | Hedef | Portlar |
| --- | --- | --- |
| `10.20.10.0/24` | `10.20.20.0/24` | `5432/tcp`, `27017/tcp` (DB erisimi) |
| `10.20.10.0/24` | `10.20.20.0/24` | `2377/tcp`, `7946/tcp`, `7946/udp`, `4789/udp` (Swarm — DB worker join) |
DB subnet (db firewall) — DB node'lari arasi trafik:
| Kaynak | Hedef | Portlar |
| --- | --- | --- |
| `10.20.20.0/24` | `10.20.20.0/24` | `5432/tcp`, `27017/tcp` (DB replication) |
| `10.20.20.0/24` | `10.20.20.0/24` | `2379/tcp`, `2380/tcp` (etcd client/peer) |
| `10.20.20.0/24` | `10.20.20.0/24` | `8008/tcp` (Patroni REST API) |
DB → App trafigi (swarm firewall'da izin verilir):
| Kaynak | Hedef | Portlar |
| --- | --- | --- |
| `10.20.20.0/24` | `10.20.10.0/24` | `2377/tcp`, `7946/tcp`, `7946/udp`, `4789/udp` (Swarm — manager portlari) |
## Kabul Kriterleri

View File

@ -12,7 +12,7 @@ Terraform test ortaminda sunlari olusturur:
- DB subnet: `10.10.20.0/24`
- Firewall:
- Public ingress: sadece `22/tcp`, `80/tcp`, `443/tcp`
- Private ingress: `07-private-network-port-matrisi.md` dosyasindaki test kurallari
- Private ingress: `01-private-network-port-matrisi.md` dosyasindaki test kurallari
- SSH key
- Placement group: `iklim-test-spread`
- Floating IP: swarm entry point icin sabit IPv4
@ -51,8 +51,8 @@ Minimum degiskenler:
hcloud_token = "secret"
location = "fsn1"
image = "rocky-10"
server_type_swarm = "cx32"
server_type_db = "cx42"
server_type_swarm = "cpx42"
server_type_db = "cpx42"
admin_ssh_public_key_path = "~/.ssh/id_ed25519.pub"
admin_allowed_cidrs = ["X.X.X.X/32"]
```
@ -61,6 +61,11 @@ admin_allowed_cidrs = ["X.X.X.X/32"]
`location` icin tek lokasyonla baslanir. Farkli region/lokasyon felaket kurtarma bu asamada konu disidir; ileride dokumana eklenmelidir.
Server type karari `../hetzner-sizing-report.md` dokumanindaki mevcut test
ortami metriklerine dayanir. Test app node uzerinde 10 mikroservis ve altyapi
servisleri birlikte calistigi icin `cpx32` RAM acisindan riskli bulunmustur.
Test DB node icin de tek node CPU spike riski nedeniyle `cpx42` onerilir.
## Server Rolleri
| Server | Private IP | Rol |
@ -70,6 +75,14 @@ admin_allowed_cidrs = ["X.X.X.X/32"]
Private IP'ler Terraform icinde sabit tanimlanmalidir. Ansible inventory ve firewall kurallari deterministik kalir.
## Onerilen Kaynaklar ve Maliyet
| Server | Rol | Server Type | CPU | RAM | SSD | Aylik |
| --- | --- | --- | ---: | ---: | ---: | ---: |
| `iklim-app-01` | Swarm manager + app worker + Gitea runner | `cpx42` | 8 AMD | 16 GB | 320 GB | $29.99 |
| `iklim-db-01` | PostgreSQL/PostGIS + MongoDB node | `cpx42` | 8 AMD | 16 GB | 320 GB | $29.99 |
| **Toplam** | 2 server | | **16 vCPU** | **32 GB** | **640 GB** | **$59.98** |
## Firewall Kurallari
Public ingress:
@ -82,21 +95,49 @@ Public ingress:
Public ingress icin `8200/tcp`, `5432/tcp`, `27017/tcp`, `5672/tcp`, `15672/tcp`, `6379/tcp`, `2379/tcp`, `9000/tcp`, `9180/tcp`, `9090/tcp`, `3000/tcp` acilmayacak.
Private ingress (app subnet `10.10.10.0/24` kaynakli):
### App (swarm) Firewall — Private Ingress
App subnet kaynakli (iklim-app-01):
| Port | Servis | Erisim yontemi |
| --- | --- | --- |
| `2377/tcp` | Docker Swarm control plane | App subnet icinden |
| `7946/tcp,udp` | Docker Swarm node discovery | App subnet icinden |
| `4789/udp` | Docker Swarm VXLAN overlay | App subnet icinden |
| `8200/tcp` | Vault | Docker overlay / private network |
| `6379/tcp` | Redis | App subnet icinden |
| `5672/tcp` | RabbitMQ AMQP | App subnet icinden |
| `61613/tcp` | RabbitMQ STOMP | App subnet icinden |
| `15674/tcp` | RabbitMQ Web STOMP | App subnet icinden |
| `15672/tcp` | RabbitMQ Management | SWAG arkasinda `443` — IP kisitli |
| `9000/tcp` | APISIX Dashboard | SWAG arkasinda `443` — IP kisitli |
| `9180/tcp` | APISIX Admin API | Docker overlay icinden sadece Dashboard erisir |
| `9090/tcp` | Prometheus | SWAG arkasinda `443` — IP kisitli |
| `3000/tcp` | Grafana | SWAG arkasinda `443` — IP kisitli |
| `9000/tcp` | APISIX Dashboard | SWAG arkasinda `443` — IP kisitli |
| `9180/tcp` | APISIX Admin API | Docker overlay icinden sadece Dashboard erisir; insan erisimi gerekmez |
| `8200/tcp` | Vault | Docker overlay / private network |
DB subnet kaynakli (`iklim-db-01` Swarm'a worker olarak katildigi icin):
| Port | Servis | Kaynak |
| --- | --- | --- |
| `2377/tcp` | Docker Swarm control plane | `10.10.20.0/24` |
| `7946/tcp,udp` | Docker Swarm node discovery | `10.10.20.0/24` |
| `4789/udp` | Docker Swarm VXLAN overlay | `10.10.20.0/24` |
### DB Firewall — Private Ingress
| Port | Servis | Kaynak |
| --- | --- | --- |
| `22/tcp` | SSH | `admin_allowed_cidrs` |
| `5432/tcp` | PostgreSQL | `10.10.10.0/24` (app subnet) |
| `27017/tcp` | MongoDB | `10.10.10.0/24` (app subnet) |
| `2377/tcp` | Docker Swarm control plane | `10.10.10.0/24` (app subnet) |
| `7946/tcp,udp` | Docker Swarm node discovery | `10.10.10.0/24` (app subnet) |
| `4789/udp` | Docker Swarm VXLAN overlay | `10.10.10.0/24` (app subnet) |
IP kisitlamasi Hetzner firewall'da degil, SWAG nginx konfigurasyonunda yapilir.
Bu portlarin hicbiri `admin_allowed_cidrs` kaynagiyla public'ten acilmaz.
Diger private ingress kurallari icin `07-private-network-port-matrisi.md` kaynak alinacak.
Diger private ingress kurallari icin `01-private-network-port-matrisi.md` kaynak alinacak.
## Placement Group
@ -172,4 +213,3 @@ Kasitli silmek icin once lifecycle blogunu gecici olarak kaldir.
- Public internetten sadece `22`, `80`, `443` firewall seviyesinde aciktir.
- Vault `8200` public'ten kapali kalir.
- Terraform state repo'ya commit edilmez.

View File

@ -36,12 +36,17 @@ ansible/
Tum test node'larina uygulanir:
- `dnf update`
- temel paketler:
- temel paketler (sirasıyla kurulur):
- `epel-release` — fail2ban ve davfs2 bu repo'dan gelir; once kurulur
- `curl`
- `wget`
- `git`
- `jq`
- `tar`
- `unzip`
- `bash-completion`
- `gettext` — envsubst icin; CI/CD deploy pipeline'larinda gerekli
- `tree`
- `ca-certificates`
- `fail2ban`
- `chrony`
@ -67,6 +72,59 @@ Tum test node'larina uygulanir:
- outgoing: allow
- Public SSH sadece admin CIDR'dan acilir.
### SELinux Karari
Rocky Linux 10 SELinux enforcing modda gelir. Karar: **disabled**.
Gerekce:
- Hetzner Cloud firewall (dis perimeter) + firewalld (host) iki katman ag guvenligini saglar.
- Docker + davfs2 + firewalld kombinasyonu SELinux enforcing modda ek policy ve volume label yonetimi gerektirir.
- Utils VPS'te de disabled yapilmis; tutarlilik saglanir.
```bash
# /etc/selinux/config icinde:
SELINUX=disabled
# Degisiklik reboot sonrasi aktif olur
reboot
```
Ansible'da:
```yaml
- name: Disable SELinux
ansible.posix.selinux:
state: disabled
register: selinux_change
- name: Reboot if SELinux state changed
ansible.builtin.reboot:
when: selinux_change.changed
```
### fail2ban Konfigurasyonu
`/etc/fail2ban/jail.local` icerigi:
```ini
[DEFAULT]
ignoreip = 127.0.0.1/8 {{ admin_allowed_cidrs }}
bantime = 21600
findtime = 300
maxretry = 5
banaction = iptables-multiport
backend = systemd
[sshd]
enabled = true
```
- `bantime`: 6 saat ban
- `findtime`: 5 dakika icinde
- `maxretry`: 5 basarisiz giris → ban
- `ignoreip`: admin CIDR'lari ban'dan muaf tutar
Ansible'da `admin_allowed_cidrs` listesi space-separated stringe donusturulup template'e basilir.
Not: Docker iptables kurallari firewalld ile etkilesebilir. Hetzner Cloud firewall asil dis perimeter kabul edilir; firewalld host icinde ikinci katman olarak kullanilir.
## Docker Role
@ -243,10 +301,69 @@ vault_storagebox_password: "SUB_ACCOUNT_PAROLASI"
### Notlar
- `davfs2` paketi EPEL repository'sinde bulunur; base role'de `dnf install epel-release` yapilmalidir.
- `davfs2` paketi EPEL repository'sinde bulunur; base role `epel-release`'i zaten kurar.
- StorageBox sifreleri asla plaintext olarak repository'e eklenmez; Ansible Vault zorunludur.
- Mount noktasi reboot'ta `_netdev` flag'i sayesinde network hazir olduktan sonra otomatik mount edilir.
- Docker volume'lari bu dizin altindaki bir alt klasore yonlendirilir, ornegin `/mnt/storagebox/volumes/`.
- Docker Swarm servisleri `/mnt/storagebox/<env>/<service>/` altindaki dizinleri bind mount olarak kullanir.
## StorageBox SSH Key Role
Her iki node'a uygulanir (`iklim-app-01` ve `iklim-db-01`).
### Amac
Sunucu uzerinde ed25519 SSH anahtar cifti uretilir ve StorageBox ana hesabina yuklenir.
Bu sayede CI/CD pipeline'lari `STORAGEBOX_SSH_PRIV` Gitea secret'ini kullanarak
sifre girmeden StorageBox'a erisebilir.
### Adimlar
1. **SSH key uret** (eger yoksa)
```yaml
- name: Generate SSH key for storagebox
ansible.builtin.user:
name: root
generate_ssh_key: yes
ssh_key_type: ed25519
ssh_key_file: /root/.ssh/id_ed25519_storagebox
ssh_key_comment: "{{ inventory_hostname }}-storagebox"
```
2. **Public key'i StorageBox'a yukle**
Bu adim manuel yapilir (ilk kez sifre gerektirir):
```bash
cat /root/.ssh/id_ed25519_storagebox.pub | ssh -p23 u469968-sub1@u469968-sub1.your-storagebox.de install-ssh-key
```
Sonraki erisimler sifresiz calisir:
```bash
sftp -P23 u469968-sub1@u469968-sub1.your-storagebox.de
```
3. **Private ve public key'leri Gitea'ya ekle**
Gitea → Organization Settings → Actions → Secrets:
| Secret Adi | Deger |
| --- | --- |
| `STORAGEBOX_SSH_PRIV` | `/root/.ssh/id_ed25519_storagebox` icerigi |
| `STORAGEBOX_SSH_PUB` | `/root/.ssh/id_ed25519_storagebox.pub` icerigi |
Key icerigini almak icin:
```bash
cat /root/.ssh/id_ed25519_storagebox
cat /root/.ssh/id_ed25519_storagebox.pub
```
### Notlar
- Her sunucu icin ayri key uretilir; tum public key'ler StorageBox ana hesabina yuklenir.
- Private key asla repo'ya commit edilmez; yalnizca Gitea secret olarak saklanir.
## Kabul Kriterleri

View File

@ -0,0 +1,286 @@
# 08 - Test DB Docker Kurulumu (Swarm)
Bu asamanin amaci `iklim-db-01` node'unu Swarm'a worker olarak eklemek ve PostgreSQL ile MongoDB'yi Swarm servisi olarak calistirmaktir. Veri kaliciligini StorageBox saglar.
DB yazilimi Ansible tarafindan kurulmaz; bu belge DB node uzerinde elle veya ayri bir Ansible role ile uygulanir. `03-test-ansible-bootstrap.md` tamamlandiktan sonra baslayiniz.
## Mimari
```
iklim-app-01 (Swarm manager, 10.10.10.11)
|
|-- iklimco-net (overlay)
|
iklim-db-01 (Swarm worker, 10.10.20.11) [role=db]
|-- postgresql-v17 (Swarm service, placement: role=db)
|-- mongo-v8 (Swarm service, placement: role=db)
|
/mnt/storagebox/test/db/
postgresql/data/
mongodb/data/
mongodb/log/
mongodb/config/
```
## On Kosullar
- `03-test-ansible-bootstrap.md` her iki node'da tamamlanmis olmali.
- StorageBox `/mnt/storagebox` olarak her iki node'da mount edilmis olmali.
- Docker `iklim-db-01` uzerinde kurulu olmali (bootstrap role bunu yapar).
## 1. Firewall Guncellemesi
`iklim-db-01`'in Swarm'a katilabilmesi icin ek kurallara ihtiyac var.
`terraform/hetzner/test/firewall.tf` dosyasina asagidaki kurallari ekle:
`hcloud_firewall.swarm` icine (DB subnet'ten Swarm portlarina erisim):
```hcl
rule {
direction = "in"
protocol = "tcp"
port = "2377"
source_ips = [local.db_subnet_cidr]
description = "Docker Swarm control plane from DB subnet"
}
rule {
direction = "in"
protocol = "tcp"
port = "7946"
source_ips = [local.db_subnet_cidr]
description = "Docker Swarm node discovery (TCP) from DB subnet"
}
rule {
direction = "in"
protocol = "udp"
port = "7946"
source_ips = [local.db_subnet_cidr]
description = "Docker Swarm node discovery (UDP) from DB subnet"
}
rule {
direction = "in"
protocol = "udp"
port = "4789"
source_ips = [local.db_subnet_cidr]
description = "Docker Swarm VXLAN overlay from DB subnet"
}
```
`hcloud_firewall.db` icine (app subnet'ten Swarm portlarina erisim):
```hcl
rule {
direction = "in"
protocol = "tcp"
port = "2377"
source_ips = [local.app_subnet_cidr]
description = "Docker Swarm control plane from app subnet"
}
rule {
direction = "in"
protocol = "tcp"
port = "7946"
source_ips = [local.app_subnet_cidr]
description = "Docker Swarm node discovery (TCP) from app subnet"
}
rule {
direction = "in"
protocol = "udp"
port = "7946"
source_ips = [local.app_subnet_cidr]
description = "Docker Swarm node discovery (UDP) from app subnet"
}
rule {
direction = "in"
protocol = "udp"
port = "4789"
source_ips = [local.app_subnet_cidr]
description = "Docker Swarm VXLAN overlay from app subnet"
}
```
Sonra uygula:
```bash
cd terraform/hetzner/test
terraform apply
```
## 2. DB Node'u Swarm'a Ekleme
**iklim-app-01 uzerinde** join token al:
```bash
docker swarm join-token worker
```
**iklim-db-01 uzerinde** Swarm'a katil:
```bash
docker swarm join --token <TOKEN> 10.10.10.11:2377
```
**iklim-app-01 uzerinde** node'u etiketle:
```bash
docker node update --label-add role=db iklim-db-01
docker node ls
```
## 3. StorageBox Dizin Yapisi
**iklim-db-01 uzerinde:**
```bash
mkdir -p /mnt/storagebox/test/db/postgresql/data
mkdir -p /mnt/storagebox/test/db/mongodb/data
mkdir -p /mnt/storagebox/test/db/mongodb/log
mkdir -p /mnt/storagebox/test/db/mongodb/config
```
## 4. PostgreSQL Stack
### mongod.conf
`/mnt/storagebox/test/db/mongodb/config/mongod.conf` dosyasini olustur:
```yaml
processManagement:
pidFilePath: "/data/db/mongod.pid"
net:
port: 27017
storage:
engine: "wiredTiger"
dbPath: "/data/db"
directoryPerDB: true
systemLog:
verbosity: 0
timeStampFormat: "iso8601-local"
destination: file
path: "/data/log/mongo.log"
logAppend: true
logRotate: rename
security:
authorization: enabled
```
### Stack Dosyasi
`/opt/iklimco/stacks/db.yml`:
```yaml
version: "3.8"
networks:
iklimco-net:
external: true
services:
postgresql:
image: postgis/postgis:17-3.5
environment:
POSTGRES_USER: "${DATABASE_POSTGRES_ROOT_USER}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_DB: postgres
PGDATA: /var/lib/postgresql/data/pgdata
TZ: "Europe/Istanbul"
volumes:
- /mnt/storagebox/test/db/postgresql/data:/var/lib/postgresql/data
- /opt/iklimco/init/postgresql:/docker-entrypoint-initdb.d:ro
networks:
- iklimco-net
deploy:
replicas: 1
placement:
constraints:
- node.labels.role == db
restart_policy:
condition: on-failure
mongodb:
image: mongo:8
environment:
MONGO_INITDB_ROOT_USERNAME: mongo-root
MONGO_INITDB_ROOT_PASSWORD: "${MONGO_ROOT_PASSWORD}"
volumes:
- /mnt/storagebox/test/db/mongodb/data:/data/db
- /mnt/storagebox/test/db/mongodb/log:/data/log
- /mnt/storagebox/test/db/mongodb/config:/data/configdb
networks:
- iklimco-net
command: ["--config", "/data/configdb/mongod.conf"]
deploy:
replicas: 1
placement:
constraints:
- node.labels.role == db
restart_policy:
condition: on-failure
```
### .env Dosyasi
`/opt/iklimco/stacks/.env` (repo'ya commit edilmez):
```env
DATABASE_POSTGRES_ROOT_USER=<kullanici-adi>
POSTGRES_PASSWORD=<guclu-sifre>
MONGO_ROOT_PASSWORD=<guclu-sifre>
```
## 5. Deploy
```bash
# iklim-app-01 uzerinde (Swarm manager)
docker stack deploy --compose-file /opt/iklimco/stacks/db.yml \
--with-registry-auth \
$(set -a; source /opt/iklimco/stacks/.env; set +a; echo "") \
iklim-db
```
Alternatif olarak env-file destegi icin:
```bash
docker stack deploy \
--compose-file <(docker-compose -f /opt/iklimco/stacks/db.yml config) \
iklim-db
```
Kontrol:
```bash
docker stack services iklim-db
docker service logs iklim-db_postgresql
docker service logs iklim-db_mongodb
```
## 6. App Servislerinden Erisim
Overlay network (`iklimco-net`) uzerinden servis adlariyla erisim:
| Servis | Host (overlay DNS) | Port |
| --- | --- | --- |
| PostgreSQL | `iklim-db_postgresql` | `5432` |
| MongoDB | `iklim-db_mongodb` | `27017` |
Spring Boot vb. uygulama konfigurasyon ornegi:
```
spring.datasource.url=jdbc:postgresql://iklim-db_postgresql:5432/iklimdb
spring.data.mongodb.uri=mongodb://mongo-root:<sifre>@iklim-db_mongodb:27017/iklimdb?authSource=admin
```
## Kabul Kriterleri
- `docker stack services iklim-db` her iki servisi `1/1` olarak gosterir.
- `iklim-app-01` uzerinden `postgresql` servisine TCP 5432 overlay ile ulasilabilir.
- `iklim-app-01` uzerinden `mongodb` servisine TCP 27017 overlay ile ulasilabilir.
- `/mnt/storagebox/test/db/postgresql/data/` dizininde veri dosyalari olusur.
- Servis yeniden baslatildiginda veri korunur.
- `5432` ve `27017` portlari public internet'ten kapalidir.

View File

@ -101,12 +101,49 @@ Runner kurulumu ve pipeline calismasi icin secret'lar:
- Gitea runner registration token
- Harbor username/password veya token
- StorageBox credential
- StorageBox SSH key (priv + pub)
- SSH deploy key
- Hetzner token gerekmez; Terraform asamasinda kullanilir
Bu secret'lar repo'ya yazilmayacak.
## Gitea Organizasyon Secret'lari
Gitea → `git.tarla.io` → Organization → **Settings****Actions****Secrets** altina eklenir.
| Secret | Aciklama | Kaynak |
| --- | --- | --- |
| `STORAGEBOX_SSH_PRIV` | StorageBox erisimi icin private SSH key | Her sunucuda `/root/.ssh/id_ed25519_storagebox` |
| `STORAGEBOX_SSH_PUB` | Eslesen public key | Her sunucuda `/root/.ssh/id_ed25519_storagebox.pub` |
| `REPO_ACCESS_TOKEN` | Pipeline'in Gitea API'sini cagirabileceği token | Gitea → User Settings → Applications → Access Tokens |
Workflow'da kullanim ornegi:
```yaml
- name: Set up StorageBox SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.STORAGEBOX_SSH_PRIV }}" > ~/.ssh/id_ed25519_storagebox
echo "${{ secrets.STORAGEBOX_SSH_PUB }}" > ~/.ssh/id_ed25519_storagebox.pub
chmod 600 ~/.ssh/id_ed25519_storagebox
ssh-keyscan -p 23 u469968-sub1.your-storagebox.de >> ~/.ssh/known_hosts
```
### Ansible Vault Secret'lari
CI/CD disindaki secret'lar `ansible/group_vars/test-vault.yml` icinde Ansible Vault ile sifrelenir:
```bash
ansible-vault edit ansible/group_vars/test-vault.yml
```
Icerik:
```yaml
vault_storagebox_password: "SUB_ACCOUNT_PAROLASI"
vault_gitea_runner_token: "RUNNER_REGISTRATION_TOKEN"
```
## Kabul Kriterleri
- `systemctl status gitea-act-runner` active gorunur.

View File

@ -12,7 +12,7 @@ Terraform prod ortaminda sunlari olusturur:
- DB subnet: `10.20.20.0/24`
- Firewall:
- Public ingress: sadece `22/tcp`, `80/tcp`, `443/tcp`
- Private ingress: `07-private-network-port-matrisi.md` dosyasindaki prod kurallari
- Private ingress: `01-private-network-port-matrisi.md` dosyasindaki prod kurallari
- SSH key
- Placement groups:
- `iklim-prod-app-spread`
@ -60,13 +60,18 @@ Minimum degiskenler:
hcloud_token = "secret"
location = "fsn1"
image = "rocky-10"
server_type_swarm = "cx42"
server_type_db = "cx52"
server_type_swarm = "cpx42"
server_type_db = "cpx32"
admin_ssh_public_key_path = "~/.ssh/id_ed25519.pub"
admin_allowed_cidrs = ["X.X.X.X/32"]
```
Server type degerleri kapasiteye gore degisebilir. Bu dokuman topoloji ve guvenlik kararini tanimlar; sizing daha sonra revize edilebilir.
Server type karari `../hetzner-sizing-report.md` dokumanindaki mevcut test
ortami metrikleri ve prod cluster topolojisi dikkate alinarak belirlenmistir.
Prod app node'lar icin Java mikroservis bellek baskisi nedeniyle `cpx42`,
prod DB node'lar icin ise 3 node cluster baslangici nedeniyle ekonomik
`cpx32` onerilir. Kapasite ihtiyaci metriklerle dogrulandiginda node ekleme
veya in-place rescale yapilabilir.
## Server Rolleri ve Private IP Plani
@ -81,6 +86,18 @@ Server type degerleri kapasiteye gore degisebilir. Bu dokuman topoloji ve guvenl
Private IP'ler `locals.tf` icinde `swarm_private_ips` ve `db_private_ips` map'leri olarak sabit tanimlanir. Sunucu listesi `for_each` ile bu map'lerden turetilir.
## Onerilen Kaynaklar ve Maliyet
| Server | Rol | Server Type | CPU | RAM | SSD | Aylik |
| --- | --- | --- | ---: | ---: | ---: | ---: |
| `iklim-app-01` | Swarm manager + app worker + runner | `cpx42` | 8 AMD | 16 GB | 320 GB | $29.99 |
| `iklim-app-02` | Swarm manager + app worker + runner | `cpx42` | 8 AMD | 16 GB | 320 GB | $29.99 |
| `iklim-app-03` | Swarm manager + app worker + runner | `cpx42` | 8 AMD | 16 GB | 320 GB | $29.99 |
| `iklim-db-01` | DB cluster node | `cpx32` | 4 AMD | 8 GB | 160 GB | $16.49 |
| `iklim-db-02` | DB cluster node | `cpx32` | 4 AMD | 8 GB | 160 GB | $16.49 |
| `iklim-db-03` | DB cluster node | `cpx32` | 4 AMD | 8 GB | 160 GB | $16.49 |
| **Toplam** | 6 server | | **36 vCPU** | **72 GB** | **1,440 GB** | **$139.44** |
## Placement Group Karari
Prod icin iki ayri spread placement group:
@ -127,30 +144,56 @@ Prod'da su portlar public acilmayacak:
## Private Firewall
Private ingress (app subnet `10.20.10.0/24` kaynakli):
### App (swarm) Firewall — Private Ingress
App subnet kaynakli (`10.20.10.0/24`):
| Port | Servis | Erisim yontemi |
| --- | --- | --- |
| `15672/tcp` | RabbitMQ Management | SWAG arkasinda `443` — IP kisitli |
| `9090/tcp` | Prometheus | SWAG arkasinda `443` — IP kisitli |
| `3000/tcp` | Grafana | SWAG arkasinda `443` — IP kisitli |
| `9000/tcp` | APISIX Dashboard | SWAG arkasinda `443` — IP kisitli |
| `9180/tcp` | APISIX Admin API | Docker overlay icinden sadece Dashboard erisir |
| `8200/tcp` | Vault | Docker overlay / private network |
| `2377/tcp` | Docker Swarm control plane | App subnet icinden |
| `7946/tcp`, `7946/udp` | Docker Swarm node discovery | App subnet icinden |
| `7946/tcp,udp` | Docker Swarm node discovery | App subnet icinden |
| `4789/udp` | Docker Swarm VXLAN overlay | App subnet icinden |
| `8200/tcp` | Vault | Docker overlay / private network |
| `6379/tcp` | Redis | App subnet icinden |
| `5672/tcp` | RabbitMQ AMQP | App subnet icinden |
| `61613/tcp` | RabbitMQ STOMP | App subnet icinden |
| `15674/tcp` | RabbitMQ Web STOMP | App subnet icinden |
| `15672/tcp` | RabbitMQ Management | SWAG arkasinda `443` — IP kisitli |
| `9000/tcp` | APISIX Dashboard | SWAG arkasinda `443` — IP kisitli |
| `9180/tcp` | APISIX Admin API | Docker overlay icinden sadece Dashboard erisir |
| `9090/tcp` | Prometheus | SWAG arkasinda `443` — IP kisitli |
| `3000/tcp` | Grafana | SWAG arkasinda `443` — IP kisitli |
DB firewall ek kurallar (db subnet `10.20.20.0/24` kaynakli):
DB subnet kaynakli (`iklim-db-*` node'lari Swarm'a worker olarak katildigi icin):
| Port | Servis | Kural |
| Port | Servis | Kaynak |
| --- | --- | --- |
| `5432/tcp` | PostgreSQL replication | DB subnet icinden |
| `27017/tcp` | MongoDB replica set | DB subnet icinden |
| `2377/tcp` | Docker Swarm control plane | `10.20.20.0/24` |
| `7946/tcp,udp` | Docker Swarm node discovery | `10.20.20.0/24` |
| `4789/udp` | Docker Swarm VXLAN overlay | `10.20.20.0/24` |
### DB Firewall — Private Ingress
App subnet kaynakli (`10.20.10.0/24`):
| Port | Servis | Not |
| --- | --- | --- |
| `22/tcp` | SSH | `admin_allowed_cidrs` |
| `5432/tcp` | PostgreSQL (Patroni primary) | App subnet erisimi |
| `27017/tcp` | MongoDB replica set endpoint | App subnet erisimi |
| `2377/tcp` | Docker Swarm control plane | App subnet icinden |
| `7946/tcp,udp` | Docker Swarm node discovery | App subnet icinden |
| `4789/udp` | Docker Swarm VXLAN overlay | App subnet icinden |
DB subnet icindeki karsilikli erisim (`10.20.20.0/24`):
| Port | Servis | Not |
| --- | --- | --- |
| `5432/tcp` | PostgreSQL Patroni replication | DB node'lari arasi |
| `27017/tcp` | MongoDB replica set internal | DB node'lari arasi |
| `2379/tcp` | etcd client | Patroni → etcd erisimi |
| `2380/tcp` | etcd peer | etcd cluster internal |
| `8008/tcp` | Patroni REST API | Patroni leader election ve saglik kontrolu |
IP kisitlamasi Hetzner firewall'da degil, SWAG nginx konfigurasyonunda yapilir.
@ -196,7 +239,7 @@ once lifecycle blogunu gecici olarak kaldir.
- Swarm node'lari `iklim-prod-app-spread` placement group icindedir.
- DB node'lari `iklim-prod-db-spread` placement group icindedir.
- Public firewall sadece `22`, `80`, `443` ingress'e izin verir.
- Private firewall `07-private-network-port-matrisi.md` ile uyumludur.
- Private firewall `01-private-network-port-matrisi.md` ile uyumludur.
- DB replication portlari yalnizca DB subnet'ten erisilebilir.
- Floating IP olusur ve `iklim-app-01`'e atanir.
- Terraform state ve secret tfvars commit edilmez.

View File

@ -39,17 +39,20 @@ ansible/
Tum prod node'larina uygulanir:
- Paket cache update
- Temel paketler:
- Temel paketler (sirasıyla kurulur):
- `epel-release` — fail2ban ve davfs2 bu repo'dan gelir; once kurulur
- `curl`
- `wget`
- `git`
- `jq`
- `tar`
- `unzip`
- `bash-completion`
- `gettext` — envsubst icin; CI/CD deploy pipeline'larinda gerekli
- `tree`
- `ca-certificates`
- `gnupg`
- `lsb-release`
- `ufw`
- `fail2ban`
- `firewalld`
- `chrony`
- `python3`
- `python3-pip`

View File

@ -0,0 +1,831 @@
# 09 - Prod DB Cluster Kurulumu (Swarm)
Bu aşamanın amacı üç DB node'unu Docker Swarm'a worker olarak eklemek, MongoDB replica set ve Patroni + etcd ile yönetilen PostgreSQL yüksek erişilebilirlik konfigürasyonunu yapmaktır.
`07-prod-ansible-bootstrap.md` tüm DB node'larında tamamlanmış olmalıdır.
## Mimari
```
iklim-app-01/02/03 (Swarm manager'lar, 10.20.10.11/12/13)
|
|-- iklimco-net (overlay)
|
iklim-db-01 (Swarm worker, 10.20.20.11)
mongodb-01 [rs0 member 0 — preferred primary]
etcd-01 [etcd cluster member]
patroni-01 [Patroni + PostgreSQL — ilk primary adayı]
iklim-db-02 (Swarm worker, 10.20.20.12)
mongodb-02 [rs0 member 1]
etcd-02 [etcd cluster member]
patroni-02 [Patroni + PostgreSQL — standby]
iklim-db-03 (Swarm worker, 10.20.20.13)
mongodb-03 [rs0 member 2]
etcd-03 [etcd cluster member]
patroni-03 [Patroni + PostgreSQL — standby]
```
DB container'ları birbirlerini overlay DNS adıyla değil, **Hetzner private IP üzerinden** tanıyor. Bu nedenle her servis portunu `host` modda yayımlar; replikasyon ve etcd trafiği doğrudan private network üzerinden gecer. Hetzner Cloud firewall ve prod `db` firewall zaten bu portlara izin vermektedir.
## 1. Firewall Güncellemesi
`terraform/hetzner/prod/firewall.tf` dosyasına aşağıdaki kuralları ekle.
`hcloud_firewall.swarm` içine (DB subnet'ten Swarm portlarına):
```hcl
rule {
direction = "in"
protocol = "tcp"
port = "2377"
source_ips = [local.db_subnet_cidr]
description = "Docker Swarm control plane from DB subnet"
}
rule {
direction = "in"
protocol = "tcp"
port = "7946"
source_ips = [local.db_subnet_cidr]
description = "Docker Swarm node discovery (TCP) from DB subnet"
}
rule {
direction = "in"
protocol = "udp"
port = "7946"
source_ips = [local.db_subnet_cidr]
description = "Docker Swarm node discovery (UDP) from DB subnet"
}
rule {
direction = "in"
protocol = "udp"
port = "4789"
source_ips = [local.db_subnet_cidr]
description = "Docker Swarm VXLAN overlay from DB subnet"
}
```
`hcloud_firewall.db` içine (app subnet'ten Swarm portlarına + overlay; DB subnet içi etcd/Patroni trafiği):
```hcl
rule {
direction = "in"
protocol = "tcp"
port = "2377"
source_ips = [local.app_subnet_cidr]
description = "Docker Swarm control plane from app subnet"
}
rule {
direction = "in"
protocol = "tcp"
port = "7946"
source_ips = [local.app_subnet_cidr]
description = "Docker Swarm node discovery (TCP) from app subnet"
}
rule {
direction = "in"
protocol = "udp"
port = "7946"
source_ips = [local.app_subnet_cidr]
description = "Docker Swarm node discovery (UDP) from app subnet"
}
rule {
direction = "in"
protocol = "udp"
port = "4789"
source_ips = [local.app_subnet_cidr]
description = "Docker Swarm VXLAN overlay from app subnet"
}
rule {
direction = "in"
protocol = "tcp"
port = "2379"
source_ips = [local.db_subnet_cidr]
description = "etcd client port within DB subnet"
}
rule {
direction = "in"
protocol = "tcp"
port = "2380"
source_ips = [local.db_subnet_cidr]
description = "etcd peer port within DB subnet"
}
rule {
direction = "in"
protocol = "tcp"
port = "8008"
source_ips = [local.db_subnet_cidr]
description = "Patroni REST API within DB subnet"
}
```
```bash
cd terraform/hetzner/prod
terraform apply
```
## 2. DB Node'larını Swarm'a Ekleme
**Swarm manager'lardan birinde** (iklim-app-01) join token al:
```bash
docker swarm join-token worker
```
**Her DB node'unda** (iklim-db-01, iklim-db-02, iklim-db-03):
```bash
docker swarm join --token <TOKEN> 10.20.10.11:2377
```
**iklim-app-01 üzerinde** node'ları etiketle:
```bash
docker node update --label-add role=db --label-add db-index=01 iklim-db-01
docker node update --label-add role=db --label-add db-index=02 iklim-db-02
docker node update --label-add role=db --label-add db-index=03 iklim-db-03
docker node ls
```
## 3. StorageBox Dizin Yapısı
Her DB node'unda (`/mnt/storagebox` zaten mount edilmiş olmalı):
```bash
# iklim-db-01 üzerinde:
mkdir -p /mnt/storagebox/prod/db/mongodb-01/{data,log,config}
mkdir -p /mnt/storagebox/prod/db/postgresql-01/{data,config}
mkdir -p /mnt/storagebox/prod/db/etcd-01/data
# iklim-db-02 üzerinde:
mkdir -p /mnt/storagebox/prod/db/mongodb-02/{data,log,config}
mkdir -p /mnt/storagebox/prod/db/postgresql-02/{data,config}
mkdir -p /mnt/storagebox/prod/db/etcd-02/data
# iklim-db-03 üzerinde:
mkdir -p /mnt/storagebox/prod/db/mongodb-03/{data,log,config}
mkdir -p /mnt/storagebox/prod/db/postgresql-03/{data,config}
mkdir -p /mnt/storagebox/prod/db/etcd-03/data
```
## 4. MongoDB Replica Set
### mongod.conf
Her DB node'unda `/mnt/storagebox/prod/db/mongodb-0X/config/mongod.conf`:
```yaml
net:
port: 27017
storage:
engine: "wiredTiger"
dbPath: "/data/db"
directoryPerDB: true
systemLog:
verbosity: 0
timeStampFormat: "iso8601-local"
destination: file
path: "/data/log/mongo.log"
logAppend: true
logRotate: rename
replication:
replSetName: "rs0"
security:
authorization: enabled
keyFile: "/data/configdb/rs-auth.key"
```
### Replica Set Auth Key
Tüm DB node'larında **aynı** key dosyası olmalıdır:
```bash
# iklim-db-01 üzerinde oluştur:
openssl rand -base64 756 > /mnt/storagebox/prod/db/mongodb-01/config/rs-auth.key
chmod 400 /mnt/storagebox/prod/db/mongodb-01/config/rs-auth.key
# Aynı içeriği diğer node'lara kopyala:
cat /mnt/storagebox/prod/db/mongodb-01/config/rs-auth.key \
> /mnt/storagebox/prod/db/mongodb-02/config/rs-auth.key
cat /mnt/storagebox/prod/db/mongodb-01/config/rs-auth.key \
> /mnt/storagebox/prod/db/mongodb-03/config/rs-auth.key
chmod 400 /mnt/storagebox/prod/db/mongodb-0{2,3}/config/rs-auth.key
```
### Stack Dosyası — MongoDB
`/opt/iklimco/stacks/prod-db-mongo.yml`:
```yaml
version: "3.8"
networks:
iklimco-net:
external: true
services:
mongodb-01:
image: mongo:8
environment:
MONGO_INITDB_ROOT_USERNAME: mongo-root
MONGO_INITDB_ROOT_PASSWORD: "${MONGO_ROOT_PASSWORD}"
volumes:
- /mnt/storagebox/prod/db/mongodb-01/data:/data/db
- /mnt/storagebox/prod/db/mongodb-01/log:/data/log
- /mnt/storagebox/prod/db/mongodb-01/config:/data/configdb
networks:
- iklimco-net
ports:
- target: 27017
published: 27017
protocol: tcp
mode: host
command: ["--config", "/data/configdb/mongod.conf"]
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-01
restart_policy:
condition: on-failure
mongodb-02:
image: mongo:8
environment:
MONGO_INITDB_ROOT_USERNAME: mongo-root
MONGO_INITDB_ROOT_PASSWORD: "${MONGO_ROOT_PASSWORD}"
volumes:
- /mnt/storagebox/prod/db/mongodb-02/data:/data/db
- /mnt/storagebox/prod/db/mongodb-02/log:/data/log
- /mnt/storagebox/prod/db/mongodb-02/config:/data/configdb
networks:
- iklimco-net
ports:
- target: 27017
published: 27017
protocol: tcp
mode: host
command: ["--config", "/data/configdb/mongod.conf"]
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-02
restart_policy:
condition: on-failure
mongodb-03:
image: mongo:8
environment:
MONGO_INITDB_ROOT_USERNAME: mongo-root
MONGO_INITDB_ROOT_PASSWORD: "${MONGO_ROOT_PASSWORD}"
volumes:
- /mnt/storagebox/prod/db/mongodb-03/data:/data/db
- /mnt/storagebox/prod/db/mongodb-03/log:/data/log
- /mnt/storagebox/prod/db/mongodb-03/config:/data/configdb
networks:
- iklimco-net
ports:
- target: 27017
published: 27017
protocol: tcp
mode: host
command: ["--config", "/data/configdb/mongod.conf"]
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-03
restart_policy:
condition: on-failure
```
### Replica Set Başlangıç
Stack deploy edildikten sonra **bir kez** çalıştırılır:
```bash
# iklim-db-01 üzerinde:
docker exec -it $(docker ps -q -f name=iklim-db_mongodb-01) mongosh \
-u mongo-root -p "${MONGO_ROOT_PASSWORD}" --authenticationDatabase admin
# mongosh içinde:
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "10.20.20.11:27017", priority: 2 },
{ _id: 1, host: "10.20.20.12:27017", priority: 1 },
{ _id: 2, host: "10.20.20.13:27017", priority: 1 }
]
})
# Durum kontrol:
rs.status()
```
`"stateStr": "PRIMARY"` ve iki `"SECONDARY"` görülünce replica set hazırdır.
## 5. PostgreSQL — Patroni + etcd
Patroni, PostgreSQL primary/standby rollerini etcd üzerinden koordine eder. Primary düşerse diğer node'lardan biri otomatik olarak seçim kazanır ve primary olur. Swarm servisi container'ı yeniden başlatır; Patroni kaldığı yerden devam eder.
### 5.1 Özel Image (Patroni + PostGIS)
`postgis/postgis:17-3.5` imajı üzerine Patroni kurulur. Bu imaj Harbor'a push edilip stack'te kullanılır.
`Environment_Infrastructure/docker/patroni-postgis/Dockerfile`:
```dockerfile
FROM postgis/postgis:17-3.5
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
python3-pip \
python3-dev \
gcc \
libpq-dev \
&& pip3 install --no-cache-dir 'patroni[etcd3]' \
&& apt-get purge -y gcc python3-dev \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
USER postgres
ENTRYPOINT ["patroni", "/etc/patroni/patroni.yml"]
```
Build ve push:
```bash
cd Environment_Infrastructure/docker/patroni-postgis
docker build -t registry.iklim.co/infra/patroni-postgis:17-3.5 .
docker push registry.iklim.co/infra/patroni-postgis:17-3.5
```
### 5.2 etcd Kümesi
#### Stack Dosyası — etcd
`/opt/iklimco/stacks/prod-db-etcd.yml`:
```yaml
version: "3.8"
networks:
iklimco-net:
external: true
services:
etcd-01:
image: bitnami/etcd:3
environment:
ALLOW_NONE_AUTHENTICATION: "yes"
ETCD_NAME: etcd-01
ETCD_INITIAL_ADVERTISE_PEER_URLS: http://10.20.20.11:2380
ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
ETCD_ADVERTISE_CLIENT_URLS: http://10.20.20.11:2379
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_INITIAL_CLUSTER: "etcd-01=http://10.20.20.11:2380,etcd-02=http://10.20.20.12:2380,etcd-03=http://10.20.20.13:2380"
ETCD_INITIAL_CLUSTER_STATE: new
ETCD_INITIAL_CLUSTER_TOKEN: iklimco-etcd-prod
volumes:
- /mnt/storagebox/prod/db/etcd-01/data:/bitnami/etcd/data
networks:
- iklimco-net
ports:
- target: 2379
published: 2379
protocol: tcp
mode: host
- target: 2380
published: 2380
protocol: tcp
mode: host
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-01
restart_policy:
condition: on-failure
etcd-02:
image: bitnami/etcd:3
environment:
ALLOW_NONE_AUTHENTICATION: "yes"
ETCD_NAME: etcd-02
ETCD_INITIAL_ADVERTISE_PEER_URLS: http://10.20.20.12:2380
ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
ETCD_ADVERTISE_CLIENT_URLS: http://10.20.20.12:2379
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_INITIAL_CLUSTER: "etcd-01=http://10.20.20.11:2380,etcd-02=http://10.20.20.12:2380,etcd-03=http://10.20.20.13:2380"
ETCD_INITIAL_CLUSTER_STATE: new
ETCD_INITIAL_CLUSTER_TOKEN: iklimco-etcd-prod
volumes:
- /mnt/storagebox/prod/db/etcd-02/data:/bitnami/etcd/data
networks:
- iklimco-net
ports:
- target: 2379
published: 2379
protocol: tcp
mode: host
- target: 2380
published: 2380
protocol: tcp
mode: host
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-02
restart_policy:
condition: on-failure
etcd-03:
image: bitnami/etcd:3
environment:
ALLOW_NONE_AUTHENTICATION: "yes"
ETCD_NAME: etcd-03
ETCD_INITIAL_ADVERTISE_PEER_URLS: http://10.20.20.13:2380
ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380
ETCD_ADVERTISE_CLIENT_URLS: http://10.20.20.13:2379
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_INITIAL_CLUSTER: "etcd-01=http://10.20.20.11:2380,etcd-02=http://10.20.20.12:2380,etcd-03=http://10.20.20.13:2380"
ETCD_INITIAL_CLUSTER_STATE: new
ETCD_INITIAL_CLUSTER_TOKEN: iklimco-etcd-prod
volumes:
- /mnt/storagebox/prod/db/etcd-03/data:/bitnami/etcd/data
networks:
- iklimco-net
ports:
- target: 2379
published: 2379
protocol: tcp
mode: host
- target: 2380
published: 2380
protocol: tcp
mode: host
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-03
restart_policy:
condition: on-failure
```
**Önemli:** etcd cluster ilk kez başlatılırken üç node aynı anda `ETCD_INITIAL_CLUSTER_STATE: new` ile deploy edilmelidir. Cluster kurulduktan sonra bu değer `existing` olarak güncellenmeli ve stack yeniden deploy edilmelidir (node yeniden başlasın diye `new` kalmamalı; aksi hâlde data dizini sıfırlanır).
### 5.3 Patroni Konfigürasyonu
Her node için ayrı bir `patroni.yml` dosyası oluşturulur. Farklılıklar yalnızca `name` ve `connect_address` alanlarındadır.
**Node 01** — `/mnt/storagebox/prod/db/postgresql-01/config/patroni.yml`:
```yaml
scope: iklim-postgres
namespace: /db/
name: postgresql-01
restapi:
listen: 0.0.0.0:8008
connect_address: 10.20.20.11:8008
etcd3:
hosts:
- 10.20.20.11:2379
- 10.20.20.12:2379
- 10.20.20.13:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
parameters:
wal_level: replica
hot_standby: "on"
wal_keep_size: 512
max_wal_senders: 5
max_replication_slots: 5
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- host replication replicator 10.20.20.0/24 scram-sha-256
- host all all 10.20.10.0/24 scram-sha-256
- host all all 10.20.20.0/24 scram-sha-256
users:
postgres:
password: "${POSTGRES_PASSWORD}"
options:
- superuser
postgresql:
listen: 0.0.0.0:5432
connect_address: 10.20.20.11:5432
data_dir: /var/lib/postgresql/data/pgdata
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: "${REPLICATOR_PASSWORD}"
superuser:
username: postgres
password: "${POSTGRES_PASSWORD}"
parameters:
unix_socket_directories: "/var/run/postgresql"
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
```
**Node 02** — `/mnt/storagebox/prod/db/postgresql-02/config/patroni.yml`:
Node 01 ile aynı içerik, yalnızca şu alanlar farklı:
```yaml
name: postgresql-02
restapi:
connect_address: 10.20.20.12:8008
postgresql:
connect_address: 10.20.20.12:5432
data_dir: /var/lib/postgresql/data/pgdata
```
**Node 03** — `/mnt/storagebox/prod/db/postgresql-03/config/patroni.yml`:
```yaml
name: postgresql-03
restapi:
connect_address: 10.20.20.13:8008
postgresql:
connect_address: 10.20.20.13:5432
data_dir: /var/lib/postgresql/data/pgdata
```
### 5.4 Stack Dosyası — Patroni
`/opt/iklimco/stacks/prod-db-patroni.yml`:
```yaml
version: "3.8"
networks:
iklimco-net:
external: true
services:
patroni-01:
image: registry.iklim.co/infra/patroni-postgis:17-3.5
environment:
DATABASE_POSTGRES_ROOT_USER: "${DATABASE_POSTGRES_ROOT_USER}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
REPLICATOR_PASSWORD: "${REPLICATOR_PASSWORD}"
TZ: "Europe/Istanbul"
volumes:
- /mnt/storagebox/prod/db/postgresql-01/data:/var/lib/postgresql/data
- /mnt/storagebox/prod/db/postgresql-01/config/patroni.yml:/etc/patroni/patroni.yml:ro
networks:
- iklimco-net
ports:
- target: 5432
published: 5432
protocol: tcp
mode: host
- target: 8008
published: 8008
protocol: tcp
mode: host
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-01
restart_policy:
condition: on-failure
patroni-02:
image: registry.iklim.co/infra/patroni-postgis:17-3.5
environment:
DATABASE_POSTGRES_ROOT_USER: "${DATABASE_POSTGRES_ROOT_USER}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
REPLICATOR_PASSWORD: "${REPLICATOR_PASSWORD}"
TZ: "Europe/Istanbul"
volumes:
- /mnt/storagebox/prod/db/postgresql-02/data:/var/lib/postgresql/data
- /mnt/storagebox/prod/db/postgresql-02/config/patroni.yml:/etc/patroni/patroni.yml:ro
networks:
- iklimco-net
ports:
- target: 5432
published: 5432
protocol: tcp
mode: host
- target: 8008
published: 8008
protocol: tcp
mode: host
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-02
restart_policy:
condition: on-failure
patroni-03:
image: registry.iklim.co/infra/patroni-postgis:17-3.5
environment:
DATABASE_POSTGRES_ROOT_USER: "${DATABASE_POSTGRES_ROOT_USER}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
REPLICATOR_PASSWORD: "${REPLICATOR_PASSWORD}"
TZ: "Europe/Istanbul"
volumes:
- /mnt/storagebox/prod/db/postgresql-03/data:/var/lib/postgresql/data
- /mnt/storagebox/prod/db/postgresql-03/config/patroni.yml:/etc/patroni/patroni.yml:ro
networks:
- iklimco-net
ports:
- target: 5432
published: 5432
protocol: tcp
mode: host
- target: 8008
published: 8008
protocol: tcp
mode: host
deploy:
replicas: 1
placement:
constraints:
- node.hostname == iklim-db-03
restart_policy:
condition: on-failure
```
### 5.5 Durum Kontrolü
```bash
# Herhangi bir DB node'unda:
docker exec -it $(docker ps -q -f name=iklim-patroni_patroni-01) \
patronictl -c /etc/patroni/patroni.yml list
```
Beklenen çıktı: bir `Leader` ve iki `Replica` satırı, hepsinin `State` sütunu `running`.
```bash
# etcd cluster sağlığı:
docker exec -it $(docker ps -q -f name=iklim-etcd_etcd-01) \
etcdctl endpoint health \
--endpoints=http://10.20.20.11:2379,http://10.20.20.12:2379,http://10.20.20.13:2379
```
```bash
# Mevcut primary'i öğren:
docker exec -it $(docker ps -q -f name=iklim-patroni_patroni-01) \
patronictl -c /etc/patroni/patroni.yml topology
```
## 6. Deploy
Sıra önemlidir: önce etcd, ardından MongoDB ve Patroni stack'leri.
```bash
# iklim-app-01 üzerinde (Swarm manager):
export $(cat /opt/iklimco/stacks/.env | xargs)
# 1. etcd cluster:
docker stack deploy \
--compose-file /opt/iklimco/stacks/prod-db-etcd.yml \
--with-registry-auth \
iklim-etcd
# etcd cluster'ın kurulmasını bekle (yaklaşık 30 saniye):
sleep 30
# etcd sağlığını doğrula:
docker exec -it $(docker ps -q -f name=iklim-etcd_etcd-01) \
etcdctl endpoint health \
--endpoints=http://10.20.20.11:2379,http://10.20.20.12:2379,http://10.20.20.13:2379
# 2. MongoDB:
docker stack deploy \
--compose-file /opt/iklimco/stacks/prod-db-mongo.yml \
--with-registry-auth \
iklim-db
# 3. Patroni (PostgreSQL):
docker stack deploy \
--compose-file /opt/iklimco/stacks/prod-db-patroni.yml \
--with-registry-auth \
iklim-patroni
docker stack services iklim-etcd
docker stack services iklim-db
docker stack services iklim-patroni
```
**etcd'yi ikinci kez deploy etmeden önce** `ETCD_INITIAL_CLUSTER_STATE` değerini `new` yerine `existing` olarak güncelle; aksi hâlde veri dizini sıfırlanır. İlk deploy başarılı olduktan sonra `prod-db-etcd.yml` içindeki her serviste bu değeri `existing` yap ve `docker stack deploy` ile güncelle.
### .env Dosyası
`/opt/iklimco/stacks/.env` (repo'ya commit edilmez):
```env
DATABASE_POSTGRES_ROOT_USER=postgres
POSTGRES_PASSWORD=<güçlü-şifre>
REPLICATOR_PASSWORD=<güçlü-şifre>
MONGO_ROOT_PASSWORD=<güçlü-şifre>
```
### MongoDB Replica Set Başlatma
MongoDB stack deploy edildikten sonra bir kez çalıştırılır:
```bash
docker exec -it $(docker ps -q -f name=iklim-db_mongodb-01) mongosh \
-u mongo-root -p "${MONGO_ROOT_PASSWORD}" --authenticationDatabase admin
# mongosh içinde:
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "10.20.20.11:27017", priority: 2 },
{ _id: 1, host: "10.20.20.12:27017", priority: 1 },
{ _id: 2, host: "10.20.20.13:27017", priority: 1 }
]
})
```
## 7. App Servislerinden Erişim
### MongoDB Replica Set Connection String
```
mongodb://mongo-root:<SIFRE>@10.20.20.11:27017,10.20.20.12:27017,10.20.20.13:27017/<db>?replicaSet=rs0&authSource=admin
```
### PostgreSQL — Patroni
Patroni her an primary olan node'u yönetir. Uygulama katmanı tüm üç IP'yi vererek primary'e yazabilir, secondary'den okuyabilir:
```
# Yazma — sadece primary kabul eder:
jdbc:postgresql://10.20.20.11:5432,10.20.20.12:5432,10.20.20.13:5432/iklimdb?targetServerType=primary
# Okuma (yük dengeleme):
jdbc:postgresql://10.20.20.11:5432,10.20.20.12:5432,10.20.20.13:5432/iklimdb?targetServerType=preferSecondary
```
PostgreSQL JDBC sürücüsü `targetServerType=primary` ile bağlanmaya çalışacağı tüm node'lara bağlanır ve primary olanı otomatik bulur.
### Patroni REST API
Patroni, 8008 portundan HTTP endpoint sunar. Bu endpoint HAProxy veya benzeri bir load balancer ile kullanılarak primary'i otomatik yönlendirme sağlanabilir:
```bash
# Primary kontrolü (HTTP 200 = primary, HTTP 503 = replica):
curl -s http://10.20.20.11:8008/primary
```
## Kabul Kriterleri
- `docker stack services iklim-etcd` — üç servis `1/1`
- `docker stack services iklim-db` — üç MongoDB servisi `1/1`
- `docker stack services iklim-patroni` — üç Patroni servisi `1/1`
- `patronictl list` — 1 `Leader`, 2 `Replica`, hepsi `running`
- `etcdctl endpoint health` — üç endpoint `healthy`
- `rs.status()` — 1 PRIMARY, 2 SECONDARY
- App node'larından MongoDB ve PostgreSQL'e erişim sağlanır
- `5432`, `27017`, `2379`, `2380`, `8008` portları public internet'ten kapalıdır
- Bir DB node yeniden başlatıldığında Patroni otomatik seçim yapar, yeni primary belirlenir
- Patroni primary geçişi sırasında eski primary standby olarak re-join olur (split-brain yoktur)