Prometheus: створення Custom Prometheus Exporter на Python
0 (0)

18 Лютого 2023

У Прометеуса є багато готових до використання експортерів, але інколи може з’явитися потреба у зборі своїх власних метрик.

Для цього Прометеус надає клієнтські бібліотеки, які можемо використати для генерації метрик з потрібними лейблами.

Експортер можна включити прямо в код вашого додатку, або можна запускати окремим сервісом, який буде звертатися до якогось вашого сервісу і отримувати від нього дані, які потім буде конвертувати в Prometheus-формат та віддавати серверу Prometheus.

Prometheus Metrics

Загальна схема роботи Prometheus-серверу та його експортерів у Kubernetes виглядає так:

Тут маємо:

  1. Prometheus Server, який в нашому випадку розгортається за допомогою Kube Promeheus Stack та Prometheus Operator
  2. за допомогою ServiceMonitor через Operator ми створюємо Scrape Job, яка має один чи декілька Targets, тобто сервісів, які буде опитувати Prometheus для отримання метрик, які він зберігає у своїй Time Series Database
  3. за URL, які вказані в Target, Prometheus звертається до ендпоінту Prometheus Exporter
  4. а Prometheus Exporter збирає метрики з вашого додатку, які потім віддає до Prometheus

Типи метрик Prometheus

Коли плануємо писати свій експортер, необхідно знати які типи метрик ми можемо в ньому використовувати. Основні типи:

  • Counter: може тільки збільшувати своє значення, наприклад для підрахунку кількості HTTP-запитів
  • Enum: має попередньо задані значення, використовується наприклад для моніторингу кількості подів у статус Running або Failed
  • Histograms: зберігає значення за проміжок часу, можна використовавти для, наприклад, отримання часу відповіді веб-северу за період часу – rate(metric_name{}[5m])
  • Gauges: може приймати будь-яке значення, можемо використовувати для, наприклад, зберігання значень нагрузки на CPU
  • Info: key-value storage, наприклад для Build information, Version information, або metadata

У кожного типу є свої методи, тож варто подивитись документацію, там ще й приклади є. Див. Prometheus Python Client, або в документації самої бібліотеки:

[simterm]

>>> import prometheus_client
>>> help(prometheus_client.Enum)

[/simterm]

Python Custom Prometheus Exporter

Python HTTPServer Exporter з Counter

Для початку, давайте подивимось як воно взагалі працює – напишимо скрипт на Python, в якому на порту 8080 буде звичайний HTTP-сервер, а на порту 9000 – експортер, який буде збирати статистику по запитах з кодами відповідей і створювати метрику http_requests з двома лейблами – в одній будемо зберігати код відповіді, а в іншій – ім’я хоста, з якого метрика була отримана.

Встановлюємо бібліотеку:

[simterm]

$ pip install prometheus_client

[/simterm]

Пишемо скрипт:

#!/usr/bin/env python

import os
import re
import platform
from time import sleep

from http.server import BaseHTTPRequestHandler, HTTPServer

from prometheus_client import start_http_server, Counter, REGISTRY

class HTTPRequestHandler(BaseHTTPRequestHandler):
    
    def do_GET(self):
        if self.path == '/':
            self.send_response(200)
            self.send_header('Content-type','text/html')
            self.end_headers()
            self.wfile.write(bytes("<b> Hello World !</b>", "utf-8"))
            request_counter.labels(status_code='200', instance=platform.node()).inc()
        else:
            self.send_error(404)
            request_counter.labels(status_code='404', instance=platform.node()).inc()
    
                
if __name__ == '__main__':
            
    start_http_server(9000)
    request_counter = Counter('http_requests', 'HTTP request', ["status_code", "instance"])

    webServer = HTTPServer(("localhost", 8080), HTTPRequestHandler).serve_forever()
    print("Server started")

Тут ми:

  • start_http_server() – запускаємо HTTP-сервер самого експортеру на порту 9000
  • request_counter – створюємо метрику з ім’ям http_requests, типом Counter, і додаємо їй дві labels – status_code та instance
  • webServer – запускаємо звичайний HTTP-сервер на Python на порту 8080

Далі, коли ми робитимо HTTP-запит на localhost:8080, він буде попадати до do_GET(), в якому буде перевірятися URI. Якщо йдемо на / – то отримаємо код 200, якщо будь-який інший – то 404.

І там же оновлюємо значення метрики http_requests – додаємо код відповіді та ім’я хоста, і викликаємо метод Counter.inc(), який інкрементить значення метрики на одиницю. Таким чином кожен запит, який буде оброблено веб-сервером webServer буде додавати +1 в нашу метрику, а в залежності від коду відповіді – ми отримаємо цю метрику з двома різними лейблами – 200 та 404.

Перевіряємо – запускаємо сам скрипт:

[simterm]

$ ./py_http_exporter.py

[/simterm]

Робимо декілька запитів з різними URI:

[simterm]

$ curl -I -X GET localhost:8080/
HTTP/1.0 200 OK
$ curl -I -X GET localhost:8080/
HTTP/1.0 200 OK
$ curl -I -X GET localhost:8080/
HTTP/1.0 200 OK

$ curl -I -X GET localhost:8080/blablabla
HTTP/1.0 404 Not Found
$ curl -I -X GET localhost:8080/blablabla
HTTP/1.0 404 Not Found
$ curl -I -X GET localhost:8080/blablabla
HTTP/1.0 404 Not Found

[/simterm]

А тепер перевіримо, що маємо на ендпоінті експортеру:

[simterm]

$ curl -X GET localhost:9000
...
# HELP http_requests_total HTTP request
# TYPE http_requests_total counter
http_requests_total{instance="setevoy-wrk-laptop",status_code="200"} 3.0
http_requests_total{instance="setevoy-wrk-laptop",status_code="404"} 3.0

[/simterm]

Чудово – маємо три запроси з кодом 200, та три – з кодом 404.

Jenkins Jobs Exporter з Gauage

Або інший варіант – коли експортер буде звертатися до якось зовнішнього ресурсу, отримувати значення, і вносити їх до метрики.

Наприклад, ми можемо звертатись до якогось API, і від нього отримати дані, в цьому прикладі це буде Jenkins:

#!/usr/bin/env python

import time
import random
from prometheus_client import start_http_server, Gauge
from api4jenkins import Jenkins

jenkins_client = Jenkins('http://localhost:8080/', auth=('admin', 'admin'))

jenkins_jobs_counter = Gauge('jenkins_jobs_count', "Number of Jenkins jobs")

def get_metrics():
   jenkins_jobs_counter.set(len(list(jenkins_client.iter_jobs())))

if __name__ == '__main__':
    start_http_server(9000)
    while True:
        get_metrics()
        time.sleep(15)

Тут ми за допомогою бібліотеки api4jenkins створюємо об’єкт jenkins_client, який підключається до інстансу Jenkins та отримує кількість його jobs. Потім в функції get_metrics() ми рахуємо кількість об’єктів із jenkins_client.iter_jobs(), та вносимо їх до метрики jenkins_jobs_counter.

Запускаємо в Docker:

[simterm]

$ docker run -p 8080:8080 jenkins:2.60.3

[/simterm]

Створюємо тестову задачу:

В результаті отримуємо такий результат:

[simterm]

$ curl localhost:9000
...
# HELP jenkins_jobs_count Number of Jenkins jobs
# TYPE jenkins_jobs_count gauge
jenkins_jobs_count 1.0

[/simterm]

Prometheus Exporter та Kubernetes

І давайте протестимо якийсь більш реальний приклад.

У нас є API-сервіс, який використовує базу даних PostgreSQL. Для перевірки підключення девелопери створили ендпоінт, на який ми можемо звертатися для отримання поточного статусу – є чи нема підключення до серверу баз даних.

Зараз для його моніторингу ми використовуємо Blackbox Exporter, але згодом хочется трохи розширити можливості, тож спробуємо створити експортер, котрий поки що буде просто перевіряти код відповіді він цього ендпоінту.

Exporter з Enum та Histogram

#!/usr/bin/env python

import os
import requests
import time
from prometheus_client import start_http_server, Enum, Histogram

hitl_psql_health_status = Enum("hitl_psql_health_status", "PSQL connection health", states=["healthy", "unhealthy"])
hitl_psql_health_request_time = Histogram('hitl_psql_health_request_time', 'PSQL connection response time (seconds)')

def get_metrics():

    with hitl_psql_health_request_time.time():
        resp = requests.get(url=os.environ['HITL_URL'])
    
    print(resp.status_code)
            
    if not (resp.status_code == 200):
        hitl_psql_health_status.state("unhealthy")
            
if __name__ == '__main__':
    start_http_server(9000)
    while True:
        get_metrics()
        time.sleep(1)

Задаємо змінну оточення з URL:

[simterm]

$ export HITL_URL=https://hitl.qa.api.example.com/api/v1/postgres/health-check

[/simterm]

Та запускаємо скрипт:

[simterm]

$ ./py_hitl_exporter.py 
500
500
500

[/simterm]

Ммм… Чудово – ендпоінт лежить 🙂 То в понеділок нехай девелопери перевірять, нам підходить і це.

Docker-образ

Далі, збираємо Docker-образ – спочатку створимо requirements.txt з залежностями:

requests
prometheus_client

Теперь Dockerfile:

FROM python:3.8

COPY py_hitl_exporter.py /app/py_hitl_exporter.py
COPY requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install -r requirements.txt
ENV HITL_URL $HITL_URL
CMD ["python3", "/app/py_hitl_exporter.py"]

Збираємо образ:

[simterm]

$ docker build -t setevoy/test-exporter .

[/simterm]

Запускаємо локально для перевірки:

[simterm]

$ docker run -p 9000:9000 -e HITL_URL=https://hitl.qa.api.example.com/api/v1/postgres/health-check setevoy/test-exporter

[/simterm]

Перевіряємо метрики:

[simterm]

$ curl localhost:9000
...
# HELP hitl_psql_health_status PSQL connection health
# TYPE hitl_psql_health_status gauge
hitl_psql_health_status{hitl_psql_health_status="healthy"} 0.0
hitl_psql_health_status{hitl_psql_health_status="unhealthy"} 1.0
# HELP hitl_psql_health_request_time PSQL connection response time (seconds)
# TYPE hitl_psql_health_request_time histogram
hitl_psql_health_request_time_bucket{le="0.005"} 0.0
hitl_psql_health_request_time_bucket{le="0.01"} 0.0
hitl_psql_health_request_time_bucket{le="0.025"} 0.0
hitl_psql_health_request_time_bucket{le="0.05"} 0.0
hitl_psql_health_request_time_bucket{le="0.075"} 0.0
hitl_psql_health_request_time_bucket{le="0.1"} 0.0
hitl_psql_health_request_time_bucket{le="0.25"} 0.0
hitl_psql_health_request_time_bucket{le="0.5"} 0.0
hitl_psql_health_request_time_bucket{le="0.75"} 0.0
hitl_psql_health_request_time_bucket{le="1.0"} 0.0
hitl_psql_health_request_time_bucket{le="2.5"} 0.0
hitl_psql_health_request_time_bucket{le="5.0"} 0.0
hitl_psql_health_request_time_bucket{le="7.5"} 0.0
hitl_psql_health_request_time_bucket{le="10.0"} 0.0
hitl_psql_health_request_time_bucket{le="+Inf"} 9.0
hitl_psql_health_request_time_count 9.0
hitl_psql_health_request_time_sum 96.56228125099824

[/simterm]

Добре.

Пушимо його в Docker Hub:

[simterm]

$ docker login
$ docker push setevoy/test-exporter

[/simterm]

Kubernetes Pod, Service та ServiceMonitor для Prometheus

Далі – треба цей образ запустити в Kubernetes, та створити ServiceMonitor для Prometheus, який там вже запущено.

Створюємо маніфест з Pod, Service та ServiceMonitor:

apiVersion: v1
kind: Pod
metadata:
  name: hitl-exporter-pod
  labels:
    app: hitl-exporter
spec:
  containers:
    - name: hitl-exporter-container
      image: setevoy/test-exporter
      env:
      - name: HITL_URL
        value: https://hitl.qa.api.example.com/api/v1/postgres/health-check
      ports:
        - name: metrics
          containerPort: 9000
          protocol: TCP
---     
apiVersion: v1
kind: Service
metadata:
  name: hitl-exporter-service
  labels:
    app: hitl-exporter
spec:
  selector:
    app: hitl-exporter
  ports:
    - name: metrics
      port: 9000
      targetPort: metrics
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    release: kps
    app: hitl-exporter
  name: hitl-exporter-monitor
spec:
  endpoints:
    - port: metrics
      interval: 15s
      scrapeTimeout: 10s
  namespaceSelector:
    matchNames:
    - monitoring
  selector:
    matchLabels:
      app: hitl-exporter

Запускаємо:

[simterm]

$ kubectl -n monitoring apply -f hitl_exporter.yaml 
pod/hitl-exporter-pod created
service/hitl-exporter-service created
servicemonitor.monitoring.coreos.com/hitl-exporter-monitor created

[/simterm]

І за хвилину-дві перевіряємо Service Discovery:

Targets:

Та Jobs:

Переходимо до графіків – і маємо наші метрики:

Готово.

Loading

GitLab: Helm-чарт values, залежності та деплой у Kubernetes з AWS S3
0 (0)

4 Лютого 2023

Продовжуємо зайомство з GitLab та його деплоєм у Kubernetes. Перша частина –  GitLab: компоненти, архітектура, інфраструктура та запуск із Helm-чарту в Minikube, тепер давайте готуватися деплоїти в AWS Elastic Kubernetes Service.

Що робитимемо і де:

  • деплоїмо в AWS з Helm-чарту, для початку якийсь “test env”
  • Kubernetes – AWS EKS
  • object store – AWS S3
  • PostgreSQL – з operator
  • Redis – поки використовуємо дефолтний, потім переїдемо на KeyDB, який теж розгортається оператором
  • Gitaly – спробуємо в кластері, можливо, на окремій ноді – у нас всього 150-200 користувачів, навантаження не повинно бути великим, скейлінг і тим більше Praefik не потрібні

GitLab Operator виглядає в цілому цікаво, але в документації через слово “Experimental” і “Beta”, тому поки не чіпаємо – використовуємо чарт.

Helm chart prerequisites

По чарту: для початку треба пройтися по доступним параметрам чарту, і подивитися, що нам взагалі треба буде із зовнішніх ресурсів (лоад-балансери, корзини S3, PostgreSQL), і що ще можна налаштувати через чарт.

Не забуваємо подивитися GitLab chart prerequisites , з якого нам поки що цікаві:

  • PostgreSQL: ми будемо використовувати оператор, та розгорнемо свій кластер з блекджеком і базами
  • Redis: у нас є KeyDB-оператор, потім будемо використовувати його, поки дефолтний із чарту
  • Networking and DNS: використовуємо AWS ALB Contoller, створимо ACM сертифікат, DNS у Route53, записи створюються через External DNS
  • Persistence: цікавий нюанс, можливо, треба буде налаштувати reclaimPolicy to Retain, див. Configuring Cluster Storage
  • Prometheus: теж поінт на подумати – у нас є Kube Prometheus Stack зі своїм Prometheus та Alertmanager, треба буде подумати чи відключати вбудований у GitLab, чи залишати
  • Outgoing email: поки не налаштовуватимемо, але потім треба буде подумати. У принципі є підтримка AWS SES, десь у документації зустрічав, так що нормально
  • RBAC: у нас є, підтримується, тож залишаємо за замовчуванням, тобто включаємо

Враховуємо, що чарт включає цілу пачку залежностей, і корисно пройтися і подивитися що там ще деплоїться і з якими параметрами.

Структура Helm-чарту та його values.yaml

У GitLab Helm-чарту досить складна структура його values, так як чарт включає набір сабчартів і залежностей, див. GitLab Helm subcharts .

Щоб краще зрозуміти структуру його values – можна подивитися структуру каталогу charts з дочірніми чартами:

[simterm]

$ tree -d gitlab/charts/
gitlab/charts/
|-- certmanager-issuer
|   `-- templates
|-- gitlab
|   |-- charts
|   |   |-- geo-logcursor
|   |   |   `-- templates
|   |   |-- gitaly
|   |   |   `-- templates
|   |   |-- gitlab-exporter
|   |   |   `-- templates
|   |   |-- gitlab-grafana
|   |   |   `-- templates
|   |   |-- gitlab-pages
|   |   |   `-- templates
|   |   |-- gitlab-shell
|   |   |   `-- templates
|   |   |-- kas
|   |   |   `-- templates
|   |   |-- mailroom
|   |   |   `-- templates
|   |   |-- migrations
|   |   |   `-- templates
|   |   |-- praefect
|   |   |   `-- templates
|   |   |-- sidekiq
|   |   |   `-- templates
|   |   |-- spamcheck
|   |   |   `-- templates
|   |   |-- toolbox
|   |   |   `-- templates
|   |   `-- webservice
|   |       `-- templates
|   |           `-- tests
|   `-- templates
|-- minio
|   `-- templates
|-- nginx-ingress
|   `-- templates
|       `-- admission-webhooks
|           `-- job-patch
`-- registry
    `-- templates

[/simterm]

Крім того, є набір зовнішніх залежностей, див requirements.yaml.

Відповідно у values основного чарту параметри розбиті на global (див. Subcharts and Global Values), які використовуються дочірніми чартами, та параметри для конкретних чартів, див. Configure charts using globals, тобто наш values.yaml виглядатиме так:

global: 

  hosts:
    domain:
    hostSuffix: # використовується у ./charts/gitlab/charts/webservice, ./charts/gitlab/charts/toolbox/, ./charts/registry/, etc
  ...

gitlab:
  kas:
    enabled: # використовується у ./charts/gitlab/charts/kas/
...

nginx-ingress:
  enabled: # використовується для ./gitlab/charts/nginx-ingress
...

postgresql:
  install: # використовується при встановленні зовнішнього чарту postgresql

