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

19 KiB
Raw Blame History

Стандарт централизованного сбора логов: PLG + Prometheus

Стек: Loki · Grafana Alloy · Grafana · kube-prometheus-stack
Namespace: monitoring
Доступ: http://10.203.0.96:10003 (Traefik port 10003)
Исследование: research/logging/plg.md


Архитектура

┌──────────────────────────────────────────────────────────────────┐
│  k8s-worker-01 / k8s-master-01                                   │
│  [Pod stdout]  → /var/log/pods/      ← Alloy: loki.source.k8s   │
│  [journald]    → kubelet, containerd ← Alloy: loki.source.journal│
│  [node-exporter DaemonSet]           ← CPU / RAM / диск / сеть  │
│                                                                  │
│         [Grafana Alloy DaemonSet]  (namespace: monitoring)       │
└────────────┬──────────────────────────────────────────────────────┘
             │ HTTP push (loki.write)
             ▼
┌──────────────────────────────────────────────────────────────────┐
│  Loki single-binary (namespace: monitoring)                      │
│  S3 backend: MinIO bucket loki-chunks                            │
│  10 GiB PVC longhorn — только WAL/temp (не логи)                 │
│  service/loki:3100                                               │
└─────────────────────┬────────────────────────────────────────────┘
                      │ S3 API
                      ▼
┌──────────────────────────────────────────────────────────────────┐
│  MinIO (namespace: minio) — bucket: loki-chunks                  │
│  StorageClass: longhorn-minio (Retain, 1 TiB)                   │
│  Пользователь: loki (только bucket loki-chunks)                  │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│  kube-prometheus-stack (namespace: monitoring)                   │
│  Prometheus PVC 20 GiB · node-exporter · kube-state-metrics     │
│  ~27 готовых дашбордов кластера → ConfigMaps → Grafana sidecar  │
└─────────────────────┬────────────────────────────────────────────┘
                      │ datasource
                      ▼
┌──────────────────────────────────────────────────────────────────┐
│  Grafana (namespace: monitoring)                                  │
│  Datasource: Loki (isDefault) + Prometheus (provisioned)         │
│  Sidecar: подхватывает ConfigMaps grafana_dashboard=1 (ALL ns)   │
│  PVC: 5 GiB longhorn                                             │
│  Traefik → port 10003 → http://10.203.0.96:10003                 │
└──────────────────────────────────────────────────────────────────┘

Компоненты

Компонент Chart Версия Примечание
Loki grafana/loki 7.0.0 single-binary, S3 backend, retention 31d
Grafana Alloy grafana/alloy 1.8.2 DaemonSet; Promtail EOL с 2026-03-02
kube-prometheus-stack prometheus-community/kube-prometheus-stack 68.4.4 Prometheus + node-exporter + kube-state-metrics
Grafana grafana/grafana 10.5.15 provisioned Loki + Prometheus datasources, sidecar

Promtail не используется — объявлен EOL 2 марта 2026. Grafana Alloy — официальная замена.


Предварительные условия

1. Создать namespace и Secret с паролем Grafana

kubectl create namespace monitoring

kubectl create secret generic grafana-admin-secret \
  --from-literal=admin-user=admin \
  --from-literal=admin-password='СИЛЬНЫЙ_ПАРОЛЬ' \
  -n monitoring

Секрет создаётся вручную — пароль не хранится в Ansible-переменных и не попадает в Git.
Если секрет не создать до запуска playbook, задача Grafana | Assert grafana-admin-secret exists упадёт с подробным сообщением об ошибке.

2. Задать переменную окружения

Переменная Описание
LOKI_MINIO_PASSWORD Пароль пользователя loki в MinIO — Ansible создаст пользователя с этим паролем
GRAFANA_ADMIN_PASSWORD Не читается Ansible — только для справки при ручном создании Secret выше

Пароль LOKI_MINIO_PASSWORD можно посмотреть в любой момент:

kubectl get secret loki-minio-secret -n monitoring \
  -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d

Запуск

# Локально (с заданной переменной окружения)
LOKI_MINIO_PASSWORD=АРОЛЬ_LOKI' \
  ansible-playbook -i inventory/prod playbooks/setup_logging.yml

# Через GitLab CI/CD
# → Pipelines → setup:logging (ручной запуск, ветка main)

Playbook выполняет задачи в порядке:

  1. Создаёт namespace monitoring
  2. Создаёт bucket loki-chunks в MinIO, пользователя loki, политику доступа, Secret loki-minio-secret
  3. Устанавливает Loki (Helm)
  4. Устанавливает Grafana Alloy (Helm)
  5. Устанавливает kube-prometheus-stack (Helm) — Prometheus + node-exporter + kube-state-metrics + ~27 дашбордов
  6. Устанавливает Grafana (Helm) — проверяет наличие grafana-admin-secret перед установкой

Playbook идемпотентен: повторный запуск пропустит уже выполненные шаги.


Конфигурация

Loki: параметры хранения

