# Prod Ortamı Kurulum Geçmişi

Prod kurulum adımları ve mevcut yapı.

## Terraform

### Hetzner Cloud Yapılandırması

Test ve prod ayrı Hetzner Cloud projelerinde çalışır; her proje için ayrı API token kullanılır. `terraform.tfvars.example` dosyasından kopyalanarak doldurulur:

```hcl
hcloud_token              = "<iklim_prod proje token'ı>"
location                  = "fsn1"
image                     = "rocky-10"
server_type_app           = "cpx42"
server_type_db            = "cpx32"
admin_ssh_public_key_path = "~/.ssh/id_rsa.pub"
admin_allowed_cidrs       = ["78.187.87.109/32", "95.70.151.248/32"]
```

`admin_allowed_cidrs` Ansible `group_vars/all/vars.yml` ile birebir uyumlu olmalıdır. Prod sunucuları `lifecycle { prevent_destroy = true }` ile korunur.

### Apply

```bash
cd Environment_Infrastructure/terraform/hetzner/prod
terraform init
terraform plan
terraform apply
```

### Inventory Üretimi

```bash
mkdir -p ../../../ansible/prod/inventory/generated
terraform output -raw ansible_inventory_yaml > ../../../ansible/prod/inventory/generated/prod.yml
```

## Ansible

### ansible.cfg

`roles_path = roles:../roles` ile prod'a özgü roller (`roles/`) yanı sıra ortak roller (`../roles/`) de kullanılır.

```ini
[defaults]
inventory = inventory/generated/prod.yml
remote_user = root
host_key_checking = False
retry_files_enabled = False
interpreter_python = auto_silent
roles_path = roles:../roles

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
```

### group_vars/all/vars.yml

```yaml
storagebox_account: "u469968"
storagebox_user: "{{ storagebox_account }}-sub5"
storagebox_url: "https://{{ storagebox_user }}.your-storagebox.de/"
storagebox_mount_point: "/mnt/storagebox"
storagebox_password: "{{ vault_storagebox_password }}"
storagebox_managed_directories:
  - path: "{{ storagebox_mount_point }}/ssl"
    mode: "0755"
  - path: "{{ storagebox_mount_point }}/swag/config"
    mode: "0755"
  - path: "{{ storagebox_mount_point }}/swag/site-confs"
    mode: "0755"
  - path: "{{ storagebox_mount_point }}/grafana/data"
    mode: "0755"
  - path: "{{ storagebox_mount_point }}/precipitation/images"
    mode: "0755"

iklim_password: "{{ vault_iklim_password }}"
act_runner_labels: "prod-runner,ubuntu-24.04,{{ inventory_hostname }}"
swarm_manager_ip: "10.20.10.11"
mongodb_replset_name: "rs0"
admin_allowed_cidrs: "78.187.87.109/32 95.70.151.248/32"
admin_ssh_public_key_path: "~/.ssh/id_rsa.pub"
timezone: "Europe/Istanbul"
```

`admin_allowed_cidrs` Terraform `terraform.tfvars` ile birebir uyumlu olmalıdır.

### group_vars/all/vault.yml

Ansible Vault ile şifrelidir. Tanımlaması gereken değişken:

```yaml
vault_storagebox_password: "<storagebox-sub-user-parolası>"
```

### group_vars/db/vars.yml

DB node'larında StorageBox `uid/gid=999` ile mount edilir (MongoDB ve PostgreSQL container user'ı ile uyumlu):

```yaml
storagebox_uid: "999"
storagebox_gid: "999"
```

### host_vars — Per-Host Vault

`vault_iklim_password` her sunucu için ayrı `host_vars/<hostname>/vault.yml` dosyasında tanımlıdır. Hardening rolü bu değeri `iklim` OS kullanıcısının sistem parolası olarak uygular (`password_hash('sha512')`). Her sunucuya farklı parola verilebilir.

```text
prod/
  host_vars/
    iklim-app-01/vault.yml   ← vault_iklim_password: "<iklim OS kullanıcısı parolası>"
    iklim-app-02/vault.yml
    iklim-app-03/vault.yml
    iklim-db-01/vault.yml
    iklim-db-02/vault.yml
    iklim-db-03/vault.yml
```

Her dosya ayrı şifrelenir: `ansible-vault encrypt host_vars/<hostname>/vault.yml`

Hardening rolü `PermitRootLogin prohibit-password` uygular — key tabanlı root girişi açık, parola ile kapalıdır.

### StorageBox Mount

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 — base, hardening, docker, dizinler, storagebox
ansible-playbook prod-bootstrap.yml \
  --tags base,hardening,docker,node_dirs,storagebox,storagebox_ssh_key \
  --vault-password-file=../.vault_pass

# 2. Swarm kurulumu
ansible-playbook prod-bootstrap.yml \
  --tags swarm \
  --vault-password-file=../.vault_pass

# 3. DB node label'ları
ansible-playbook prod-bootstrap.yml \
  --tags db_labels \
  --vault-password-file=../.vault_pass

# 4. DB node konfigürasyonu (StorageBox dizinleri, patroni.yml, mongod.conf, keyfile)
ansible-playbook prod-bootstrap.yml \
  --tags db_stack \
  --limit db \
  --vault-password-file=../.vault_pass

# 5. Act runner kurulumu
ansible-playbook prod-bootstrap.yml \
  --tags act_runner \
  --vault-password-file=../.vault_pass
```

## 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 \
   /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 |
| --- | --- |
| Terraform — 6 sunucu, ağ, firewall, floating IP | ✅ |
| Ansible base + hardening + docker + node_dirs | ✅ |
| Ansible storagebox + storagebox_ssh_key | ✅ |
| Ansible swarm (3 manager app + 3 worker db) | ✅ |
| Ansible db_labels | ✅ |
| Ansible db_stack (StorageBox DB dizinleri + config) | ✅ |
| Ansible act_runner (3 prod runner Gitea'da Idle) | ✅ |
| 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 |
