- PVSM.RU - https://www.pvsm.ru -
Краткое знакомство с kubernetes для разработчиков на примере разворачивания простого шаблонного сайта, с постановкой его на мониторинг, выполнением джобов по расписанию и health checks (все исходники прилагаются)
— Установка Kubernetes [1]
— Установка UI [2]
— Запуск своего приложения в кластере [3]
— Добавление кастомных метрик в приложение [4]
— Сбор метрик через Prometheus [5]
— Отображение метрик в Grafana [6]
— Выполнение задач по расписанию [7]
— Отказоустойчивость [8]
— Выводы [9]
— Заметки [10]
— Список литературы [11]
не подходит для linux-пользователей, вам придётся использовать minikube [12]

kubectl versionkubectl cluster-infokubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended/kubernetes-dashboard.yaml
kubectl describe secret
И копируй

kubectl proxy

FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src
COPY ./MetricsDemo.csproj .
RUN ls
RUN dotnet restore "MetricsDemo.csproj"
COPY . .
RUN dotnet build "MetricsDemo.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "MetricsDemo.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "MetricsDemo.dll"]
docker build -t metricsdemo3 .
docker create -p 5000:5000 --restart always --name registry registry:2
{
"registry-mirrors": [],
"insecure-registries": [
"localhost:5000"
],
"debug": true,
"experimental": false
}
docker start registry
docker tag metricsdemo3 localhost:5000/sansys/metricsdemo3
docker push localhost:5000/sansys/metricsdemo3

Создай файл деплоя
kind: Deployment
apiVersion: apps/v1
metadata:
name: metricsdemo
labels:
app: web
spec:
replicas: 2 # сколько подов поднять (инстансов запущенных приложений)
# селектор решает, на какие шаблоны распространяется деплой
selector:
matchLabels:
app: metricsdemo
template:
metadata:
labels:
app: metricsdemo # по этой метке ищет selector в kind: Service
spec:
containers:
- name: metricsdemo # имя деплоя
image: localhost:5000/sansys/metricsdemo3 # образ в докере
ports:
- containerPort: 80 # какой порт слушает приложение внутри докера
# ВАЖНО: три дефиса делят файл, как бы на два отдельных ямла
---
kind: Service
apiVersion: v1
metadata:
name: metricsdemo # имя для прометеуса __meta_kubernetes_service_name="metricsdemo", см https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config
labels:
apptype: business # имя для прометеуса __meta_kubernetes_service_label_apptype="business" - запомни
instancetype: web # имя для прометеуса __meta_kubernetes_service_label_instancetype="web"
spec:
selector:
app: metricsdemo # селектор приложений по labels:app
type: LoadBalancer # реверспрокси из вне до подов
ports:
- protocol: TCP # имя для прометеуса _meta_kubernetes_service_port_protocol="TCP"
port: 9376
targetPort: 80
name: portapi # имя для прометеуса __meta_kubernetes_service_port_name="portapi"
Небольшое описание
Далее:
kubectl create -f .1-deployment-app.yaml
И вы должны увидеть в интерфейсе свой деплой http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#!/deployment?namespace=default [18]

Внутри которого есть Replica Set, показывающий, что приложение запущено в двух экземплярах (Pods) и есть один связанный сервис с адресом из вне, чтобы открыть задеплоенное приложение в браузере

В приложение добавил пакет https://www.app-metrics.io/ [19]
Расписывать подробно, как их добавлять не буду, пока кратко — регистрирую мидлварю для инкремента счётчиков вызовов методов апи
private static void AutoDiscoverRoutes(HttpContext context)
{
if (context.Request.Path.Value == "/favicon.ico")
return;
List<string> keys = new List<string>();
List<string> vals = new List<string>();
var routeData = context.GetRouteData();
if (routeData != null)
{
keys.AddRange(routeData.Values.Keys);
vals.AddRange(routeData.Values.Values.Select(p => p.ToString()));
}
keys.Add("method"); vals.Add(context.Request.Method);
keys.Add("response"); vals.Add(context.Response.StatusCode.ToString());
keys.Add("url"); vals.Add(context.Request.Path.Value);
Program.Metrics.Measure.Counter.Increment(new CounterOptions
{
Name = "api",
//ResetOnReporting = true, // обнулять, если коллетор собрал данные
MeasurementUnit = Unit.Calls,
Tags = new MetricTags(keys.ToArray(), vals.ToArray())
});
}
И собранные метрики доступны по адресу http://localhost:9376/metrics [20]

