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.
This commit is contained in:
Murat ÖZDEMİR 2026-05-21 21:48:11 +03:00
parent e3787d80f6
commit c568e31515
6 changed files with 165 additions and 76 deletions

View File

@ -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"

View File

@ -11,6 +11,9 @@
state: directory state: directory
mode: '0755' mode: '0755'
- name: Sync StorageBox after directory creation
ansible.builtin.command: sync
- name: Deploy mongod.conf to StorageBox - name: Deploy mongod.conf to StorageBox
ansible.builtin.template: ansible.builtin.template:
src: mongod.conf.j2 src: mongod.conf.j2
@ -22,3 +25,30 @@
src: patroni.yml.j2 src: patroni.yml.j2
dest: "{{ storagebox_mount_point }}/db/postgresql-{{ inventory_hostname.split('-')[-1] }}/config/patroni.yml" dest: "{{ storagebox_mount_point }}/db/postgresql-{{ inventory_hostname.split('-')[-1] }}/config/patroni.yml"
mode: '0644' 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

View File

@ -4,13 +4,9 @@ storage:
engine: "wiredTiger" engine: "wiredTiger"
dbPath: "/data/db" dbPath: "/data/db"
directoryPerDB: true directoryPerDB: true
systemLog: wiredTiger:
verbosity: 0 engineConfig:
timeStampFormat: "iso8601-local" configString: "verbose=[]"
destination: file
path: "/data/log/mongo.log"
logAppend: true
logRotate: rename
replication: replication:
replSetName: "{{ mongodb_replset_name }}" replSetName: "{{ mongodb_replset_name }}"
security: security:

View File

@ -1,3 +1,6 @@
log:
level: WARNING
scope: iklim-postgres scope: iklim-postgres
namespace: /db/ namespace: /db/
name: postgresql-{{ inventory_hostname.split('-')[-1] }} name: postgresql-{{ inventory_hostname.split('-')[-1] }}
@ -34,13 +37,19 @@ bootstrap:
- data-checksums - data-checksums
pg_hba: 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 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.10.0/24 scram-sha-256
- host all all 10.20.20.0/24 scram-sha-256 - host all all 10.20.20.0/24 scram-sha-256
users: users:
postgres: postgres:
password: "${DATABASE_POSTGRES_ROOT_PASSWD}" password: "${POSTGRES_PASSWORD}"
options: options:
- superuser - superuser
@ -52,12 +61,22 @@ postgresql:
authentication: authentication:
replication: replication:
username: replicator username: replicator
password: "${DATABASE_POSTGRES_REPLICATOR_PASSWORD}" password: "${REPLICATOR_PASSWORD}"
superuser: superuser:
username: postgres username: postgres
password: "${DATABASE_POSTGRES_ROOT_PASSWD}" password: "${POSTGRES_PASSWORD}"
parameters: parameters:
unix_socket_directories: "/var/run/postgresql" 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: tags:
nofailover: false nofailover: false

View File

@ -22,7 +22,8 @@
- name: Add fstab entry for StorageBox - name: Add fstab entry for StorageBox
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: /etc/fstab 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 state: present
- name: Mount StorageBox - name: Mount StorageBox
@ -30,7 +31,7 @@
path: "{{ storagebox_mount_point }}" path: "{{ storagebox_mount_point }}"
src: "{{ storagebox_url }}" src: "{{ storagebox_url }}"
fstype: davfs 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 state: mounted
- name: Write mount marker - name: Write mount marker

View File