Тепер подивимося на самі values, і які нам можуть бути корисні.

Helm chart values

“Стартова точка” для роботи з чартом – Installing GitLab by using Helm .

Ще добре пройтися дефолтним values.yaml– там у коментарях є посилання на сервіси/параметри.

Власне, параметри, які нам можуть бути цікаві, див. у Configure charts using globals та GitLab Helm chart deployment options :

  • global.hosts:
    • domain: вказуємо домен, в якому будуть створюватися записи для GitLab
    • externalIP: він начебто обов’язковий, але оскільки у нас буде AWS ALB, то не використовуємо
    • hostSuffixtest – додамо суффікс до субдоменів, що створюються, отримаємо запис виду gitlab-test.example.com
    • httpstrue , буде ALB із AWS Certificate Manager
    • ssh: тут потрібно буде задати окремий субдомен для доступу до GitLab Shell
  • global.ingress: так як у нас AWS ALB, то див. alb-full.yaml
    • annotations.*annotation-key*: додамо аннотації для ALB
    • configureCertmanager: треба буде відключити, так як SSL термінейтимо на лоад-балансері
    • tls.secretName: нам не потрібен, так як SSL термінейтимо на лоад-балансері
    • path: для ALB буде потрібен /*
    • provider: для ALB == aws
  • global.gitlabVersion: думав тут задати версію Gitlab, яку будемо деплоїти, але виявилося, що ні – правильніше через версію чарту, див. GitLab Version
  • global.psql: параметри для підключення до PostgreSQL сервісами GitLab, див. Configure PostgreSQL settings
    • host: адреса PosgtreSQL Service
    • databaseusername: зрозуміло
    • password:
      • useSecret: пароль зберігатимемо в Secret
      • secret: ім’я секрету
      • key: ключ/поле у ​​Секреті, за яким отримуємо пароль
  • global.redis: взагалі буде зовнішній, KeyDB, і тут треба буде вказати параметри доступу до нього, але спочатку залишимо дефолтний, див. Configure Redis settings
  • global.grafana.enabled: включаємо – подивимося, які там є дашборди (таки треба буде налаштовувати окремо, див. Grafana JSON Dashboards )
  • global.registry: див. Using the Container Registry
    • bucket: треба буде створити S3 бакет, цей параметр використовується /charts/gitlab/charts/toolbox/ для бекапів, сам GitLab Registry налаштовується через окремі параметри, розглянемо нижче
  • global.gitaly: поки використовуємо з чарту, незважаючи на рекомендації – будемо деплоїти у вигляді Kubernetes Pod в кластер, залишаємо за замовчуванням, але маємо на увазі
  • global.minio: відключимо – використовуємо відразу AWS S3
  • global.appConfig: ось тут прям багато всього, див. Configure appConfig settings – містить загальні параметри для чартів WebserviceSidekiq та Gitaly
    • cdnHost: взагалі-то корисна начебто річ, але подивимося, як його використовувати, поки не чіпаємо
    • contentSecurityPolicy: теж корисна штука, але налаштуємо потім, див. Content Security Policy
    • enableUsagePing: “телеметрія” для самого GitLab Inc, не бачу сенсу, відключимо
    • enableSeatLink: не зрозумів, що це, посилання seat link support веде в “нікуди” – на сторінці інформації про seat не знайшов, але мабуть це щось пов’язане з кількістю користувачів в ліцензії, а оскільки ми її не купуємо – то можна вимкнути
    • object_store: загальні параметри для роботи з S3 типу ключів доступу та proxy, див. Consolidated object storage
      • connection: тут потрібно буде створити секрет, в якому описуються налаштування підключення до корзин, у тому числі ACCESS/SECRET ключі, але ми замість ключів використовуємо ServiceAccount та IAM role
    • Specify buckets : які корзини потрібні, є набір дефолтних імен типу gitlab-artifacts , але для dev- та test- має сенс перевизначити
      • storage_options: шифрування для бакетів, має сенс змінювати якщо використовується AWS KMS, але ми швидше за все залишимо дефолтне шифрування
    • LFS, Artifacts, Uploads, Packages, External MR diffs, and Dependency Proxy – налаштування корзин для різних сервісів, поки не чіпатимемо, подивимося по ходу використання
    • gitlab_kas: налаштування для GitLab Agent for Kubernetes, поки залишимо за замовчуванням, не впевнений, що він нам знадобиться
    • omniauth: налаштування для SSO, якось потім, взагалі будемо підключати аутентифікацію через Google, див. OmniAuth
  • global.serviceAccounts: замість ACCESS/SECRET будемо використовувати Kubernetes ServiceAccounts з IAM Role для подів:
    • create: включаємо створення SA для сервісів
    • annotations: а тут вкажемо ARN IAM-ролі
  • global.nodeSelector: корисно, потім будемо на виділених нодах
  • global.affinity&& global.antiAffinity: плюс можна налаштувати Affinity
  • global.common.labels: задамо тут всякі environment , team , etc
  • global.tracing: поки не чіпаємо, але маємо на увазі на майбутнє – потім додамо якийсь Jaeger/Grafana Tempo
  • global.priorityClassName: можливо корисно або навіть потрібно, потім потикаємо, див. Pod Priority and Preemption

З глобальних змінних це начебто все, крім них нам треба буде:

  • вимкнути certmanager – використуємо AWS Certificate Manager на AWS LoadBalncer
  • вимкнути postgresql – використуємо зовнішній
  • відключити gitlab-runner – у нас вже є запущені раннери, потім спробуємо прикрутити їх до цього інстансу GitLab
  • відключити nginx-ingress – у нас AWS ALB Controller та AWS ALB балансери
  • налаштувати Services для webservice та gitlab-shell
  • налаштувати registry

Тепер перед деплоєм чарту треба підготувати зовнішні сервіси:

  • кластер та база PostgreSQL
  • бакети AWS S3
  • сертифікат в AWS Certificate Manager для лоад-балансеру

Підготовка – створення зовнішніх ресурсів

PostgreSQL

У нас використовується PostgreSQL Operator – описуємо створення кластера.

Параметри по коннектам, пам’яті, диску та реквестам-лімітам поки що дефолтні – подивимося по ходу діла, як сильно навантажуватиметься база.

Описуємо створення бази gitlabhq_test та дефолтних юзерів – defaultUsers=true:

kind: postgresql
apiVersion: acid.zalan.do/v1
metadata:
  name: gitlab-cluster-test-psql
  namespace: gitlab-cluster-test
  labels:
    team: devops
    environment: test
spec:
  teamId: devops
  postgresql:
    version: "14"
    parameters:
      max_connections: '100'
      shared_buffers: 256MB
      work_mem: 32MB
  numberOfInstances: 3
  preparedDatabases:
    gitlabhq_test:
      defaultUsers: true
      schemas:
        public:
          defaultRoles: false
  enableMasterLoadBalancer: false
  enableReplicaLoadBalancer: false
  enableConnectionPooler: false
  enableReplicaConnectionPooler: false
  volume:
    size: 10Gi
    storageClass: encrypted
  resources:
    requests:
      cpu: "100m"
      memory: 100Mi
    limits:
      memory: 1024Mi
  enableLogicalBackup: true
  logicalBackupSchedule: "0 1 * * *"
  sidecars:
    - name: exporter
      image: quay.io/prometheuscommunity/postgres-exporter:v0.11.1
      ports:
        - name: exporter
          containerPort: 9187
          protocol: TCP
      resources:
        limits:
          memory: 50M
        requests:
          cpu: 50m
          memory: 50M
      env:
      - name: DATA_SOURCE_URI
        value: localhost/postgres?sslmode=disable
      - name: DATA_SOURCE_USER
        value: "$(POSTGRES_USER)"
      - name: DATA_SOURCE_PASS
        value: "$(POSTGRES_PASSWORD)"
      - name: PG_EXPORTER_AUTO_DISCOVER_DATABASES
        value: "true"

Створюємо неймспейс:

[simterm]

$ kk create ns gitlab-cluster-test

[/simterm]

Деплоїмо кластер:

[simterm]

$ kk apply -f postgresql.yaml 
postgresql.acid.zalan.do/gitlab-cluster-test-psql created

[/simterm]

Перевіряємо поди:

[simterm]

$ kk -n gitlab-cluster-test get pod
NAME                                READY   STATUS              RESTARTS   AGE
devops-gitlab-cluster-test-psql-0   2/2     Running             0          24s
devops-gitlab-cluster-test-psql-1   0/2     ContainerCreating   0          4s

[/simterm]

Окей, створюються.

Перевіряємо Секрети – потім використуємо секрет юзера postgres у конфігах GitLab:

[simterm]

$ kk -n gitlab-cluster-test get secret
NAME                                                                                             TYPE                                  DATA   AGE
default-token-2z2ct                                                                              kubernetes.io/service-account-token   3      4m12s
gitlabhq-test-owner-user.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do    Opaque                                2      65s
gitlabhq-test-reader-user.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do   Opaque                                2      65s
gitlabhq-test-writer-user.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do   Opaque                                2      66s
postgres-pod-token-p7b4g                                                                         kubernetes.io/service-account-token   3      66s
postgres.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do                    Opaque                                2      66s
standby.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do                     Opaque                                2      66s

[/simterm]

Наче ОК.

AWS S3

Тепер створимо корзини.

Нам будуть потрібні:

Щоб уникнути помилки BucketAlreadyExists, до імен корзин додаємо or як власний “ідентифікатор”, бо імена загальні, і ім’я оточення – test , тобто список виходить такий:

  • or-gitlab-registry-test
  • or-gitlab-artifacts-test
  • or-git-lfs-test
  • or-gitlab-packages-test
  • or-gitlab-uploads-test
  • or-gitlab-mr-diffs-test
  • or-gitlab-terraform-state-test
  • or-gitlab-ci-secure-files-test
  • or-gitlab-dependency-proxy-test
  • or-gitlab-pages-test
  • or-gitlab-backups-test
  • or-gitlab-tmp-test

Не факт, що знадобляться всі, подивимося по ходу налаштування GitLab та його фіч, але поки що створимо.

Створюємо з AWS CLI:

[simterm]

$ aws --profile internal s3 mb s3://or-gitlab-registry-test
make_bucket: or-gitlab-registry-test

[/simterm]

Повторюємо для решти корзин.

AWS Certificate Manager

Для Ingress нам потрібен TLS-сертифікат, див. Requesting a public certificate, а для отримання сертифіката – вочевидь, що домен.

У нашому випадку використовуємо internal.example.com, для якого створимо wildcard-сертифікат в ACM:

У FQDN вказуємо *.internal.example.com, щоб включити всі субдомени. Потім для GitLab використуємо параметри global.hosts.domain=internal.example.com та hostSuffix=test, що в результаті створить кілька Ingress та Services, які через ExternalDNS створять необхідні записи у Route53.

У Validation Method вибираємо DNS – найпростіший, тим більш що доменна зона хоститься в Route53 – все створюється в пару кліків:

Переходимо до сертифікату – він зараз у Pending validation, клікаємо Create records in Route53 :

Тепер статус Issued, запам’ятовуємо його ARN – він нам знадобиться у values:

AWS IAM Policy та IAM Role для ServiceAccount

Для ServiceAccount, який повинен буде давати доступ до AWS S3, потрібно створити IAM Policy та IAM Role. Детально див. Kubernetes ServiceAccounts з IAM Role для подів, тут швиденько.

Створюємо поліси:

{
    "Statement": [
        {
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:ListMultipartUploadParts",
                "s3:AbortMultipartUpload"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::or-gitlab-registry-test/*",
                "arn:aws:s3:::or-gitlab-artifacts-test/*",
                "arn:aws:s3:::or-git-lfs-test/*",
                "arn:aws:s3:::or-gitlab-packages-test/*",
                "arn:aws:s3:::or-gitlab-uploads-test/*",
                "arn:aws:s3:::or-gitlab-mr-diffs-test/*",
                "arn:aws:s3:::or-gitlab-terraform-state-test/*",
                "arn:aws:s3:::or-gitlab-ci-secure-files-test/*",
                "arn:aws:s3:::or-gitlab-dependency-proxy-test/*",
                "arn:aws:s3:::or-gitlab-backups-test/*",
                "arn:aws:s3:::or-gitlab-tmp-test/*"
            ]
        },
        {
            "Action": [
                "s3:ListBucket",
                "s3:ListAllMyBuckets",
                "s3:GetBucketLocation",
                "s3:ListBucketMultipartUploads"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::or-gitlab-registry-test",
                "arn:aws:s3:::or-gitlab-artifacts-test",
                "arn:aws:s3:::or-git-lfs-test",
                "arn:aws:s3:::or-gitlab-packages-test",
                "arn:aws:s3:::or-gitlab-uploads-test",
                "arn:aws:s3:::or-gitlab-mr-diffs-test",
                "arn:aws:s3:::or-gitlab-terraform-state-test",
                "arn:aws:s3:::or-gitlab-ci-secure-files-test",
                "arn:aws:s3:::or-gitlab-dependency-proxy-test",
                "arn:aws:s3:::or-gitlab-backups-test",
                "arn:aws:s3:::or-gitlab-tmp-test"
            ]
        }
    ],
    "Version": "2012-10-17"
}

Створюємо IAM Role – вибираємо Web identity:

Підключаємо створену вище Policy:

Наче все? Можна писати values.

Деплой Gitlab Helm

Створення values.yaml

Всі дефолтні values ​​тут>>>, можна використати як приклад, але не варто повністю копіювати – у свій values ​​пишемо тільки те, що у нас відрізняється від дефолтного.

global

hosts

Configure Host settings

Вказуємо домен, використовуючи який чарт пропише значення для Ingress та Services які буде створювати – отримаємо набір доменів виду gitlab.internal.example.comregistry.internal.example.com і т.д.

Для SSH у прикладі для Ingress вказаний окремий субдомен, оскільки під SSH буде створюватися окремий Service з Network Load Balancer для доступу до 22 TCP.

hostSuffix додасть суфікс до створюваних записів, тобто в результаті будуть субдомени виду gitlab-test.internal.example.com та registry-test.internal.example.com .

Але для SSH hostSuffix не застосовується, тому вказуємо відразу з суфіксом.

Виходить так:

global:
  hosts:
    domain: internal.example.com
    hostSuffix: test
    ssh: gitlab-shell-test.internal.example.com

ingress

Configure Ingress settings

Беремо для прикладу alb-full.yaml, від себе додамо тільки load_balancing.algorithm.type=least_outstanding_requests:

ingress:
  # Common annotations used by kas, registry, and webservice
  annotations:
    alb.ingress.kubernetes.io/backend-protocol: HTTP
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-central-1:514***799:certificate/7227a8fa-1124-441c-81d7-ec168180190d
    alb.ingress.kubernetes.io/group.name: gitlab
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-group-attributes: load_balancing.algorithm.type=least_outstanding_request
    alb.ingress.kubernetes.io/target-type: ip
    kubernetes.io/ingress.class: alb
    nginx.ingress.kubernetes.io/connection-proxy-header: "keep-alive"
  class: none
  configureCertmanager: false
  enabled: true
  path: /*
  pathType: ImplementationSpecific
  provider: aws
  tls:
    enabled: false

Якщо використовувати KAS, йому потрібен буде окремий LoadBalancer, налаштуємо його трохи пізніше в блоці gitlab.kas.

psql

Configure PostgreSQL settings

Знаходимо ім’я Service, який був створений під час деплою PostgreSQL кластеру:

[simterm]

$ kk -n gitlab-cluster-test get svc
NAME                                     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
devops-gitlab-cluster-test-psql          ClusterIP   172.20.67.135    <none>        5432/TCP   63m
devops-gitlab-cluster-test-psql-config   ClusterIP   None             <none>        <none>     63m
devops-gitlab-cluster-test-psql-repl     ClusterIP   172.20.165.249   <none>        5432/TCP   63m

[/simterm]

Користувача поки що візьмемо дефолтного postgress, з готового секрету:

[simterm]

$ kk -n gitlab-cluster-test get secret postgres.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do        
NAME                                                                            TYPE     DATA   AGE
postgres.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do   Opaque   2      67m

[/simterm]

Для production таки треба буде робити окремого, бо postgress == root .

Готуємо конфіг:

psql:
  host: devops-gitlab-cluster-test-psql
  database: gitlabhq_test
  username: postgres
  password:
    useSecret: true    
    secret: postgres.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do
    key: password

redis

Configure Redis settings

Configure Redis chart-specific settings

Configure the GitLab chart with an external Redis

Поки залишимо все за замовчуванням – Redis використовується тільки для кешування, подивимося, як він працюватиме і що по ресурсам.

registry

Configure Registry settings

В globals для registry вказуємо тільки корзину:

registry:
  bucket: or-gitlab-registry-test

gitaly

Поки що залишимо, як є. Потім подумаємо, може винесемо на окрему ноду.

Using the GitLab-Gitaly chart

Треба буде моніторити як він ресурси використовує, але у випадку з нашими 150-200 користувачів навряд чи там буде необхідність сильно допилювати його або тим більше розгортати кластер Praefect .

minio

Відключаємо, ходитимемо відразу в AWS S3:

minio:
  enabled: false

grafana

Включимо, подивимося, що в ній є (нічого 🙂):

grafana:
  enabled: true

appConfig

Список корзин є у values.

object_store таки треба налаштовувати – додати секрет, в якому буде вказано провайдер і регіон бакетів. Ключі не використовуємо – буде ServiceAccount.

Приклад візьмемо з , rails.s3.yaml, див. connection:

provider: AWS
region: eu-central-1

Створюємо Secret:

[simterm]

$ kk -n gitlab-cluster-test create secret generic gitlab-rails-storage --from-file=connection=rails.s3.yaml

[/simterm]

І описуємо appConfig, виходить так:

appConfig:
  enableUsagePing: false
  enableSeatLink: true # disable?
  object_store:
    enabled: true
    proxy_download: true
    connection:  
      secret: gitlab-rails-storage
      key: connection
  artifacts:
    bucket: or-gitlab-artifacts-test
  lfs:
    bucket: or-git-lfs-test
  packages:
    bucket: or-gitlab-packages-test
  uploads:
    bucket: or-gitlab-uploads-test
  externalDiffs:
    bucket: or-gitlab-mr-diffs-test
  terraformState:
    bucket: or-gitlab-terraform-state-test
  ciSecureFiles:
    bucket: or-gitlab-ci-secure-files-test
  dependencyProxy:
    bucket: or-gitlab-dependency-proxy-test
  backups:
    bucket: or-gitlab-backups-test
    tmpBucket: or-gitlab-tmp-test

serviceAccount

Див. Kubernetes: ServiceAccount з AWS IAM Role для Kubernetes Pod.

Начебто можна без створення ServiceAccount, просто через анотації до сервісів – IAM roles for AWS when using the GitLab chart.

Але ми будемо використовувати ServiceAccount, роль із політикою вже робили – додаємо:

serviceAccount:
  enabled: true
  create: true
  annotations: 
    eks.amazonaws.com/role-arn: arn:aws:iam::514***799:role/S3GitlabClusterTest

Так, з globals начебто все.

registry

Defining the Registry Configuration

Тут треба налаштувати storage, який зберігається в Secret, і в якому теж необхідно вказати ім’я корзини: параметр globals.registry буде використовуватися для бекапів, а параметр тут – самим сервісом Registry, див Docker Registry images .

Для прикладу візьмемо файл registry.s3.yaml, але без ключів, тому що для Registry буде створено свій ServiceAccoumt з IAM Role:

s3:
  bucket: or-gitlab-registry-test
  region: eu-central-1
  v4auth: true

Створюємо секрет:

[simterm]

$ kk -n gitlab-cluster-test create secret generic registry-storage --from-file=config=registry.s3.yaml

[/simterm]

Описуємо конфіг:

registry:
  enabled: true
  service:
    type: NodePort
  storage:
    secret: registry-storage
    key: config

gitlab – Services

Окремо описуємо Services для kaswebservice та gitlab-shell, з того ж прикладу alb-full.yaml.

Для gitlab-shell Service в аннотації external-dns.alpha.kubernetes.io/hostname вказуємо ім’я хоста:

gitlab:
  kas:
    enabled: true
    ingress:
      # Specific annotations needed for kas service to support websockets
      annotations:
        alb.ingress.kubernetes.io/healthcheck-path: /liveness
        alb.ingress.kubernetes.io/healthcheck-port: "8151"
        alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
        alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=4000,routing.http2.enabled=false
        alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=86400
        alb.ingress.kubernetes.io/target-type: ip
        kubernetes.io/tls-acme: "true"
        nginx.ingress.kubernetes.io/connection-proxy-header: "keep-alive"
        nginx.ingress.kubernetes.io/x-forwarded-prefix: "/path"
    # k8s services exposed via an ingress rule to an ELB need to be of type NodePort
    service:
      type: NodePort
  webservice:
    enabled: true
    service:
      type: NodePort
  # gitlab-shell (ssh) needs an NLB
  gitlab-shell:
    enabled: true
    service:
      annotations:
        external-dns.alpha.kubernetes.io/hostname: "gitlab-shell-test.internal.example.com"
        service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
        service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
        service.beta.kubernetes.io/aws-load-balancer-type: "external"
      type: LoadBalancer

Інше

Відключаємо непотрібні нам сервіси:

certmanager:
  install: false

postgresql:
  install: false

gitlab-runner:
  install: false

nginx-ingress:
  enabled: false

Повний values.yaml

В результаті отримуємо такий конфіг:

global:

  hosts:
    domain: internal.example.com
    hostSuffix: test
    ssh: gitlab-shell-test.internal.example.com

  ingress:
    # Common annotations used by kas, registry, and webservice
    annotations:
      alb.ingress.kubernetes.io/backend-protocol: HTTP
      alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-central-1:514***799:certificate/7227a8fa-1124-441c-81d7-ec168180190d
      alb.ingress.kubernetes.io/group.name: gitlab
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-group-attributes: load_balancing.algorithm.type=least_outstanding_requests
      alb.ingress.kubernetes.io/target-type: ip
      kubernetes.io/ingress.class: alb
      nginx.ingress.kubernetes.io/connection-proxy-header: "keep-alive"
    class: none
    configureCertmanager: false
    enabled: true
    path: /*
    pathType: ImplementationSpecific
    provider: aws
    tls:
      enabled: false

  psql:
    host: devops-gitlab-cluster-test-psql
    database: gitlabhq_test
    username: postgres
    password:
      useSecret: true
      secret: postgres.devops-gitlab-cluster-test-psql.credentials.postgresql.acid.zalan.do
      key: password

  registry:
    bucket: or-gitlab-registry-test

  minio:
    enabled: false

  grafana:
    enabled: true

  appConfig:
    enableUsagePing: false
    enableSeatLink: true # disable?
    object_store:
      enabled: true
      proxy_download: true
      connection:
        secret: gitlab-rails-storage
        key: connection
    artifacts:
      bucket: or-gitlab-artifacts-test
    lfs:
      bucket: or-git-lfs-test
    packages:
      bucket: or-gitlab-packages-test
    uploads:
      bucket: or-gitlab-uploads-test
    externalDiffs:
      bucket: or-gitlab-mr-diffs-test
    terraformState:
      bucket: or-gitlab-terraform-state-test
    ciSecureFiles:
      bucket: or-gitlab-ci-secure-files-test
    dependencyProxy:
      bucket: or-gitlab-dependency-proxy-test
    backups:
      bucket: or-gitlab-backups-test
      tmpBucket: or-gitlab-tmp-test

  serviceAccount:
    enabled: true
    create: true
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::514***799:role/S3GitlabClusterTest

  common:
    labels:
      environment: test

gitlab:
  kas:
    enabled: true
    ingress:
      # Specific annotations needed for kas service to support websockets
      annotations:
        alb.ingress.kubernetes.io/healthcheck-path: /liveness
        alb.ingress.kubernetes.io/healthcheck-port: "8151"
        alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
        alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=4000,routing.http2.enabled=false
        alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=86400
        alb.ingress.kubernetes.io/target-type: ip
        kubernetes.io/tls-acme: "true"
        nginx.ingress.kubernetes.io/connection-proxy-header: "keep-alive"
        nginx.ingress.kubernetes.io/x-forwarded-prefix: "/path"
    # k8s services exposed via an ingress rule to an ELB need to be of type NodePort
    service:
      type: NodePort
  webservice:
    enabled: true
    service:
      type: NodePort
  # gitlab-shell (ssh) needs an NLB
  gitlab-shell:
    enabled: true
    service:
      annotations:
        external-dns.alpha.kubernetes.io/hostname: "gitlab-shell-test.internal.example.com"
        service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
        service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
        service.beta.kubernetes.io/aws-load-balancer-type: "external"
      type: LoadBalancer

registry:
  enabled: true
  service:
    type: NodePort
  storage:
    secret: registry-storage
    key: config

certmanager:
  install: false

postgresql:
  install: false

gitlab-runner:
  install: false

nginx-ingress:
  enabled: false

Gitlab deploy

Ну і начебто все? Давайте деплоїти:

[simterm]

$ helm repo add gitlab https://charts.gitlab.io/
$ helm repo update
$ helm upgrade --install --namespace gitlab-cluster-test gitlab gitlab/gitlab --timeout 600s -f gitlab-cluster-test-values.yaml

[/simterm]

Перевіряємо Ingresses:

[simterm]

$ kk -n gitlab-cluster-test get ingress
NAME                        CLASS    HOSTS                                ADDRESS                                                           PORTS   AGE
gitlab-grafana-app          <none>   gitlab-test.internal.example.com     k8s-***.eu-central-1.elb.amazonaws.com   80      117s
gitlab-kas                  <none>   kas-test.internal.example.com        k8s-***.eu-central-1.elb.amazonaws.com   80      117s
gitlab-registry             <none>   registry-test.internal.example.com   k8s-***.eu-central-1.elb.amazonaws.com   80      117s
gitlab-webservice-default   <none>   gitlab-test.internal.example.com     k8s-***.eu-central-1.elb.amazonaws.com   80      117s

[/simterm]

Відкриваємо URL https://gitlab-test.internal.example.com:

Отримуємо рутовий пароль:

[simterm]

$ kubectl -n gitlab-cluster-test get secret gitlab-gitlab-initial-root-password -ojsonpath='{.data.password}' | base64 --decode ; echo
cV6***y1t

[/simterm]

Логінимося під root:

Перевірка деплою

Не віриться мені, що з першого разу все завелося.

Repository

Перевіряємо роботу з репозиторієм, тобто роботу сервісів Gitaly та GitLab Shell.

Створюємо тестовий репозиторій:

Копіюємо адресу – з субдоменом gitlab-shell-test.internal.example.com, який вказували в конфігах:

Клонуємо:

[simterm]

$ git clone [email protected]:gitlab-instance-da4355a9/test-repo.git
Cloning into 'test-repo'...
The authenticity of host 'gitlab-shell-test.internal.example.com (3.***.***.79)' can't be established.
ED25519 key fingerprint is SHA256:xhC1Q/lduNbg49kGljYUb21YlBBsxrG89xE+iCHD+xc.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'gitlab-shell-test.internal.example.com' (ED25519) to the list of known hosts.
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.

[/simterm]

І зміст:

[simterm]

$ ll test-repo/
total 8
-rw-r--r-- 1 setevoy setevoy 6274 Feb  4 13:24 README.md

[/simterm]

Спробуємо пушнути якісь зміни назад:

[simterm]

$ cd test-repo/
$ echo test > test.txt
$ git add test.txt
$ git commit -m "test"
$ git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 16 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 285 bytes | 285.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To gitlab-shell-test.internal.example.com:gitlab-instance-da4355a9/test-repo.git
   7217947..4eb84db  main -> main

[/simterm]

І в WebUI:

Окей, працює.

Container Registry

Далі перевіримо Registry – приклади команд є у веб-інтерфейсі > Container Registry :

Логінимося з тим же логіном:паролем, які використовували для логіна в веб-інтерфейс GitLab (користувач root, пароль з секрету gitlab-gitlab-initial-root-password):

[simterm]

$ docker login registry-test.internal.example.com
Username: root
Password:
...
Login Succeeded

[/simterm]

Створюємо Dockerfile:

FROM busybox
RUN echo "hello world"

Збираємо образ, тегаємо з ім’ям нашого Registry:

[simterm]

$ docker build -t registry-test.internal.example.com/gitlab-instance-da4355a9/test-repo:1 .

[/simterm]

І пушимо:

[simterm]

$ docker push registry-test.internal.example.com/gitlab-instance-da4355a9/test-repo:1  
The push refers to repository [registry-test.internal.example.com/gitlab-instance-da4355a9/test-repo]
b64792c17e4a: Mounted from gitlab-instance-da4355a9/test 
1: digest: sha256:eb45a54c2c0e3edbd6732b454c8f8691ad412b56dd10d777142ca4624e223c69 size: 528

[/simterm]

Перевіряємо корзину or-gitlab-registry-test:

[simterm]

$ aws --profile internal s3 ls or-gitlab-registry-test/docker/registry/v2/repositories/gitlab-instance-da4355a9/test-repo/_manifests/tags/1/
                           PRE current/
                           PRE index/

[/simterm]

Окей – Registry працює, доступ до S3 є, з ServiceAccounts проблем начебто немає.

ServiceAccounts та S3 access

Але про всяк випадок перевіримо ServiceAccounts та доступ до S3 в інших сервісах, наприклад з поду Toolbox:

[simterm]

$ kk -n gitlab-cluster-test exec -ti gitlab-toolbox-565889b874-vgqcb -- bash
git@gitlab-toolbox-565889b874-vgqcb:/$ aws s3 ls or-gitlab-registry-test
                           PRE docker/

[/simterm]

Окей – тут також все працює.

Grafana

Перевіряємо Grafana – https://gitlab-test.internal.example.com/-/grafana/.

Логін root, пароль беремо із секрету:

[simterm]

$ kubectl -n gitlab-cluster-test get secret gitlab-grafana-initial-password -ojsonpath='{.data.password}' | base64 --decode ; echo
Cqc***wFS

[/simterm]

Працює. Дашборди треба підключати окремо, потім займемося, як дійдемо до моніторингу взагалі, поки див. Grafana JSON Dashboards .

Загалом, начебто все.

Подивимося, що далі – адміністрування, міграція проектів, моніторинг, секьюріті.

Loading

GitLab: компоненти, архітектура, інфраструктура та запуск з Helm-чарту в Minikube
0 (0)

2 Лютого 2023

Оскільки GitLab нещодавно змінив політику надання Free-доступу, і тепер по Free підписці буде доступно лише 5 користувачів, то вирішили ми переїжджати на self-hosted версію.

Взагалі з ліцензією у них цікаво: ціна залежить від кількості користувачів, купити можна щонайменше на рік, і після покупки зменшити кількість користувачів у ліцензії не можна (але можна збільшити).

GitLab буде жити в Kubernetes, і питань перед запуском багато, тим більше особисто я раніше GitLab взагалі не дуже користувався.

Деплоїти GitLab будемо через ArgoCD, запускати будемо в AWS Elastic Kubernetes Service, для object store використовуємо AWS S3. Але про це все потім, а для початку подивимося що GitLab є “зсередини” і як його взагалі деплоїти.

GitLab Operator vs GitLab Helm chart

Перше – GitLab Operator чи GitLab Helm chart?

Подивився на можливості – не побачив особливої різниці між Оператором та чартом, більше того, Оператор під капотом використовує той самий Helm-чарт.

За можливостями бекапа та відновлення – використовуються аналогічні утіліти та процес однаковий, див. General backup and restore guidance.

А ось в оновленні версії GitLab-інстансу документація Оператора виглядає простіше. Див. Upgrade the GitLab chart vs How the Operator handles GitLab upgrades.

І навіть сам Gitlab.com деплоїться їх чартом:

The largest known GitLab instance is on GitLab.com, which is deployed using our official GitLab Helm chart

Але в документації до Оператора прямо через слово зістрічаються “Experimental” та “Beta“, тому поки що напевно без нього.

Та й у будь-якому випадку все зав’язано на чарт, тож знайомитися будемо в основному з ним та його параметрами.

Архітектура та компоненти GitLab

Для ознайомлення:

Спрощена схема з документації:

Тут:

  • NGINX: приймаємо вхідні з’єднання
  • GitLab Workhorse: реверс-проксі для завантаження та вивантаження файлів, Git push/pull і т.д.
  • Puma: веб-сервер на Ruby, використовується GitLab для API та своїх веб-сторінок
  • Sidekiq: для створення та управління чергами завдань
    • використовує Redis для зберігання інформації про джоби
  • PostgreSQL: зберігання інформації про користувачів, права доступу, метаданих і т.д.
  • GitLab Shell: працює з репозиторіями по SSH та керує ключами доступів
    • звертається до Gitaly для обробки Git-об’єктів
    • надсилає інформацію в Redis для створення завдань у Sidekiq
  • Gitaly: Git RPC server, займається завданнями в репозиторіях, які отримує від GitLab Shell і GitLab веб-додатки

Інфраструктура

Для роботи GitLab потрібні PostgreSQL, Redis, бакети AWS S3 та поштовий сервіс для отримання та надсилання листів.

Helm-чарт за замовчуванням встановлює PostgreSQL та Redis, але для production PostgreSQL рекомендується встановлювати окремо, див. Configure the GitLab chart with an external database та Configure PostgreSQL, хоча ми будемо використовувати PostgreSQL Operator і крутити кластер PostgreSQL у Kubernetes.

Аналогічні вимоги Redis та Gitaly – їх теж бажано запускати не з чарту та не в Kubernetes-кластері. Див. Installing GitLab by using Helm. У нас замість Redis швидше за все буде KeyDB, теж через оператор, також у Kubernetes.

Документація з розгортання Gitaly говорить, що до 500 користувачів достатньо однієї окремої віртуальної машини з самим Gitaly. Якщо планується 1000 і більше користувачів, то рекомендується запускати Gitaly Cluster з Praefect. Див. Gitaly > High Availability. Враховуючи кількість користувачів у нас – не бачу сенсу виносити на окремий EC2, тому будемо деплоїти разом із чартом, потім подивимося на його роботу та ресурси, можливо винесемо на окрему ноду Kubernetes.

Для роботи з object store в чарті встановлюється Minio, для production рекомендується зовнішній сервіс, такий як AWS S3. Див. Configure the GitLab chart with an external object storage.

Оскільки ми в AWS і використовуємо AWS ALB Controller, тож є сенс вимкнути NGINX Controller, див. Customize the GitLab Ingress options.

Є гарний приклад інфраструктури в GitLab on AWS Partner Solution Deployment Guide, але це для прям серйозних масштабів, ми будемо робити простіше. Проте, сама схема цікава і корисна для розуміння загальної концепції при плануванні інфраструктури:

Крім того, є Reference architectures, в яких описані варіанти запуску GitLab під різні навантаження. З них нам можуть бути особливо цікаві Cloud native hybrid, які описують запуск у Kubernetes (hybrid – тому що частину сервісів таки рекомендується запускати не в кластері):

Для LoadBalancer рекомендується least outstanding requests замість стандартного round-robin – треба не забути.

Корисно пройтися по всіх доступних опціях, подивитися що і як можна налаштувати, див. GitLab Helm chart deployment options, займемося цим в наступному пості.

Моніторинг GitLab – окрема тема, дійдемо до неї пізніше, поки див. Monitoring GitLab.

Запуск GitLab в Minikube

Взагалі використовується для розробників, які пиляють фічі під Kubernetes, але ми використовуємо для знайомства з чартом GitLab. Див. Developing for Kubernetes with minikube.

Запускаємо Minikube:

[simterm]

$ minikube start --cpus 4 --memory 10240

[/simterm]

Включаємо Ingress плагін:

[simterm]

$ minikube addons enable ingress

[/simterm]

Клонуємо репозиторій та встановлюємо залежності – корисно на них подивитися, хоча вони описані в документації до чарту:

[simterm]

$ git clone https://gitlab.com/gitlab-org/charts/gitlab.git
$ cd gitlab
$ helm dependency update
...
Dependency gitlab did not declare a repository. Assuming it exists in the charts directory
Dependency certmanager-issuer did not declare a repository. Assuming it exists in the charts directory
Dependency minio did not declare a repository. Assuming it exists in the charts directory
Dependency registry did not declare a repository. Assuming it exists in the charts directory
Downloading cert-manager from repo https://charts.jetstack.io/
Downloading prometheus from repo https://prometheus-community.github.io/helm-charts
Downloading postgresql from repo https://raw.githubusercontent.com/bitnami/charts/eb5f9a9513d987b519f0ecd732e7031241c50328/bitnami
Downloading gitlab-runner from repo https://charts.gitlab.io/
Downloading grafana from repo https://grafana.github.io/helm-charts
Downloading redis from repo https://raw.githubusercontent.com/bitnami/charts/eb5f9a9513d987b519f0ecd732e7031241c50328/bitnami
Dependency nginx-ingress did not declare a repository. Assuming it exists in the charts directory
...

[/simterm]

Перевіряємо IP Мінікуба:

[simterm]

$ minikube ip
192.168.49.2

[/simterm]

І встановлюємо з values-minikube.yaml:

[simterm]

$ helm upgrade --install gitlab . --timeout 600s -f https://gitlab.com/gitlab-org/charts/gitlab/raw/master/examples/values-minikube.yaml --set global.hosts.domain=$(minikube ip).nip.io --set global.hosts.externalIP=$(minikube ip)

[/simterm]

Перевіряємо поди – тут прям багато всього:

[simterm]

$ kubectl get pod
NAME                                          READY   STATUS      RESTARTS      AGE
gitlab-gitaly-0                               1/1     Running     0             11m
gitlab-gitlab-exporter-5c8dbdc954-hr7jj       1/1     Running     0             11m
gitlab-gitlab-runner-7c4488ff58-bg8f5         0/1     Running     3 (39s ago)   8m30s
gitlab-gitlab-shell-7f9f5bb9ff-qlpxr          1/1     Running     0             11m
gitlab-gitlab-shell-7f9f5bb9ff-wc9rx          1/1     Running     0             11m
gitlab-kas-84cb5c548b-jbp69                   1/1     Running     0             11m
gitlab-kas-84cb5c548b-jxqqr                   1/1     Running     0             11m
gitlab-migrations-2-vw5jd                     0/1     Completed   0             8m30s
gitlab-minio-74467697bb-z8nms                 1/1     Running     0             11m
gitlab-minio-create-buckets-2-b6zl6           0/1     Completed   0             8m30s
gitlab-postgresql-0                           2/2     Running     0             11m
gitlab-prometheus-server-6bf4fffc55-6xpfm     2/2     Running     0             11m
gitlab-redis-master-0                         2/2     Running     0             11m
gitlab-registry-cd64f65dc-frsmt               1/1     Running     0             8m30s
gitlab-registry-cd64f65dc-nvfv2               1/1     Running     0             8m10s
gitlab-sidekiq-all-in-1-v2-59ccd7d6b9-9mpmz   1/1     Running     0             8m30s
gitlab-toolbox-6586c478f5-ktj5x               1/1     Running     0             7m58s
gitlab-webservice-default-6787f4b5db-kfp9h    2/2     Running     0             6m55s
gitlab-webservice-default-6787f4b5db-q62f9    2/2     Running     0             8m30s

[/simterm]

Сервіси:

[simterm]

$ kubectl get svc
NAME                         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                               AGE
gitlab-gitaly                ClusterIP   None             <none>        8075/TCP,9236/TCP                     11m
gitlab-gitlab-exporter       ClusterIP   10.110.108.219   <none>        9168/TCP                              11m
gitlab-gitlab-shell          NodePort    10.110.231.31    <none>        32022:32022/TCP                       11m
gitlab-kas                   ClusterIP   10.102.124.129   <none>        8150/TCP,8153/TCP,8154/TCP,8151/TCP   11m
gitlab-minio-svc             ClusterIP   10.99.213.94     <none>        9000/TCP                              11m
gitlab-postgresql            ClusterIP   10.102.38.18     <none>        5432/TCP                              11m
gitlab-postgresql-headless   ClusterIP   None             <none>        5432/TCP                              11m
gitlab-postgresql-metrics    ClusterIP   10.107.123.35    <none>        9187/TCP                              11m
gitlab-prometheus-server     ClusterIP   10.104.133.44    <none>        80/TCP                                11m
gitlab-redis-headless        ClusterIP   None             <none>        6379/TCP                              11m
gitlab-redis-master          ClusterIP   10.96.133.252    <none>        6379/TCP                              11m
gitlab-redis-metrics         ClusterIP   10.105.193.58    <none>        9121/TCP                              11m
gitlab-registry              ClusterIP   10.109.96.193    <none>        5000/TCP                              11m
gitlab-webservice-default    ClusterIP   10.108.231.229   <none>        8080/TCP,8181/TCP,8083/TCP            11m
kubernetes                   ClusterIP   10.96.0.1        <none>        443/TCP                               23m

[/simterm]

Заходимо на https://gitlab.192.168.49.2.nip.io:

Знаходимо пароль:

[simterm]

$ kubectl get secret gitlab-gitlab-initial-root-password -ojsonpath='{.data.password}' | base64 --decode ; echo
yNU***njU

[/simterm]

І логінімося під юзером root:

Давайте подивимося, що ми тут надеплоїли:

  • gitaly – знаємо
  • gitlab-exporter – збір метрик GitLab для його Prometheus
  • gitlab-runner – воркери для CI/CD
  • gitlab-shell – вже читали вище
  • kas – Kubernetes agent server для Gitlab Agent, “GitLab Agent for Kubernetes is an active in-cluster component for solving any GitLab<->Kubernetes integration tasks” – подивимось на нього уважніше іншим разом
  • migrations – джоби для роботи з міграціями бази даних
  • minioHigh Performance Object Storage, API compatible with Amazon S3 – використовується, коли немає S3
  • postgresql – база даних, читали
  • prometheus-server – моніторинг GitLab
  • redis-master – Редіс)
  • registry – Container Registry для зберігання образів
  • sidekiq – вже читали
  • toolbox – бекапи та інші утіліти
  • webservice-defaultGitLab Rails webserver

Подивимося, що є всередині – переходимо до Адмінки:

Ммм… Смачно) І багато. З адмініструванням GitLab треба буде розбиратися окремо.

Поки можна приступати до вивчення чарту, і пробувати його деплоїти в Kubernetes – див. GitLab: Helm-чарт values, залежності та деплой у Kubernetes з AWS S3.

Loading

Kubernetes: моніторинг вартості кластеру – Kubernetes Resource Report та Kubecost
0 (0)

23 Січня 2023

Дуже правильне діло – моніторити, наскільки ефективно використовується кластер, особливо, якщо ресурси деплояться розробниками, які не сильно вникають у requests, і встановлюють завищені значення “про запас”. Запас, звичайно, потрібен, але й просто так реквестити ресурси ідеї погана.

Наприклад, у вас є WorkerNode з 4 vCPU (4000 milicpu) та 16 ГБ оперативної пам’яті, і ви створюєте Kubernetes Deployment, у якому для подів задаєте CPU requests 2500m і 4 Гб пам’яті. Після запуску одного пода – він зареквестить більше половини доступного процесорного часу, і під час запуску другого поду Kubernetes повідомить про нестачу ресурсів на доступних нодах, що призведе до запуску ще одного WorkerNode, який, зрозуміло, відразиться на загальній вартості кластера.

Щоб уникнути цього, є кілька утиліт, таких як Kubernetes Resource Report та Kubecost.

Kube Resource Report

Kubernetes Resource Report – найпростіший у запуску та можливостях: просто виводить ресурси групуючи їх за типом, і відображає статистику – скільки CPU/MEM requested і скільки реально використовується.

Мені вона подобається саме простотою – просто запускаємо, раз в пару тижнів дивимося що відбувається в кластері, і за необхідності пінгуємо розробників із питанням “А вам насправді потрібно 100500 гіг пам’яті для цієї апки? »

Є Helm-чарт, але він оновлюється рідко, тому простіше встановити з маніфестів.

Створюємо Namespace:

[simterm]

$ kk create ns kube-resource-report
namespace/kube-resource-report created

[/simterm]

Завантажуємо репозиторій з kube-resource-report:

[simterm]

$ git clone https://codeberg.org/hjacobs/kube-resource-report
$ cd kube-resource-report/

[/simterm]

У каталозі  deploy вже є файл Kustomize – задамо в ньому деплой в наш неймспейс:

[simterm]

$ echo "namespace: kube-resource-report" >> deploy/kustomization.yaml

[/simterm]

Перевіряємо:

[simterm]

$ cat deploy/kustomization.yaml 
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - rbac.yaml
  - service.yaml
  - configmap.yaml
namespace: kube-resource-report

[/simterm]

Встановлюємо:

[simterm]

$ kubectl apply -k deploy/
serviceaccount/kube-resource-report created
clusterrole.rbac.authorization.k8s.io/kube-resource-report created
clusterrolebinding.rbac.authorization.k8s.io/kube-resource-report created
configmap/kube-resource-report created
service/kube-resource-report created
deployment.apps/kube-resource-report created

[/simterm]

Відкриваємо собі доступ до Service:

[simterm]

$ kubectl -n kube-resource-report port-forward svc/kube-resource-report 8080:80

[/simterm]

Відкриваємо репорт в браузері:

Далі переходимо, наприклад, до Namespaces, сортуємо колонки за CR (CPU Requested):

При наведенні курсора на повзунок Kube Resource Report підкаже оптимальне з його точки зору значення.

Далі думаємо самі – чи дійсно потрібно стільки requests, чи його можна зменшити.

У цьому випадку у нас Apache Druid з 16 подами, в кожній працює JVM, яка і процесор і пам’ять любить, і для Druid процесор бажано виділяти по одному ядру на кожен thread виконання Java, тому ОК – нехай буде 14,65 процесора.

Kubecost

Kubecost – це Kube Resource Report на стероїдах. Вміє рахувати трафік, відправляти алерти, генерує метрики для Prometheus, має свої дашборди Grafana, може підключатися до кількох кластерів Kubernetes та багато іншого.

Місцями не без багів, але в цілому штука приємна.

Правда, вартість бізнес-ліцензії в 499 доларів трохи завищена, як на мене. Втім для базових речей цілком доступна Free версія.

“Під капотом” використовує свій Prometheus для зберігання даних. Можна відключити і використовувати зовнішній, але не рекомендується .

Installation

Основні доступні параметри описані в документації на Github , також можна переглянути дефолтні values його Helm-чарту.

Потребує реєстрації для отримання ключа – заходимо на https://www.kubecost.com/install.html , вказуємо пошту – і відразу переадресує на інструкцію з вашим ключем:

Helm chart values

Поспішати з установкою не будемо – спочатку створимо свої values.

Якщо ви вже маєте Kube Prometheus Stack, є Grafana і NodeExporter – то в Kubecost їх має сенс відключити. Крім того, відключимо kube-state-metrics, що б не дублювали дані в моніторингу.

Що б Prometheus із нашого власного стека почав збирати метрики Kubecost – налаштуємо створення ServiceMonitor і додамо йому labels – тоді можна буде генерувати свої алерти та використовувати Grafana dashboard.

А ось якщо відключити запуск вбудованої Grafana – под с kubecost-cost-analyzer не стартує. Не знаю – бага чи фіча. Але в ній є свої дашборды які можуть бути корисними, тож можна залишити.

Ще можна включити networkCosts, але мені так і не вдалося побачити адекватних витрат на трафік – можливо, неправильно налаштував.

networkCosts може бути достатньо прожорливим по ресурсам – треба моніторити використання ЦПУ.

Власне, сам values.yaml:

kubecostToken: "c2V***f98"

kubecostProductConfigs:
  clusterName: development-qa-data-services

prometheus:
  kube-state-metrics:
    disabled: true
  nodeExporter:
    enabled: false
  serviceAccounts:
    nodeExporter:
      create: false

serviceMonitor:
  enabled: true
  additionalLabels:
    release: prometheus

networkCosts:
  enabled: true
  podMonitor:
    enabled: true
  config:
    services:
      amazon-web-services: true

Встановлюємо в Namespace kubecost:

[simterm]

$ helm repo add kubecost https://kubecost.github.io/cost-analyzer/
$ helm upgrade --install -n kubecost --create-namespace -f values.yaml kubecost kubecost/cost-analyzer

[/simterm]

Відкриваємо порт:

[simterm]

$ kubectl -n kubecost port-forward svc/kubecost-cost-analyzer 9090:9090

[/simterm]

Перевіряємо поды – чи запустився kubecost-cost-analyzer:

[simterm]

$ kubectl -n kubecost get pod
NAME                                         READY   STATUS    RESTARTS   AGE
kubecost-cost-analyzer-5f5b85bf59-f22ld      2/2     Running   0          59s
kubecost-grafana-6bd995d6f9-kslh2            2/2     Running   0          63s
kubecost-network-costs-22dps                 1/1     Running   0          64s
kubecost-network-costs-m7rf5                 1/1     Running   0          64s
kubecost-network-costs-tcdvn                 1/1     Running   0          64s
kubecost-network-costs-xzvsz                 1/1     Running   0          64s
kubecost-prometheus-server-ddb597d5c-dvrgc   2/2     Running   0          6m49s

[/simterm]

Відкриваємо доступ до kubecost-cost-analyzer Service:

[simterm]

$ kubectl -n kubecost port-forward svc/kubecost-cost-analyzer 9090:9090

[/simterm]

І переходимо на http://localhost:9090.

Тут скрин з Kubecost, який вже майже тиждень запущено на одному з наших кластерів:

Коротко оглянемо основні пункти меню.

Assets

Для розуміння витрат краще почати з пункту Assets , де виводиться вартість “заліза”:

Бачимо, що в день наш кластер коштує 43 долари.

Можна “провалитися” глибше в деталі кластера, і побачити розбивку по ресурсам – WorkerNodes, лоад-балансерам, дискам та вартість самого AWS Elastic Kubernetes Service:

Переходимо ще далі, в Nodes:

І дивимось деталі вартості по конкретній ноді:

Перевіряємо:

0.167 за годину, як Kubecost і репортить в Hourly Rate.

Для налаштування витрат на AWS Spot Instances – див. Spot Instance data feed та AWS Spot Instances.

Cost Allocation

Відображає куди в самому Кубері витрачаються ресурси:

Kubecost враховує вартість CPU та RAM на WorkerNodes і, відповідно, виводить вартість кожного неймспейса залежно від його requests і usage.

Див. Pod resource efficiency.

Тут наш Apache Druid має реквестів CPU на цілих 48 долларів за тиждень або 4.08 в день.

Переходимо далі, і маємо картину по конкретним контролерам – StatefulSet, Deployment:

Колонки тут:

  • CPU, RAM: вартість використовуваних ресурсів в залежності від вартості ресурсів WorkerNode
  • PV: вартість PersistentVolume у вибраному контролері, тобто для StaefulSet MiddleManager маємо PV, котрий являє собою AWS EBS, за який ми платимо гроші
  • Network: треба перевіряти, бо якось дивно рахує – дуже мало
  • LB: LoadBalancers по вартості в AWS
  • Shared: загальні ресурси, які не будут рахуватися окремо, наприклад неймспейс kube-system, налаштовується в http://localhost:9090/settings > Shared Cost
  • Efficiency: утилізація vs реквесты за формулою:
    ((CPU Usage / CPU Requested) * CPU Cost) + (RAM Usage / RAM Requested) * RAM Cost) / (RAM Cost + CPU Cost))
    основний показник ефективності ресурсу, див. Efficiency and Idle

Якщо перейти ще глибше – буде посилання на Grafana, де можна переглянути використання ресурсів конкретним подом:

Правда, “з коробки” не відображаються метрики в RAM Requested.

Для перевірки метрик можна зайти на локальний Pometheus:

[simterm]

$ kubectl -n kubecost port-forward svc/kubecost-prometheus-server 9091:80

[/simterm]

І так – kube_pod_container_resource_requests_memory_bytes пуста:

Тому що метрика теперь називаєтся kube_pod_container_resource_requests з resource="memory", треба оновити запрос в цій Графані:

avg(kube_pod_container_resource_requests{namespace=~"$namespace", pod="$pod", container!="POD", resource="memory"}) by (container)

__idle__

Витрати __idle__ – різниця між вартістю ресурсів виділених під існуючі об’єкти (поди, деплойменти) – їх реквести і реальний usage, та “заліза, що простоює”, на якому вони працюють, тобто не зайняті ЦПУ/пам’ять, які можна використовувати під запуск нових ресурсів.

Savings

Тут зібрані поради щодо оптимізації витрат:

Наприклад, у “Right-size your container requests” зібрані рекомендації щодо налаштувань реквестів для ресурсів – аналог репортів у Kubernetes Resource Report:

Глянемо той же Apache Druid:

Тут явний овер-реквест по CPU, і Kubecost рекомендує зменшити ці реквести:

Але про Druid вже писалося вище – JVM, на кожен под MiddleManager ми запускаємо один Supervisor із двома Tasks, а під кожну Task бажано виділяти по повному ядру. Тож залишаємо, як є.

Корисна штука “Delete unassigned resources” – у нас, наприклад, знайшлася пачка EBS, що не використовуються:

Health

Теж корисна штука, що відображає основні проблеми із кластером:

kubecost-network-costs активно використовує CPU, вище своїх реквестів, і Kubernetes його тротлить.

Alerts

Тут можемо налаштувати алерти, але мені вдалося налаштувати відправку тільки через Slack Webhook:

Документація – Alerts.

Prometehus Alertmanager можна налаштувати через values ​​, але використовується свій локальний, який запускається разом з Prometheus, а як йому налаштувати роути – не знайшов.

Приклад алерту, який можна налаштувати в Kubecost:

global:
  notifications:
    alertConfigs:
      alerts:
        - type: budget
          threshold: 1
          window: 1d
          aggregation: namespace
          filter: druid
    alertmanager:
      enabled: true
      fqdn: http://prometheus-kube-prometheus-alertmanager.monitoring.svc

Тут додаємо алерт з типом budget, в якому перевіряємо вартість неймспейсу druid за останній день, і алертимо, якщо він стає дорожчим за 1 долар.

Оновлюємо сетап:

[simterm]

$ helm upgrade --install -n kubecost -f values.yaml kubecost kubecost/cost-analyzer

[/simterm]

Алерт з’являється у списку:

Але на кнопку Test не реагує, і в локальному Alertmanager алерт не з’являється.

Slack webhook

Спробуємо через Slack webhook, документація тут>>>.

Створюємо Application:

Переходимо в Webhooks:

Активуємо та натискаємо Add New Webhook:

Вибираємо канал:

Додаємо URL у Kubecost, і тестуємо:

Final values.yaml

Зрештою для тесту зібрав такий values:

kubecostToken: "c2V***f98"
kubecostProductConfigs:
  clusterName: development-qa-data-services
global:
  notifications:
    alertConfigs:
      globalSlackWebhookUrl: https://hooks.slack.com/services/T03***c1f
      alerts:
        - type: assetBudget
          threshold: 30
          window: 1d
          aggregation: type
          filter: 'Node'

        - type: assetBudget
          threshold: 4
          window: 1d
          aggregation: type
          filter: 'LoadBalancer'

        - type: assetBudget
          threshold: 3
          window: 1d
          aggregation: type
          filter: 'Disk'

        - type: assetBudget
          threshold: 40
          window: 3d
          aggregation: cluster
          filter: 'development-qa-data-services'

        - type: spendChange
          relativeThreshold: 0.01  # change relative to baseline average cost. Must be greater than -1 (can be negative).
          window: 1d
          baselineWindow: 7d       # previous window, offset by window
          aggregation: namespace
          filter: default, druid
        
        - type: spendChange
          relativeThreshold: 0.01
          window: 1d
          baselineWindow: 7d
          aggregation: cluster
          filter: 'development-qa-data-services'
        
        - type: health              # Alerts when health score changes by a threshold
          window: 10m
          threshold: 1
prometheus:
  kube-state-metrics:
    disabled: true
  nodeExporter:
    enabled: false
  serviceAccounts:
    nodeExporter:
      create: false

#serviceMonitor:
#  enabled: true
#  additionalLabels:
#    release: prometheus

networkCosts:
  enabled: true
  podMonitor:
    enabled: true
  config:
    destinations:
      direct-classification:
      - region: "us-west-2"
        zone: "us-west-2c"
        ips:
          - "10.0.64.0/19"
          - "10.0.160.0/20"
          - "10.0.208.0/21"
      - region: "us-west-2"
        zone: "us-west-2d"
        ips:
          - "10.0.216.0/21"
          - "10.0.96.0/19"
          - "10.0.176.0/20"
    services:
      amazon-web-services: true

Тут тестові алерти, які можна буде в принципі тягнути у продакшен.

ServiceMonitor для отримання метрик у зовнішньому Prometheus відключив, бо сенсу поки що не бачу – алертити буде через Slack Webhook своїми алертами, а дашборда для Grafana у вбудованій Графані краща, і їх там кілька.

Додав direct-classification для networkCosts – подивимося, можливо покаже більш правильні дані щодо трафіку.

#TODO

З чим поки що не вдалося розібратися:

  • алерти через Alertmanager (можливо, має сенс таки спробувати відключити внутрішній Prometheus)
  • Kubecost не бачить Node Exporter (перевіряти на сторінці http://localhost:9090/diagnostics ), але це наче ні на що не впливає – основні метрики отримує від cAdvisor
  • витрати на нетворкінг занадто маленькі (але це не точно)

Не робив/не тестував:

  • не налаштовував Cost Usage Reports для AWS , див. AWS Cloud Integration
  • не налаштовував AWS Spot Instances прайсинг
  • не додав Ingress, тому що у нас AWS ALB Controller, і треба робити авторизацію, а SAML в Kubecost доступний тільки в Premium

В цілому, на цьому все.

Система цікава та корисна, але є баги та складності, з якими треба розбиратися.

Loading

Kustomize: робота з маніфестами Kubernetes – огляд, приклади
5 (2)

15 Січня 2023

Kustomizeсистема управління конфігураціями (configuration management tool) для Kubernetes, що дозволяє використовувати загальні набори маніфестів, які можуть бути змінені для кожного конкретного оточення/кластера, і може бути альтернативою шаблонам Helm (або доповнювати його).

Загальна концепція Kustomize – “where, what, and how” – “де, що і як”:

  • “де” – це наш базовий маніфест, наприклад deployment.yaml
  • “що” – що саме в маніфесті мінятимемо, наприклад кількість подів (replicas) у цьому деплойменті
  • “як” – файли конфігуарації Kustomize – kustomization.yaml

Огляд Kustomize

Для простого прикладу візьмемо файл kustomization.yaml з таким змістом:

resources:
- deployment.yaml
- service.yaml
namePrefix: dev-
namespace: development
commonLabels:
  environment: development

Тут описується, що потрібно взяти ресурси описані у файлах deployment.yaml та service.yaml, до імені кожного створюваного ресурсу додати префікс dev- ( namePrefix), деплоїти в namespace development , і додати labels environment: development.

Всі опції в Kustomize Feature List.

Крім того, Kustomize зручний для створення конфігурацій із загальних файлів, але для різних оточень.

У такому випадку використовується каталог overlays зі своїм набором kustomization.yaml:

Починаючи з версії 1.14, Kustomize вбудований у kubectl:

[simterm]

$ kubectl kustomize --help
Build a set of KRM resources using a 'kustomization.yaml' file. The DIR argument must be a path to a directory
containing 'kustomization.yaml', or a git repository URL with a path suffix specifying same with respect to the
repository root. If DIR is omitted, '.' is assumed.

Examples:
  # Build the current working directory
  kubectl kustomize
...

[/simterm]

І може використовуватися при apply, щоб спочатку зібрати (build) необхідний маніфест, і відразу відправити його в Kubernetes:

[simterm]

$ kubectl apply --help
...
  # Apply resources from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml
  kubectl apply -k dir/
...

[/simterm]

А з версії 1.16 доступний і в kubeadm.

Крім kuberctl apply, Kustomize можна використовувати для:

  • kubectl get -k отримати ресурс із Kubernetes кластера
  • kubectl describe -k опис ресурсу в Kubernetes кластері
  • kubectl diff -k – порівняти локально згенерований маніфест із ресурсом у кластері
  • kubectl delete -k видалити ресурс із кластера

Деплой з Kustomize

Створюємо тестову директорію:

[simterm]

$ mkdir -p kustomize_example/base
$ cd kustomize_example/

[/simterm]

У каталозі base створимо два файли – в одному опишемо Deployment, в іншому Service:

[simterm]

$ vim -p base/deployment.yaml base/service.yaml

[/simterm]

У файлі deployment.yaml робимо запуск поду з контейнером nginxdemo :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginxdemo
spec:
  selector:
    matchLabels:
      app: nginxdemo
  template:
    metadata:
      labels:
        app: nginxdemo
    spec:
      containers:
        - name: nginxdemo
          image: nginxdemos/hello
          ports:
          - name: http
            containerPort: 80
            protocol: TCP

І файл service.yaml із Сервісом для нього:

apiVersion: v1
kind: Service
metadata:
  name: nginxdemo
spec:
  selector:
    app: nginxdemo
  ports:
  - name: http
    port: 80

Далі, в тому ж каталозі base створюємо kustomization.yaml, в якому описуємо resources з чого ми збиратимемо наш майбутній маніфест для деплою:

resources:
  - deployment.yaml
  - service.yaml

І виконуємо сборку маніфесту:

[simterm]

$ kubectl kustomize base/
apiVersion: v1
kind: Service
metadata:
  name: nginxdemo
spec:
  ports:
  - name: http
    port: 80
  selector:
    app: nginxdemo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginxdemo
spec:
  selector:
    matchLabels:
      app: nginxdemo
  template:
    metadata:
      labels:
        app: nginxdemo
    spec:
      containers:
      - image: nginxdemos/hello
        name: nginxdemo
        ports:
        - containerPort: 80
          name: http
          protocol: TCP

[/simterm]

Або через сам kustomize:

[simterm]

$ kustomize build base/
apiVersion: v1
kind: Service
metadata:
  name: nginxdemo
spec:
...

[/simterm]

Або виконуємо сборку і відразу деплоїмо:

[simterm]

$ kubectl apply -k base/
service/nginxdemo created
deployment.apps/nginxdemo created

[/simterm]

Перевіряємо:

[simterm]

$ kubectl get all -l app=nginxdemo
NAME                             READY   STATUS    RESTARTS   AGE
pod/nginxdemo-7f8f587c74-kbczf   1/1     Running   0          26s

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/nginxdemo-7f8f587c74   1         1         1       26s

[/simterm]

Тепер подивимося, як налаштувати ці Deployment та Service для двох оточень – Dev та Prod.

Kustomize Overlays

Створюємо каталоги overlays/dev та overlays/prod:

[simterm]

$ mkdir -p overlays/{dev,prod}

[/simterm]

Отримуємо таку структуру:

[simterm]

$ tree .
.
|-- base
|   |-- deployment.yaml
|   |-- kustomization.yaml
|   `-- service.yaml
`-- overlays
    |-- dev
    `-- prod

[/simterm]

У каталогах dev та prod створюємо окремі kustomization.yaml, в яких описуємо bases:

bases:
- ../../base

Якщо зараз виконати kustomize build overlays/dev/, то отримаємо маніфест аналогічний до того, який створювали раніше.

Можливості Kustomize

namePrefix

Що б змінити цей маніфест – у файлах kustomization.yaml для Dev і Prod додамо, наприклад, namePrefix:

bases:
- ../../base

namePrefix: dev-

Перевіряємо:

[simterm]

$ kustomize build overlays/dev/
apiVersion: v1
kind: Service
metadata:
  name: dev-nginxdemo
spec:
  ports:
  - name: http
    port: 80
  selector:
    app: nginxdemo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dev-nginxdemo
...

[/simterm]

У полях name з’явився префікс dev .

patchesStrategicMerge

Далі, припустимо, ми хочемо на Dev мати 1 под, а на Prod – 3, тобто мати різні значення для поля replicas.

Використовуємо patchesStrategicMerge.

Створюємо файл патчу – overlays/dev/replicas.yaml. Тип та Ім’я ресурсу, які будемо патчити, повинні збігатися з ресурсом з base:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginxdemo
spec:
  replicas: 1

Аналогічно для Prod – файл overlays/prod/replicas.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginxdemo
spec:
  replicas: 3

У файлах overlays/dev/kustomization.yaml та overlays/prod/kustomization.yaml додаємо patchesStrategicMerge:

bases:
- ../../base

namePrefix: dev-

patchesStrategicMerge:
- replicas.yaml

Запускаємо:

[simterm]

$ kustomize build overlays/dev/
apiVersion: v1
kind: Service
metadata:
  name: dev-nginxdemo
spec:
  ports:
  - name: http
    port: 80
  selector:
    app: nginxdemo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dev-nginxdemo
spec:
  replicas: 1
...

[/simterm]

Деплоїмо:

[simterm]

$ kubectl apply -k overlays/dev/
service/dev-nginxdemo created
deployment.apps/dev-nginxdemo created

$ kubectl apply -k overlays/prod/
service/prod-nginxdemo created
deployment.apps/prod-nginxdemo created

[/simterm]

Перевіряємо:

[simterm]

$ kubectl get all -l app=nginxdemo
NAME                                  READY   STATUS    RESTARTS   AGE
pod/dev-nginxdemo-7f8f587c74-vh2gn    1/1     Running   0          37s
pod/nginxdemo-7f8f587c74-kbczf        1/1     Running   0          104m
pod/prod-nginxdemo-7f8f587c74-dpc76   1/1     Running   0          33s
pod/prod-nginxdemo-7f8f587c74-f5j4f   1/1     Running   0          33s
pod/prod-nginxdemo-7f8f587c74-zqg8z   1/1     Running   0          33s

NAME                                        DESIRED   CURRENT   READY   AGE
replicaset.apps/dev-nginxdemo-7f8f587c74    1         1         1       37s
replicaset.apps/nginxdemo-7f8f587c74        1         1         1       104m
replicaset.apps/prod-nginxdemo-7f8f587c74   3         3         3       33s

[/simterm]

configMapGenerator та secretGenerator

Kustomize також вміє генерувати нові ресурси із шаблонів.

Наприклад візьмемо ConfgiMap для алертів Grafana Loki .

Так як алерти однакові і для Dev, і для Prod – то описуємо configMapGenerator в base/kustomization.yaml:

resources:
  - deployment.yaml
  - service.yaml

configMapGenerator:
- name: loki-ruler-alerts
  files:
  - loki-ruler-alerts.yaml

У каталозі base створюємо сам файл loki-ruler-alers.yaml з контентом майбутнього ConfigMap:

groups:
  - name: systemd-alerts
    rules:
      - alert: Pod killed by OOM Killer
        expr: |
          sum(rate({job="systemd-journal"} |~ ".*OOM-killed.*" | regexp `pod=".*/(?P<pod>[a-zA-Z].*)".*` | pod!="" [15m])) by (pod, hostname) > 0.1
        for: 1s
        labels:
          severity: warning
        annotations:
          description: |-
            *OOM Killer detected in the WorkerNode's systemd-journal logs*
            WorkerNode: {{`{{ $labels.hostname }}`}}