* IMetricRoot или его абстракцию можно спокойно регистрировать в сервисах и юзать в приложении ( services.AddMetrics(Program.Metrics); )
Самая базовая настройка прометеуса: добавить в его конфиг (prometheus.yml) новый job и скормить ему новый таргет:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
# - "first.rules"
# - "second.rules"
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ['localhost:9090', 'ещё_один_сервис:порт']
Но у прометеуса есть нативная поддержка сбора метрик из кубернетиса https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config [21]
Мониторить я хочу каждый сервис в отдельности фильтруя по метке apptype: business
Ознакомившись с докой джоб получается таким:
- job_name: business-metrics # просто придумал имя джоба
metrics_path: /metrics
kubernetes_sd_configs:
- role: endpoints # какую сущность мониторить. ещё есть service,pod,ingress
static_configs:
- targets:
- localhost:9090
relabel_configs: # собираю метрики сервисов только из пространства default и приложений c меткой apptype = business
- action: keep
regex: default;business
source_labels:
- __meta_kubernetes_namespace
- __meta_kubernetes_service_label_apptype
В кубернетисе есть специальное место для хранения файлов конфигов — ConfigMap [22]
Вот там этот конфиг и сохраняю:
apiVersion: v1
kind: ConfigMap # тип сущности, обрати внимание
metadata:
name: prometheus-config # имя конфиг-маппы
namespace: default
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: EnsureExists
data:
# имя файла в конфиге
prometheus.yml: |
global:
scrape_interval: 5s # Default is every 1 minute.
evaluation_interval: 5s # The default is every 1 minute.
scrape_configs:
- job_name: prometheus
static_configs:
- targets:
- localhost:9090
- job_name: business-metrics # просто придумал имя джоба
metrics_path: /metrics
kubernetes_sd_configs:
- role: endpoints # какую сущность мониторить. ещё есть service,pod,ingress
static_configs:
- targets:
- localhost:9090
relabel_configs: # собираю метрики сервисов только из пространства default и приложений c меткой apptype = business
- action: keep
regex: default;business
source_labels:
- __meta_kubernetes_namespace
- __meta_kubernetes_service_label_apptype
Отправление в кубернетис
kubectl create -f .2-prometheus-configmap.yaml
Теперь нужно задеплоить прометеус с этим файлом конфига
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: prometheus
namespace: default
spec:
replicas: 1
template:
metadata:
labels:
app: prometheus-server
spec:
containers:
- name: prometheus
image: prom/prometheus
args:
- "--config.file=/etc/config/prometheus.yml"
- "--web.enable-lifecycle"
ports:
- containerPort: 9090
volumeMounts:
- name: prometheus-config-volume # какой вольюм монтировать
mountPath: /etc/config/ # в качестве какой директории
volumes:
- name: prometheus-config-volume # объявление вольюма в деплое
configMap:
defaultMode: 420
name: prometheus-config # имя конфиг-маппы
---
kind: Service
apiVersion: v1
metadata:
name: prometheus
spec:
selector:
app: prometheus-server # селектор приложений по labels:app
type: LoadBalancer # реверспрокси из вне до подов
ports:
- protocol: TCP
port: 9090
targetPort: 9090
Обрати внимание — файл prometheus.yml нигде не указывается
Все файлы, которые были указаны в конфиг-мапе становятся файлами в разделе prometheus-config-volume, который монтируется в директорию /etc/config/
Также у контейнера указаны аргументы запуска с путём до конфига
--web.enable-lifecycle — говорит о том, что можно дёрнуть POST /-/reload, что применит новые конфиги (полезно, если конфиг меняется «на лету» и не хочется перезапускать контейнер)
Собственно деплой
kubectl create -f .3-deployment-prometheus.yaml
Последи малость за подъёмом подов и ходи по адресу http://localhost:9090/targets [23], должен там увидеть эндпоинты своего сервиса

