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