Перевіряємо:

[simterm]

$ kustomize build base/
apiVersion: v1
data:
  loki-ruler-alerts.yaml: |
    groups:
      - name: systemd-alerts
        rules:
          - alert: Pod killed by OOM Killer
            expr: |
              sum(rate({job="systemd-journal"} |~ ".*OOM-killed.*" | regexp `pod=".*/(?P<pod>[a-zA-Z].*)".*` | pod!="" [15m])) by (pod, hostname) > 0.1
            for: 1s
            labels:
              severity: warning
            annotations:
              description: |-
                *OOM Killer detected in the WorkerNode's systemd-journal logs*
                WorkerNode: {{`{{ $labels.hostname }}`}}
kind: ConfigMap
metadata:
  name: loki-ruler-alerts-47678t7d89
---
apiVersion: v1
kind: Service
metadata:
  name: nginxdemo
...

[/simterm]

Крім того, можна згенерувати дані прямо з консолі.

Наприклад, щоб додати у файл base/kustomization.yaml новий Secret – виконуємо kustomize edit add secret:

[simterm]

$ cd base/
$ kustomize edit add secret nginx-password --from-literal=password=12345678

[/simterm]

Перевіряємо:

$ cat kustomization.yaml 
resources:
- deployment.yaml
- service.yaml