@ -1,34 +1,14 @@
# Prod Ortamı Kurulum Geçmişi # 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 ## 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: Prod sunucuları `lifecycle { prevent_destroy = true }` ile korunur.
```
SSH key not unique (uniqueness_error, <fingerprint>)
```
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 <ID>
```
### 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.
### Inventory Üretimi ### Inventory Üretimi
@ -40,43 +20,23 @@ terraform output -raw ansible_inventory_yaml > ../../../ansible/prod/inventory/g
## Ansible ## 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. `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:
Doğru değer: tüm admin IP'lerini boşlukla ayrılmış string olarak içermelidir.
```yaml ```yaml
admin_allowed_cidrs: "78.187.87.109/32 95.70.151.248/32" 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" 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. `vault_iklim_password` per-host `host_vars/<hostname>/vault.yml` dosyalarında tanımlıdır, her sunucu farklı şifreye sahiptir:
**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/<hostname>/vault.yml` yapısına geçildi:
```text ```text
prod/ prod/
group_vars/all/vault.yml ← vault_iklim_password KALDIRILDI
host_vars/ host_vars/
iklim-app-01/vault.yml ← vault_iklim_password: "<şifre>" iklim-app-01/vault.yml
iklim-app-02/vault.yml iklim-app-02/vault.yml
iklim-app-03/vault.yml iklim-app-03/vault.yml
iklim-db-01/vault.yml iklim-db-01/vault.yml
@ -84,44 +44,122 @@ prod/
iklim-db-03/vault.yml iklim-db-03/vault.yml
``` ```
Her dosya ayrı ayrı şifrelenir: `ansible-vault encrypt host_vars/<hostname>/vault.yml`
### StorageBox Mount ### 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ı ### Bootstrap Çalıştırma Sırası
```bash ```bash
cd Environment_Infrastructure/ansible/prod 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 \ ansible-playbook prod-bootstrap.yml \
--tags base,hardening,docker,node_dirs,storagebox,storagebox_ssh_key \ --tags base,hardening,docker,node_dirs,storagebox,storagebox_ssh_key \
--ask-vault-pass --vault-password-file=../.vault_pass
# 2. Swarm kurulumu # 2. Swarm kurulumu
ansible-playbook prod-bootstrap.yml \ ansible-playbook prod-bootstrap.yml \
--tags swarm \ --tags swarm \
--ask-vault-pass --vault-password-file=../.vault_pass
# 3. DB node label'ları # 3. DB node label'ları
ansible-playbook prod-bootstrap.yml \ ansible-playbook prod-bootstrap.yml \
--tags db_labels \ --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 \ ansible-playbook prod-bootstrap.yml \
--tags db_stack \ --tags db_stack \
--ask-vault-pass --limit db \
--vault-password-file=../.vault_pass
# 5. Act runner kurulumu # 5. Act runner kurulumu
ansible-playbook prod-bootstrap.yml \ ansible-playbook prod-bootstrap.yml \
--tags act_runner \ --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@<db-01-ip>
# 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 | | Adım | Durum |
| --- | --- | | --- | --- |
@ -129,10 +167,11 @@ ansible-playbook prod-bootstrap.yml \
| Ansible base + hardening + docker + node_dirs | ✅ | | Ansible base + hardening + docker + node_dirs | ✅ |
| Ansible storagebox + storagebox_ssh_key | ✅ | | Ansible storagebox + storagebox_ssh_key | ✅ |
| Ansible swarm (3 manager app + 3 worker db) | ✅ | | Ansible swarm (3 manager app + 3 worker db) | ✅ |
| Ansible db_labels (Patroni koordinasyonu için) | ✅ | | Ansible db_labels | ✅ |
| Ansible db_stack (StorageBox DB dizinleri) | ✅ | | Ansible db_stack (StorageBox DB dizinleri + config) | ✅ |
| Ansible act_runner (3 prod runner Gitea'da Idle) | ✅ | | Ansible act_runner (3 prod runner Gitea'da Idle) | ✅ |
| DB stack deploy (docker-stack-db.prod.yml) | ⏳ bekliyor | | DB stack deploy (etcd + MongoDB + Patroni) | ✅ |
| MongoDB replica set init | ⏳ bekliyor | | 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 | | Ana infra stack deploy (docker-stack-infra.prod.yml) | ⏳ bekliyor |
| Deploy pipeline ilk çalışma | ⏳ bekliyor | | Deploy pipeline ilk çalışma | ⏳ bekliyor |