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 |