configMapGenerator:
- files:
  - loki-ruler-alerts.yaml
  name: loki-ruler-alerts
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
secretGenerator:
- literals:
  - password=12345678
  name: nginx-password
  type: Opaque

generatorOptions

Якщо ми застосовуємо base/kustomization.yaml, то до імен ConfigMap і Secret будуть додані постфікси:

[simterm]

$ kubectl apply -k base/
configmap/loki-ruler-alerts-47678t7d89 created
secret/nginx-password-72mh6dg77t created
service/nginxdemo unchanged
deployment.apps/nginxdemo unchanged

[/simterm]

47678t7d89 и 72mh6dg77t.

Щоб змінити цю поведінку – додаємо generatorOptions з опцією disableNameSuffixHash:

resources:
- deployment.yaml
- service.yaml
  
configMapGenerator:
- files:
  - loki-ruler-alerts.yaml
  name: loki-ruler-alerts

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
secretGenerator:
- literals:
  - password=12345678
  name: nginx-password
  type: Opaque

generatorOptions:
  disableNameSuffixHash: true

Деплоїмо:

[simterm]

$ kubectl apply -k base/
configmap/loki-ruler-alerts created
secret/nginx-password created
service/nginxdemo unchanged
deployment.apps/nginxdemo unchanged

[/simterm]