А на главной странице можно писать запросы к прометеусу
sum by (response, action, url, app) (delta(application_api[15s]))
Язык запросов — https://prometheus.io/docs/prometheus/latest/querying/basics/ [24]
Нам повезло — до версии 5 конфиги дашборд можно было только по HTTP API подсунуть, но теперь можно проделать тот же трюк, что и с прометеусом
Графана по дефолту при запуске умеет подтягивать конфиги [25] источников данных и дашборд
/etc/grafana/provisioning/datasources/ — конфиги источников (настройки доступа к прометеус, постгрес, заббикс, эластик и т.п.)/etc/grafana/provisioning/dashboards/ — настройки доступа к дашбордам/var/lib/grafana/dashboards/ — тут буду хранить сами дашборды в виде json-файловapiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: null
name: grafana-provisioning-datasources
namespace: default
data:
all.yml: |
datasources:
- name: 'Prometheus'
type: 'prometheus'
access: 'proxy'
org_id: 1
url: 'http://prometheus:9090'
is_default: true
version: 1
editable: true
---
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: null
name: grafana-provisioning-dashboards
namespace: default
data:
all.yml: |
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10 #how often Grafana will scan for changed dashboards
options:
path: /var/lib/grafana/dashboards
---
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: null
name: grafana-dashboards
namespace: default
data:
service-http-requests.json: |
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"legend": {
"alignAsTable": false,
"avg": false,
"current": false,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum by (response, action, url, app) (delta(application_api[15s]))",
"format": "time_series",
"interval": "15s",
"intervalFactor": 1,
"legendFormat": "{{app}} {{response}} - {{url}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Http requests",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"refresh": "5s",
"schemaVersion": 16,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Business metrics",
"uid": "Dm0tD0Qik",
"version": 1
}
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: grafana
namespace: default
labels:
app: grafana
component: core
spec:
replicas: 1
template:
metadata:
labels:
app: grafana
component: core
spec:
containers:
- image: grafana/grafana
name: grafana
imagePullPolicy: IfNotPresent
resources:
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 100m
memory: 100Mi
env:
- name: GF_AUTH_BASIC_ENABLED
value: "true"
- name: GF_AUTH_ANONYMOUS_ENABLED
value: "true"
- name: GF_AUTH_ANONYMOUS_ORG_ROLE
value: Admin
readinessProbe:
httpGet:
path: /login
port: 3000
# initialDelaySeconds: 30
# timeoutSeconds: 1
volumeMounts:
- name: grafana-provisioning-datasources
mountPath: /etc/grafana/provisioning/datasources/
- name: grafana-provisioning-dashboards
mountPath: /etc/grafana/provisioning/dashboards/
- name: grafana-dashboards
mountPath: /var/lib/grafana/dashboards/
volumes:
- name: grafana-provisioning-datasources
configMap:
defaultMode: 420
name: grafana-provisioning-datasources
- name: grafana-provisioning-dashboards
configMap:
defaultMode: 420
name: grafana-provisioning-dashboards
- name: grafana-dashboards
configMap:
defaultMode: 420
name: grafana-dashboards
nodeSelector:
beta.kubernetes.io/os: linux
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: default
labels:
app: grafana
component: core
spec:
type: LoadBalancer
ports:
- protocol: TCP
port: 3000
targetPort: 3000
selector:
app: grafana
component: core
Разворачиваем
kubectl create -f .4-grafana-configmap.yaml
kubectl create -f .5-deployment-grafana.yaml
Помни, что графана не сразу поднимается, она немного шарашит миграциями sqlite, что можешь в логах пода увидеть [26]
Теперь иди по адресу http://localhost:3000/ [27]
И кликай по дашборде


Если хочешь добавить новую вьюху или изменить существующую — меняй прямо в интерфейсе, а после жми Сохранить, получишь модальное окно с json, который нужно засунуть в конфиг-мапу

