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

View File

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

View File

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

View File

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

View File

@ -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, <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.
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/<hostname>/vault.yml` yapısına geçildi:
`vault_iklim_password` per-host `host_vars/<hostname>/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/<hostname>/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@<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 |
| --- | --- |
@ -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 |