Тепер у нас такі імена, як ми їх вказали в шаблоні.

Helm && Kustomize

І приклад того, як можемо використовувати разом Helm && Kustomize, наприклад коли у вас є форк чарта, і ви не хочете змінювати дані.

Створюємо каталог хельм-чарту:

[simterm]

$ mkdir -p kustomize-helm

[/simterm]

Генеруємо в ньому чарт:

[simterm]

$ helm create kustomize-helm
Creating kustomize-helm

[/simterm]

Отримуємо структуру стандартного чарту:

[simterm]

$ tree . 
.
|-- kustomize-helm
|   |-- Chart.yaml
|   |-- charts
|   |-- templates
|   |   |-- NOTES.txt
|   |   |-- _helpers.tpl
|   |   |-- deployment.yaml
|   |   |-- hpa.yaml
|   |   |-- ingress.yaml
|   |   |-- service.yaml
|   |   |-- serviceaccount.yaml
|   |   `-- tests
|   |       `-- test-connection.yaml
|   `-- values.yaml
`-- templates

[/simterm]

Якщо виконаємо helm template kustomize-helm, то отримаємо згенеровані шаблони чарту:

[simterm]

$ helm template kustomize-helm
---
# Source: kustomize-helm/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: release-name-kustomize-helm
  labels:
    helm.sh/chart: kustomize-helm-0.1.0
    app.kubernetes.io/name: kustomize-helm
    app.kubernetes.io/instance: release-name
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm
---
# Source: kustomize-helm/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: release-name-kustomize-helm
  labels:
    helm.sh/chart: kustomize-helm-0.1.0
    app.kubernetes.io/name: kustomize-helm
    app.kubernetes.io/instance: release-name