Loki работает в режиме single-binary. Логи хранятся в MinIO (loki-chunks), не на диске.
PVC 10 GiB используется только для WAL и временных файлов компактора.

# roles/logging/defaults/main.yml (переопределять в group_vars при необходимости)
loki_retention_days: 31          # срок хранения логов
loki_wal_storage_size: "10Gi"    # PVC только для WAL
loki_wal_storage_class: longhorn
loki_minio_bucket: loki-chunks

Расчёт объёма для текущего кластера (2 ноды, ~20 подов):

  • ~170 MB/day после сжатия Loki (~10:1)
  • 31 дней → ~5 GB в bucket loki-chunks
  • MinIO PVC 1 TiB → запас до ~100 нод

Alloy: сбор логов

Alloy запускается как DaemonSet (по одному поду на каждую ноду, включая control plane).

Что собирает:

  • /var/log/pods/**/*.log — все контейнеры через loki.source.kubernetes
  • journald — kubelet, containerd, sshd через loki.source.journal

Фактические метки на каждой записи в Loki:

Метка Источник Описание
namespace Kubernetes API namespace пода
pod Kubernetes API имя пода
container Kubernetes API имя контейнера
app label пода значение app label
level JSON-парсинг уровень лога (info/warn/error)
job константа systemd-journal для journald-логов
node journald hostname ноды (только journald)
unit journald systemd unit (только journald)
stream containerd stdout или stderr

Debug-записи (label level=debug) отбрасываются на уровне агента — не попадают в Loki.

Prometheus: метрики кластера

# roles/logging/defaults/main.yml
prometheus_stack_chart_version: "68.4.4"
prometheus_storage_size: "20Gi"
prometheus_storage_class: longhorn
prometheus_retention: "30d"

kube-prometheus-stack устанавливается с отключённой встроенной Grafana (grafana.enabled: false) и forceDeployDashboards: true — создаёт ~27 ConfigMaps с дашбордами, которые Grafana sidecar подхватывает автоматически.

Rocky Linux 9 / kubeadm gotcha: controller-manager и scheduler по умолчанию слушают только 127.0.0.1 → их метрики в Prometheus будут недоступны (таргеты Down). Остальные таргеты работают без изменений. Для исправления нужен ручной патч /etc/kubernetes/manifests/kube-controller-manager.yaml и kube-scheduler.yaml: замена --bind-address=127.0.0.1 на --bind-address=0.0.0.0.

Grafana: доступ и дашборды

URL: http://10.203.0.96:10003
Логин: admin / пароль из Secret grafana-admin-secret

Datasources provisioned при старте — не требуют ручной настройки:

  • Lokihttp://loki.monitoring.svc.cluster.local:3100 (isDefault)
  • Prometheushttp://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090

Дашборды (загружаются автоматически через sidecar):

Дашборд Источник Описание
Kubernetes Logs ConfigMap grafana-dashboard-kubernetes-logs Фильтрация по namespace / pod / container / level, график интенсивности + поток логов
Kubernetes / Compute Resources / Cluster kube-prometheus-stack CPU/RAM по namespace
Kubernetes / Compute Resources / Node (Pods) kube-prometheus-stack Ресурсы по подам на ноде
Node Exporter / Nodes kube-prometheus-stack CPU, RAM, диски, сеть нод
Kubernetes / Persistent Volumes kube-prometheus-stack Статус и заполнение PVC
Kubernetes / API server kube-prometheus-stack Latency, error rate, RPS
...и ещё ~22 дашборда kube-prometheus-stack Kubelet, etcd, сеть, workloads

Дашборды Grafana.com с ID 15141, 13639, 18748 не работают с Grafana 12.x — используют устаревший API плагина. Не импортировать.


Диагностика

Проверка после установки

# Статус всех подов стека
kubectl get pod -n monitoring

# Loki готов (запрос через временный под — в контейнере нет curl/wget)
kubectl run loki-check --image=curlimages/curl --restart=Never -n monitoring \
  -- curl -s http://loki:3100/ready
sleep 5 && kubectl logs -n monitoring loki-check
kubectl delete pod loki-check -n monitoring

# Метки, которые реально есть в Loki
kubectl run loki-labels --image=curlimages/curl --restart=Never -n monitoring \
  -- curl -s 'http://loki:3100/loki/api/v1/labels'
sleep 5 && kubectl logs -n monitoring loki-labels
kubectl delete pod loki-labels -n monitoring

# Alloy открыл потоки логов со всех подов (нет level=error)
kubectl logs -n monitoring -l app.kubernetes.io/name=alloy --tail=50 | grep -v "opened log stream"

# Данные появились в MinIO
kubectl exec -n minio deployment/minio -- \
  mc ls local/loki-chunks --recursive | head -10

Loki CrashLoopBackOff при старте

Симптом 1: mkdir /loki/compactor: read-only file system
Причина: неверный путь compactor. Должен быть /var/loki/compactor, не /loki/compactor.
Проверить в ConfigMap: kubectl get configmap loki -n monitoring -o jsonpath='{.data.config\.yaml}' | grep working_directory

