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

358 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Стандарт централизованного сбора логов: PLG + Prometheus
**Стек:** Loki · Grafana Alloy · Grafana · kube-prometheus-stack
**Namespace:** `monitoring`
**Доступ:** `http://10.203.0.96:10003` (Traefik port 10003)
**Исследование:** [`research/logging/plg.md`](../../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
```bash
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` можно посмотреть в любой момент:
```bash
kubectl get secret loki-minio-secret -n monitoring \
-o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d
```
---
## Запуск
```bash
# Локально (с заданной переменной окружения)
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 и временных файлов компактора.
```yaml
# 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: метрики кластера
```yaml
# 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 при старте — не требуют ручной настройки:
- **Loki** — `http://loki.monitoring.svc.cluster.local:3100` (isDefault)
- **Prometheus** — `http://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 плагина. Не импортировать.
---
## Диагностика
### Проверка после установки
```bash
# Статус всех подов стека
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 завис в предыдущей попытке.
```bash
helm rollback loki -n monitoring
# затем повторить helm upgrade
```
**Предупреждение `loki-memberlist: no such host`** — некритично для single-binary, можно игнорировать.
### Loki 500 при запросе через Grafana
```bash
# Проверить пользователя 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 не отправляет логи
```bash
# Ошибки в логах 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)
```bash
# Проверить что 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 не открывается
```bash
# Состояние подов
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`](../../research/logging/plg.md) (раздел 11).
MinIO остаётся востребованным при Graylog — как S3 snapshot repository для OpenSearch (bucket `backups`).