...

[/simterm]

Тепер, щоб не міняти чарт, але створити свій власний Secret – в каталозі kustomize-helm створюємо файл kustomization.yaml, в якому використовуємо resources з файлом helm-all.yaml який згенеруємо за допомогою helm template:

resources:
- helm-all.yaml

secretGenerator:
- literals:
  - password=12345678
  name: nginx-password
  type: Opaque

Запускаємо

[simterm]

$ cd kustomize-helm/
$ helm template . > helm-all.yaml && kustomize build .              
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/instance: release-name
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: kustomize-helm
    app.kubernetes.io/version: 1.16.0
    helm.sh/chart: kustomize-helm-0.1.0
  name: release-name-kustomize-helm
---
apiVersion: v1
data:
  password: MTIzNDU2Nzg=
kind: Secret
metadata:
  name: nginx-password-72mh6dg77t
type: Opaque
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/instance: release-name
...

[/simterm]

Готово.

Loading

BASH: використання циклів FOR, WHILE, UNTIL – приклади
0 (0)

15 Січня 2023

terminalПереклад поста 2013 року з деякими правками, але все ще актуальний для вивчення BASH.

Короткий опис різниці у типах циклів:

  • for – виконуватиме дію доти, доки є об’єкти для виконання (наприклад – читання потоку з stdin, файлу або функції);
  • while – виконує дію доти, доки умова є істинною;
  • until – виконуватиметься до того часу, поки умова стане true, тобто, поки вона false.

Цикл FOR

Розглянемо такий варіант скрипту із циклом for:

#!/bin/bash

for variable in `ls -1`
  do
    echo "$variable"
  done

Синтаксис дуже простий і досить наочно показаний у прикладі:

  • for – запускаємо цикл
  • variable – оголошуємо змінну, над якою виконуватимемо дії
  • in – направляємо циклу потік виконання
  • `ls -1` – команда, яку необхідно виконати і передати в змінну $variable
  • do і done– “тіло” циклу, у межах яких виконуватимуться основні дії над отриманими даними
  • та echo "$variable" – безпосередньо сама дія для виконання циклом

Тепер трохи змінимо приклад, і замість явної вказівки команди застосуємо другу змінну:

#!/bin/bash

ls=`ls -1`
  for variable in $ls
    do
      echo "$variable"
    done

Тепер команда ls -1 передається в окремій змінній, що дозволяє гнучкіше працювати з циклом. Замість змінної у циклі можна використовувати і функцію:

#!/bin/bash

lsl () {
  ls -1
}

for variable in `lsl`
  do
    echo "$variable"
  done

Докладніше про функції у пості BASH: використання функцій, приклади.

Основна умова циклу for – він виконуватиметься доти, поки в переданій йому команді є об’єкти для дії. Виходячи з прикладу вище – допоки в лістингу ls -1 є файли для відображення – цикл передаватиме їх у змінну і виконуватиме “тіло циклу”. Як тільки список файлів у директорії закінчиться – цикл завершить виконання.

Давайте трохи ускладнимо приклад.

У каталозі є список файлів:

[simterm]

$ ls -1
file1
file2
file3
file4
file5
loop.sh
nofile1
nofile2
nofile3
nofile4
nofile5

[/simterm]

Нам необхідно вибрати з них лише ті, які в назві не мають слова “no“:

#!/bin/bash

lsl=`ls -1`

for variable in $lsl
  do
    echo "$variable" | grep -v "no"
  done

Запускаємо:

[simterm]

$ ./loop.sh
file1
file2
file3
file4
file5
loop.sh

[/simterm]

У циклі також можна використовувати умовні вирази (conditional expressions) для перевірки умов та оператор break для переривання циклу у разі спрацювання умови.

Розглянемо такий приклад:

#!/bin/bash

lsl=`ls -1`

for variable in $lsl
  do
    if [ $variable != "loop.sh" ]
      then
        echo "$variable" | grep -v "no"
      else
        break
    fi
  done

Цикл буде виконуватися доти, доки не буде знайдено файл loop.sh. Як тільки виконання циклу дійде до цього файлу – цикл буде перерваний командою break:

[simterm]

$ ./loop.sh
file1
file2
file3
file4
file5

[/simterm]

Ще один приклад – використання арифметичних операцій безпосередньо перед виконанням тіла циклу:

#!/bin/bash

for (( count=1; count<11; count++ ))
  do
    echo "$count"
  done

Тут ми задаємо три керуючих команди:

  • count=1 – контролююча умова
  • допоки count менше 11
  • і команду до виконання – count +1:

[simterm]

$ ./loop.sh
1
2
3
4
5
6
7
8
9
10

[/simterm]

Цикл WHILE

Простий приклад, що добре демонструє принцип роботи циклу while:

#!/bin/bash

count=0

while [ $count -lt 10 ]
  do
    (( count++ ))
    echo $count
  done

Ми задаємо змінну $count рівною нулю, після чого запускаємо цикл while з умовою “поки $count менше десяти – виконувати цикл”. У тілі циклу ми виконуємо постфіксний інкремент +1 до змінної $count і виводимо результат в stdout.

Результат виконання:

[simterm]

$ ./loop.sh
1
2
3
4
5
6
7
8
9
10

[/simterm]

Щойно значення змінної $count стало 10 – цикл прервався.

Infinite loops

Гарний приклад “нескінченного” циклу, який демонструє роботу while:

#!/bin/bash

count=10

while [ 1 = 1 ]
  do
    (( count++ ))
    echo $count
  done

Запускаємо:

[simterm]

$ ./loop.sh
...
5378
5379
5380
5381
5382
5383
^C

[/simterm]

Цикл UNTIL

Аналогічно, але “у зворотний бік” працює і цикл until:

#!/bin/bash

count=0

until [ $count -gt 10 ]
  do
    (( count++ ))
    echo $count
  done

Тут ми задаємо схожу умову, але замість “поки змінна менше 10” – вказуємо “поки змінна не стане більше ніж 10”.

Результат виконання:

[simterm]

$ ./loop.sh
1
2
3
4
5
6
7
8
9
10
11

[/simterm]

Якщо ж наведений вище приклад “нескінченного циклу” виконати з використанням until – він на відміну від while не виведе нічого:

#!/bin/bash

count=10

until [ 1 = 1 ]
  do
    (( count++ ))
    echo $count
  done

Запускаємо:

[simterm]

$ ./loop.sh
$

[/simterm]

Оскільки “умова” початково “true” – тіло циклу виконуватися не буде.

Як і в циклі for – в циклах while та until можна використовувати функції.

Для прикладу – цикл із скрипту, що реально використовується та виконує перевірку статусу сервера Tomcat (PIDбереться в системі SLES , в інших системах може відрізнятися), трохи спрощений варіант:

#!/bin/bash

check_tomcat_status () {
RUN=`ps aux | grep tomcat | grep -v grep | grep java | awk '{print $2}'`
}

while check_tomcat_status
  do
    if [ -n "$RUN" ]
      then
        printf "WARNING: Tomcat still running with PID $RUN."
      else
        printf "Tomcat stopped, proceeding...nn"
      break
    fi
  done

Результат виконання:

[simterm]

$ ./loop.sh
WARNING: Tomcat still running with PID 14435
26548.WARNING: Tomcat still running with PID 14435
26548.WARNING: Tomcat still running with PID 14435
26548.WARNING: Tomcat still running with PID 14435
26548.WARNING: Tomcat still running with PID 14435
26548.WARNING: Tomcat still running with PID 14435
26548.WARNING: Tomcat still running with PID 14435
26548.WARNING: Tomcat still running with PID 14435

[/simterm]

Повний варіант:

#!/bin/bash

check_tomcat_status () {
  RUN=`ps aux | grep tomcat | grep -v grep | grep java | awk '{print $2}'`
}

while check_tomcat_status; do
  if [ -n "$RUN" ]
    then
      printf "WARNING: Tomcat still running with PID $RUN. Stop it? "
      answer "Stopping Tomcat..." "Proceeding installation..." && $CATALINA_HOME/bin/shutdown.sh 2&>1 /dev/null || break
      sleep 2
      if [ -n "$RUN" ]
        then
          printf "Tomcat still running. Kill it? "
          answer "Killing Tomcat..." "Proceeding installation...n" && kill $RUN || break
          sleep 2
      fi
    else
      printf "Tomcat stopped, proceeding...nn"
      break
    fi
done

Функція answer описувалася у пості BASH: використання функцій, приклади, але тут трохи покращений варіант:

answer () {
  while read response; do
    echo
    case $response in
      [yY][eE][sS]|[yY])
        printf "$1n"
        return 0
        break
        ;;
      [nN][oO]|[nN])
        printf "$2n"
        return 1
        break
        ;;
      *)
        printf "Please, enter Y(yes) or N(no)! "
    esac
  done
}

Тут можна було використовувати як while так і until, але не цикл for, оскільки for спрацював би тільки один раз (отримав PID і завершився).

Loading

BASH: використання функцій, приклади
0 (0)

15 Січня 2023

terminal

Переклад поста 2013 року з деякими правками, але все ще актуальний для вивчення BASH.

По суті функція в bash є звичайною змінною, але з більшими можливостями.

Основне застосування – у тих випадках, коли один і той же код необхідно використовувати кілька разів та/або у різних зв’язаних скриптах.

Оголошення та виклик функції

Оголошується функція так:

function function_name ()
{
  function body
}

Або:

function one {
  echo "One"
}

two () {
  echo "Two"
}

function three () {
  echo "Three"
}

Однак найбільш правильним варіантом, з метою сумісності скрипта з різними shell буде такий:

two () {
  echo "Two"
}

І намагайтеся ніколи не використовувати третій варіант:

function three () {
  echo "Three"
}

Викликати функцію можна просто вказавши її ім’я у тілі скрипта:

#!/bin/bash
function one {
  echo "One"
}

one

[simterm]

$ ./example.sh
One

[/simterm]

Важливо, щоб оголошення функції було виконано до того, як вона буде викликана, інакше буде отримана помилка:

#!/bin/bash

function one {
  echo "One"
}

one

two

function two {
  echo "Two"
}

[simterm]

$ ./example.sh
One
./example.sh: line 7: two: command not found

[/simterm]

Виклик функції з аргументами

Перейдемо до більш складних функцій та розглянемо виклик функції з аргументами.

Наприклад, візьмемо функцію, яка викликається у тому місці коду, де потрібно отримати відповідь користувача:

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1"
          $2
          break
          ;;
        [nN][oO]|[nN])
          printf "$3"
          $4
          break
          ;;
        *)
          printf "Please, enter Y(yes) or N(no)! "
      esac
  done
}

echo "Run application? (Yes/No) "
answer "Run" "" "Not run" ""

У цьому випадку функція answer()очікує відповіді від користувача в стилі Yesабо No(або будь-яка варіація, задана у виразі [yY][eE][sS]|[yY]або [nN][oO]|[nN]), і в залежності від відповіді виконує певну дію.

У разі відповіді Yes буде виконано дію, задану в першому аргументі $1, з яким було викликано функцію.

Перевіримо:

[simterm]

$ bash test.sh 
Run application? (Yes/No) 
y

Run

[/simterm]

З відповіддю No:

[simterm]

$ ./example.sh

Run application? (Yes/No)
no

Not run

[/simterm]

Виклик команд безпосередньо з аргументів, а тим більше зі змінних, вважається не найкращим рішенням, тому перепишемо її і викличемо з операторами && (у разі успішного виконання, тобто при отриманні коду 0) і || – у разі помилки та отримання коду відповіді 1:

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          return 1
          break
          ;;
        *)
          printf "Please, enter Y(yes) or N(no)! "
      esac
  done
}

echo -e "\nRun application? (Yes/No) "
answer "Run" "Will not run" && echo "I'm script" || echo "Doing nothing"

Тепер ми першим аргументом передаємо функції відповідь “Run“, і у разі відповіді користувача Yes– виконуємо printf "Run"та echo "I'm script". Якщо вибрано відповідь No– ми друкуємо другий аргумент Will not run і виконуємо дію echo "Doing nothing":

[simterm]

$ bash test.sh 

Run application? (Yes/No) 
y

Run
I'm script

$ bash test.sh 

Run application? (Yes/No) 
no

Will not run
Doing nothing

[/simterm]

Відповідно, замість echo можна виконати будь-яку іншу команду:

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          return 1
          break
          ;;
        *)
          printf "Please, enter Y(yes) or N(no)! "
      esac
  done
}

echo -e "\nKill TOP application? (Yes/No) "
answer "Killing TOP" "Left it alive" && pkill top || echo "Doing nothing"

[simterm]

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP

[/simterm]

Важливо враховувати, що якщо перша команда завершиться невдало (у даному прикладі – pkillне знайде зазначений процес) – то функція поверне код 1, і буде виконана друга частина:

[simterm]

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP
Doing nothing

[/simterm]

Змінні у функціях

В аргументах також можна використовувати змінні.

Наприклад, можна визначити кілька варіантів відповідей у ​​різних змінних, і використовувати потрібну у різних випадках:

#!/bin/bash

answer () {
  while read response; do
    echo
      case $response in
        [yY][eE][sS]|[yY])
          printf "$1\n"
          return 0
          break
          ;;
        [nN][oO]|[nN])
          printf "$2\n"
          return 1
          break
          ;;
        *)
          printf "Please, enter Y(yes) or N(no)! "
     esac
  done
}

replay1="Killing TOP"
replay2="Left it alive"

echo -e "\nKill TOP application? (Yes/No) "
answer "$replay1" "$replay2" && echo "I'm script" || echo "Doing nothing"

[simterm]

$ ./example.sh

Kill TOP application? (Yes/No)
y

Killing TOP
I'm script
$ ./example.sh

Kill TOP application? (Yes/No)
n

Left it alive
Doing nothing

[/simterm]

Як і зі звичайними змінними, функції використовують “позиційні агрументи”, тобто:

  • $#– відображення кількості переданих аргументів;
  • $*– відображення списку всіх переданих аргументів;
  • $@– те саме, що і $*– але кожен аргумент вважається як просте слово (рядок);
  • $1 - $9– нумеровані аргументи залежно від позиції у списку

Наприклад – створимо такий скрипт із функцією, яка має вивести кількість переданих аргументів:

#!/bin/bash

example () {
  echo $#
  shift
}

example $*

[simterm]

$ ./example.sh 1 2 3 4
4

[/simterm]

Або просто вивести на екран усі передані їй аргументи:

#!/bin/bash

example () {
  echo $*
  shift
}

example $*

[simterm]

$ ./example.sh 1 2 3 4
1 2 3 4

[/simterm]

Або можна аргументи передавати прямо при виклику функції, а не при виклику скрипту як у прикладі вище:

#!/bin/bash

example () {
  echo $*
  shift
}

example 1 2 3 4

[simterm]

$ ./example.sh
1 2 3 4

[/simterm]

Локальні змінні

За замовчуванням, всі задані змінні в bash скриптах вважаються глобальними в рамках самого скрипту, але в функції можна оголосити змінну, яка буде доступна тільки під час її (функції) виконання.

Приклад:

#!/bin/bash

ex0=0

example () {
  local ex1=1

  echo "$ex1"
}

example

[[ $ex0 ]] && echo "Variable found" || echo "Can't find variable!"
[[ $ex1 ]] && echo "Variable found" || echo "Can't find variable!"

Перевіряємо:

[simterm]

$ bash test.sh
1
Variable found
Can't find variable!

[/simterm]

Математичні операції у функціях

Як і зі змінними, у функціях можливо використання математичних операцій.

Наприклад така функція:

#!/bin/bash

mat () {
  a=1
  (( a++ ))
  echo $a
}

mat

В результаті отримуємо значення змінної $a + одиниця:

[simterm]

$ ./mat.sh
2

[/simterm]

Більш складний варіант – з використанням кількох змінних та обчисленням їх значення:

#!/bin/bash

mat () {
  a=1
  b=2
  c=$(( a + b ))
  echo $c
}

mat

Результат:

[simterm]

$ ./mat.sh
3

[/simterm]

Ще варіант – з використанням аргументів:

#!/bin/bash

mat () {
  a=$1
  b=$2
  c=$(( a + b ))
  echo $c
}

mat $1 $2

Виконуємо:

[simterm]

$ ./mat.sh 1 1
2

[/simterm]

Рекурсивні функції

Рекурсивна функція, це функція, яка викликає сама себе.

Наприклад:

#!/bin/bash

recursion () {
  count=$(( $count + 1 ))
  echo $count
  recursion
}

recursion

Така функція буде викликати сама себе, поки її виконання не буде перервано вручну:

[simterm]

$ ./example.sh
...
913
914
915

[/simterm]

Для більшої наочності додамо цикл, який перевіряє умову: якщо змінна $count перевищить значення змінної $recursions – функція зупинить виконання:

#!/bin/bash

count=0
recursions=4

recursion () {
  count=$(( $count + 1 ))
  echo $count

  while [ $count -le $recursions ]; do
    recursion
  done
}

recursion

Виконання:

[simterm]

$ ./example.sh
1
2
3
4
5

[/simterm]

Для спрощення скрипта можна замінити вираз count=$(( $count + 1 )) на (( count++ )):

#!/bin/bash

count=0
recursions=4

recursion () {
  (( count++ ))
  echo $count

  while [ $count -le $recursions ]; do
    recursion
  done
}

recursion

Перевіряємо:

[simterm]

$ ./example.sh
1
2
3
4
5

[/simterm]

Експорт функцій

Щоб передати функцію в наступний скрипт, що викликається в новому (дочірньому) екземплярі shell – її необхідно експортувати.

Для прикладу візьмемо два файли – у файлі 1.sh ми оголосимо функцію та виклик скрипт 2.sh:

