k8s/research/logging/plg.md
2026-07-15 11:14:21 +03:00

47 KiB
Raw Permalink Blame History

Сбор логов в 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. Баги и 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. Обзор проблемы
  2. PLG-стек: архитектура
  3. Подготовка MinIO для Loki
  4. Loki: хранение в MinIO (S3)
  5. Grafana Alloy: сбор логов с нод (Promtail EOL 2026-03-02)
  6. Grafana: визуализация и алерты 6а. Мониторинг ресурсов кластера: Prometheus stack
  7. Grafana as Code: хранение настроек в GitLab
  8. Интеграция с Traefik
  9. Ansible-роли и плейбуки
  10. Масштабирование до 10 нод / 100 сервисов
  11. Переезд на Graylog
  12. Итоговое сравнение и рекомендации

1. Обзор проблемы

В кластере Kubernetes логи существуют на трёх уровнях:

Уровень Источник Путь на ноде
Приложения stdout/stderr контейнеров /var/log/pods/<ns>_<pod>_<uid>/<container>/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 Актуален
PromtailGrafana 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

# Выполнить на 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

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

Проверка доступа

# Проверить что 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

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)

Проверка после деплоя

# Статус пода
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

# Развёртывание как 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

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 с паролем администратора

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)

# 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

# Встроенную 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:

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

Проверка после деплоя

# Статус подов 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:

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

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

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

- 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

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

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

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

---
- name: Deploy PLG + Prometheus monitoring stack
  hosts: manager_nodes
  become: false
  roles:
    - logging

.gitlab-ci.yml — новый job

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.

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

# Настройка 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 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)