Для выполнения задач по крону в кубере есть понятие CronJob
С помощью CronJob можно задать расписание выполнения любых задач, самый простой пример:
# https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: runapijob
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: runapijob
image: busybox
args:
- /bin/sh
- -c
- date; wget -O - http://metricsdemo:9376/api/job/run/wakeUp > /dev/null
restartPolicy: OnFailure
В секции schedule задаётся классическое правило для крона
По триггеру запускается pod контейнера (busybox) в котором я дёргаю метод апи сервиса metricsdemo
Для слежения за джобом можно воспользоваться командой
kubectl.exe get cronjob runapijob --watch

Основной сервис, который дёргается из джоба, запущен в нескольких экземплярах, потому обращение к сервису уходит на один из подов с примерно равномерным разбросом


Небольшое демо на примере расчёта числа π, про разницу в запусках из консоли
# запуск целого деплоймента, но приложение завершается и возвращает контроль - репликасет постоянно пытается его рестартануть
kubectl run pi --image=perl -- perl -Mbignum=bpi -wle 'print bpi(2000)'
# запуск одноразового джоба. Запустится, завершится, и всё. Результат расчёта - в логах
kubectl run pi --image=perl --restart=OnFailure -- perl -Mbignum=bpi -wle 'print bpi(2000)'
# запуск кронджоба каждые 5 минут
kubectl run pi --image=perl --restart=OnFailure --schedule="0/5 * * * ?" -- perl -Mbignum=bpi -wle 'print bpi(2000)'
Если приложение неожиданно завершается, то кластер перезапускает pod
К примеру я сделал метод, который роняет апи
[HttpGet("kill/me")]
public async void Kill()
{
throw new Exception("Selfkill");
}
* исключение возникшее в api в методе async void считается Unhandled exception, что полностью крашит приложение
Делаю обращение к http://localhost:9376/api/job/kill/me [28]
В списке подов видно, что один из подов сервиса был перезапущен

