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

1276 lines
47 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.

# Сбор логов в 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/<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 | ✅ Актуален |
| ~~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`)