Симптом 2: SignatureDoesNotMatch или InvalidAccessKeyId
Причина: env var expansion ${VAR} в Loki 3.x не работает для S3 credentials в config-файле.
Решение: пароль передаётся через --set loki.storage.s3.secret_access_key=... в helm-команде (реализовано в tasks/loki.yml).

Симптом 3: error running loki при повторном запуске после CrashLoop + another operation in progress
Причина: helm upgrade завис в предыдущей попытке.

helm rollback loki -n monitoring
# затем повторить helm upgrade

Предупреждение loki-memberlist: no such host — некритично для single-binary, можно игнорировать.

Loki 500 при запросе через Grafana

# Проверить пользователя loki в MinIO
MINIO_POD=$(kubectl get pod -n minio -l app=minio -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n minio $MINIO_POD -- mc admin user info local loki

# Проверить Secret и его содержимое
kubectl get secret loki-minio-secret -n monitoring
kubectl get secret loki-minio-secret -n monitoring \
  -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d

# Тест доступа с теми же учётными данными
SECRET=$(kubectl get secret loki-minio-secret -n monitoring \
  -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d)
kubectl exec -n minio $MINIO_POD -- mc alias set lokitest http://localhost:9000 loki "$SECRET"
kubectl exec -n minio $MINIO_POD -- mc ls lokitest/

Alloy не отправляет логи

# Ошибки в логах Alloy
kubectl logs -n monitoring -l app.kubernetes.io/name=alloy --tail=100 \
  | grep -i "error\|level=error"

# Loki endpoint доступен из пода Alloy
kubectl exec -n monitoring -l app.kubernetes.io/name=alloy -c alloy -- \
  wget -qO- http://loki.monitoring.svc.cluster.local:3100/ready 2>&1

Grafana не видит данные (No Data)

# Проверить что Loki datasource здоров после рестарта Grafana
kubectl rollout restart deployment/grafana -n monitoring
kubectl rollout status deployment/grafana -n monitoring

# Проверить UID datasource (нужен для ручного исправления импортированных дашбордов)
GRAFANA_PASS=$(kubectl get secret grafana-admin-secret -n monitoring \
  -o jsonpath='{.data.admin-password}' | base64 -d)
kubectl run gf-check --image=curlimages/curl --restart=Never -n monitoring \
  -- curl -s -u "admin:${GRAFANA_PASS}" http://grafana/api/datasources
sleep 5 && kubectl logs -n monitoring gf-check | python3 -m json.tool | grep -E '"uid"|"name"'
kubectl delete pod gf-check -n monitoring

Если Grafana была запущена в момент когда Loki находился в CrashLoop — datasource кэширует нерабочее состояние. Перезапуск пода исправляет.

Grafana не открывается

# Состояние подов
kubectl get pod -n monitoring -l app.kubernetes.io/name=grafana

# IngressRoute создан
kubectl get ingressroute route-grafana -n traefik

# Если IngressRoute отсутствует — перезапустить setup_traefik.yml
# (порт 10003 уже есть в Traefik DaemonSet)
ansible-playbook -i inventory/prod playbooks/setup_traefik.yml

MinIO kubectl cp завершается ошибкой tar not found

MinIO-контейнер — минимальный образ без tar. kubectl cp использует tar внутри.
Решение: использовать kubectl exec -i ... -- mc ... /dev/stdin для передачи файлов.
Уже реализовано в tasks/minio-user.yml.


Масштабирование

До 5+ нод: Loki distributed mode

Триггер: объём > 50 GB/day или > 50 одновременных запросов.
Изменить в loki-values.yml.j2: deploymentMode: Distributed + задать реплики ingester/querier/distributor.
Данные в MinIO не мигрируют — bucket loki-chunks остаётся тем же.

Prometheus: Thanos для long-term storage

Триггер: retention > 30d или несколько кластеров.
Thanos sidecar к Prometheus pod + remote_write в объектное хранилище.

Alloy и node-exporter

Не требуют изменений — DaemonSet автоматически запускается на новых нодах.


Известные ограничения

Проблема Описание
deploymentMode обязателен на верхнем уровне В Loki chart 7.x deploymentMode: SingleBinary должен быть вне блока loki:, иначе деплоится distributed-режим с 0 репликами
Env var expansion не работает для S3 credentials Loki 3.x не раскрывает ${VAR} в полях access_key_id / secret_access_key. Пароль передаётся через helm --set
Controller-manager / Scheduler метрики недоступны kubeadm привязывает их к 127.0.0.1. Требует ручного патча static pod манифестов
Дашборды Grafana.com 15141 / 13639 / 18748 сломаны Несовместимы с Grafana 12.x. Использовать встроенный дашборд Kubernetes Logs

Переход на Graylog

Alloy поддерживает dual-write: одновременная отправка в Loki и Graylog. Порядок переезда описан в research/logging/plg.md (раздел 11).

MinIO остаётся востребованным при Graylog — как S3 snapshot repository для OpenSearch (bucket backups).