Команда logs показывает текущий вывод, а с параметром -p выдаст логи предыдущего инстанса. Таким образом можно узнать причину перезапуска
Думаю с простым падением всё ясно: упал — поднялся
Но приложение может быть и условно живым, т.е. не павшим, но и ничего не делающим, либо делающим свою работу, но медленно
Согласно документации [29] существует, как минимум, два вида проверок «на живучесть» приложений в подах
Проверю рестарт по таймауту, для этого добавляю новый метод апи, который по определённой команде начнёт замедлять метод проверки живучести на 123 сек
static bool deadlock;
[HttpGet("alive/{cmd}")]
public string Kill(string cmd)
{
if (cmd == "deadlock")
{
deadlock = true;
return "Deadlocked";
}
if (deadlock)
Thread.Sleep(123 * 1000);
return deadlock ? "Deadlocked!!!" : "Alive";
}
В файл 1-deployment-app.yaml в контейнер дописываю пару секций:
containers:
- name: metricsdemo
image: localhost:5000/sansys/metricsdemo3:6
ports:
- containerPort: 80
readinessProbe: # способно ли приложение сейчас обрабатывать запросы
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe: # живо ли приложение в принципе
httpGet:
path: /api/job/alive/check
port: 80
initialDelaySeconds: 5
periodSeconds: 5
Редеплою, убеждаюсь, что апишка запустилась и подписываюсь на события
kubectl get events --watch
Жму меню Deadlock me (http://localhost:9376/api/job/alive/deadlock [30])

И в течение пяти секунд начинаю наблюдать проблему и её решение
1s Warning Unhealthy Pod Liveness probe failed: Get http://10.1.0.137:80/api/job/alive/check: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
1s Warning Unhealthy Pod Liveness probe failed: Get http://10.1.0.137:80/api/job/alive/check: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
0s Warning Unhealthy Pod Liveness probe failed: Get http://10.1.0.137:80/api/job/alive/check: net/http: request canceled (Client.Timeout exceeded while awaiting headers)
0s Warning Unhealthy Pod Readiness probe failed: Get http://10.1.0.137:80/health: dial tcp 10.1.0.137:80: connect: connection refused
0s Normal Killing Pod Killing container with id docker://metricsdemo:Container failed liveness probe.. Container will be killed and recreated.
0s Normal Pulled Pod Container image "localhost:5000/sansys/metricsdemo3:6" already present on machine
0s Normal Created Pod Created container
0s Normal Started Pod Started container
имени сервиса:порту, что и сделано с графаной → прометеус. Для тех, кто знаком с docker-compose тут ничего новогоkubectl create -f file.yml — создать сущностьkubectl delete -f file.yml — удалить сущностьkubectl get pod — получить список всех подов (service, endpoints...)
--namespace=kube-system — фильтрация по неймспейсу-n kube-system — аналогичноkubectl -it exec grafana-d8d4d9f5c-cvnkh -- /bin/bash — аттач к подуkubectl delete service grafana — удалить сервис, pod. деплой (--all — удалить все)kubectl describe — описать сущность (можно все сразу)kubectl edit service metricsdemo — редактирование всех ямлов «на лету» через запуск блокнота

kubectl --help — отличный хелп)
Исходный код и ямлы доступны на гитхабе [17]
Автор: SanSYS
Источник [42]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/306310
Ссылки в тексте:
[1] Установка Kubernetes: #1
[2] Установка UI: #2
[3] Запуск своего приложения в кластере: #3
[4] Добавление кастомных метрик в приложение: #4
[5] Сбор метрик через Prometheus: #5
[6] Отображение метрик в Grafana: #6
[7] Выполнение задач по расписанию: #7
[8] Отказоустойчивость: #8
[9] Выводы: #9
[10] Заметки: #10
[11] Список литературы: #11
[12] minikube: https://kubernetes.io/docs/tasks/tools/install-minikube/#linux
[13] http://localhost:8001/: http://localhost:8001/
[14] https://storage.googleapis.com/kubernetes-release/release/stable.txt: https://storage.googleapis.com/kubernetes-release/release/stable.txt
[15] https://storage.googleapis.com/kubernetes-release/release/v1.13.2/bin/windows/amd64/kubectl.exe: https://storage.googleapis.com/kubernetes-release/release/v1.13.2/bin/windows/amd64/kubectl.exe
[16] http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/: http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/
[17] https://github.com/SanSYS/kuberfirst: https://github.com/SanSYS/kuberfirst
[18] http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#!/deployment?namespace=default: http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#!/deployment?namespace=default
[19] https://www.app-metrics.io/: https://www.app-metrics.io/
[20] http://localhost:9376/metrics: http://localhost:9376/metrics
[21] https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config
[22] ConfigMap: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/
[23] http://localhost:9090/targets: http://localhost:9090/targets
[24] https://prometheus.io/docs/prometheus/latest/querying/basics/: https://prometheus.io/docs/prometheus/latest/querying/basics/
[25] умеет подтягивать конфиги: http://docs.grafana.org/administration/provisioning/
[26] в логах пода увидеть: http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#!/service/default/grafana?namespace=default
[27] http://localhost:3000/: http://localhost:3000/
[28] http://localhost:9376/api/job/kill/me: http://localhost:9376/api/job/kill/me
[29] документации: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/
[30] http://localhost:9376/api/job/alive/deadlock: http://localhost:9376/api/job/alive/deadlock
[31] habr.com/ru/company/flant/blog/436112: https://habr.com/ru/company/flant/blog/436112/
[32] Kubernetes Deployment Tutorial For Beginners: https://devopscube.com/kubernetes-deployment-tutorial/
[33] Deployments: https://kubernetes.io/zh/docs/concepts/workloads/controllers/deployment/
[34] Run a Stateless Application Using a Deployment: https://kubernetes.io/docs/tasks/run-application/run-stateless-application-deployment/
[35] CronJob: https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/
[36] kubectl Cheat Sheet: https://kubernetes.io/docs/reference/kubectl/cheatsheet/
[37] Устройство и механизм работы Prometheus Operator в Kubernetes: https://habr.com/ru/company/flant/blog/353410/
[38] Подсмотреть, как делают люди: https://github.com/microservices-demo/microservices-demo/tree/master/deploy/kubernetes/manifests-monitoring
[39] Helm: https://github.com/helm/helm
[40] Ямлы для прометеуса от кубера: https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/prometheus
[41] Kubernetes Failure Stories: https://github.com/hjacobs/kubernetes-failure-stories
[42] Источник: https://habr.com/ru/post/437286/?utm_campaign=437286
Нажмите здесь для печати.