From c568e31515e30a001165366191dc7f818dd2de64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Murat=20=C3=96ZDEM=C4=B0R?= Date: Thu, 21 May 2026 21:48:11 +0300 Subject: [PATCH] Finalize production database bootstrap automation Add DB-specific StorageBox ownership variables and make the davfs mount role honor configurable uid and gid values so database containers can access mounted files. Extend the prod DB node role to sync StorageBox writes, generate and distribute the MongoDB replica set keyfile, wait for the keyfile on each node, and enforce keyfile permissions. Tune MongoDB and Patroni templates for quieter logging, correct secret variable names, local bootstrap trust, and production network pg_hba coverage. Refresh the production setup history with the current bootstrap sequence, DB stack deployment workflow, MongoDB replica set initialization, Patroni validation, and completed DB cluster status. --- ansible/prod/group_vars/db/vars.yml | 4 + ansible/prod/roles/db_stack/tasks/db_node.yml | 30 ++++ .../roles/db_stack/templates/mongod.conf.j2 | 10 +- .../roles/db_stack/templates/patroni.yml.j2 | 25 ++- ansible/roles/storagebox/tasks/main.yml | 5 +- facts/prod-kurulum-gecmisi.md | 167 +++++++++++------- 6 files changed, 165 insertions(+), 76 deletions(-) create mode 100644 ansible/prod/group_vars/db/vars.yml diff --git a/ansible/prod/group_vars/db/vars.yml b/ansible/prod/group_vars/db/vars.yml new file mode 100644 index 0000000..ff258f5 --- /dev/null +++ b/ansible/prod/group_vars/db/vars.yml @@ -0,0 +1,4 @@ +# DB node'larında StorageBox uid/gid=999 (mongodb ve postgres container user) +# davfs2 dosyaları uid 999 sahibi gösterir; container içi erişim açılır. +storagebox_uid: "999" +storagebox_gid: "999" diff --git a/ansible/prod/roles/db_stack/tasks/db_node.yml b/ansible/prod/roles/db_stack/tasks/db_node.yml index 70fc367..10fd79a 100644 --- a/ansible/prod/roles/db_stack/tasks/db_node.yml +++ b/ansible/prod/roles/db_stack/tasks/db_node.yml @@ -11,6 +11,9 @@ state: directory mode: '0755' +- name: Sync StorageBox after directory creation + ansible.builtin.command: sync + - name: Deploy mongod.conf to StorageBox ansible.builtin.template: src: mongod.conf.j2 @@ -22,3 +25,30 @@ src: patroni.yml.j2 dest: "{{ storagebox_mount_point }}/db/postgresql-{{ inventory_hostname.split('-')[-1] }}/config/patroni.yml" mode: '0644' + +- name: Sync StorageBox after config file writes + ansible.builtin.command: sync + +- name: Generate MongoDB replica set keyfile on db-01 + when: inventory_hostname == 'iklim-db-01' + ansible.builtin.shell: | + openssl rand -base64 756 > {{ storagebox_mount_point }}/db/mongodb-01/config/rs-auth.key + chmod 400 {{ storagebox_mount_point }}/db/mongodb-01/config/rs-auth.key + cp {{ storagebox_mount_point }}/db/mongodb-01/config/rs-auth.key \ + {{ storagebox_mount_point }}/db/mongodb-02/config/rs-auth.key + cp {{ storagebox_mount_point }}/db/mongodb-01/config/rs-auth.key \ + {{ storagebox_mount_point }}/db/mongodb-03/config/rs-auth.key + chmod 400 {{ storagebox_mount_point }}/db/mongodb-02/config/rs-auth.key + chmod 400 {{ storagebox_mount_point }}/db/mongodb-03/config/rs-auth.key + sync + args: + creates: "{{ storagebox_mount_point }}/db/mongodb-01/config/rs-auth.key" + +- name: Wait for MongoDB keyfile on this node's StorageBox mount + ansible.builtin.wait_for: + path: "{{ storagebox_mount_point }}/db/mongodb-{{ inventory_hostname.split('-')[-1] }}/config/rs-auth.key" + timeout: 60 + +- name: Fix MongoDB keyfile permissions on this node + ansible.builtin.shell: | + chmod 400 {{ storagebox_mount_point }}/db/mongodb-{{ inventory_hostname.split('-')[-1] }}/config/rs-auth.key diff --git a/ansible/prod/roles/db_stack/templates/mongod.conf.j2 b/ansible/prod/roles/db_stack/templates/mongod.conf.j2 index 05dac12..db6c378 100644 --- a/ansible/prod/roles/db_stack/templates/mongod.conf.j2 +++ b/ansible/prod/roles/db_stack/templates/mongod.conf.j2 @@ -4,13 +4,9 @@ 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 + wiredTiger: + engineConfig: + configString: "verbose=[]" replication: replSetName: "{{ mongodb_replset_name }}" security: diff --git a/ansible/prod/roles/db_stack/templates/patroni.yml.j2 b/ansible/prod/roles/db_stack/templates/patroni.yml.j2 index 19efdb5..0f32bcd 100644 --- a/ansible/prod/roles/db_stack/templates/patroni.yml.j2 +++ b/ansible/prod/roles/db_stack/templates/patroni.yml.j2 @@ -1,3 +1,6 @@ +log: + level: WARNING + scope: iklim-postgres namespace: /db/ name: postgresql-{{ inventory_hostname.split('-')[-1] }} @@ -34,13 +37,19 @@ bootstrap: - data-checksums pg_hba: + - local all all trust + - host all all 127.0.0.1/32 trust + - host replication replicator 127.0.0.1/32 trust + - host replication replicator 10.0.0.0/8 scram-sha-256 - host replication replicator 10.20.20.0/24 scram-sha-256 + - host all all 10.0.0.0/8 scram-sha-256 + - host all all 10.8.0.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: "${DATABASE_POSTGRES_ROOT_PASSWD}" + password: "${POSTGRES_PASSWORD}" options: - superuser @@ -52,12 +61,22 @@ postgresql: authentication: replication: username: replicator - password: "${DATABASE_POSTGRES_REPLICATOR_PASSWORD}" + password: "${REPLICATOR_PASSWORD}" superuser: username: postgres - password: "${DATABASE_POSTGRES_ROOT_PASSWD}" + password: "${POSTGRES_PASSWORD}" parameters: unix_socket_directories: "/var/run/postgresql" + pg_hba: + - local all all trust + - host all all 127.0.0.1/32 trust + - host replication replicator 127.0.0.1/32 trust + - host replication replicator 10.0.0.0/8 scram-sha-256 + - host replication replicator 10.20.20.0/24 scram-sha-256 + - host all all 10.0.0.0/8 scram-sha-256 + - host all all 10.8.0.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 tags: nofailover: false diff --git a/ansible/roles/storagebox/tasks/main.yml b/ansible/roles/storagebox/tasks/main.yml index 98bcf6d..a7698b4 100644 --- a/ansible/roles/storagebox/tasks/main.yml +++ b/ansible/roles/storagebox/tasks/main.yml @@ -22,7 +22,8 @@ - name: Add fstab entry for StorageBox ansible.builtin.lineinfile: path: /etc/fstab - line: "{{ storagebox_url }} {{ storagebox_mount_point }} davfs _netdev,auto,user,rw,uid=root,gid=root 0 0" + line: "{{ storagebox_url }} {{ storagebox_mount_point }} davfs _netdev,auto,user,rw,uid={{ storagebox_uid | default('root') }},gid={{ storagebox_gid | default('root') }} 0 0" + regexp: "^{{ storagebox_url | regex_escape() }}" state: present - name: Mount StorageBox @@ -30,7 +31,7 @@ path: "{{ storagebox_mount_point }}" src: "{{ storagebox_url }}" fstype: davfs - opts: "_netdev,auto,user,rw,uid=root,gid=root" + opts: "_netdev,auto,user,rw,uid={{ storagebox_uid | default('root') }},gid={{ storagebox_gid | default('root') }}" state: mounted - name: Write mount marker diff --git a/facts/prod-kurulum-gecmisi.md b/facts/prod-kurulum-gecmisi.md index 34e334e..84b6c09 100644 --- a/facts/prod-kurulum-gecmisi.md +++ b/facts/prod-kurulum-gecmisi.md @@ -1,34 +1,14 @@ # Prod Ortamı Kurulum Geçmişi -İlk prod kurulumunda yaşanan sorunlar, uygulanan düzeltmeler ve mevcut durum. +Prod kurulum adımları ve mevcut yapı. ## Terraform -### Ayrı Hetzner Projesi Zorunluluğu +### Hetzner Cloud Yapılandırması -Test ve prod ayrı Hetzner Cloud projelerinde çalışır. Her proje için ayrı API token alınmalıdır. +Test ve prod ayrı Hetzner Cloud projelerinde çalışır; her proje için ayrı API token kullanılır. `terraform/hetzner/prod/terraform.tfvars` içindeki `hcloud_token` değeri `iklim_prod` projesinin token'ına aittir. -Aynı token kullanılırsa `terraform apply` sırasında SSH key çakışması hatası alınır: - -``` -SSH key not unique (uniqueness_error, ) -``` - -Hetzner Cloud, aynı public key'i bir projede yalnızca bir kez kabul eder. Test projesinde oluşturulmuş key, prod projesine ait olmayan bir token ile kullanılamaz. - -**Düzeltme:** `terraform/hetzner/prod/terraform.tfvars` içindeki `hcloud_token` değeri `iklim_prod` projesinin token'ı olmalıdır. - -### State Kaybı Sonrası SSH Key Import - -Terraform state'i olmadan `terraform apply` çalıştırılırsa, Hetzner'da zaten var olan SSH key yeniden oluşturulmaya çalışılır ve yukarıdaki hata alınır. Bu durumda key Hetzner Console → Security → SSH Keys üzerinden ID'si bulunarak import edilir: - -```bash -terraform import hcloud_ssh_key.admin -``` - -### prevent_destroy - -Prod sunucuları `lifecycle { prevent_destroy = true }` ile korunur. `terraform destroy` çalıştırmak gerekirse bu değer geçici olarak `false` yapılır, işlem bittikten sonra tekrar `true`'ya alınır. +Prod sunucuları `lifecycle { prevent_destroy = true }` ile korunur. ### Inventory Üretimi @@ -40,43 +20,23 @@ terraform output -raw ansible_inventory_yaml > ../../../ansible/prod/inventory/g ## Ansible -### admin_allowed_cidrs +### Yapılandırma Notları -`group_vars/all/vars.yml` içindeki `admin_allowed_cidrs` değeri başlangıçta `127.0.0.1/8` olarak tanımlanmıştı. Bu değer hardening rolü tarafından firewalld'a uygulanır ve SSH erişimini tamamen kapatır. - -Doğru değer: tüm admin IP'lerini boşlukla ayrılmış string olarak içermelidir. +`group_vars/all/vars.yml` içindeki `admin_allowed_cidrs` tüm admin IP'lerini boşlukla ayrılmış string olarak içerir ve Terraform `terraform.tfvars` ile birebir uyumludur: ```yaml admin_allowed_cidrs: "78.187.87.109/32 95.70.151.248/32" -``` - -Terraform `terraform.tfvars` içindeki `admin_allowed_cidrs` ile birebir uyumlu olmalıdır. - -### admin_ssh_public_key_path - -`group_vars/all/vars.yml` içinde başlangıçta `~/.ssh/id_ed25519.pub` yazıyordu; bu makinede bu dosya yoktu. Doğru değer: - -```yaml admin_ssh_public_key_path: "~/.ssh/id_rsa.pub" ``` -### PermitRootLogin +Hardening rolü `PermitRootLogin prohibit-password` uygular — key tabanlı root girişi açık, parola ile root girişi kapalıdır. -Hardening rolü başlangıçta `PermitRootLogin no` uyguluyordu. Bu ayar sshd handler'ı ilk play'in sonunda tetiklediğinden, aynı Ansible çalışmasının devamındaki play'ler (swarm, db_labels, db_stack, act_runner) root ile bağlanamıyordu. - -**Düzeltme:** `PermitRootLogin prohibit-password` olarak değiştirildi. Key tabanlı root girişi açık kalır; parola ile root girişi engellenir. Ansible tüm bootstrap boyunca root ile bağlanmaya devam edebilir. - -### vault_iklim_password — Per-Host Şifre - -`vault_iklim_password` başlangıçta `group_vars/all/vault.yml` içinde tek bir değer olarak tanımlıydı; tüm sunuculara aynı şifre uygulanıyordu. - -Her sunucuya farklı şifre vermek için `host_vars//vault.yml` yapısına geçildi: +`vault_iklim_password` per-host `host_vars//vault.yml` dosyalarında tanımlıdır, her sunucu farklı şifreye sahiptir: ```text prod/ - group_vars/all/vault.yml ← vault_iklim_password KALDIRILDI host_vars/ - iklim-app-01/vault.yml ← vault_iklim_password: "<şifre>" + iklim-app-01/vault.yml iklim-app-02/vault.yml iklim-app-03/vault.yml iklim-db-01/vault.yml @@ -84,44 +44,122 @@ prod/ iklim-db-03/vault.yml ``` -Her dosya ayrı ayrı şifrelenir: `ansible-vault encrypt host_vars//vault.yml` - ### StorageBox Mount -StorageBox WebDAV mount (`/mnt/storagebox`) davfs2 ile yapılır. Aynı anda 6 sunucunun mount denemesi zaman zaman sorun çıkarabilir; Ansible idempotent çalıştığından tekrar çalıştırmak yeterlidir. +StorageBox WebDAV mount (`/mnt/storagebox`) davfs2 ile yapılır. DB node'larında `uid=999,gid=999` parametreleriyle mount edilir (PostgreSQL/MongoDB container uid'i ile uyumlu). `group_vars/db/vars.yml` içinde tanımlanır: + +```yaml +storagebox_uid: "999" +storagebox_gid: "999" +``` ### Bootstrap Çalıştırma Sırası ```bash cd Environment_Infrastructure/ansible/prod -# 1. Tüm node'lar +# 1. Tüm node'lar — base, hardening, docker, dizinler, storagebox ansible-playbook prod-bootstrap.yml \ --tags base,hardening,docker,node_dirs,storagebox,storagebox_ssh_key \ - --ask-vault-pass + --vault-password-file=../.vault_pass # 2. Swarm kurulumu ansible-playbook prod-bootstrap.yml \ --tags swarm \ - --ask-vault-pass + --vault-password-file=../.vault_pass # 3. DB node label'ları ansible-playbook prod-bootstrap.yml \ --tags db_labels \ - --ask-vault-pass + --vault-password-file=../.vault_pass -# 4. DB node konfigürasyonu +# 4. DB node konfigürasyonu (StorageBox dizinleri, patroni.yml, mongod.conf, keyfile) ansible-playbook prod-bootstrap.yml \ --tags db_stack \ - --ask-vault-pass + --limit db \ + --vault-password-file=../.vault_pass # 5. Act runner kurulumu ansible-playbook prod-bootstrap.yml \ --tags act_runner \ - --ask-vault-pass + --vault-password-file=../.vault_pass ``` -## Mevcut Durum (2026-05-19) +## DB Stack Deploy + +### Custom Image Build + +`build/patroni-postgis/` altında PostGIS + Patroni imajı bulunur (`registry.tarla.io/iklimco/custom-patroni-postgis:18-3.6`). `ops/push-harbor-custom-images.sh` ile Harbor'a push edilir. + +### Stack Deploy + +```bash +# Lokal → app-01 +scp ./docker-stack-* root@178.104.210.41:/home/iklim/ + +# app-01'de +cd /home/iklim +# password; 'https://passwords.tarla.io' içinde "tarla.io›[Hetzner] Utils Server" klasörünün altında +scp -P 23 u469968@u469968.your-storagebox.de:prod/secrets/iklim.co/.env.secrets.shared \ + /tmp/.env.secrets.shared +chmod 600 /tmp/.env.secrets.shared +scp -P 23 u469968@u469968.your-storagebox.de:prod/secrets/iklim.co/.env.secrets \ + /tmp/.env +chmod 600 /tmp/.env + +export $(grep -v '^\s*#' /tmp/.env.secrets.shared | grep -v '^\s*$' | xargs) +export $(grep -v '^\s*#' /tmp/.env | grep -v '^\s*$' | xargs) +docker stack deploy --with-registry-auth -c docker-stack-db.prod.yml iklim-db + +# deploy başarılı bir şekilde tamamlanınca +rm /tmp/.env +rm /tmp/.env.secrets.shared +history -c && history -w +``` + +### MongoDB Replica Set Init + +```bash +ssh root@ + +# password; 'https://passwords.tarla.io' içinde "tarla.io›[Hetzner] Utils Server" klasörünün altında +scp -P 23 u469968@u469968.your-storagebox.de:prod/secrets/iklim.co/.env.secrets.shared \ + /tmp/.env.secrets.shared +chmod 600 /tmp/.env.secrets.shared +scp -P 23 u469968@u469968.your-storagebox.de:prod/secrets/iklim.co/.env.secrets \ + /tmp/.env +chmod 600 /tmp/.env + +export $(grep -v '^\s*#' /tmp/.env.secrets.shared | grep -v '^\s*$' | xargs) +export $(grep -v '^\s*#' /tmp/.env | grep -v '^\s*$' | xargs) + +MONGO_CID=$(docker ps --filter name=iklim-db_mongodb-01 --format "{{.ID}}" | head -1) +docker exec -it $MONGO_CID mongosh \ + -u "$DATABASE_MONGODB_ROOT_USER" \ + -p "$DATABASE_MONGODB_ROOT_PASSWD" \ + --authenticationDatabase admin --eval ' +rs.initiate({ + _id: "rs0", + members: [ + { _id: 0, host: "mongodb-01:27017" }, + { _id: 1, host: "mongodb-02:27017" }, + { _id: 2, host: "mongodb-03:27017" } + ] +})' + +rm /tmp/.env +rm /tmp/.env.secrets.shared +history -c && history -w +``` + +### Patroni Cluster Doğrulama + +```bash +# app-01'den +curl -s http://10.20.20.11:8008/cluster | python3 -m json.tool +``` + +## Mevcut Durum (2026-05-21) | Adım | Durum | | --- | --- | @@ -129,10 +167,11 @@ ansible-playbook prod-bootstrap.yml \ | Ansible base + hardening + docker + node_dirs | ✅ | | Ansible storagebox + storagebox_ssh_key | ✅ | | Ansible swarm (3 manager app + 3 worker db) | ✅ | -| Ansible db_labels (Patroni koordinasyonu için) | ✅ | -| Ansible db_stack (StorageBox DB dizinleri) | ✅ | +| Ansible db_labels | ✅ | +| Ansible db_stack (StorageBox DB dizinleri + config) | ✅ | | Ansible act_runner (3 prod runner Gitea'da Idle) | ✅ | -| DB stack deploy (docker-stack-db.prod.yml) | ⏳ bekliyor | -| MongoDB replica set init | ⏳ bekliyor | +| DB stack deploy (etcd + MongoDB + Patroni) | ✅ | +| MongoDB replica set init (rs0: 1 primary, 2 secondary) | ✅ | +| Patroni HA cluster (1 leader, 2 replica, lag=0) | ✅ | | Ana infra stack deploy (docker-stack-infra.prod.yml) | ⏳ bekliyor | | Deploy pipeline ilk çalışma | ⏳ bekliyor |