diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..6e54f59 --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,374 @@ +# iklim.co Ansible Kullanım Rehberi + +Bu dizin, iklim.co test ve prod sunucu ortamlarının Ansible ile hazırlanması için kullanılan playbook, role, inventory ve değişken dosyalarını içerir. + +Mevcut kapsam: + +- Test ortamı bootstrap ve post-stack hazırlıkları. +- Ortak Ansible rollerinin yönetimi. +- Terraform tarafından üretilen inventory dosyalarının kullanımı. +- Ansible Vault ile şifreli değişken yönetimi. +- Prod ortamı için başlangıç playbook yapısı. + +Prod ortamı tamamlandıkça bu README içindeki prod başlıkları genişletilecektir. + +## Dizin Yapısı + +```text +Environment_Infrastructure/ansible/ + README.md + requirements.yml + roles/ + test/ + ansible.cfg + inventory/generated/test.yml + group_vars/ + host_vars/ + test-bootstrap.yml + test-app-post-stack.yml + test-db-post-stack.yml + prod/ + ansible.cfg + inventory/generated/prod.yml + group_vars/ + prod-bootstrap.yml +``` + +## Temel Kavramlar + +| Kavram | Açıklama | +| --- | --- | +| Playbook | Çalıştırılacak Ansible iş akışını tanımlar. Örn. `test-bootstrap.yml`. | +| Role | Tekrarlanabilir kurulum birimidir. Örn. `docker`, `swarm`, `db_stack`. | +| Inventory | Ansible'ın bağlanacağı hostları ve host gruplarını tanımlar. | +| Tag | Playbook içindeki belirli role/task'ları seçerek çalıştırmayı sağlar. | +| Vault | Şifreli değişkenlerin güvenli tutulması için Ansible Vault kullanımıdır. | +| `ansible.cfg` | Inventory, role path, remote user ve privilege escalation gibi varsayılanları belirler. | + +## Inventory Kaynağı + +Inventory dosyaları elle yazılan ana kaynak değildir. Terraform tarafından üretilen `ansible_inventory_yaml` output'u dosyaya aktarılır. + +Test inventory üretimi: + +```bash +cd Environment_Infrastructure/terraform/hetzner/test +terraform output -raw ansible_inventory_yaml > ../../../ansible/test/inventory/generated/test.yml +``` + +Prod inventory üretimi: + +```bash +cd Environment_Infrastructure/terraform/hetzner/prod +terraform output -raw ansible_inventory_yaml > ../../../ansible/prod/inventory/generated/prod.yml +``` + +Inventory içeriği Terraform'un oluşturduğu Hetzner kaynaklarından gelir: + +- Host adı +- Public IP: `ansible_host` +- SSH kullanıcısı: `ansible_user` +- Private network IP: `private_ip` + +Sunucu ekleme/silme, public IP değişimi veya private IP değişimi Terraform tarafında yapıldıktan sonra inventory yeniden üretilmelidir. Aksi halde Ansible eski host bilgileriyle çalışır. + +## Ortak Komut Parametreleri + +| Parametre | Açıklama | +| --- | --- | +| `ansible-playbook` | Bir playbook dosyasını çalıştırır. | +| `-i ` | Kullanılacak inventory dosyasını belirtir. | +| `--tags ` | Sadece belirtilen tag'e sahip role/task'ları çalıştırır. | +| `--limit ` | Çalışmayı belirli host veya host grubuyla sınırlar. | +| `--vault-password-file=` | Ansible Vault ile şifrelenmiş değişkenleri açmak için parola dosyasını kullanır. | +| `--check` | Değişiklikleri uygulamadan deneme çalıştırması yapar. | +| `--list-tasks` | Çalışacak task listesini gösterir. | + +## Test Ortamı + +Test komutları `Environment_Infrastructure/ansible/test` dizini altından çalıştırılmalıdır: + +```bash +cd Environment_Infrastructure/ansible/test +``` + +Bu dizindeki `ansible.cfg` şu varsayılanları tanımlar: + +- `inventory = inventory/generated/test.yml` +- `remote_user = root` +- `roles_path = ../roles` +- `become = True` + +Bu nedenle `-i inventory/generated/test.yml` parametresi teknik olarak zorunlu değildir; komutlarda görünürlük için kullanılabilir. + +### Test Inventory Grupları + +| Grup | Host | Amaç | +| --- | --- | --- | +| `app` | `iklim-app-01` | Uygulama node'u, runner ve deploy işleri | +| `db` | `iklim-db-01` | Veritabanı node'u, DB stack ve WireGuard | + +`--limit` örnekleri: + +```bash +--limit iklim-app-01 +--limit app +--limit db +``` + +### Test Playbook'ları + +| Playbook | Host kapsamı | Amaç | +| --- | --- | --- | +| `test-bootstrap.yml` | `all`, `app`, `db` | Base, hardening, Docker, node dizinleri, StorageBox ve Swarm hazırlıkları | +| `test-app-post-stack.yml` | `app` | Gitea runner ve app node deploy ön koşulları | +| `test-db-post-stack.yml` | `db` | DB stack ve WireGuard hazırlıkları | + +### Test Bootstrap + +Sadece `base` tag'ini `iklim-app-01` üzerinde çalıştırmak için: + +```bash +ansible-playbook test-bootstrap.yml \ + -i inventory/generated/test.yml \ + --tags base \ + --limit iklim-app-01 \ + --vault-password-file=../.vault_pass +``` + +`test-bootstrap.yml` içindeki başlıca tag'ler: + +| Tag | Rol | Amaç | +| --- | --- | --- | +| `base` | `base` | Temel paketler, sistem ayarları ve genel hazırlıklar | +| `hardening` | `hardening` | SSH ve güvenlik sıkılaştırmaları | +| `docker` | `docker` | Docker kurulumu ve ayarları | +| `node_dirs` | `node_dirs` | Ortak node dizinlerinin hazırlanması | +| `storagebox` | `storagebox` | StorageBox mount/dizin hazırlıkları | +| `storagebox_ssh_key` | `storagebox_ssh_key` | StorageBox SSH key hazırlığı | +| `swarm` | `swarm` | Docker Swarm kurulumu | + +Örnekler: + +```bash +# Tüm hostlarda Docker rolünü çalıştır +ansible-playbook test-bootstrap.yml \ + --tags docker \ + --vault-password-file=../.vault_pass + +# Sadece app grubunda hardening rolünü çalıştır +ansible-playbook test-bootstrap.yml \ + --tags hardening \ + --limit app \ + --vault-password-file=../.vault_pass + +# Swarm kurulumunu çalıştır +ansible-playbook test-bootstrap.yml \ + --tags swarm \ + --vault-password-file=../.vault_pass +``` + +### Test App Node Hazırlıkları + +```bash +ansible-playbook test-app-post-stack.yml \ + -i inventory/generated/test.yml \ + --tags act_runner \ + --vault-password-file=../.vault_pass +``` + +Bu komut `app` grubundaki hostlarda `act_runner` rolünü çalıştırır. + +Amaç: + +- Gitea Actions runner kurulumu. +- Runner servis/dizin hazırlığı. +- Deploy ön koşullarının hazırlanması. + +Ön koşul: + +- Gitea arayüzünden `Organization -> Settings -> Actions -> Runners` altında registration token alınır. +- Token, Vault ile şifreli değişkenlerde `vault_gitea_runner_token` olarak tanımlanır. + +Not: + +- Token tanımlı değilse kurulum tamamlanabilir ama runner kayıt adımı atlanır. +- `.runner` dosyası varsa kayıt tekrar yapılmaz; rol idempotent çalışacak şekilde tasarlanmıştır. + +### Test DB Node Hazırlıkları + +```bash +ansible-playbook test-db-post-stack.yml \ + -i inventory/generated/test.yml \ + --tags db_stack \ + --vault-password-file=../.vault_pass +``` + +Bu komut `db` grubundaki hostlarda `db_stack` rolünü çalıştırır. + +Amaç: + +- PostgreSQL/PostGIS ve MongoDB için host hazırlıkları. +- DB ile ilgili dizin, config ve secret bağımlılıklarının hazırlanması. +- Test ortamındaki DB node'un stack sonrası operasyonel hale getirilmesi. + +`test-db-post-stack.yml` içindeki tag'ler: + +| Tag | Rol | Amaç | +| --- | --- | --- | +| `db_stack` | `db_stack` | PostgreSQL/MongoDB stack hazırlıkları | +| `wireguard` | `wireguard` | WireGuard VPN client/server ayarları | + +Sadece WireGuard güncellemek için: + +```bash +ansible-playbook test-db-post-stack.yml \ + --tags wireguard \ + --vault-password-file=../.vault_pass +``` + +WireGuard client eklemek için `group_vars/all/vars.yml` içindeki `wireguard_clients` listesine client public key'i eklenir. + +### Test İçin Önerilen Çalıştırma Sırası + +```bash +# 1. Temel sunucu hazırlıkları +ansible-playbook test-bootstrap.yml \ + --tags base,hardening,docker,node_dirs,storagebox,storagebox_ssh_key \ + --vault-password-file=../.vault_pass + +# 2. Swarm kurulumu +ansible-playbook test-bootstrap.yml \ + --tags swarm \ + --vault-password-file=../.vault_pass + +# 3. App node runner/deploy hazırlıkları +ansible-playbook test-app-post-stack.yml \ + --tags act_runner \ + --vault-password-file=../.vault_pass + +# 4. DB node hazırlıkları +ansible-playbook test-db-post-stack.yml \ + --tags db_stack,wireguard \ + --vault-password-file=../.vault_pass +``` + +Rolleri tek tek çalıştırmak, hata durumunda hangi adımın problem çıkardığını daha net görmeyi sağlar. + +## Prod Ortamı + +Prod ortamı için mevcut başlangıç dosyaları: + +```text +prod/ansible.cfg +prod/prod-bootstrap.yml +prod/group_vars/ +``` + +Prod komutları `Environment_Infrastructure/ansible/prod` dizini altından çalıştırılmalıdır: + +```bash +cd Environment_Infrastructure/ansible/prod +``` + +Bu dizindeki `ansible.cfg` şu varsayılanları tanımlar: + +- `inventory = inventory/generated/prod.yml` +- `remote_user = root` +- `roles_path = ../roles` +- `become = True` + +Prod inventory, prod Terraform çıktısından üretilir: + +```bash +cd Environment_Infrastructure/terraform/hetzner/prod +terraform output -raw ansible_inventory_yaml > ../../../ansible/prod/inventory/generated/prod.yml +``` + +### Prod Playbook Planı + +Şu an prod tarafında ana playbook `prod-bootstrap.yml` dosyasıdır. + +Mevcut rol/tag kapsamı: + +| Tag | Rol | Amaç | +| --- | --- | --- | +| `base` | `base` | Temel sistem hazırlığı | +| `hardening` | `hardening` | SSH ve güvenlik sıkılaştırmaları | +| `docker` | `docker` | Docker kurulumu | +| `node_dirs` | `node_dirs` | Node dizin hazırlıkları | +| `storagebox` | `storagebox` | StorageBox hazırlıkları | +| `storagebox_ssh_key` | `storagebox_ssh_key` | StorageBox SSH key hazırlığı | +| `swarm` | `swarm` | Prod Swarm kurulumu | +| `db_labels` | inline task | DB node label'ları | +| `db_stack` | `db_stack` | DB node konfigürasyonu | +| `act_runner` | `act_runner` | App node runner kurulumu | + +Prod ortamı tamamlandığında bu bölüm aşağıdaki başlıklarla genişletilecektir: + +- Prod inventory grupları +- Prod bootstrap çalıştırma sırası +- Prod app node hazırlıkları +- Prod DB node hazırlıkları +- Prod runner kurulumu +- Prod rollback ve tekrar çalıştırma notları + +## Vault Kullanımı + +Bu Ansible yapısı şifreli değerler için Ansible Vault kullanır. + +Örnek şifreli dosyalar: + +```text +test/group_vars/all/vault.yml +test/host_vars/iklim-app-01/vault.yml +test/host_vars/iklim-db-01/vault.yml +``` + +Komutlarda kullanılan parametre: + +```bash +--vault-password-file=../.vault_pass +``` + +Bu parametre, Ansible'ın Vault ile şifrelenmiş değişkenleri açabilmesini sağlar. `.vault_pass` dosyası repository dışına sızdırılmamalı ve CI/CD ortamında güvenli secret olarak yönetilmelidir. + +## Kontrol Komutları + +Komutu gerçekten çalıştırmadan hangi host ve task'ların etkileneceğini görmek için: + +```bash +ansible-playbook test-bootstrap.yml \ + --tags base \ + --limit iklim-app-01 \ + --vault-password-file=../.vault_pass \ + --check +``` + +Hangi task'ların çalışacağını listelemek için: + +```bash +ansible-playbook test-bootstrap.yml \ + --tags base \ + --list-tasks \ + --vault-password-file=../.vault_pass +``` + +Inventory'deki hostları görmek için: + +```bash +ansible-inventory \ + -i inventory/generated/test.yml \ + --list +``` + +Belirli host'a ping atmak için: + +```bash +ansible iklim-app-01 \ + -i inventory/generated/test.yml \ + -m ping \ + --vault-password-file=../.vault_pass +``` + +Prod için aynı komutlar `ansible/prod` dizininden ve `inventory/generated/prod.yml` inventory'siyle çalıştırılmalıdır. diff --git a/ansible/prod/group_vars/prod.yml b/ansible/prod/group_vars/prod.yml index 394988f..7d4c04d 100644 --- a/ansible/prod/group_vars/prod.yml +++ b/ansible/prod/group_vars/prod.yml @@ -1,6 +1,7 @@ # Prod environment specific variables -storagebox_user: "{{ storagebox_account }}-sub2" # Prod sub-account suffix +storagebox_user: "{{ storagebox_account }}-sub5" # Prod sub-account storagebox_url: "https://{{ storagebox_user }}.your-storagebox.de/" storagebox_mount_point: "/mnt/storagebox" swarm_manager_ip: "10.20.10.11" +mongodb_replset_name: "rs0" # storagebox_password: "{{ vault_storagebox_password }}" diff --git a/ansible/prod/prod-bootstrap.yml b/ansible/prod/prod-bootstrap.yml index 86b8e3d..9f72635 100644 --- a/ansible/prod/prod-bootstrap.yml +++ b/ansible/prod/prod-bootstrap.yml @@ -13,6 +13,8 @@ tags: [node_dirs] - role: storagebox tags: [storagebox] + - role: storagebox_ssh_key + tags: [storagebox_ssh_key] - name: Swarm Infrastructure Setup (Prod HA) hosts: iklim-app-* @@ -29,3 +31,32 @@ roles: - role: swarm tags: [swarm] + +# db-index label'ları Patroni node koordinasyonu için gereklidir; Swarm join tamamlandıktan sonra çalışır +- name: Add db-index Labels for Patroni + hosts: iklim-app-01 + become: yes + tags: [db_labels] + tasks: + - name: Label DB nodes with db-index + ansible.builtin.command: > + docker node update --label-add db-index={{ item.index }} {{ item.node }} + loop: + - { node: "iklim-db-01", index: "01" } + - { node: "iklim-db-02", index: "02" } + - { node: "iklim-db-03", index: "03" } + ignore_errors: true + +- name: DB Node Configuration (MongoDB Config) + hosts: iklim-db-* + become: yes + roles: + - role: db_stack + tags: [db_stack] + +- name: Act Runner Setup (App Nodes) + hosts: iklim-app-* + become: yes + roles: + - role: act_runner + tags: [act_runner] diff --git a/ansible/roles/db_stack/tasks/main.yml b/ansible/roles/db_stack/tasks/main.yml index 3aff898..4cf43b9 100644 --- a/ansible/roles/db_stack/tasks/main.yml +++ b/ansible/roles/db_stack/tasks/main.yml @@ -1,6 +1,2 @@ --- - include_tasks: db_node.yml - when: inventory_hostname in groups['db'] - -- include_tasks: app_node.yml - when: inventory_hostname in groups['app'] diff --git a/ansible/roles/db_stack/templates/mongod.conf.j2 b/ansible/roles/db_stack/templates/mongod.conf.j2 index aa05a45..22bbe87 100644 --- a/ansible/roles/db_stack/templates/mongod.conf.j2 +++ b/ansible/roles/db_stack/templates/mongod.conf.j2 @@ -1,9 +1,21 @@ systemLog: verbosity: 0 + timeStampFormat: "iso8601-local" + destination: file + path: "/data/log/mongo.log" + logAppend: true + logRotate: rename storage: dbPath: /data/db + engine: "wiredTiger" + directoryPerDB: true net: port: 27017 bindIp: 0.0.0.0 security: authorization: enabled +{% if mongodb_replset_name is defined %} + keyFile: /data/configdb/rs-auth.key +replication: + replSetName: "{{ mongodb_replset_name }}" +{% endif %} diff --git a/facts/swarm-node-recovery-swag-failover.md b/facts/swarm-node-recovery-swag-failover.md new file mode 100644 index 0000000..29ceecb --- /dev/null +++ b/facts/swarm-node-recovery-swag-failover.md @@ -0,0 +1,156 @@ +# 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. + +### Çözüm + +```bash +# 1. Ansible bootstrap — yeni node otomatik join olur +cd ansible/test +ansible-playbook -i inventory/generated/test.yml test-bootstrap.yml --ask-vault-pass + +# 2. iklim-app-01 üzerinde — eski dead node kaydını temizle +docker node ls # eski node ID'yi bul +docker node rm + +# 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 ` (manager'da) | Büyük ölçüde evet | +| 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 — SWAG Failover + +SWAG cluster-native değildir; her zaman tek instance çalışır ve `iklim-app-01`'e (Floating IP node) sabitlenmiştir. `iklim-app-01` çöktüğünde SWAG ve cert-reloader da durur; DNS ve HTTPS erişimi kesilir. Swarm quorum 2 manager ile devam eder; mikroservisler ve Vault başka node'lara taşınır. + +SWAG konfigürasyonu (`/config`, letsencrypt sertifikaları dahil) StorageBox'ta tutulduğu için (`SWAG_CONFIG_DIR=/mnt/storagebox/prod/swag/config`) manuel failover hızlı yapılabilir. + +## Prod Senaryo: `iklim-app-01` Çöktü + +### 1. SWAG'ı Başka Node'a Taşı + +```bash +# iklim-app-02 veya iklim-app-03 üzerinde (aktif manager): +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 +``` + +SWAG StorageBox'taki mevcut letsencrypt sertifikalarını bulur; yeni sertifika talep etmez. cert-reloader yeni node'da başlar ve `/mnt/storagebox/prod/ssl`'e yazar. + +### 2. Floating IP'yi Yeni Node'a Taşı + +**CLI ile:** + +```bash +hcloud floating-ip assign +``` + +**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. Açılan listeden **`iklim-app-02`**'yi seç → **Reassign** butonuna tıkla. + +DNS A kaydı zaten Floating IP'yi gösterdiği için ek DNS değişikliği gerekmez. + +### 3. Doğrula + +```bash +docker service ps iklimco_swag +docker service ps iklimco_cert-reloader +curl -si https://api.iklim.co/health +``` + +### `iklim-app-01` Geri Döndüğünde + +Node Swarm'a yeniden katıldıktan sonra servisleri tekrar `iklim-app-01`'e taşı ve Floating IP'yi geri aktar: + +```bash +docker service update \ + --constraint-add "node.hostname == iklim-app-01" \ + --constraint-rm "node.hostname == iklim-app-02" \ + iklimco_swag + +docker service update \ + --constraint-add "node.hostname == iklim-app-01" \ + --constraint-rm "node.hostname == iklim-app-02" \ + iklimco_cert-reloader + +hcloud floating-ip assign +``` + +## Ö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 | +| TLS sertifikaları | StorageBox'ta; failover node hemen erişir, yeniden istek gerekmez | diff --git a/facts/swarm-node-recovery.md b/facts/swarm-node-recovery.md deleted file mode 100644 index ff7e849..0000000 --- a/facts/swarm-node-recovery.md +++ /dev/null @@ -1,76 +0,0 @@ -# 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. - -### Çözüm - -```bash -# 1. Ansible bootstrap — yeni node otomatik join olur -cd ansible/test -ansible-playbook -i inventory/generated/test.yml test-bootstrap.yml --ask-vault-pass - -# 2. iklim-app-01 üzerinde — eski dead node kaydını temizle -docker node ls # eski node ID'yi bul -docker node rm - -# 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 ` (manager'da) | Büyük ölçüde evet | -| 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. diff --git a/roadmap/prod-env/01-swarm-init-multinode.md b/roadmap/prod-env/01-swarm-init-multinode.md index 6bc4e1f..d4e0434 100644 --- a/roadmap/prod-env/01-swarm-init-multinode.md +++ b/roadmap/prod-env/01-swarm-init-multinode.md @@ -4,12 +4,12 @@ - **Repo:** `iklim.co` root - **Environment:** prod - **Topology:** - - 3 × app nodes (`iklim-app-01/02/03`) — all act as **Swarm managers AND app workers** (Raft quorum: 1 can fail) + - 3 × app nodes (`iklim-app-01/02/03`) — all act as **Swarm managers AND app workers** with `type=service` label (Raft quorum: 1 can fail) - 3 × DB nodes (`iklim-db-01/02/03`) — join Swarm as **workers** with `role=db` label; DB services are placed exclusively on them - **Sizing:** app nodes are `cpx42`, DB nodes are `cpx32`; see `../../hetzner-sizing-report.md` - All 6 nodes are in the same private network. - Pipeline trigger: push to `prod-env` branch → Gitea runner on `prod-runner` (first app node). -- App Swarm managers: 3 nodes all manager-eligible and carry app workloads (no dedicated worker-only app nodes). +- App Swarm managers: 3 nodes all manager-eligible and carry app workloads with `type=service` label (no dedicated worker-only app nodes). ## Node labeling plan @@ -22,6 +22,25 @@ | `iklim-db-02` | PostgreSQL (Patroni), etcd | Worker | `role=db` | | `iklim-db-03` | MongoDB replica + PostgreSQL (Patroni), etcd | Worker | `role=db` | +### Label scheme rationale + +App nodes carry `type=service`, DB nodes carry `role=db`. The two different label keys are not an inconsistency — they operate on different semantic planes: + +- **`type=service`** — "this node carries service workload"; determines which node group microservices and infrastructure services (APISIX, Vault, RabbitMQ, Redis, SWAG, etc.) are scheduled on. +- **`role=db`** — "this node is a database node"; pins PostgreSQL (Patroni), MongoDB, and their proxy services exclusively to DB nodes. + +Docker Swarm's **built-in** `node.role` property (`manager` / `worker`) does **not** conflict with the custom `node.labels.role` label — the placement constraint syntax distinguishes them explicitly: + +``` +node.role == manager ← Swarm built-in (manager/worker distinction) +node.labels.type == service ← custom label (app node workload target) +node.labels.role == db ← custom label (DB node workload target) +``` + +This scheme is applied consistently across `docker-stack-infra.yml` and all 10 microservice `docker-stack-service.yml` files. The test environment uses the same `type=service` label on its single node, so both environments share the same constraint syntax. + +`node.role == worker` is intentionally not used anywhere. DB nodes are Swarm workers, but targeting them via `node.role == worker` would also match any future worker-only app nodes. The explicit `node.labels.role == db` label provides precise, unambiguous targeting regardless of Swarm role. + ## Step 1 — Init Swarm on iklim-app-01 (the prod-runner node) ```bash @@ -113,12 +132,10 @@ not via the Gitea pipeline. ## Placement constraints used in `docker-stack-infra.yml` -| Constraint | Resolves to | -|------------|-------------| -| `node.role == manager` | iklim-app-01, iklim-app-02, iklim-app-03 | -| `node.labels.type == service` | iklim-app-01, iklim-app-02, iklim-app-03 | -| `node.labels.role == db` | iklim-db-01, iklim-db-02, iklim-db-03 | +| Constraint | Resolves to | Services | +|------------|-------------|----------| +| `node.hostname == iklim-app-01` | iklim-app-01 only | SWAG, cert-reloader | +| `node.labels.type == service` | iklim-app-01, iklim-app-02, iklim-app-03 | Vault, Redis, RabbitMQ, APISIX, Prometheus, Grafana, etcd | +| `node.labels.role == db` | iklim-db-01, iklim-db-02, iklim-db-03 | PostgreSQL, MongoDB, pg-proxy, mongo-proxy | -SWAG, Vault, cert-reloader: pinned to `node.role == manager`. -Microservices: no constraint (distributed across all app nodes by Swarm scheduler). -DB services (Patroni, etcd, MongoDB): pinned to `node.labels.role == db` in separate DB stacks. +SWAG and cert-reloader are pinned to `iklim-app-01` (the Floating IP node) because SWAG does not support clustering and must match the public entry point. Vault floats across all service nodes; its TLS cert is read from StorageBox (`/mnt/storagebox/prod/ssl`) so it is available on whichever node Vault is scheduled on. Microservices carry no placement constraint and are distributed by the Swarm scheduler across all app nodes. DB services are pinned to DB nodes via separate DB stacks. diff --git a/roadmap/prod-env/04-swag-nginx-configs.md b/roadmap/prod-env/04-swag-nginx-configs.md index d59ed65..6ebb957 100644 --- a/roadmap/prod-env/04-swag-nginx-configs.md +++ b/roadmap/prod-env/04-swag-nginx-configs.md @@ -13,6 +13,12 @@ RABBITMQ_SUBDOMAIN=rabbitmq.iklim.co GRAFANA_SUBDOMAIN=grafana.iklim.co RESTRICTED_IP_1=78.187.87.109 RESTRICTED_IP_2=95.70.151.248 + +# SWAG storage paths — StorageBox so certs are accessible from any app node +# cert-reloader writes here; Vault reads from here on any manager node +SWAG_CERT_DIR=/mnt/storagebox/prod/ssl +# SWAG full config dir (includes letsencrypt state) — enables clean node failover +SWAG_CONFIG_DIR=/mnt/storagebox/prod/swag/config ``` ## Template files (already created in test step 04) diff --git a/roadmap/prod-env/08-deploy-pipeline-update.md b/roadmap/prod-env/08-deploy-pipeline-update.md index 0844f06..0483629 100644 --- a/roadmap/prod-env/08-deploy-pipeline-update.md +++ b/roadmap/prod-env/08-deploy-pipeline-update.md @@ -4,6 +4,10 @@ - **File:** `.gitea/workflows/deploy-prod.yml` - Same changes as test pipeline (`test-env-setup/07-deploy-pipeline-update.md`), adapted for prod paths and prod runner. +- **Prod-specific differences from test:** + - `SPRING_PROFILES_ACTIVE=prod` (not `test`) in Run APISIX Init + - DB hostnames use Swarm VIP prefixes: `iklimco_postgresql`, `iklimco_mongodb` + - Storagebox paths use `prod/` instead of `test/` ## Step 1 — Remove manual cert scp lines from `Initialize Servers` @@ -21,40 +25,98 @@ Also remove from `Prepare Init Files`: ## Step 2 — Add `Prepare SWAG Directories` step -Insert **before** `Deploy Swarm Stack`: +Insert **before** `Bootstrap Vault TLS Placeholder`: ```yaml - name: Prepare SWAG Directories run: | - set -a; . ./.env; . ./.env.secrets.shared; set +a + set -a; . ./.env; . ./.env.secrets.swag; set +a - sudo mkdir -p /opt/iklimco/swag/dns-conf - envsubst < swag/dns-conf/godaddy.ini.tpl | sudo tee /opt/iklimco/swag/dns-conf/godaddy.ini > /dev/null - sudo chmod 600 /opt/iklimco/swag/dns-conf/godaddy.ini + docker run --rm -v /opt/iklimco/swag:/output alpine \ + mkdir -p /output/dns-conf /output/proxy-confs /output/site-confs + + envsubst < swag/dns-conf/godaddy.ini.tpl | docker run --rm -i \ + -v /opt/iklimco/swag/dns-conf:/output \ + alpine sh -c "cat > /output/godaddy.ini && chmod 600 /output/godaddy.ini" echo "✅ godaddy.ini written" - sudo mkdir -p /opt/iklimco/swag/proxy-confs /opt/iklimco/swag/site-confs - - export RESTRICTED_IP_1="78.187.87.109" - export RESTRICTED_IP_2="95.70.151.248" + export RESTRICTED_IPS_BLOCK="$(echo "$RESTRICTED_IPS" | tr ',' '\n' | sed 's|.*| allow &;|')" + SWAG_VARS='${API_SUBDOMAIN}${APIGW_SUBDOMAIN}${GRAFANA_SUBDOMAIN}${RABBITMQ_SUBDOMAIN}${RESTRICTED_IPS_BLOCK}' for tpl in swag/proxy-confs/*.conf.tpl; do - out="/opt/iklimco/swag/proxy-confs/$(basename "${tpl%.tpl}")" - envsubst < "$tpl" | sudo tee "$out" > /dev/null - echo "✅ $out" + fname=$(basename "${tpl%.tpl}") + envsubst "$SWAG_VARS" < "$tpl" | docker run --rm -i \ + -v /opt/iklimco/swag/site-confs:/output \ + alpine sh -c "cat > /output/${fname}" + echo "✅ ${fname}" done - sudo cp swag/site-confs/default.conf /opt/iklimco/swag/site-confs/default.conf + cat swag/site-confs/default.conf | docker run --rm -i \ + -v /opt/iklimco/swag/site-confs:/output \ + alpine sh -c "cat > /output/default.conf" + echo "✅ SWAG directories ready" + + SWAG_CTR=$(docker ps -q -f name=iklimco_swag 2>/dev/null | head -1) + if [ -n "$SWAG_CTR" ]; then + docker exec "$SWAG_CTR" nginx -t && docker exec "$SWAG_CTR" nginx -s reload + echo "✅ SWAG nginx reloaded" + fi working-directory: /workspace/iklim.co ``` > `.env` is sourced first so `API_SUBDOMAIN=api.iklim.co` (prod values) are used. > Ensure these vars are in `prod/secrets/iklim.co/.env.prod` on storagebox. -## Step 3 — Add `Bootstrap SWAG Certificate` step +## Step 3 — Add `Wait for etcd` step -Insert **after** `Deploy Swarm Stack`: +Insert **after** `Deploy Swarm Stack` and **before** `Run APISIX Init`. +APISIX reads its entire configuration from etcd; init script will fail silently if etcd is not ready. + +```yaml + - name: Wait for etcd + run: | + echo "⏳ Waiting for etcd..." + for i in $(seq 1 30); do + if docker run --rm --network iklimco-net alpine \ + sh -c "wget -qO- http://etcd:2379/health 2>/dev/null | grep -q '\"health\":\"true\"'"; then + echo "✅ etcd ready" + break + fi + [ "$i" -eq 30 ] && echo "❌ etcd did not become ready in time" && exit 1 + echo " attempt $i/30 — waiting 5s..." + sleep 5 + done +``` + +## Step 4 — Add `Run APISIX Init` step + +Insert **after** `Wait for etcd` and **before** `Bootstrap SWAG Certificate`. + +```yaml + - name: Run APISIX Init + run: | + set -a; . ./.env; . ./.env.secrets.shared; set +a + echo "⏳ Waiting for Swarm APISIX..." + until curl -sf -o /dev/null \ + -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \ + "http://apisix:9180/apisix/admin/upstreams" 2>/dev/null; do + sleep 5 + done + export SPRING_PROFILES_ACTIVE=prod + /bin/bash init/apisix-core/init.sh + echo "✅ APISIX routes configured" + working-directory: /workspace/iklim.co +``` + +> **Prod-specific:** `SPRING_PROFILES_ACTIVE=prod` — test pipeline uses `test`. +> `APISIX_ADMIN_KEY` is sourced from `.env.secrets.shared`. +> The init script is idempotent (PUT semantics); safe to re-run on subsequent deploys. +> With `replicas: 2` in prod, both APISIX instances read the same etcd state — no per-replica init needed. + +## Step 5 — Add `Bootstrap SWAG Certificate` step + +Insert **after** `Run APISIX Init`: ```yaml - name: Bootstrap SWAG Certificate @@ -89,16 +151,62 @@ Insert **after** `Deploy Swarm Stack`: exit 1 fi - sudo mkdir -p /opt/iklimco/ssl docker exec "$SWAG_CTR" cat "$CERT_PATH" | \ - sudo tee /opt/iklimco/ssl/STAR.iklim.co.full.crt > /dev/null + docker run --rm -i -v /opt/iklimco/ssl:/output alpine \ + sh -c "cat > /output/STAR.iklim.co.full.crt && chmod 644 /output/STAR.iklim.co.full.crt" docker exec "$SWAG_CTR" cat "/config/etc/letsencrypt/live/iklim.co/privkey.pem" | \ - sudo tee /opt/iklimco/ssl/STAR.iklim.co_key.txt > /dev/null + docker run --rm -i -v /opt/iklimco/ssl:/output alpine \ + sh -c "cat > /output/STAR.iklim.co_key.txt && chmod 644 /output/STAR.iklim.co_key.txt" echo "✅ Cert bootstrapped to /opt/iklimco/ssl/" working-directory: /workspace/iklim.co ``` -## Step 4 — Ensure subdomain env vars are in prod `.env` +## Step 6 — Add `Run Database Init Scripts` step + +Insert **after** `Bootstrap SWAG Certificate` and **before** `Review Environment`. + +```yaml + - name: Run Database Init Scripts + run: | + set -a; . ./.env; . ./.env.secrets.shared; set +a + + echo "⏳ Waiting for PostgreSQL..." + until docker run --rm --network iklimco-net \ + -e PGPASSWORD="${DATABASE_POSTGRES_ROOT_PASSWD}" \ + postgis/postgis:17-3.5 \ + pg_isready -h iklimco_postgresql -U "${DATABASE_POSTGRES_ROOT_USER}" -q 2>/dev/null; do + sleep 5 + done + for sql_file in $(ls ./init/postgresql/*.sql 2>/dev/null | sort); do + echo "▶ $(basename "$sql_file")" + docker run --rm -i --network iklimco-net \ + -e PGPASSWORD="${DATABASE_POSTGRES_ROOT_PASSWD}" \ + postgis/postgis:17-3.5 \ + psql -h iklimco_postgresql -U "${DATABASE_POSTGRES_ROOT_USER}" < "$sql_file" + done + + echo "⏳ Waiting for MongoDB..." + until docker run --rm --network iklimco-net mongo:8 \ + mongosh "mongodb://${DATABASE_MONGODB_ROOT_USER}:${DATABASE_MONGODB_ROOT_PASSWD}@iklimco_mongodb/admin" \ + --eval "db.runCommand({ping:1})" --quiet 2>/dev/null; do + sleep 5 + done + for js_file in $(ls ./init/mongodb/*.js 2>/dev/null | sort); do + echo "▶ $(basename "$js_file")" + docker run --rm -i --network iklimco-net mongo:8 \ + mongosh "mongodb://${DATABASE_MONGODB_ROOT_USER}:${DATABASE_MONGODB_ROOT_PASSWD}@iklimco_mongodb/admin" \ + --quiet < "$js_file" + done + echo "✅ Database init scripts completed" + working-directory: /workspace/iklim.co +``` + +> **Prod-specific:** DB hostnames are `iklimco_postgresql` ve `iklimco_mongodb` (Swarm VIP service names). +> Test pipeline uses `postgresql` / `mongodb` (unqualified aliases within the same stack). +> SQL and JS files are generated by `Prepare Init Files` step via `init_postgresql` / `init_mongodb` functions in `common-functions.sh`. +> Step is idempotent — scripts use `CREATE IF NOT EXISTS` / `createCollection` semantics. + +## Step 7 — Ensure subdomain env vars are in prod `.env` Add to `prod/secrets/iklim.co/.env.prod` on storagebox: @@ -109,22 +217,26 @@ RABBITMQ_SUBDOMAIN=rabbitmq.iklim.co GRAFANA_SUBDOMAIN=grafana.iklim.co ``` -## Step 5 — Final step order for prod pipeline +## Step 8 — Final step order for prod pipeline 1. Checkout Branch 2. Prepare Folders -3. Set up SSH Key -4. Install Required Tools +3. Set up SSH Key and Add to known_hosts +4. Update Apt Repository and Install Required Tools 5. Fetch Service Secret Files -6. Initialize Servers ← cert scp lines removed +6. Initialize Servers ← cert scp lines removed 7. Upload Updated Secrets to Storagebox 8. Provision Vault AppRole IDs and Docker Secrets 9. Upload Updated Env to Storagebox -10. Prepare Init Files ← cert copy lines removed +10. Prepare Init Files ← cert copy lines removed 11. Initialize Docker Swarm 12. Stop Docker Compose Services 13. Docker Login to Harbor -14. **Prepare SWAG Directories** ← NEW -15. Deploy Swarm Stack -16. **Bootstrap SWAG Certificate** ← NEW -17. Review Environment +14. **Prepare SWAG Directories** ← NEW +15. Bootstrap Vault TLS Placeholder +16. Deploy Swarm Stack +17. **Wait for etcd** ← NEW +18. **Run APISIX Init** ← NEW (`SPRING_PROFILES_ACTIVE=prod`) +19. **Bootstrap SWAG Certificate** ← NEW +20. **Run Database Init Scripts** ← NEW (`iklimco_postgresql`, `iklimco_mongodb`) +21. Review Environment diff --git a/setup/07-prod-ansible-bootstrap.md b/setup/07-prod-ansible-bootstrap.md index c6dca58..bde093a 100644 --- a/setup/07-prod-ansible-bootstrap.md +++ b/setup/07-prod-ansible-bootstrap.md @@ -124,6 +124,8 @@ ansible/ node_dirs/ storagebox/ storagebox_ssh_key/ + act_runner/ + db_stack/ ``` ## Base Role @@ -198,9 +200,11 @@ Prod Swarm 3 manager ile kurulacak: 4. Overlay network oluşturulur: `iklimco-net` 5. Node etiketleri: - `iklim-app-*` -> `type=service` - - `iklim-db-*` -> `role=db` + - `iklim-db-*` -> `role=db`, `db-index=01/02/03` (Patroni node koordinasyonu için) 6. Tüm node'lar `AVAILABILITY=Active` kalır. +`db-index` etiketleri prod-bootstrap.yml içinde ayrı bir play ile `iklim-app-01` üzerinden eklenir (swarm role tarafından değil). + ## Node Directory Role Tüm `iklim-app-*` node'larında: @@ -231,7 +235,32 @@ Her node'a uygulanır (tüm `iklim-app-*` ve `iklim-db-*`). ## StorageBox SSH Key Role -Her node'a uygulanır. Sunucu üzerinde ed25519 SSH anahtar çifti üretilir ve StorageBox ana hesabına yüklenir. +Her node'a uygulanır. Sunucu üzerinde `/root/.ssh/id_ed25519_storagebox` ed25519 anahtar çifti üretilir. Üretilen public key'in StorageBox ana hesabına yüklenmesi (SSH authorized_keys) ayrı bir manuel adımdır: + +```bash +# Her node için: +cat /root/.ssh/id_ed25519_storagebox.pub | \ + ssh -p 23 STORAGEBOX_USER@STORAGEBOX_USER.your-storagebox.de \ + "cat >> .ssh/authorized_keys" +``` + +## Act Runner Role + +`iklim-app-*` node'larına uygulanır. Her app node'a Gitea Act Runner kurulur ve systemd servisi olarak başlatılır. Prod ortamında 3 app node üzerinde runner çalışır; deploy pipeline bu runner'lardan herhangi birinde tetiklenebilir. + +## DB Stack Role + +`iklim-db-*` node'larına uygulanır. MongoDB için `/opt/iklimco/db/mongodb/config/` dizinini ve `mongod.conf` dosyasını oluşturur. `group_vars/prod.yml` içinde tanımlı `mongodb_replset_name: "rs0"` değişkeniyle `mongod.conf` replicaSet ve keyFile bloklarını otomatik içerir. + +## /opt/iklimco/stacks/.env + +DB cluster stack'lerinin gerektirdiği şifre değişkenleri `/opt/iklimco/stacks/.env` dosyasında saklanır. Bu dosya StorageBox'ta `prod/secrets/iklim.co/.env.stacks` olarak tutulur. İlk deploy öncesinde `iklim-app-01` üzerinde aşağıdaki komutla çekilir: + +```bash +scp -P 23 STORAGEBOX_USER@STORAGEBOX_USER.your-storagebox.de:prod/secrets/iklim.co/.env.stacks \ + /opt/iklimco/stacks/.env +chmod 600 /opt/iklimco/stacks/.env +``` ## Kabul Kriterleri @@ -240,6 +269,8 @@ Her node'a uygulanır. Sunucu üzerinde ed25519 SSH anahtar çifti üretilir ve - 3 DB node `docker node ls` içinde Worker olarak görünür. - Manager quorum sağlanır (3 manager, 1 kayıp tolere edilir). - `iklimco-net` overlay network vardır. -- Node etiketleri (`type=service`, `role=db`) inspect ile doğrulanır. +- Node etiketleri (`type=service`, `role=db`, `db-index=01/02/03`) inspect ile doğrulanır. - Her node'da `/mnt/storagebox` mount edilmiştir. +- Her app node'da Gitea Act Runner servisi çalışmaktadır. +- DB node'larında `/opt/iklimco/db/mongodb/config/mongod.conf` oluşturulmuştur ve `replSetName: rs0` içermektedir. - Public firewall sadece `22`, `80`, `443` ingress'e izin verir. diff --git a/setup/08-prod-db-cluster-kurulum.md b/setup/08-prod-db-cluster-kurulum.md index 94f8839..9148da6 100644 --- a/setup/08-prod-db-cluster-kurulum.md +++ b/setup/08-prod-db-cluster-kurulum.md @@ -368,12 +368,13 @@ USER postgres ENTRYPOINT ["patroni", "/etc/patroni/patroni.yml"] ``` -Build ve push: +Build ve push (`ops/push-harbor-custom-images.sh` ile yapılır veya aşağıdaki komutları çalıştır): ```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 +docker build -t registry.tarla.io/iklimco/patroni-postgis:17-3.5 . +echo "$HARBOR_CI_TOKEN" | docker login registry.tarla.io -u robot-ci-push-iklimco --password-stdin +docker push registry.tarla.io/iklimco/patroni-postgis:17-3.5 ``` ### 5.2 etcd Kümesi @@ -490,7 +491,7 @@ services: 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). +**Önemli:** `ETCD_INITIAL_CLUSTER_STATE` değeri ilk deploy'da `new`, sonraki tüm deploy'larda `existing` olmalıdır. Yanlış değer bırakılırsa data dizini sıfırlanır. Aşağıdaki Section 6'daki deploy adımları bu durumu otomatik tespit eder — manuel güncelleme gerekmez. ### 5.3 Patroni Konfigürasyonu @@ -606,7 +607,7 @@ networks: services: patroni-01: - image: registry.iklim.co/infra/patroni-postgis:17-3.5 + image: registry.tarla.io/iklimco/patroni-postgis:17-3.5 environment: DATABASE_POSTGRES_ROOT_USER: "${DATABASE_POSTGRES_ROOT_USER}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" @@ -635,7 +636,7 @@ services: condition: on-failure patroni-02: - image: registry.iklim.co/infra/patroni-postgis:17-3.5 + image: registry.tarla.io/iklimco/patroni-postgis:17-3.5 environment: DATABASE_POSTGRES_ROOT_USER: "${DATABASE_POSTGRES_ROOT_USER}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" @@ -664,7 +665,7 @@ services: condition: on-failure patroni-03: - image: registry.iklim.co/infra/patroni-postgis:17-3.5 + image: registry.tarla.io/iklimco/patroni-postgis:17-3.5 environment: DATABASE_POSTGRES_ROOT_USER: "${DATABASE_POSTGRES_ROOT_USER}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" @@ -720,23 +721,66 @@ docker exec -it $(docker ps -q -f name=iklim-patroni_patroni-01) \ Sıra önemlidir: önce etcd, ardından MongoDB ve Patroni stack'leri. +### .env Dosyası + +`/opt/iklimco/stacks/.env` dosyası StorageBox'ta `prod/secrets/iklim.co/.env.stacks` olarak saklanır. İlk kez oluşturulurken güçlü şifrelerle doldurulup StorageBox'a yüklenir; sonraki deploy'larda buradan çekilir: + +```bash +# iklim-app-01 üzerinde (bir kez): +scp -P 23 STORAGEBOX_USER@STORAGEBOX_USER.your-storagebox.de:prod/secrets/iklim.co/.env.stacks \ + /opt/iklimco/stacks/.env +chmod 600 /opt/iklimco/stacks/.env +``` + +Dosya içeriği (`/opt/iklimco/stacks/.env`, repo'ya commit edilmez): + +```env +DATABASE_POSTGRES_ROOT_USER=postgres +POSTGRES_PASSWORD= +REPLICATOR_PASSWORD= +MONGO_ROOT_PASSWORD= +``` + +### Deploy Adımları + ```bash # iklim-app-01 üzerinde (Swarm manager): export $(cat /opt/iklimco/stacks/.env | xargs) +# ETCD_INITIAL_CLUSTER_STATE otomatik tespiti — ilk deploy'da 'new', sonrakinde 'existing' +ETCD_STATE="new" +if docker service ls --filter name=iklim-etcd -q 2>/dev/null | grep -q .; then + echo "ℹ️ etcd servisleri mevcut, 'existing' state kullanılıyor..." + ETCD_STATE="existing" +else + echo "ℹ️ İlk deploy, 'new' state kullanılıyor..." +fi +sed -i \ + "s/ETCD_INITIAL_CLUSTER_STATE: new/ETCD_INITIAL_CLUSTER_STATE: ${ETCD_STATE}/g; \ + s/ETCD_INITIAL_CLUSTER_STATE: existing/ETCD_INITIAL_CLUSTER_STATE: ${ETCD_STATE}/g" \ + /opt/iklimco/stacks/prod-db-etcd.yml +echo "✅ ETCD_INITIAL_CLUSTER_STATE=${ETCD_STATE}" + # 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 +# etcd cluster'ın kurulmasını bekle: +echo "⏳ etcd bekleniyor..." +for i in $(seq 1 18); do + if docker exec $(docker ps -q -f name=iklim-etcd_etcd-01 | head -1) \ + etcdctl endpoint health \ + --endpoints=http://10.20.20.11:2379,http://10.20.20.12:2379,http://10.20.20.13:2379 \ + 2>/dev/null | grep -q "is healthy"; then + echo "✅ etcd hazır" + break + fi + [ "$i" -eq 18 ] && echo "❌ etcd timeout" && exit 1 + echo " attempt $i/18 — 10s bekleniyor..." + sleep 10 +done # 2. MongoDB: docker stack deploy \ @@ -755,19 +799,6 @@ 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= -REPLICATOR_PASSWORD= -MONGO_ROOT_PASSWORD= -``` - ### MongoDB Replica Set Başlatma MongoDB stack deploy edildikten sonra bir kez çalıştırılır: diff --git a/terraform/hetzner/prod/servers.tf b/terraform/hetzner/prod/servers.tf index d6547ec..0245dc4 100644 --- a/terraform/hetzner/prod/servers.tf +++ b/terraform/hetzner/prod/servers.tf @@ -7,7 +7,7 @@ resource "hcloud_server" "app" { for_each = local.app_private_ips name = each.key - server_type = var.server_type_app + server_type = var.server_type_swarm image = var.image location = var.location ssh_keys = [hcloud_ssh_key.admin.id] diff --git a/terraform/hetzner/prod/variables.tf b/terraform/hetzner/prod/variables.tf index f00ad10..de5a4c1 100644 --- a/terraform/hetzner/prod/variables.tf +++ b/terraform/hetzner/prod/variables.tf @@ -16,10 +16,10 @@ variable "image" { description = "Server image" } -variable "server_type_app" { +variable "server_type_swarm" { type = string default = "cpx42" - description = "Hetzner server type for App nodes" + description = "Hetzner server type for App/Swarm nodes" } variable "server_type_db" {