# Сбор логов в Kubernetes: PLG-стек и переезд на Graylog **Кластер:** Rocky Linux 9 · Kubernetes 1.33 · Flannel CNI **Текущая топология:** k8s-manager-01 (10.203.0.92) · k8s-master-01 (10.203.0.97) · k8s-worker-01 (10.203.0.96) **Дата исследования:** 2026-05-28 **Проверено на кластере:** 2026-05-28 > **⚠️ ВАЖНО — EOL-статус компонентов (проверено 2026-05-28):** > > | Компонент | Статус | Детали | > |---|---|---| > | **Promtail** | **EOL с 2 марта 2026** | [Официальное объявление Grafana Labs](https://community.grafana.com/t/promtail-end-of-life-eol-march-2026-how-to-migrate-to-grafana-alloy-for-existing-loki-server-deployments/159636). Баги и CVE не исправляются. Замена: **Grafana Alloy** | > | **Loki** | Активно развивается | Последняя: v3.6.7. Helm chart 7.0.0 актуален. EOL не объявлен | > | **Grafana** | Активно развивается | EOL не объявлен | > > **Все разделы про Promtail (5, 9, 11) обновлены на Grafana Alloy.** --- ## Состояние кластера на дату исследования ``` MinIO (namespace: minio) pod/minio-696468d6b6-wnm7c 1/1 Running service/minio ClusterIP 10.101.211.198 9000/TCP service/minio-console ClusterIP 10.111.84.4 9001/TCP pvc/minio Bound 1Ti longhorn-minio (Retain) IngressRoutes: route-minio-api (10005), route-minio-console (10006) ✓ Buckets: loki-chunks ✓ backups ✓ artifacts ✓ (все пустые) Пользователь loki: не создан ← нужно создать перед деплоем Loki Namespace monitoring: не существует ← PLG не развёрнут StorageClasses: longhorn (default), longhorn-minio (Retain), longhorn-static ``` **Вывод:** MinIO полностью готов к роли S3 backend для Loki. Filesystem-режим не нужен — Loki деплоится сразу с S3/MinIO. --- ## Содержание 1. [Обзор проблемы](#1-обзор-проблемы) 2. [PLG-стек: архитектура](#2-plg-стек-архитектура) 3. [Подготовка MinIO для Loki](#3-подготовка-minio-для-loki) 4. [Loki: хранение в MinIO (S3)](#4-loki-хранение-в-minio-s3) 5. [Grafana Alloy: сбор логов с нод](#5-grafana-alloy-сбор-логов-с-нод) *(Promtail EOL 2026-03-02)* 6. [Grafana: визуализация и алерты](#6-grafana-визуализация-и-алерты) 6а. [Мониторинг ресурсов кластера: Prometheus stack](#6а-мониторинг-ресурсов-кластера-prometheus-stack) 7. [Grafana as Code: хранение настроек в GitLab](#7-grafana-as-code-хранение-настроек-в-gitlab) 8. [Интеграция с Traefik](#8-интеграция-с-traefik) 9. [Ansible-роли и плейбуки](#9-ansible-роли-и-плейбуки) 10. [Масштабирование до 10 нод / 100 сервисов](#10-масштабирование-до-10-нод--100-сервисов) 11. [Переезд на Graylog](#11-переезд-на-graylog) 12. [Итоговое сравнение и рекомендации](#12-итоговое-сравнение-и-рекомендации) --- ## 1. Обзор проблемы В кластере Kubernetes логи существуют на трёх уровнях: | Уровень | Источник | Путь на ноде | |---|---|---| | Приложения | stdout/stderr контейнеров | `/var/log/pods/__//N.log` | | Control plane | apiserver, etcd, scheduler, controller-manager | `journald` (Rocky Linux 9) | | Нода | kubelet, kube-proxy, containerd | `journald` | containerd по умолчанию ротирует файлы логов (10 MB × 5 файлов), что означает потерю истории при интенсивной нагрузке. Без централизованного сбора: - Логи недоступны после рестарта пода - Нет единого поиска по неймспейсам - Нет алертов на паттерны ошибок - Корреляция событий между сервисами невозможна --- ## 2. PLG-стек: архитектура ``` ┌──────────────────────────────────────────────────────────────────┐ │ k8s-worker-01 / k8s-master-01 │ │ │ │ [Pod stdout] → containerd → /var/log/pods/ │ │ ↓ │ │ [Grafana Alloy DaemonSet] (файлы + journald) │ └───────────────────────┬──────────────────────────────────────────┘ │ HTTP push (loki.write) ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Loki (namespace: monitoring) │ │ single-binary pod │ │ ├── Ingester — буферизует и пишет чанки │ │ ├── Querier — выполняет LogQL запросы │ │ └── Compactor — уплотняет, применяет retention │ └───────────────────────┬──────────────────────────────────────────┘ │ S3 API (chunks + index) ▼ ┌──────────────────────────────────────────────────────────────────┐ │ MinIO (namespace: minio) — УЖЕ ЗАПУЩЕН ✓ │ │ pod/minio-696468d6b6-wnm7c Running │ │ pvc/minio 1Ti longhorn-minio │ │ bucket: loki-chunks ✓ (пустой, готов) │ │ http://minio.minio.svc.cluster.local:9000 │ └──────────────────────────────────────────────────────────────────┘ │ datasource ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Grafana (namespace: monitoring) │ │ ├── Loki datasource — LogQL explore + dashboards │ │ └── Alerting — правила на паттерны ошибок │ │ Traefik → port 10003 → http://10.203.0.96:10003 │ └──────────────────────────────────────────────────────────────────┘ ``` ### Выбор компонентов | Компонент | Helm chart | Версия chart | Статус | |---|---|---|---| | Loki | `grafana/loki` | 7.0.0 | ✅ Актуален | | ~~Promtail~~ → **Grafana Alloy** | `grafana/alloy` | 1.8.2 | ⚠️ Promtail EOL 2026-03-02; Alloy — официальная замена | | Grafana | `grafana/grafana` | 10.5.15 | ✅ Актуален | --- ## 3. Подготовка MinIO для Loki MinIO уже запущен. Требуется только создать выделенного пользователя с доступом исключительно к бакету `loki-chunks`. ### Создание пользователя loki ```bash # Выполнить на k8s-manager-01 или через kubectl exec kubectl exec -n minio deployment/minio -- \ mc alias set local http://localhost:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD # Создать пользователя kubectl exec -n minio deployment/minio -- \ mc admin user add local loki ПАРОЛЬ_LOKI # Создать политику — только бакет loki-chunks kubectl exec -n minio deployment/minio -- \ mc admin policy create local loki-policy /dev/stdin <<'EOF' { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject","s3:PutObject","s3:DeleteObject","s3:ListBucket","s3:GetBucketLocation"], "Resource": [ "arn:aws:s3:::loki-chunks", "arn:aws:s3:::loki-chunks/*" ] } ] } EOF # Привязать политику kubectl exec -n minio deployment/minio -- \ mc admin policy attach local loki-policy --user loki ``` ### Secret для Loki в namespace monitoring ```bash kubectl create namespace monitoring kubectl create secret generic loki-minio-secret \ --from-literal=AWS_ACCESS_KEY_ID=loki \ --from-literal=AWS_SECRET_ACCESS_KEY='ПАРОЛЬ_LOKI' \ -n monitoring ``` ### Проверка доступа ```bash # Проверить что loki-user видит только свой бакет kubectl exec -n minio deployment/minio -- \ mc alias set loki-test http://localhost:9000 loki ПАРОЛЬ_LOKI kubectl exec -n minio deployment/minio -- \ mc ls loki-test # Ожидаем: [дата] 0B loki-chunks/ kubectl exec -n minio deployment/minio -- \ mc ls loki-test/loki-chunks # Ожидаем: пустой список (бакет пустой и готов) ``` --- ## 4. Loki: хранение в MinIO (S3) Loki деплоится сразу в режиме **single-binary с S3 backend** — MinIO уже в кластере, filesystem не нужен. ### Helm values: `loki-values.yml` ```yaml loki: auth_enabled: false commonConfig: replication_factor: 1 # S3 backend — MinIO в namespace minio storage: type: s3 s3: endpoint: http://minio.minio.svc.cluster.local:9000 region: us-east-1 # MinIO игнорирует region, поле обязательно bucketnames: loki-chunks access_key_id: loki secret_access_key: "${LOKI_MINIO_SECRET_KEY}" # из Secret через envFrom insecure: true # http, не https s3forcepathstyle: true # обязательно для MinIO schemaConfig: configs: - from: "2024-01-01" store: tsdb object_store: s3 schema: v13 index: prefix: loki_index_ period: 24h limits_config: retention_period: 744h # 31 день ingestion_rate_mb: 16 ingestion_burst_size_mb: 32 max_streams_per_user: 10000 max_chunks_per_query: 2000000 compactor: working_directory: /loki/compactor retention_enabled: true delete_request_store: s3 # ← s3, не filesystem singleBinary: replicas: 1 # Нет PVC — данные в MinIO, только небольшой volume для WAL persistence: enabled: true storageClass: longhorn size: 10Gi # только WAL/temp, не логи extraEnv: - name: LOKI_MINIO_SECRET_KEY valueFrom: secretKeyRef: name: loki-minio-secret key: AWS_SECRET_ACCESS_KEY # Отключаем встроенные компоненты (используем Promtail отдельно) gateway: enabled: false backend: replicas: 0 read: replicas: 0 write: replicas: 0 ``` ### Объём хранилища: расчёт для текущего кластера ``` 2 ноды, ~20 подов, средний lograte 1 KB/s: 20 pods × 1 KB/s × 86400 s/day ≈ 1.7 GB/day (raw) Loki сжимает ~10:1 → ~170 MB/day 31 день retention → ~5 GB в loki-chunks бакете MinIO MinIO PVC = 1 TiB → запас до ~100 нод (≈ 500 MB/day × 31 дней ≈ 15 GB) ``` ### Проверка после деплоя ```bash # Статус пода kubectl get pod -n monitoring -l app.kubernetes.io/name=loki # Готовность API kubectl exec -n monitoring deployment/loki -- wget -qO- http://localhost:3100/ready # Появились ли объекты в MinIO kubectl exec -n minio deployment/minio -- mc ls local/loki-chunks --recursive | head -20 ``` --- ## 5. Grafana Alloy: сбор логов с нод > **Promtail EOL с 2 марта 2026.** Официальная замена — **Grafana Alloy** (`grafana/alloy`, chart 1.8.2). > Alloy — дистрибутив OpenTelemetry Collector от Grafana Labs. Собирает логи, метрики и трейсы единым агентом. > Конфиг Promtail конвертируется автоматически: `alloy convert --source-format=promtail --output=config.alloy promtail.yml` ### Что собирает - `/var/log/pods/**/*.log` — логи всех контейнеров (через `loki.source.kubernetes`) - `journald` — kubelet, kube-proxy, containerd, sshd (через `loki.source.journal`) ### Как работает Alloy Alloy использует собственный язык конфигурации (River/Alloy syntax), а не YAML. Конфиг передаётся в Helm как строка в `alloy.configMap.content`. ### Helm values: `alloy-values.yml` ```yaml # Развёртывание как DaemonSet — один под на каждую ноду controller: type: daemonset tolerations: # Запускать и на control plane (k8s-master-01) - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule # Монтирование journald с хоста alloy: mounts: varlog: true # /var/log (поды) dockercontainers: false extraVolumes: - name: journal hostPath: path: /var/log/journal extraVolumeMounts: - name: journal mountPath: /var/log/journal readOnly: true configMap: create: true content: | // ── Обнаружение подов Kubernetes ─────────────────────────────── discovery.kubernetes "pods" { role = "pod" } // ── Relabeling: namespace, pod, container, app из метаданных ── discovery.relabel "pod_logs" { targets = discovery.kubernetes.pods.targets rule { source_labels = ["__meta_kubernetes_namespace"] target_label = "namespace" } rule { source_labels = ["__meta_kubernetes_pod_name"] target_label = "pod" } rule { source_labels = ["__meta_kubernetes_pod_container_name"] target_label = "container" } rule { source_labels = ["__meta_kubernetes_pod_label_app"] target_label = "app" } // Отброс debug-логов на уровне агента rule { source_labels = ["__meta_kubernetes_pod_annotation_filter_debug"] regex = "true" action = "drop" } } // ── Чтение файлов логов подов ────────────────────────────────── loki.source.kubernetes "pods" { targets = discovery.relabel.pod_logs.output forward_to = [loki.process.parse.receiver] } // ── Парсинг JSON-логов, отброс debug ────────────────────────── loki.process "parse" { // Попытка распарсить как JSON и извлечь level stage.json { expressions = {level = "level", msg = "message"} } stage.labels { values = {level = ""} } // Отброс debug-записей stage.drop { expression = ".*level=\"debug\".*" drop_counter_reason = "debug_dropped" } forward_to = [loki.write.local.receiver] } // ── Сбор journald (kubelet, containerd, sshd) ───────────────── loki.source.journal "systemd" { path = "/var/log/journal" max_age = "12h" labels = {job = "systemd-journal"} forward_to = [loki.write.local.receiver] relabel_rules = discovery.relabel.journal.rules } discovery.relabel "journal" { targets = [] rule { source_labels = ["__journal__systemd_unit"] target_label = "unit" } rule { source_labels = ["__journal__hostname"] target_label = "node" } } // ── Отправка в Loki ──────────────────────────────────────────── loki.write "local" { endpoint { url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push" } } ``` --- ## 6. Grafana: визуализация и алерты ### Helm values: `grafana-values.yml` ```yaml grafana.ini: server: root_url: http://10.203.0.96:10003 security: admin_user: admin auth.anonymous: enabled: false unified_alerting: enabled: true alerting: enabled: false # legacy alerting отключаем admin: existingSecret: grafana-admin-secret userKey: admin-user passwordKey: admin-password persistence: enabled: true storageClassName: longhorn size: 5Gi # Datasources — provisioning при старте пода (Loki + Prometheus) datasources: datasources.yaml: apiVersion: 1 datasources: - name: Loki type: loki url: http://loki.monitoring.svc.cluster.local:3100 access: proxy isDefault: true jsonData: maxLines: 5000 derivedFields: - name: TraceID matcherRegex: '"trace_id":"(\w+)"' url: '$${__value.raw}' urlDisplayLabel: Open Trace # Prometheus datasource — появляется после деплоя kube-prometheus-stack (раздел 6а) - name: Prometheus type: prometheus url: http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090 access: proxy isDefault: false jsonData: timeInterval: 30s # Автоимпорт дашбордов из ConfigMap с label grafana_dashboard=1 # Подхватывает: ручные дашборды + ConfigMaps от kube-prometheus-stack (раздел 6а) sidecar: dashboards: enabled: true label: grafana_dashboard labelValue: "1" folder: /var/lib/grafana/dashboards/default searchNamespace: ALL # искать ConfigMaps во всех namespace datasources: enabled: true label: grafana_datasource resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi ``` ### Создание Secret с паролем администратора ```bash kubectl create secret generic grafana-admin-secret \ --from-literal=admin-user=admin \ --from-literal=admin-password='ВАШ_ПАРОЛЬ' \ -n monitoring ``` ### Ключевые дашборды для Kubernetes | Дашборд | Grafana ID | Назначение | |---|---|---| | Kubernetes Cluster Logs | 15141 | Логи всех нод и неймспейсов | | Loki Dashboard | 13639 | Состояние самого Loki | | Pod Logs | 18748 | Логи конкретного пода с фильтрами | ### Алерт на высокий уровень ошибок (provisioning) ```yaml # ConfigMap: grafana-alert-rules, label: grafana_dashboard=1 apiVersion: 1 groups: - orgId: 1 name: kubernetes-errors folder: Kubernetes interval: 1m rules: - uid: high-error-rate title: High Error Rate condition: C data: - refId: A model: expr: 'sum(rate({namespace=~".+"} |= "error" [5m])) by (namespace)' - refId: C type: classic_conditions model: conditions: - evaluator: type: gt params: [10] # > 10 ошибок/сек query: params: [A] for: 2m annotations: summary: "Высокий уровень ошибок в {{ $labels.namespace }}" ``` --- ## 6а. Мониторинг ресурсов кластера: Prometheus stack ### Что покрывает PLG — и чего не хватает Текущий PLG-стек собирает **только логи**. Метрики (CPU, RAM, диски, сеть, состояние объектов k8s) не собираются вообще. Без метрик невозможно: - Видеть загрузку нод в реальном времени - Строить алерты на исчерпание RAM/диска **до** инцидента - Отслеживать статус Deployments, PVC, ReplicaSet - Мониторить latency и error rate API-сервера ### Решение: kube-prometheus-stack `prometheus-community/kube-prometheus-stack` — стандартный Helm chart, который включает: | Компонент | Назначение | |---|---| | **Prometheus** | Сбор и хранение метрик (time-series DB, pull model) | | **node-exporter** | Метрики нод: CPU, RAM, диски, сеть (DaemonSet) | | **kube-state-metrics** | Метрики объектов k8s: pod count, deployment status, PVC | | **ServiceMonitors** | Auto-discovery k8s компонентов: kubelet, apiserver, etcd | | **Dashboard ConfigMaps** | Готовые дашборды Kubernetes для Grafana sidecar | Встроенную Grafana в chart отключаем (`grafana.enabled: false`) — используем уже развёрнутую. ConfigMaps с дашбордами создаём через `forceDeployDashboards: true` — Grafana sidecar подхватывает их автоматически. ### Обновлённая архитектура ``` ┌──────────────────────────────────────────────────────────────────┐ │ k8s-worker-01 / k8s-master-01 │ │ │ │ [Pod stdout] → /var/log/pods/ │ │ [node-exporter DaemonSet] ← CPU/RAM/диск/сеть (метрики нод) │ │ [Grafana Alloy DaemonSet] ← файлы + journald (логи) │ └──────────┬────────────────────────┬─────────────────────────────┘ │ HTTP push (logs) │ HTTP scrape (metrics) ▼ ▼ ┌──────────────────┐ ┌────────────────────────────┐ │ Loki │ │ Prometheus │ │ (monitoring) │ │ (monitoring) │ │ S3 → MinIO │ │ PVC 20Gi (longhorn) │ └────────┬─────────┘ └────────────┬───────────────┘ │ datasource │ datasource └────────────┬─────────────┘ ▼ ┌────────────────────────────┐ │ Grafana (monitoring) │ │ ├── Loki datasource │ ← логи │ ├── Prometheus datasource │ ← метрики │ ├── Logs dashboards │ ← из ConfigMaps │ └── K8s cluster dashboards│ ← из kube-prometheus-stack │ Traefik → :10003 │ └────────────────────────────┘ ``` ### Helm values: `prometheus-values.yml` ```yaml # Встроенную Grafana отключаем — используем свою grafana: enabled: false # Создать ConfigMaps с готовыми k8s дашбордами для нашего Grafana sidecar forceDeployDashboards: true sidecar: dashboards: label: grafana_dashboard labelValue: "1" prometheus: prometheusSpec: # Хранение метрик на Longhorn PVC storageSpec: volumeClaimTemplate: spec: storageClassName: longhorn accessModes: ["ReadWriteOnce"] resources: requests: storage: 20Gi retention: 30d scrapeInterval: 30s evaluationInterval: 30s # Разрешить сбор метрик из всех namespace ruleSelectorNilUsesHelmValues: false serviceMonitorSelectorNilUsesHelmValues: false podMonitorSelectorNilUsesHelmValues: false alertmanager: enabled: false # алерты через Grafana Alerting nodeExporter: enabled: true kubeStateMetrics: enabled: true # Rocky Linux 9: явно указать IP control plane для etcd/scheduler/controller-manager kubeControllerManager: enabled: true endpoints: - 10.203.0.97 service: enabled: true port: 10257 targetPort: 10257 kubeScheduler: enabled: true endpoints: - 10.203.0.97 service: enabled: true port: 10259 targetPort: 10259 kubeEtcd: enabled: true endpoints: - 10.203.0.97 service: enabled: true port: 2381 targetPort: 2381 kubeProxy: enabled: true endpoints: - 10.203.0.97 - 10.203.0.96 ``` > **Rocky Linux 9 / kubeadm gotcha:** kubeadm по умолчанию настраивает `controller-manager` и `scheduler` слушать только `127.0.0.1`. Если метрики этих компонентов нужны — потребуется патч `/etc/kubernetes/manifests/kube-controller-manager.yaml` и `kube-scheduler.yaml`: заменить `--bind-address=127.0.0.1` на `--bind-address=0.0.0.0`. Остальные компоненты (kubelet, node-exporter, kube-state-metrics, etcd на порту 2381) работают без изменений. ### Готовые дашборды после деплоя `forceDeployDashboards: true` создаёт ConfigMaps в namespace `monitoring`. Grafana sidecar подхватывает их автоматически при старте: | Дашборд | Что показывает | |---|---| | Kubernetes / Compute Resources / Cluster | CPU/RAM/сеть по неймспейсам | | Kubernetes / Compute Resources / Node (Pods) | Ресурсы по подам на конкретной ноде | | Kubernetes / Compute Resources / Workload | Потребление по Deployment/StatefulSet | | Node Exporter / Nodes | CPU, RAM, диски, сеть по нодам | | Kubernetes / Persistent Volumes | Статус и заполнение PVC | | Kubernetes / API server | Latency, error rate, RPS API-сервера | | Kubernetes / Kubelet | Состояние kubelet на каждой ноде | | Kubernetes / etcd | Состояние etcd, latency, лидер | ### Datasource Prometheus в Grafana Добавляется в `grafana-values.yml.j2` рядом с Loki: ```yaml datasources: datasources.yaml: apiVersion: 1 datasources: - name: Loki type: loki url: http://loki.monitoring.svc.cluster.local:3100 access: proxy isDefault: true jsonData: maxLines: 5000 - name: Prometheus type: prometheus url: http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090 access: proxy isDefault: false jsonData: timeInterval: 30s ``` ### Проверка после деплоя ```bash # Статус подов Prometheus stack kubectl get pod -n monitoring -l release=kube-prometheus-stack # Метрики node-exporter доступны kubectl exec -n monitoring -l app.kubernetes.io/name=prometheus -- \ wget -qO- 'http://localhost:9090/api/v1/query?query=up' | python3 -m json.tool # ConfigMaps с дашбордами созданы (должно быть ~20+ штук) kubectl get configmap -n monitoring -l grafana_dashboard=1 # Prometheus видит все таргеты (все должны быть UP) kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090 # → http://localhost:9090/targets ``` --- ## 7. Grafana as Code: хранение настроек в GitLab ### Проблема Дашборды и datasource, созданные через UI Grafana, хранятся в SQLite внутри пода. При пересоздании пода — настройки теряются. ### Решение: отдельный GitLab-проект `k8s/grafana-config` ``` gitlab.gigacoms.info/k8s/grafana-config ├── dashboards/ │ ├── kubernetes-logs.json # экспорт из UI: Share → Export JSON │ ├── loki-overview.json │ └── pod-errors.json ├── provisioning/ │ ├── datasources/ │ │ └── loki.yaml │ ├── dashboards/ │ │ └── provider.yaml │ └── alerting/ │ ├── rules.yaml │ └── contact-points.yaml └── README.md ``` > Секреты (пароли, токены) — **не хранить в Git**. Создаются через `kubectl create secret` или GitLab CI Variables. ### Flux: автосинхронизация ConfigMap из grafana-config репо Flux уже запущен в кластере (`flux bootstrap gitlab`). Добавить синхронизацию grafana-config: ```yaml # clusters/production/monitoring/grafana-config-source.yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: grafana-config namespace: flux-system spec: interval: 1m url: https://gitlab.gigacoms.info/k8s/grafana-config.git secretRef: name: flux-system # тот же PAT что используется для fleet-repo ref: branch: main --- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: grafana-config namespace: flux-system spec: interval: 5m path: ./ prune: true sourceRef: kind: GitRepository name: grafana-config targetNamespace: monitoring ``` Grafana sidecar подхватывает любой ConfigMap с label `grafana_dashboard: "1"` — Flux обновляет ConfigMap, Grafana перечитывает дашборды без рестарта. ### Рабочий процесс изменения дашборда ``` Grafana UI → изменил дашборд ↓ Share → Export → Save to file → dashboard.json ↓ git commit в grafana-config репо ↓ Flux (~1 мин) → ConfigMap обновляется ↓ Grafana sidecar (~30 сек) → дашборд применён ``` --- ## 8. Интеграция с Traefik Добавить в `inventory/prod/group_vars/traefik.yml`: ```yaml traefik_port_map: # ... существующие записи (10001 dashboard, 10002 longhorn, 10005 minio-api, 10006 minio-console) ... - name: grafana description: "Grafana (Logs & Metrics)" port: 10003 backend: namespace: monitoring service: grafana port: 80 # service port (targetPort: 3000 — внутренний порт контейнера) scheme: http basicauth: enabled: false # Grafana имеет собственную аутентификацию ``` После добавления запустить `setup_traefik.yml`. **Grafana:** http://10.203.0.96:10003 > Порт 10003 выбран как следующий свободный после 10001 (dashboard), 10002 (longhorn), 10005 (minio-api), 10006 (minio-console). --- ## 9. Ansible-роли и плейбуки ### Структура роли `roles/logging/` ``` roles/logging/ ├── defaults/main.yml ├── tasks/ │ ├── main.yml │ ├── namespace.yml — создать namespace monitoring │ ├── minio-user.yml — создать пользователя loki в MinIO + Secret │ ├── loki.yml — helm install loki (S3 backend) │ ├── alloy.yml — helm install grafana/alloy (DaemonSet) │ ├── prometheus.yml — helm install kube-prometheus-stack (метрики + дашборды) │ └── grafana.yml — helm install grafana (Loki + Prometheus datasources) └── templates/ ├── loki-values.yml.j2 ├── alloy-values.yml.j2 ├── prometheus-values.yml.j2 └── grafana-values.yml.j2 ``` ### `defaults/main.yml` ```yaml logging_namespace: monitoring loki_chart_version: "7.0.0" alloy_chart_version: "1.8.2" grafana_chart_version: "10.5.15" prometheus_stack_chart_version: "68.4.4" # prometheus-community/kube-prometheus-stack # MinIO — уже в кластере, только реквизиты loki_minio_endpoint: "http://minio.minio.svc.cluster.local:9000" loki_minio_bucket: "loki-chunks" loki_minio_user: "loki" loki_minio_secret_name: "loki-minio-secret" loki_retention_days: 31 loki_wal_storage_size: "10Gi" loki_wal_storage_class: longhorn prometheus_storage_size: "20Gi" prometheus_storage_class: longhorn prometheus_retention: "30d" grafana_storage_size: "5Gi" grafana_storage_class: longhorn grafana_admin_secret: "grafana-admin-secret" grafana_root_url: "http://10.203.0.96:10003" ``` ### `tasks/minio-user.yml` ```yaml - name: Loki | Create MinIO user for loki kubernetes.core.k8s_exec: namespace: minio pod: "{{ minio_pod.resources[0].metadata.name }}" command: > mc admin user add local {{ loki_minio_user }} {{ loki_minio_password }} no_log: true - name: Loki | Attach loki-policy in MinIO kubernetes.core.k8s_exec: namespace: minio pod: "{{ minio_pod.resources[0].metadata.name }}" command: mc admin policy attach local loki-policy --user {{ loki_minio_user }} - name: Loki | Create Secret with MinIO credentials kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ loki_minio_secret_name }}" namespace: "{{ logging_namespace }}" stringData: AWS_ACCESS_KEY_ID: "{{ loki_minio_user }}" AWS_SECRET_ACCESS_KEY: "{{ loki_minio_password }}" no_log: true ``` ### `tasks/main.yml` ```yaml --- - ansible.builtin.import_tasks: namespace.yml - ansible.builtin.import_tasks: minio-user.yml - ansible.builtin.import_tasks: loki.yml - ansible.builtin.import_tasks: alloy.yml - ansible.builtin.import_tasks: prometheus.yml # метрики + готовые дашборды кластера - ansible.builtin.import_tasks: grafana.yml ``` ### `tasks/prometheus.yml` ```yaml --- - name: Prometheus | Add prometheus-community Helm repo kubernetes.core.helm_repository: name: prometheus-community repo_url: https://prometheus-community.github.io/helm-charts - name: Prometheus | Render values ansible.builtin.template: src: prometheus-values.yml.j2 dest: /tmp/prometheus-values.yml mode: "0600" - name: Prometheus | Install kube-prometheus-stack kubernetes.core.helm: name: kube-prometheus-stack chart_ref: prometheus-community/kube-prometheus-stack chart_version: "{{ prometheus_stack_chart_version }}" release_namespace: "{{ logging_namespace }}" create_namespace: false values_files: - /tmp/prometheus-values.yml wait: true wait_timeout: 600s ``` ### `templates/prometheus-values.yml.j2` ```yaml grafana: enabled: false forceDeployDashboards: true sidecar: dashboards: label: grafana_dashboard labelValue: "1" prometheus: prometheusSpec: storageSpec: volumeClaimTemplate: spec: storageClassName: {{ prometheus_storage_class }} accessModes: ["ReadWriteOnce"] resources: requests: storage: {{ prometheus_storage_size }} retention: {{ prometheus_retention }} scrapeInterval: 30s evaluationInterval: 30s ruleSelectorNilUsesHelmValues: false serviceMonitorSelectorNilUsesHelmValues: false podMonitorSelectorNilUsesHelmValues: false alertmanager: enabled: false nodeExporter: enabled: true kubeStateMetrics: enabled: true kubeControllerManager: enabled: true endpoints: {% for host in groups['control_plane'] %} - {{ hostvars[host]['ansible_host'] }} {% endfor %} service: enabled: true port: 10257 targetPort: 10257 kubeScheduler: enabled: true endpoints: {% for host in groups['control_plane'] %} - {{ hostvars[host]['ansible_host'] }} {% endfor %} service: enabled: true port: 10259 targetPort: 10259 kubeEtcd: enabled: true endpoints: {% for host in groups['control_plane'] %} - {{ hostvars[host]['ansible_host'] }} {% endfor %} service: enabled: true port: 2381 targetPort: 2381 kubeProxy: enabled: true endpoints: {% for host in groups['k8s_cluster'] %} - {{ hostvars[host]['ansible_host'] }} {% endfor %} ``` ### `playbooks/setup_logging.yml` ```yaml --- - name: Deploy PLG + Prometheus monitoring stack hosts: manager_nodes become: false roles: - logging ``` ### `.gitlab-ci.yml` — новый job ```yaml setup/logging: stage: setup script: - ansible-playbook -i inventory/prod playbooks/setup_logging.yml rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: manual resource_group: production ``` ### GitLab CI/CD Variables (добавить) | Переменная | Описание | |---|---| | `LOKI_MINIO_PASSWORD` | Пароль пользователя `loki` в MinIO | | `GRAFANA_ADMIN_PASSWORD` | Пароль администратора Grafana | --- ## 10. Масштабирование до 10 нод / 100 сервисов ### Promtail Не требует изменений — DaemonSet автоматически запустится на новых нодах. Убедиться, что `tolerations` включают control-plane taint (уже в конфиге выше). ### Loki: переход на distributed-mode Триггер для перехода: объём > 50 GB/day или > 50 одновременных запросов Grafana. ```yaml # loki-values.yml — distributed режим loki: deploymentMode: Distributed ingester: replicas: 3 querier: replicas: 2 distributor: replicas: 2 compactor: replicas: 1 queryFrontend: replicas: 2 # S3 backend остаётся тем же — только меняется deploymentMode loki: storage: type: s3 s3: endpoint: http://minio.minio.svc.cluster.local:9000 # ... те же параметры MinIO ``` **Преимущество текущей архитектуры:** переход single-binary → distributed не требует миграции данных — бакет `loki-chunks` в MinIO остаётся тем же, меняется только Helm release Loki. ### Расчёт ресурсов при 10 нодах / 100 сервисах ``` 100 сервисов × средний lograte 2 KB/s = 17 GB/day (raw) Сжатие Loki ~10:1 → 1.7 GB/day 31 день → ~53 GB в loki-chunks бакете MinIO MinIO PVC = 1 TiB → запас ~19x от текущей нагрузки ``` --- ## 11. Переезд на Graylog ### Когда переезд оправдан | Триггер | Описание | |---|---| | Нужен поиск по содержимому | Loki: grep по чанкам; Graylog: полнотекстовый индекс OpenSearch | | Нужны встроенные Pipelines | Нормализация, маскирование PII без Logstash | | Встроенные алерты без Grafana | Graylog имеет собственный alerting | | Объём > 100 GB/day | Loki становится менее эффективным для full-text search | ### Архитектура Graylog ``` [Grafana Alloy / Fluent Bit] ← агент остаётся (Promtail EOL) ↓ GELF HTTP (port 12202) [Graylog Server] ↓ ↓ [OpenSearch] [MongoDB] (хранит логи) (конфиг, метаданные) ↓ Traefik → port 10004 → http://10.203.0.96:10004 ``` ### MinIO при переезде на Graylog MinIO продолжает использоваться — OpenSearch поддерживает S3 snapshot repository. Бэкапы индексов OpenSearch можно хранить в бакете `backups` MinIO: ```bash # Настройка S3 snapshot repository в OpenSearch curl -X PUT http://opensearch:9200/_snapshot/minio-backup \ -H 'Content-Type: application/json' -d '{ "type": "s3", "settings": { "bucket": "backups", "endpoint": "minio.minio.svc.cluster.local:9000", "protocol": "http", "path_style_access": "true" } }' ``` ### Стратегия миграции: Dual-write Alloy поддерживает несколько `loki.write` и `otelcol.exporter` — dual-write настраивается в его конфиге: ```alloy // alloy config — dual-write период (2 недели) loki.write "loki_backend" { endpoint { url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push" } } // Экспорт в Graylog через OTLP или loki.write с GELF-форматом otelcol.exporter.otlphttp "graylog" { client { endpoint = "http://graylog.monitoring.svc.cluster.local:4318" } } ``` ### Порядок действий при переезде ``` 1. Развернуть OpenSearch + MongoDB + Graylog (параллельно с PLG) 2. Настроить Graylog inputs (OTLP HTTP port 4318 или GELF HTTP port 12202) 3. Переключить Grafana Alloy на dual-write (loki.write + otelcol.exporter.otlphttp) 4. Настроить Streams, Pipelines, алерты в Graylog 5. Добавить порт 10004 в traefik_port_map → http://10.203.0.96:10004 6. Переключить команду на Graylog UI 7. Через 2 недели: убрать loki.write из конфига Alloy 8. Удалить Loki Helm release (бакет loki-chunks оставить для истории) 9. Grafana остаётся для метрик Prometheus; Alloy продолжает работать как агент Graylog ``` --- ## 12. Итоговое сравнение и рекомендации | Критерий | ALG (Alloy + Loki + Grafana) | Graylog | |---|---|---| | RAM стека | ~1 GB | ~6-8 GB (JVM: Graylog + OpenSearch + MongoDB) | | Агент сбора | **Grafana Alloy** (активен, замена EOL Promtail) | Grafana Alloy / Fluent Bit | | Поиск по тексту | LogQL grep по чанкам (медленнее) | Полнотекстовый индекс OpenSearch (быстрее) | | Обработка логов | Alloy pipeline stages (River syntax) | Встроенные Pipelines (мощнее) | | Алерты | Grafana Alerting | Встроены в Graylog | | Единый UI с метриками | Да (Prometheus + Loki + трейсы в Grafana) | Нет (отдельный UI) | | Переход S3 → distributed | Без миграции данных | — | | Сложность эксплуатации | Низкая | Высокая | | EOL-риски | Loki ✅, Alloy ✅ (Promtail заменён) | Нет | ### Рекомендуемый путь ``` Сейчас (1-2 ноды) └── Loki (логи) + Prometheus/kube-prometheus-stack (метрики) + Grafana (единый UI) └── Alloy DaemonSet собирает логи; node-exporter + kube-state-metrics собирают метрики └── Grafana: Loki + Prometheus datasources provisioned, ~20 готовых k8s дашбордов └── Promtail НЕ использовать — EOL с 2026-03-02 При росте до 5+ нод └── Loki: distributed (только смена deploymentMode, данные в MinIO без миграции) └── Prometheus: увеличить PVC или перейти на Thanos для long-term storage └── Alloy / node-exporter: DaemonSet автоматически разворачивается на новых нодах При требовании глубокого поиска или 10+ нод └── Параллельный запуск Graylog (dual-write 2 недели через Alloy) └── Grafana остаётся для метрик Prometheus └── MinIO — snapshot repository для OpenSearch └── Alloy продолжает работать как единый агент (Loki → Graylog смена в конфиге) ``` ### Следующие шаги **Подготовка (ручные действия перед запуском playbook):** - [ ] Создать Secret `grafana-admin-secret` в namespace `monitoring` - [ ] Создать GitLab-проект `k8s/grafana-config` на `gitlab.gigacoms.info` (для хранения дашбордов — раздел 7) **Ansible-роль `roles/logging/` (по структуре из раздела 9):** - [ ] Написать `tasks/namespace.yml` - [ ] Написать `tasks/minio-user.yml` + создать пользователя `loki` в MinIO - [ ] Написать `tasks/loki.yml` + шаблон `loki-values.yml.j2` - [ ] Написать `tasks/alloy.yml` + шаблон `alloy-values.yml.j2` (**Alloy, не Promtail**) - [ ] Написать `tasks/prometheus.yml` + шаблон `prometheus-values.yml.j2` (метрики + дашборды кластера) - [ ] Написать `tasks/grafana.yml` + шаблон `grafana-values.yml.j2` (Loki + Prometheus datasources) **CI/CD:** - [ ] Добавить `grafana` в `traefik_port_map` (порт 10003) и запустить `setup_traefik.yml` - [ ] Добавить переменные `LOKI_MINIO_PASSWORD`, `GRAFANA_ADMIN_PASSWORD` в GitLab CI/CD Variables - [ ] Добавить job `setup/logging` в `.gitlab-ci.yml` с `resource_group: production` **Проверка после деплоя:** - [ ] Grafana открывается на http://10.203.0.96:10003 - [ ] В Grafana → Connections → Data sources: видны Loki и Prometheus со статусом OK - [ ] В Grafana → Dashboards: папка `kubernetes-mixin` с ~20 готовыми дашбордами кластера - [ ] Prometheus Targets: все таргеты в состоянии UP (`kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090`)