diff --git a/.gitea/workflows/deploy-monitoring-test.yml b/.gitea/workflows/deploy-monitoring-test.yml new file mode 100644 index 0000000..ed67990 --- /dev/null +++ b/.gitea/workflows/deploy-monitoring-test.yml @@ -0,0 +1,128 @@ +name: Deploy Environment Monitoring to Test Environment + +on: + push: + branches: + - test + paths: + - 'docker-stack-monitoring.yml' + - 'swag/**' + - '.gitea/workflows/deploy-monitoring-test.yml' + +concurrency: + group: test-monitoring-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: test-runner + steps: + - name: Checkout Branch + uses: actions/checkout@v4 + + - name: Connect Runner to Overlay Network + run: | + docker network connect iklimco-net $(hostname) || true + + - name: Install Required Tools + run: | + sudo sed -i 's|http://archive.ubuntu.com/ubuntu|http://mirror.hetzner.com/ubuntu/packages|g' /etc/apt/sources.list.d/ubuntu.sources || true + sudo sed -i 's|http://archive.ubuntu.com/ubuntu|http://mirror.hetzner.com/ubuntu/packages|g' /etc/apt/sources.list || true + sudo sed -i 's|http://security.ubuntu.com/ubuntu|http://mirror.hetzner.com/ubuntu/packages|g' /etc/apt/sources.list.d/ubuntu.sources || true + sudo sed -i 's|http://security.ubuntu.com/ubuntu|http://mirror.hetzner.com/ubuntu/packages|g' /etc/apt/sources.list || true + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list + sudo rm -f /etc/apt/sources.list.d/git-core-ubuntu-ppa*.list + sudo rm -f /etc/apt/sources.list.d/github_git-lfs.list + sudo apt-get update + sudo apt-get install -y gettext jq + + - name: Set up SSH Key and Add to known_hosts + run: | + mkdir -p ~/.ssh + echo "${{ secrets.STORAGEBOX_SSH_PRIV }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -p 23 ${{ vars.STORAGEBOX_USER }}.your-storagebox.de >> ~/.ssh/known_hosts + + - name: Download Deploy Inputs + run: | + rm -f .env .env.secrets.swag + scp -P 23 ${{ vars.STORAGEBOX_USER }}@${{ vars.STORAGEBOX_USER }}.your-storagebox.de:test/secrets/iklim.co/.env ./.env + scp -P 23 ${{ vars.STORAGEBOX_USER }}@${{ vars.STORAGEBOX_USER }}.your-storagebox.de:test/secrets/iklim.co/.env.secrets.swag ./.env.secrets.swag + test -s .env + test -s .env.secrets.swag + + - name: Deploy Monitoring Stack + run: | + set -a; . ./.env; set +a + export IMAGE_LOKI="${IMAGE_LOKI}" + export IMAGE_PROMTAIL="${IMAGE_PROMTAIL}" + + docker stack deploy \ + --with-registry-auth \ + --resolve-image changed \ + -c docker-stack-monitoring.yml \ + iklimco-monitoring + + - name: Wait for Loki + run: | + for i in $(seq 1 36); do + REPLICAS=$(docker service ls --filter name=iklimco-monitoring_loki --format "{{.Replicas}}" | head -1) + if echo "$REPLICAS" | awk -F'[/ ]' '$1>0 && $1==$2{found=1} END{exit !found}'; then + echo "Loki is ready: $REPLICAS" + exit 0 + fi + echo "Loki not ready yet (${REPLICAS:-missing}), waiting 5s..." + sleep 5 + done + docker service ps iklimco-monitoring_loki || true + exit 1 + + - name: Configure SWAG Reverse Proxy + run: | + set -a; . ./.env; . ./.env.secrets.swag; set +a + export PORTAINER_SUBDOMAIN="${PORTAINER_SUBDOMAIN:-portainer-test.iklim.co}" + export RESTRICTED_IPS_BLOCK="$(echo "$RESTRICTED_IPS" | tr ',' '\n' | sed 's|.*| allow &;|')" + + SWAG_VARS='${PORTAINER_SUBDOMAIN}${RESTRICTED_IPS_BLOCK}' + for tpl in swag/site-confs/*.conf.tpl; do + fname=$(basename "${tpl%.tpl}") + envsubst "$SWAG_VARS" < "$tpl" | docker run --rm -i \ + -v "${SWAG_SITE_CONFS_DIR}:/output" \ + alpine sh -c "cat > /output/${fname}" + echo "${fname} written" + done + + SWAG_CTR=$(docker ps -q -f name=iklimco_swag 2>/dev/null | head -1) + if [ -n "$SWAG_CTR" ]; then + docker exec "$SWAG_CTR" nginx -t && docker exec "$SWAG_CTR" nginx -s reload + fi + + - name: Update DNS Records + run: | + set -a; . ./.env; . ./.env.secrets.swag; set +a + FLOATING_IP="${{ vars.TEST_FLOATING_IP }}" + DOMAIN="iklim.co" + + for record in portainer-test; do + CURRENT=$(curl -s \ + -H "Authorization: sso-key ${GODADDY_KEY}:${GODADDY_SECRET}" \ + "https://api.godaddy.com/v1/domains/${DOMAIN}/records/A/${record}" \ + 2>/dev/null | jq -r '.[0].data // empty' 2>/dev/null || true) + + if [ "$CURRENT" = "$FLOATING_IP" ]; then + echo "${record}.${DOMAIN} -> ${FLOATING_IP} exists, skipping" + else + curl -sf -X PUT \ + -H "Authorization: sso-key ${GODADDY_KEY}:${GODADDY_SECRET}" \ + -H "Content-Type: application/json" \ + "https://api.godaddy.com/v1/domains/${DOMAIN}/records/A/${record}" \ + -d "[{\"data\":\"${FLOATING_IP}\",\"ttl\":600}]" + echo "${record}.${DOMAIN} -> ${FLOATING_IP} added/updated" + fi + done + + - name: Verify Deployment + run: | + docker service ps iklimco-monitoring_loki \ + --filter "desired-state=running" \ + --format "table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.Image}}" | head -20 diff --git a/docker-stack-monitoring.yml b/docker-stack-monitoring.yml index 1385d62..b148182 100644 --- a/docker-stack-monitoring.yml +++ b/docker-stack-monitoring.yml @@ -106,6 +106,31 @@ services: labels: project: co.iklim + health-agent: + image: ${CUSTOM_IMAGE_REGISTRY}${IMAGE_HEALTH_AGENT} + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ${HEALTH_AGENT_CONFIG_GENERATED_DIR}:/app/config/generated + - /mnt/storagebox:/mnt/storagebox:ro + env_file: + - ${HEALTH_AGENT_ENV_FILE} + networks: + - iklimco-net + deploy: + mode: replicated + replicas: 1 + placement: + constraints: + - node.role == manager + restart_policy: + condition: any + delay: 10s + update_config: + parallelism: 1 + order: stop-first + labels: + project: co.iklim + networks: portainer-net: driver: overlay diff --git a/health-agent/.env.example b/health-agent/.env.example index 7855246..90abc6a 100644 --- a/health-agent/.env.example +++ b/health-agent/.env.example @@ -8,3 +8,12 @@ REDIS_MODE=sentinel EXTERNAL_DOMAIN=iklim.co EXTERNAL_SUBDOMAIN_SUFFIX= UK_PUSH_URL_BASE=https://status.iklim.co/api/push +SLACK_WEBHOOK_IKLIM_PROD_OPS= +RABBITMQ_USER= +RABBITMQ_PASS= +MONGO_URI= +REDIS_PASSWORD= +REDIS_MASTER_NAME= +REDIS_SENTINEL_HOSTS= +STORAGEBOX_PATH= +APISIX_ADMIN_KEY= diff --git a/health-agent/.gitignore b/health-agent/.gitignore new file mode 100644 index 0000000..c5f529c --- /dev/null +++ b/health-agent/.gitignore @@ -0,0 +1,3 @@ +config/generated/uk_tokens.yml +.env +.env.setup diff --git a/health-agent/README.md b/health-agent/README.md new file mode 100644 index 0000000..dd1c525 --- /dev/null +++ b/health-agent/README.md @@ -0,0 +1,196 @@ +# iklim.co Health Agent + +Docker Swarm cluster içinde çalışan, push modeli üzerinden Uptime Kuma'ya sağlık durumu ileten ve Docker olaylarını Slack'e doğrudan bildiren hafif bir Python servisidir. Gelen bağlantı gerekmez — tüm trafik dışa yönelik HTTPS'tir. + +--- + +## Mimari + +Agent, Swarm manager node üzerinde tek replica olarak çalışır ve şu kaynaklara erişir: + +- `/var/run/docker.sock` (salt okunur) — Swarm servislerini, node'ları ve event stream'ini dinler +- `iklimco-net` overlay ağı — tüm iç servislere DNS adıyla erişir +- StorageBox bind mount (salt okunur) — sertifika dosyaları ve yapılandırma varlığını kontrol eder +- `config/generated/uk_tokens.yml` — `setup_uptime_kuma.py` tarafından üretilir, agent bu dosyayı okur + +Her check bağımsız çalışır ve kendi Uptime Kuma monitörüne push yapar. Bir check'in başarısız olması diğerlerini etkilemez. + +Slack bildirimleri iki kanaldan gelir: +- **`[Uptime Kuma]`** — Uptime Kuma'nın kendi HTTP/DNS/Ping monitörleri tarafından üretilir +- **`[Health Agent]`** — health-agent'ın push check'lerinden; Uptime Kuma'nın group monitor mekanizması üzerinden iletilir +- **`[Health Agent / Events]`** — Docker events stream'den gelen anlık restart/OOM bildirimleri; doğrudan Slack webhook'una gönderilir, Uptime Kuma'dan geçmez + +--- + +## Dizin Yapısı + +``` +Environment_Monitoring/health-agent/ +├── config/ +│ ├── monitors.yml # tüm monitor/group/tag/status-page tanımları; node IP'leri buraya yazılır +│ └── generated/ +│ └── uk_tokens.yml # setup_uptime_kuma.py tarafından üretilir; health-agent okur +├── src/ +│ └── health_agent/ +│ ├── main.py # giriş noktası; scheduler loop +│ ├── config.py # .env + uk_tokens.yml yükler; ortam ayarlarını expose eder +│ ├── uptime_kuma.py # push(token, status, msg, ping_ms) yardımcısı +│ ├── slack.py # notify(webhook, source, priority, title, detail, uk_group_url) — kaynak etiketli + UK grup linki +│ ├── state.py # restart sayısı gibi session-arası state'i dosyaya yazar/okur +│ ├── checks/ +│ │ ├── swarm.py # Docker API: node listesi, servis replica sayıları +│ │ ├── http.py # genel HTTP check + uygulama bazlı parser'lar (Patroni, Vault, RabbitMQ...) +│ │ ├── tcp.py # TCP port erişilebilirliği +│ │ ├── tls.py # TLS sertifika son kullanma tarihi (dosyadan veya handshake'den) +│ │ ├── redis_sentinel.py # Redis Sentinel — redis-py ile quorum ve master kontrolü +│ │ ├── mongodb.py # MongoDB rs.status() — pymongo ile PRIMARY ve lag kontrolü +│ │ └── filesystem.py # StorageBox mount varlığı ve SSL cert sync durumu +│ └── events/ +│ └── docker_events.py # arka plan thread'i; Docker /events stream'ini dinler, restart/OOM bildirir +├── scripts/ +│ └── setup_uptime_kuma.py # monitors.yml'i okur, UK'da oluşturur, uk_tokens.yml'e yazar +├── Dockerfile +├── pyproject.toml +├── .env.example # health-agent runtime değişkenleri (credentials, ENV, CLUSTER_SIZE_*) +└── .env.setup.example # setup script değişkenleri (UK_API_KEY, Slack webhook'ları) +``` + +--- + +## Yapılandırma + +**`config/monitors.yml`** — Tüm monitor, group, tag ve status page tanımları bu dosyadadır. Yeni bir monitor eklemek için kod değişikliği gerekmez; `monitors.yml`'e yeni bir blok eklenir ve `setup_uptime_kuma.py` çalıştırılır. + +**`.env`** — Runtime değişkenler: + +| Variable | Description | +|----------|-------------| +| `UK_PUSH_URL_BASE` | Uptime Kuma push base URL (e.g. `https://status.iklim.co/api/push`) | +| `ENV` | `prod` or `test` | +| `CLUSTER_SIZE_ETCD` | etcd node count (prod: 3, test: 1) | +| `CLUSTER_SIZE_PATRONI` | Patroni node count | +| `CLUSTER_SIZE_MONGODB` | MongoDB node count | +| `CLUSTER_SIZE_RABBITMQ` | RabbitMQ node count | +| `CLUSTER_SIZE_VAULT` | Vault node count | +| `REDIS_MODE` | `sentinel` or `standalone` | +| `EXTERNAL_DOMAIN` | Base domain — `iklim.co` in both environments | +| `EXTERNAL_SUBDOMAIN_SUFFIX` | Subdomain suffix — empty for prod, `-test` for test → `api-test.iklim.co` | +| `SLACK_WEBHOOK_IKLIM_{ENV}_OPS` | Direct Slack webhook for container crash/OOM events — e.g. `SLACK_WEBHOOK_IKLIM_PROD_OPS` | +| `RABBITMQ_USER` / `RABBITMQ_PASS` | RabbitMQ management credentials | +| `MONGO_URI` | MongoDB connection URI | +| `REDIS_PASSWORD` | Redis / Sentinel password | +| `REDIS_MASTER_NAME` | Redis Sentinel master name | +| `REDIS_SENTINEL_HOSTS` | Sentinel host list (comma-separated `host:port`) | +| `STORAGEBOX_PATH` | StorageBox mount path for filesystem check | +| `APISIX_ADMIN_KEY` | APISIX admin API key for health check | + +Check periyotları `monitors.yml`'de her monitor için tanımlanır; `.env`'e eklenmez. + +Push token'ları `config/generated/uk_tokens.yml`'den otomatik okunur — bu dosya `setup_uptime_kuma.py` tarafından üretilir ve `.env`'e elle kopyalanmaz. + +--- + +## Yeni Check Ekleme + +1. `src/health_agent/checks/` altına yeni bir dosya ekle veya uygun mevcut dosyaya yeni bir fonksiyon ekle. +2. Fonksiyon `(ok: bool, msg: str, ping_ms: int)` tuple'ı döndürmeli. +3. `config/monitors.yml`'e yeni monitor bloğu ekle (isim, grup, öncelik, bildirim kanalı, check periyodu). +4. `setup_uptime_kuma.py`'yi çalıştır — yeni monitor UK'da oluşturulur ve token `uk_tokens.yml`'e yazılır. +5. `main.py`'de check fonksiyonunu token ve yapılandırmayla kaydet. + +--- + +## İlk Kurulum (Uptime Kuma) + +Health-agent deploy edilmeden önce kurulum script'i çalıştırılır. Script, `monitors.yml`'i okuyarak tüm monitor, tag, group ve status page'leri Uptime Kuma'da oluşturur; push token'larını `config/generated/uk_tokens.yml`'e yazar. + +Script [`uptime-kuma-api`](https://pypi.org/project/uptime-kuma-api/) kütüphanesini kullanır. API key authentication desteği implementasyon öncesi doğrulanmalıdır; desteklenmiyorsa `requests` + `Authorization: Bearer ` ile REST API doğrudan çağrılır. + +```bash +cd Environment_Monitoring/health-agent + +# setup değişkenlerini doldur +cp .env.setup.example .env.setup + +# önce dry-run ile ne yapılacağını gör +python scripts/setup_uptime_kuma.py --dry-run + +# tüm kaynakları oluştur +python scripts/setup_uptime_kuma.py + +# sadece belirli bileşenleri güncelle +python scripts/setup_uptime_kuma.py --only monitors +python scripts/setup_uptime_kuma.py --only notifications +python scripts/setup_uptime_kuma.py --only status-page +``` + +Script idempotent çalışır — CI/CD pipeline'ında her deploy'da güvenle tetiklenebilir. + +--- + +## Notification Flood Önleme + +Uptime Kuma group monitor mekanizması kullanılır: Slack bildirimi child monitor'lere değil, **yalnızca group monitor'e** bağlanır. Bir grup içinde birden fazla monitor aynı anda çökse dahi tek bildirim üretilir. + +Planlı bakım/deploy sırasında etkilenecek group için Uptime Kuma'da Maintenance penceresi açılır (API üzerinden otomatik yapılabilir). Bu süre zarfında alarm üretilmez. + +--- + +## Yerel Geliştirme + +```bash +cd Environment_Monitoring/health-agent + +# bağımlılıkları kur +pip install -e ".[dev]" + +# .env dosyasını hazırla +cp .env.example .env + +# tek döngü çalıştır (scheduler olmadan, credential kontrolü için) +python -m health_agent.main --once + +# tam scheduler'ı başlat +python -m health_agent.main +``` + +`--once` bayrağı her check'i bir kez çalıştırıp çıkar — credential ve bağlantı doğrulama için kullanılır. + +--- + +## Deployment + +Monitoring stack ve health-agent ayrı Gitea workflow'larıyla deploy edilir: + +- `.gitea/workflows/deploy-monitoring-prod.yml` — prod ortamı +- `.gitea/workflows/deploy-monitoring-test.yml` — test ortamı + +Her iki workflow da şu sırayı izler: (1) `setup_uptime_kuma.py` çalıştır → `uk_tokens.yml` üret, (2) monitoring stack'i deploy et, (3) health-agent stack'ini deploy et. + +Manuel deploy için: + +```bash +# önce setup_uptime_kuma.py çalıştır +python scripts/setup_uptime_kuma.py + +# tek stack: portainer + loki + promtail + health-agent +docker stack deploy \ + --with-registry-auth \ + -c docker-stack-monitoring.yml \ + iklimco-monitoring +``` + +Health-agent `iklimco-net` overlay ağına bağlı olmalı ve Docker socket'a salt okunur erişimi olmalıdır. + +--- + +## Log Formatı + +Agent JSON formatında log üretir. Grafana Explore (Loki datasource, `{service="iklimco-monitoring_health-agent"}`) veya `docker service logs iklimco-monitoring_health-agent` ile izlenebilir. Her log girdisi şu alanları içerir: + +- `check` — monitor adı +- `status` — `up` veya `down` +- `msg` — Uptime Kuma'ya iletilen mesaj +- `ping_ms` — check süresi +- `source` — `health-agent` veya `health-agent/events` +- `error` — yalnızca hata durumunda; exception detayı diff --git a/health-agent/config/generated/.gitkeep b/health-agent/config/generated/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/health-agent/config/monitors.yml b/health-agent/config/monitors.yml index dbc724e..26edf01 100644 --- a/health-agent/config/monitors.yml +++ b/health-agent/config/monitors.yml @@ -46,32 +46,32 @@ notifications: type: slack webhook_env: UK_SLACK_WEBHOOK_LOW groups: - - name: "Altyapı" + - name: "Infrastructure" status_page: "iklim-{env}-ops" notifications: [slack-high] tags: [internal, infrastructure] children: [SWARM-CLUSTER, VAULT-CLUSTER, STORAGEBOX-MOUNT, SWAG-TLS] - - name: "Veri Katmanı" + - name: "Data Layer" status_page: "iklim-{env}-ops" notifications: [slack-high] tags: [internal, database] children: [ETCD-CLUSTER, PATRONI-CLUSTER, MONGODB-REPLICASET] - - name: "Gateway & Mesajlaşma" + - name: "Gateway & Messaging" status_page: "iklim-{env}-ops" notifications: [slack-high] tags: [internal, gateway] children: [APISIX-GATEWAY, RABBITMQ-CLUSTER, REDIS-SENTINEL] - - name: "Dış Erişilebilirlik - Kritik" + - name: "External Availability - Critical" status_page: "iklim-{env}-ops" notifications: [slack-high] tags: [external, high] children: [EXT-HTTPS-API, EXT-DNS-API, EXT-DNS-ROOT, EXT-PING-APP01, EXT-PING-APP02, EXT-PING-APP03] - - name: "Dış Erişilebilirlik - Genel" + - name: "External Availability - General" status_page: "iklim-{env}-ops" notifications: [slack-medium] tags: [external, medium] children: [EXT-HTTPS-GRAFANA, EXT-PING-DB01, EXT-PING-DB02, EXT-PING-DB03] - - name: "Gözlemlenebilirlik" + - name: "Observability" status_page: "iklim-{env}-tools" notifications: [slack-low] tags: [internal, observability] @@ -178,19 +178,19 @@ ping_monitors: max_retries: 1 status_pages: - slug: "iklim-{env}-status" - title: "iklim.co API Durumu" + title: "iklim.co API Status" public: true - groups: ["Dış Erişilebilirlik - Kritik"] + groups: ["External Availability - Critical"] - slug: "iklim-{env}-ops" - title: "iklim.co [{env}] Altyapı" + title: "iklim.co [{env}] Infrastructure" public: false groups: - - "Altyapı" - - "Veri Katmanı" - - "Gateway & Mesajlaşma" - - "Dış Erişilebilirlik - Kritik" - - "Dış Erişilebilirlik - Genel" + - "Infrastructure" + - "Data Layer" + - "Gateway & Messaging" + - "External Availability - Critical" + - "External Availability - General" - slug: "iklim-{env}-tools" - title: "iklim.co [{env}] Araçlar" + title: "iklim.co [{env}] Tools" public: false - groups: ["Gözlemlenebilirlik"] + groups: ["Observability"] diff --git a/users.yml.example b/users.yml.example deleted file mode 100644 index f48cddf..0000000 --- a/users.yml.example +++ /dev/null @@ -1,11 +0,0 @@ -# Store the real file in Gitea secret DOZZLE_USERS_YML_B64 or DOZZLE_USERS_YML. -# Do not commit production password hashes. -# -# Example shape for Dozzle simple auth: -# -# users: -# admin: -# name: Admin -# email: admin@iklim.co -# password: "$2a$10$replace-with-bcrypt-hash" -# roles: none