#!/bin/bash

one () {
  echo "one"
}

bash 2.sh

А у файлі 2.sh спробуємо цю функцію викликати:

#!/bin/bash

one

Перевіряємо:

[simterm]

$ ./1.sh 
2.sh: line 3: one: command not found

[/simterm]

Тепер експортуємо функцію за допомогою export та ключа -f:

#!/bin/bash

one () {
  echo "one"
}

export -f one

bash 2.sh

Виконуємо:

[simterm]

$ ./1.sh 
one

[/simterm]

Інший варіант – викликати наступний скрипт у тому ж екземплярі шела:

#!/bin/bash

one () {
  echo "one"
}

source 2.sh

Або так:

#!/bin/bash

one () {
  echo "one"
}

. 2.sh

Обидва варіанти рівнозначні і дадуть один результат:

[simterm]

$ ./1.sh
one

[/simterm]

Перевірка наявності функцій

Іноді перед виконанням функції потрібно перевірити її наявність. Для цього зручно використовувати команду declare.

Викликана з ключем -f і без аргументів declare виведе зміст усіх функцій:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -f

Результат:

[simterm]

$ ./test.sh 
one () 
{ 
    echo "one"
}
two () 
{ 
    echo "two"
}

[/simterm]

З ключем -F лише назви:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -F

Результат:

[simterm]

$ ./test.sh
declare -f one
declare -f two

[/simterm]

Якщо задати імена функцій як аргументи – declare просто виведе їх імена:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -F one two

Перевіряємо:

[simterm]

$ ./test.sh
one
two

[/simterm]

Можна задати ключ -f та ім’я функції – тоді буде виведено лише тіло вказаної функції:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

declare -f one

Запускаємо:

[simterm]

$ ./test.sh
one () {
echo "one"
}

[/simterm]

Перевірити наявність функцій перед їх виконанням можна за допомогою додаткової функції, якій передаються імена функцій, що перевіряються:

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

isDefined() {
  declare -f "$@" > /dev/null && echo "Functions exist" || echo "There is no some functions!"
}

isDefined one two 

Зверніть увагу на використання “ $@” – як писалося вище, саме такий параметр виводить аргумент “як є”, без будь-яких інтерпретацій bash.

Запустимо скрипт для перевірки:

[simterm]

$ ./test.sh
Functions exist

[/simterm]

А тепер – спробуємо додати одну “зайву” функцію до виклику isDefined() :

#!/bin/bash

one () {
  echo "one"
}

two () {
  echo "two"
}

isDefined() {
  declare -f "$@" > /dev/null && echo "Functions exist" || echo "There is no some functions!"
}

isDefined one two three

Результат:

[simterm]

$ ./test.sh
There is no some functions!

[/simterm]

declare виявив відсутність функції з ім’ям three та повернув код 1, що викликало спрацювання оператора ||.

Loading

Підготовка до зими 2022-2023: інтернет, електрика, опалення, їжа та вода
0 (0)

10 Січня 2023

Вже давно просили написати пост про те, як я готувався до зими – ось, таки вмовили.

Хоча вже трошки запізно, бо половина зими пройдено, але – нехай буде.

Голове, що дуже спасає цією зимою це те, що ЖК, в якому живу, по-перше має газові плити, по-друге – опалення газовими котлами.

Однак, все ж були проблеми, которі довелося вирішувати.

В перші дні, коли почали вимикати світло, я мав пару павербанків на 10.000 mAh, і досі пам’ятаю це відчуття, коли в ноутбуку сіла батарея, провайдер інтернету вирубився, мобільний інтернет теж не працював, ще й телефон почав сідати і не було можливості навіть почитати книгу (хоча є ще паперова бібліотека – але чим світити ввечері?)

Повне відчуття ізоляції, як в печері.

Тож перше, що я зробив – це докупив пару павербанок на 20.000 для телефона. Але це було тільки початком.

Інтернет

Друге питання, яке потрібно було вирішити – це інтернет. Живу за містом, і як вимикається світло – то вежі мобільного вирубаються теж, лишається тільки одна. З телефону інтернет через неї не тягнуло зовсім, тож докупив собі комплект зовнішньої антени з 3/4G роутером. Див детальнішне тут – Networking: коли немає світла – модем 4G ZTE + зовнішня антена:

Антена стоїть за вікном, в квартирі мав два модеми з двома операторами – Водафон та Лайф: то один, то другий працювали більш-менш, і свої 1-2 мб/с я мав.

Обидва модеми були підключені до павербанок, тож працювали постійно: як тільки виключалось світло, і провайдер “падав” – ноутбук переключався на один з модемів.

Ще пізніше в ЖК нарешті дотягнули оптику, і тепер маю GPON з гігабітним підключенням:

Медіаконвертор підключений до павербанки через підвищуючий перехідник на 12 вольт:

Так як це GPON (Gigabit Passive Optical Network) – то ми не дуже залежимо від електрохарчування (с), і інтернет працює стабільно навіть декілька діб без світла поспіль.

Електрохарчування (с)

Заряд ноубтука

Ще до того, як я затарився купою всяких акумуляторів (зараз до них дойдемо) я максимально оптимізував роботу свого ноутбука – визначив, які саме процеси/сервіси найбільше витрачають заряд батареї, та почав іх відключати, див. Linux: збереження заряду батареї ноутбуку.

Щоб спростити процесс – накидав скрипта:

#!/usr/bin/bash

#sudo ifconfig enp2s0f0 down
sudo nmcli radio wifi off
sudo bash -c  'echo -n 60 > /sys/class/backlight/amdgpu_bl0/brightness'
#xrandr --output eDP-1 --auto --mode 1280x720 --output HDMI-1 --auto --left-of eDP-1 --output DP-1 --off
##xrandr --output eDP-1 --auto --mode 1280x720 --primary --output HDMI-1 --off --output DP-1 --off
#systemctl --user stop pulseaudio.socket
#systemctl --user stop pulseaudio.service
sudo systemctl stop bluetooth
sudo ip link set docker0 down
sudo systemctl stop docker.service
sudo systemctl stop docker.socket
killall polybar
killall ktorrent
killall slack
killall thunderbird

Далі в rc.xml (в мене Arch Linux та Openbox) додав дві комбінації для клавіатури – одна запускає скрипт setPowerDown.sh, наведений вище, друга – setPowerUp.sh, в якому робиться все теж саме, тільки навпаки – включає сервіси:

...
    <keybind key="C-A-d">
      <action name="Execute">
        <command>/home/setevoy/.local/bin/setPowerDown.sh</command>
      </action>
    </keybind>
    <keybind key="C-A-u">
      <action name="Execute">
        <command>/home/setevoy/.local/bin/setPowerUp.sh</command>
      </action>
    </keybind>
...

Але цього все ще було замало – я хотів довести свою автономність мінімум до 3-х діб без світла, тож пішов далі.

Павербанки

По-перше – потрібно було мати павербанки, які могли б заряджати ноутбук, батареї якого вистачає на 5-6 годин максимум.

Першими були куплені два Basesus на 30.000 mAh, які видають 60 ват по Type-C (на olx.ua можна взяти дешевше).

До них докупив два зарядних Baseus GaN3 з виходом на 30 ват, щоб банки швидше заряджались.

Ще пізніше докупив банку O2 Project 60000 mAh 65 wat – теж працює чудово.

Акумулятори, інвертори

По-перше – замовив зарядну станцію. Мене душила жаба віддавати купу грошей, та ще й чекати на доставку, тож я замовив не EcoFlow, а Kseon-168 – 168.000 mAh (14.000 грн, для військових дешевше), плюс інвертор на 500 ват. 4 виходи USB (без QuickCharge, нажаль), та з двома автомобільними виходами. І вже для автомобільного конектора – докупив Baseus Particular Digital Display QC+PPS Dual Quick Charger 65W Dark, тож спокійно можу заряджати і мобільні, і ноутбук:

Єдиний, як на мене, недолік Kseon – довга зарядка, до 18 годин для повного заряду. Але пізніше купив ще один, тож маю запас.

Ще треба було вирішити питання з опаленням: газовий котел теж потребує електроенергії для свого насоса, яким він жене воду по системі.

Спеціально для нього замовив звичайні автомобільні акумулятори та інвертор:

Інвертор до 600 ват, тож котел тягне без проблем, можна підключити ще й холодильник. Обійшлося все це приблизно в 15.000 грн – не скажу, де бралося, бо цим займався знайомий прораб.

Два кумулятори по 60 ампер-годин вистачає на 5-6 годин роботи котла, але квартира та будинок нові, і тепло тримають добре.

Ще пізніше купив аналогічний набор, але вже з гелевими AGM на 72 а/г (близько 10.000 грн) та інвертором CyberPower CPS1000E (обійшовся тоді у 23.000 грн – зато привезли все на наступний день – ніяких тобі “місяць чекати”, та ще й мати справу з поштою):

Цей акум та інвертор переважно живлять настільну лампу, колонки, ноутбук, зовнішній монітор та роутер.

Тож зараз в мене запаси по енергії:

  • 3х павербанка на 10.000 mAh
  • 1х павербанк на 20.000 mAh
  • 2х павербанка Baseus на 30.000 mAh, 65 ват
  • 1х павербанк на 60.000 mAh, 65 ват
  • 2х автоакуми на 60.000 mAh + інвертор 600 ват
  • 1х автоакум на 72.000 mAh + інвертор 700 ват
  • 2х зарядні станції Kseon по 168.000 mAh + інвертор 500 ват

Зараз, думаю, можу спокійно (якщо економно) прожити без світла до тижня.

Безпека

Акумлятори стоять на балконі, де температура мінімум +7. До того ж докупив декілька Вологовбирач Pouce, бо трохи переймався високою вологістю повітря та кондесатом.

Але першим жеж ділом купив два вогнегасники ВП-3:

Один в коридорі, біля вхідних дверей, другий в шкафчику біля балкона.

Брав два, бо ніколи ними не користувався, і якщо з першого разу щось не вийде – то буде друга спроба.

Продукти харчування

Ну, тут все просто: затарився консервами від Вербена, брав на Prom.ua (хоча взагалі цю площадку дуже не люблю) – коробок 20 різноманітних каш з м’ясом, плюс “мівіни”, паштети, галети і все таке інше із супермаркету.

Знайомі купляли Консерви Tushe, прям ящиками – теж рекомендують.

Крім того, купив 1.5 кг сала – лежить в морозильнику.

Ще гарна їдея мати запас шоколаду.

Вода

В ЖК свої насоси і ми не залежимо від міського водопостачання, але тут проблема, бо насоси – сюрпрайз! – працють від електрики.

Тому маю запас 2х20 літрів технічної води, плюс 2х20 питної бутильованної:

Пізніше ЖК купив генератор, мешканці щомісяця скидаються по 200 грн на паливо, і тепер маємо воду шонайменш двічі на добу.

Також, так як вода є не завжди, купив собі ось такий рукомийник:

Ще купив звичайний чайник для плити Ofenbach Happy Kettle 2л, бо гріти воду в кастрюльці швидко набридло.

Так наче і все.

В цілому – зараз проблеми не відчуваються зовсім, хоча спочатку було трохи… Неспокійно, скажімо так, да. Але все вийшло чудово 🙂

Loading

Grafana Loki: можливості LogQL для роботи з логами та створення метрик для алертів
0 (0)

30 Грудня 2022

Добре – Loki запускати навчились – Grafana Loki: архітектура та запуск в Kubernetes з AWS S3 storage та boltdb-shipper, як налаштовувати алерти теж розібрались – Grafana Loki: алерти з Ruler та labels з логів.

Тепер час розібратися з тим, що взагалі ми можемо робити в Loki використовуючи її LogQL.

Підготовка

Далі для прикладів будемо використовувати два поди – один з nginxdemo/hello для звичайних логів nginx, а інший thorstenhans/fake-logger, який буде писати логи в JSON.

Для Nginx додамо Service, что б мати можливість слати запити з curl:

apiVersion: v1
kind: Pod
metadata:
  name: nginxdemo
  labels:
    app: nginxdemo
    logging: test
spec:
  containers:
  - name: nginxdemo
    image: nginxdemos/hello
    ports:
      - containerPort: 80
        name: nginxdemo-svc
---
apiVersion: v1
kind: Service
metadata:
  name: nginxdemo-service
  labels:
    app: nginxdemo
    logging: test
spec:
  selector:
    app: nginxdemo
  ports:
  - name: nginxdemo-svc-port
    protocol: TCP
    port: 80
    targetPort: nginxdemo-svc
---
apiVersion: v1
kind: Pod
metadata:
  name: fake-logger
  labels:
    app: fake-logger
    logging: test
spec:
  containers:
  - name: fake-logger
    image: thorstenhans/fake-logger:0.0.2

Деплоїмо та прокидуємо порт:

[simterm]

$ kk port-forward svc/nginxdemo-service 8080:80

[/simterm]

І запустимо curl зі звичайним GET в циклі:

[simterm]

$ watch -n 1 curl localhost:8080 2&>1 /dev/null &

[/simterm]

Та ще один – з POST:

[simterm]

$ watch -n 1 curl -X POST localhost:8080 2&>1 /dev/null

[/simterm]

Поїхали.

Grafana Explore: Loki – інтерфейс

Декілька слів про сам інтерфейс Grafana Explore > Loki.

Ви можете використовувати декілька запитів одночасно:

Також, є можливість розділити інтефейс на дві частини, і в кожній виконувати окремі запити:

Як і в звичайних дашбордах Grafana, є можливість вибрати період, за який ви хочете отримати дані, та задати інтервал для автооновлювання:

Або можете включити Live-режим – тоді дані будуть з’являтися як тільки вони потраплять до Loki:

Для створення запитів є два режими – Builder та Code.

В режимі Builder Loki видає список доступних тегів та фільтрів:

В режимі Code вони будуть підставлятися автоматично по мірі набору:

Функція Explain буде роз’яснювати що саме ваш запит робить:

А Inspector відобразить деталі про ваш запит – скільки часу і ресурсів було використано для формування відповіді – корисно для оптимізації запитів:

Крім того, завжди можна відкрити Loki Cheat Sheet, натиснувши (?) з правої сторони від поля для запиту:

LogQL: overview

В цілому, робота з Loki та її LogQL майже аналогічна роботі з Prometheus та його PromQL – майже всі тіж самі функції та загальний підхід, це навіть відображено в опису Loki: “Like Prometheus, but for logs”.

Отже, основна вибірка базується на проіндексованих лейблах (або тегах, кому як більше до вподоби), за допомогою яких ми робимо основний пошук в логах – вибираємо стрім.

Типи запитів в Loki залежать від фінального результату:

  • Log queries: формують строки з лог-файлів
  • Metric queries: включають в себе Log queries, але в результаті формують числові значення, які можна використовувати для формування графіків в Grafana або для алертів в Ruler

В цілому, будь-який запит складається із трьох основних частин:

{Log Stream Selectors} <Log Pipeline "Log filter">

Тобто в запиті:

{app="nginxdemo"} |= "172.17.0.1"

{app="nginxdemo"} – це Log Stream Selector, в якому ми вибираємо конкретний стрім из Loki, |= – початок Log Pipeline, який включає в себе Log Filter Expression – "172.17.0.1".

Окрім Log filter, пайплайн може включати в себе Log або Tag formatting expression, який міняє отримані в пайплайн дані.

Обов’язковим є Log Stream Selector, тоді як Log Pipeline з його expressions являється опціональним, і використовуюється для уточненя або форматування результатів.

Log queries

Log Stream Selectors

Для селекторів викристовуються лейбли, які задаються агентом, який збирає логи – promtail, fluentd або іншими.

Log Stream Selector визначає скільки індексів та блоків данних будуть завантажені для повернення результату, тобто напряму впливає на швидкість роботи і ресурси CPU/RAM, задіяні для формування відповіді.

В прикладі вище в селекторі {app="nginxdemo"} ми використовуємо оператор “=“, який може бути:

  • = : дорівнює
  • != : не дорівнює
  • =~ : regex
  • !~ : негативний regex

Отже, за запитом {app="nginxdemo"} ми отримаємо логи всіх подів, у яких є тег app зі значенням nginxdemo:

Можемо комбінувати декілька селекторів, наприклад отримати всі логи з logging=test, але без app=nginxdemo:

{logging="test", app!="nginxdemo"}

Або використати regex:

{app=~"nginx.+"}

Або просто вибрати взагалі всі логи (стріми), в яких є тег app:

Log Pipeline

Данні, отримані зі стриму можна передати в пайплайн для подальшого фільтрування або форматування. При цьому результат роботи одного пайплайну можна передати в наступний, і так далі.

Pipeline може включати в себе:

  • Log line filtering expressions – для фільтрування попередніх результатів
  • Parser expressions – для отримання тегів з логів, які можна передати в Tag filtering
  • Tag filtering expressions – для фільтрування даних по тегам
  • Log line formatting expressions – використовується для редагування отриманних результатів
  • Tag formatting expressions – редагування тегів/лейбл

Log Line filtering

Фільтри використовуються для… фільтрування)

Тобто, коли ми отримали дані із стриму, і хочемо з нього вибрати окремі строки – то використовуємо log filter.

Фільтр складається з оператора та строкового запиту, за яким робиться вибірка данних.

Операторами можуть буди:

  • |=: строка містить строковий запит
  • ! =: строка НЕ містить строковий запит
  • |~: строка дорівнює регулярному виразу
  • ! ~: строка НЕ дорівнює регулярному виразу

При використанні regex майте на увазі, что використовується синтаксис Golang RE2, і за замочуванням він є case-sensitive. Щоб переключити його на незалежний від регистру режим – додаємо (i?).

Окрім того, Log Line filtering краще використовувати на початку запиту, бо вони працють швидко, і позбавлять наступні пайплайни він зайвої роботи.

Прикладом log filter може бути вибірка за строкою:

{job=~".+"} |= "promtail"

Або декілька виразів, використовуючи регулярку:

Parser expressions

Парсери… парсять) (гвинтокрили гвинтять) вхідні дані, та отримують з них лейбли, які потім можна використати в подальших фільтрах або для формування Metric queries.

Наразі, LogQL підтримує json, logfmt, pattern, regexp та unpack для роботи з тегами.

json

Наприклад, json формує всі json-ключі в лейбли, тобто запит {app="fake-logger"} | json замість:

Сформує новий набір тегів:

Отримані через json теги можна далі використати для додаткових фільтрів, наприклад – вибрати тільки строки з level=debug:

logfmt

Для формування тегів з логів не в форматі JSON можно використати logfmt, який всі знайдені поля перетворить на лейбли.

Наприклад, job="monitoring/loki-read" має поля ключ=значення:

level=info ts=2022-12-28T14:31:11.645759285Z caller=metrics.go:170 component=frontend org_id=fake latency=fast

Які за допомогою logfmt перетворяться на лейбли:

regexp

Парсер regex приймає аргумент, в якому вказується regex-група, яка сформує тег із запиту.

Наприклад, зі строки:

10.0.44.12 – – [28/Dec/2022:14:42:58 +0000] 204 “POST /loki/api/v1/push HTTP/1.1” 0 “-” “promtail/” “-“

Ми можемо динамічно сформувати теги ip та status_code:

{container="nginx"} | regexp "^(?P<ip>[0-9]{1,3}.{3}[0-9]{1,3}).*(?P<status_code>[0-9]{3})"

pattern

pattern дозволяє сформувати лейбли за шаблонами лог-запису, тобто строка:

10.0.7.188 – – [28/Dec/2022:15:27:04 +0000] 204 “POST /loki/api/v1/push HTTP/1.1” 0 “-” “promtail/2.7.0” “-“

Може бути описана у вигляді:

{container="nginx"} | pattern `<ip> - - [<date>] <status_code> "<request> <uri> <_>" <size> "<_>" "<agent>" <_>`

де <_> ігнорить, тобто не створює тегу.

І в результаті отримаємо набір лейбл за цим шаблоном:

Див. більше тут – Introducing the pattern parser.

Tag filtering expressions

Як видно з назви, дозволяє створювати нові фільтри з тегів які вже є в запису, або які були створені за допомогою попереднього парсеру, наприклад logfmt.

Візьмемо строку:

level=info ts=2022-12-28T15:56:31.449162304Z caller=table_manager.go:252 msg=”query readiness setup completed” duration=1.965µs distinct_users_len=0

Якщо пропустимо її через парсер logrmt, то отримаємо теги caller, msg, durarion та distinct_users_len:

Далі, можемо створити фільтр за цими тегами:

Доступні оператори тут ==, =, !=, >, >=, <, <=.

Також, можемо використати оператори and або or:

{job="monitoring/loki-read"} | logfmt | caller="table_manager.go:252" or org_id="fake" and caller!~"metrics.go.+"

Log line formatting expressions

Далі, можемо формувати те, які саме дані нам будуть відображені в записі.

Наприклад, візьмемо той же loki-read, в якому маємо теги:

Серед них нам цікаво відобразити тільки component та duration – використовуємо форматування:

{job="monitoring/loki-read"} | logfmt | line_format "{{.component}} {{.duration}}"

Label format expressions

За допомогою label_format можемо перейменувати, змінити чи додати нові лейбли.

Для цього, аргументом передаємо ім’я лейбли з оператором =, за яким йде потрібне значення.

Наприклад, маємо лейблу app:

Яку хочемо перейменувати в application – використовуємо label_format application=app:

Або можемо використати значення існуючого тегу для створення нового, для цього використовуємо шаблонізатор у вигляді {{.field_name}}, де можемо комбінувати декілька полів.

Тобто, якщо хочемо створити тег error_message в якому будуть значення полів level та msg – формуємо такий запит:

{job="default/fake-logger"} | json | label_format error_message="{{.level}} {{.msg}}"

Log Metrics

І розглянемо як із логів можна створювати метрики, які можна використовувати для формування графіків або алертів (див. Grafana Loki: алерти з Ruler та labels з логів).

Interval vectors

Для роботи з векторами за часом наразі є чотири доступні функції, які в принципі вже знайомі по Prometheus:

  • rate: кількість логів в секунду
  • count_over_time: підрахувати кількість записів стріму за заданий проміжок часу
  • bytes_rate: кількість байт в секунду
  • bytes_over_time: підрахувати кількість байт стріму за заданий проміжок часу

Наприклад, отримати queries per second для джоби fake-logger:

rate({job="default/fake-logger"}[5m])

Може бути корисно, щоб створити алерт на випадок як якийсь сервіс почав писати багато логів, що може бути ознакою того, що “щось пішло не так”.

Отримати кількість записів з рівнем warning за останні 5 хвилин можно за допомогою такого запиту:

count_over_time({job="default/fake-logger"} | json | level="warning" [5m])

Aggregation Functions

Також, можемо використовувати функції агрегації для об’єднання вихідних даних, всі також знайомі по PromQL:

  • sum: сумма за лейблою
  • min, max та avg: мінімальне, максимальне да середнє значення
  • stdev, stdvar: стандартне відхилення та розбіжність
  • count: кількість елементів у векторі
  • bottomk та topk: мінімальний та максимальний елементи

Синтаксис функцій агрегації:

<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]

Наприклад, отримати кількість записів в секунду від джоби fake-logger, та поділити їх по тегу label:

sum(rate({job="default/fake-logger"} | json [5m])) by (level)

Або з прикладів вище:

  • отримати записи з подів loki-read
  • із результату створити дві нові лейбли – component та duration
  • отримати кількість записів в секунду
  • прибрати записи без компоненту
  • та відобразити суму по кожному компоненту
sum(rate({job="monitoring/loki-read"} | logfmt | line_format "{{.component}} {{.duration}}" | component != "" [10s])) by (component)

Інші оператори

І зовсім вже коротко про інші можливості.

Математичні оператори:

  • + – додавання
  • - – віднімання
  • * – множення
  • / – ділення
  • % – коефіцієнт
  • ^ – зведення у ступінь

Логічні оператори:

  • and: та
  • or: або
  • unless: за виключенням

Оператори порівняння:

  • ==: дорівнює
  • !=: не дорівнює
  • >: більше ніж
  • >=: більше ніж або дорівнює
  • <: менше ніж
  • <=: менше ніж або дорівнює

Знов-таки з прикладів, які використовували раньше.

Створюємо лейблу request:

{container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>`

Отримаємо рейт запросів POST в секунду за останні 5 хвилин:

sum(rate({container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>` | request="POST" [5m]))

Спочатку перевіримо на графіку кількість запитів GET та POST:

sum(rate({container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>` [5m]))  by (request)

А теперь отримаємо процент з типом POST від загальної кількості запитів:

  • всі запити POST ділимо на загальну кількість запитів
  • результат множимо на 100
sum(rate({container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>` | request="POST" [5m])) / sum(rate({container="nginx"} | pattern `<_> - - [<_>] <_> "<request> <_> <_>" <_> "<_>" "<_>" <_>` [5m])) * 100

На цьому все.

Посилання по темі

Loading

Grafana Loki: алерти з Ruler та labels з логів
0 (0)

28 Грудня 2022

Загальну інформацію по Grafana Loki див. у Grafana Loki: архітектура та запуск в Kubernetes з AWS S3 storage та boltdb-shipper.

Серед інших сервісів, які складають собою Loki, є окремий сервіс ruler, який відповідає за роботу з алертами, які можно генерити прямо з логів.

Ідея дуже проста:

  • створюємо файл з алертами в  Prometheus-like форматі
  • підключаємо його до поду ruler (loki-read у випадку simple-scalable deployment)
  • ruler парсить логи по заданним в файлі конфігурації правилам, і якщо якийсь expression спрацьовує – то Ruler пушить Alertmanager, передаючи йому алерт

Алерти будемо описувати в ConfigMap, який потім підключимо до подів з Ruler.

Документація – Rules and the Ruler.

Тестовый под для OOM-Killed

Мені хочеться потестити на спрацювання OOM Killed, тому створимо под явно заниженими лімітами, який буде вбиватися “на зльоті”:

---
apiVersion: v1
kind: Pod
metadata:
  name: oom-test
  labels:
    test: "true"
spec:
  containers:
    - name: oom-test
      image: openjdk
      command: [ "/bin/bash", "-c", "--" ]
      args: [ "while true; do sleep 30; done;" ]
      resources:
        limits:
          memory: "1Mi"
  nodeSelector:
    kubernetes.io/hostname: eks-node-dev_data_services-i-081719890438d467f

Задаємо в nodeSelector ім’я ноди, щоб було простіше шукати в Локі.

При старті цього поду Kubernetes його вбиватиме через перевищення лімітів, а journald на WorkerNode записуватиме подію в системний журнал, який збирається promtail:

[simterm]

$ kk -n monitoring get cm logs-promtail -o yaml
...
    - job_name: journal
       journal:
        labels:
          job: systemd-journal
        max_age: 12h
        path: /var/log/journal
      relabel_configs:
      - source_labels:
        - __journal__systemd_unit
        target_label: unit
      - source_labels:
        - __journal__hostname
        target_label: hostname

[/simterm]

Запускаємо наш под:

[simterm]

$ kk apply -f test-oom.yaml 
pod/oom-test created

[/simterm]

Перевіряємо:

[simterm]

$ kk describe pod oom-test
...
Events:
  Type     Reason                  Age                 From               Message
  ----     ------                  ----                ----               -------
  Normal   Scheduled               91s                 default-scheduler  Successfully assigned default/oom-test to ip-10-0-0-27.us-west-2.compute.internal
  Normal   SandboxChanged          79s (x12 over 90s)  kubelet            Pod sandbox changed, it will be killed and re-created.
  Warning  FailedCreatePodSandBox  78s (x13 over 90s)  kubelet            Failed to create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox container for pod "oom-test": Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: container init was OOM-killed (memory limit too low?): unknown

[/simterm]

І перевіряємо логі Loki:

Окей, тепер у нас є oom-killed под для тестів – давайте формувати запит для майбутнього алерту.

Формування запиту в Loki

В логах ми дивилися по запиту  {hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" – використовуємо його ж для тестового алерту.

Спочатку перевіримо що нам намалює сама Локі – використовуємо rate() та sum(), див. Log range aggregations:

sum(rate({hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" [5m])) by (hostname)

Гуд!

З цим вже можна працювати – створювати тестовий алерт.

Створення алерту для Loki Ruler

Створюємо файл з ConfigMap:

kind: ConfigMap
apiVersion: v1
metadata:
  name: rules-alerts
  namespace: monitoring
data:
  rules.yaml: |-
    groups:
      - name: systemd-alerts
        rules:
          - alert: TESTLokiRuler Systemd journal
            expr: |
              sum(rate({hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" [5m])) by (hostname) > 1
            for: 1s
            labels:
                severity: info
            annotations:
                summary: Test Loki OOM Killer Alert

Деплоїмо його:

[simterm]

$ kk apply -f rule-cm.yaml 
configmap/rules-alerts created

[/simterm]

Ruler та ConfigMap volume

Далі, нам треба підключити цей ConfigMap в под з ruler в каталог, який вказаний в конфізі Loki для компонента ruler:

...
    ruler:
      storage:
        local:
          directory: /var/loki/rules
...

Ruler у нас працює в loki-read подах – відкриваємо їх StatefulSet:

[simterm]

$ kk -n monitoring edit sts loki-read

[/simterm]

Описуємо новий volume:

...
      volumes:
      - configMap:
          defaultMode: 420
          name: rules-alerts
        name: rules
...

І його мапінг у под як  /var/loki/rules/fake/rules.yaml, де fake – це tenant_id, якщо використовується:

...
        volumeMounts:
        - mountPath: /etc/loki/config
          name: config
        - mountPath: /tmp
          name: tmp
        - mountPath: /var/loki
          name: data
        - mountPath: /var/loki/rules/fake/rules.yaml
          name: rules
          subPath: rules.yaml
...

В subPath вказуємо key з ConfigMap, щоб підключити саме як файл.

Налаштування Ruler alerting

Знаходимо Alertmanager URL:

[simterm]

$ kk -n monitoring get svc
NAME                                             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
...
prometheus-kube-prometheus-alertmanager          ClusterIP   172.20.240.159   <none>        9093/TCP                     110d
...

[/simterm]

У ConfigMap Loki для ruler вказуємо цю адресу:

...
    ruler:
      storage:
        local:
          directory: /var/loki/rules
        type: local
      alertmanager_url: http://prometheus-kube-prometheus-alertmanager:9093
...

Усі параметри для ruler – тут>>> .

Відкриваємо собі доступ до Alertmanager, щоб перевіряти алерти:

[simterm]

$ kk -n monitoring port-forward svc/prometheus-kube-prometheus-alertmanager 9093:9093

[/simterm]

Рестартим поди loki-read, можна просто через kubectl delete pod, і перевіряємо їх логи:

[simterm]

$ kk -n monitoring  logs -f loki-read-0
...
level=info ts=2022-12-13T16:37:33.837173256Z caller=metrics.go:133 component=ruler org_id=fake latency=fast query="(sum by(hostname)(rate({hostname=\"eks-node-dev_data_services-i-081719890438d467f\"} |~ \".*OOM-killed.*\"[5m])) > 1)" query_type=metric range_type=instant length=0s step=0s duration=120.505858ms status=200 limit=0 returned_lines=0 throughput=48MB total_bytes=5.8MB total_entries=1 queue_time=0s subqueries=1
...

[/simterm]

Перевіряємо Алерти в Алертменеджері – http://localhost:9093:

Loki та додаткові labels

В алертах хочеться виводити трохи більше інформації, ніж просто повідомлення “Test Loki OOM Killer Alert”, наприклад – відобразити ім’я пода, який був вбитий.

Додавання labels в Promtail

Перший варіант – це створювати нові лейбли ще на етапі збору логів, в самому Promtail через pipeline_stages, див. Grafana: Loki – Prometheus-like счётчики и функции агрегации в LogQL и графики DNS запросов к dnsmasq, наприклад так:

- job_name: journal
  pipeline_stages:
  - match:
      selector: '{job="systemd-journal"}'
      stages:
      - regex:
          expression: '.*level=(?P<level>[a-zA-Z]+).*'
      - labels:
          level:
      - regex:
          expression: '.*source="(?P<source>[a-zA-Z]+)".*'
      - labels:
          source:
  journal:
    labels:
      job: systemd-journal
    max_age: 12h
    path: /var/log/journal
  relabel_configs:
  - source_labels:
    - __journal__systemd_unit
    target_label: unit
  - source_labels:
    - __journal__hostname
    target_label: hostname

Тут я для тестів створював нові лейбли, які підключалися до логів – source і level.

Інший варіант із Promtail – використовуючи static_labels.

Але тут є проблема: оскільки Loki на кожний набір лейбл створює окремий лог-стрім, для якого створюються окремі індекси та блоки даних, то в результаті отримаємо по-перше проблеми з продуктивністю, по-друге – з вартістю, т.к. на кожен індекс і блок даних будуть виконуватися запити читання-запису в shared store, у нашому випадку це AWS S3, де за кожен запит доводиться платити гроші.

Дивись чудовий пост на цю тему тут – Grafana Loki and what can go wrong with label cardinality.

Додавання labels із запитів в Loki

Натомість, ми можемо створювати нові лейбли прямо із запиту за допомогою самої Loki.

Візьмемо запис із лога, в якому йдеться про спрацювання OOM Killer:

E1213 16:52:25.879626 3382 pod_workers.go:951] “Error syncing pod, skipping” err=”failed to \”CreatePodSandbox\” for \”oom-test_default(f02523a9-43a7-4370-85dd-1da7554496e6)\” with CreatePodSandboxError: \”Failed to create sandbox for pod \\\”oom-test_default(f02523a9-43a7-4370-85dd-1da7554496e6)\\\”: rpc error: code = Unknown desc = failed to start sandbox container for pod \\\”oom-test\\\”: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: container init was OOM-killed (memory limit too low?): unknown\”” pod=”default/oom-test” podUID=f02523a9-43a7-4370-85dd-1da7554496e6

Тут ми маємо поле pod з ім’ям пода, який був вбитий – pod="default/oom-test".

Використовуємо regex у вигляді pod=".*/(?P<pod>[a-zA-Z].*)".* щоб створити Named Capturing Group , перевіряємо наприклад на https://regex101.com :

Доповнюємо вибірку в Loki:

{hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" | regexp `pod=".*/(?P<pod>[a-zA-Z].*)".*`

І в результаті отримуємо лейблу pod  зі значенням ” oom-test “:

Перевіряємо запит алерту з sum() і rate():

sum(rate({hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" | regexp `pod=".*/(?P<pod>[a-zA-Z].*)".*` | pod!="" [5m])) by (pod)

Результат:

Оновлюємо алерт – додамо description в якому використовуємо {{ $labels.pod }}:

- alert: TESTLokiRuler Systemd journal
  expr: |
    sum(rate({hostname="eks-node-dev_data_services-i-081719890438d467f"} |~ ".*OOM-killed.*" | regexp `.*pod=".*/(?P<pod>[a-zA-Z].*)".*` | pod!="" [15m])) by (pod) > 1
  for: 1s
  labels:
      severity: info
  annotations:
      summary: Test Loki OOM Killer Alert
      description: "Killed pod: `{{ $labels.pod }}`"

Чекаємо на його спрацювання:

І в Слаку:

Grafana Loki и ошибки 502 и 504

Зараз не вдається зарепрод’юсити, але іноді Grafana не може дочекатися відповіді від Loki виконання запиту і падає з помилками 502 або 504.

Є тред в Girthub , мені допомогло збільшення тайм-аутів HTTP в Loki ConfigMap:

...
server:
  http_server_read_timeout: 600s
  http_server_write_timeout: 600s
...

Загалом, на цьому поки що все.

Посилання по темі

Loading