<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Lev Aminov</title><generator>teletype.in</generator><description><![CDATA[Lev Aminov]]></description><image><url>https://img1.teletype.in/files/0e/1c/0e1c2be6-bb8b-4943-a11f-ffa454061000.png</url><title>Lev Aminov</title><link>https://levaminov.ru/</link></image><link>https://levaminov.ru/?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/levaminov?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/levaminov?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Wed, 06 May 2026 12:19:23 GMT</pubDate><lastBuildDate>Wed, 06 May 2026 12:19:23 GMT</lastBuildDate><item><guid isPermaLink="true">https://levaminov.ru/9SXROqaOypv</guid><link>https://levaminov.ru/9SXROqaOypv?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/9SXROqaOypv?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Ошибка ERR_NGROK_9040. Ngrok закрыл доступ для всех российских IP-адресов</title><pubDate>Wed, 02 Apr 2025 15:17:14 GMT</pubDate><description><![CDATA[Пользователи массово столкнулись с проблемами в работе ngrok в России — при попытке запустить туннель отображается ошибка: &quot;We do not allow agents to connect to ngrok from your IP address&quot;. Это связано с санкциями, которые запрещают предоставление сервиса пользователям из России.]]></description><content:encoded><![CDATA[
  <p id="gx0g">Пользователи массово столкнулись с проблемами в работе ngrok в России — при попытке запустить туннель отображается ошибка: &quot;We do not allow agents to connect to ngrok from your IP address&quot;. Это связано с санкциями, которые запрещают предоставление сервиса пользователям из России.</p>
  <p id="LpL6">В качестве решения проблемы с ошибкой <code>ERR_NGROK_9040</code> мы предлагаем воспользоваться нашим сервисом <a href="https://tuna.am" target="_blank">Tuna</a> – простой интерфейс, понятный функционал, в целом, всё, как надо.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/Hz1dz27VGd_</guid><link>https://levaminov.ru/Hz1dz27VGd_?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/Hz1dz27VGd_?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Синхронизация образов в свой Docker Registry</title><pubDate>Wed, 27 Sep 2023 14:42:52 GMT</pubDate><description><![CDATA[Есть такой инструмент – skopeo, в числе прочего он умеет перепушивать образы из одного регистри в другой, что может быть удобно, если периодически возникает проблема с рейт-лимитом на получение образа. Копировать образ в свой регистри можно было бы и вручную, но не всегда удобно, можно забыть про передачу нужной платформу или у того, кому это срочно надо, нет доступа на пуш. Поэтому в данной заметке предлагаю несложный способ как это можно худо-бедно автоматизировать в Gitlab.]]></description><content:encoded><![CDATA[
  <p id="l7xj">Есть такой инструмент – <a href="https://github.com/containers/skopeo" target="_blank">skopeo</a>, в числе прочего умеет перепушивать образы из одного регистри в другой, что может быть удобно, если периодически возникает проблема с рейт-лимитом на получение образа. Копировать образ в свой регистри можно было бы и вручную, но не всегда удобно, можно забыть про передачу нужной платформы или у того, кому это срочно надо, нет доступа на пуш. Поэтому в данной заметке предлагаю несложный способ как это можно худо-бедно автоматизировать в Gitlab.</p>
  <p id="H3qs">Создаем пустой репозиторий, в файл <code>.gitlab-ci.yml</code> прописываем следующий контент:</p>
  <pre id="sMvm" data-lang="yaml">---
variables:
  REGISTRY_ADDR: my.registry/aabbccdd

stages:
  - lint
  - sync

lint:
  image: quay.io/skopeo/stable:v1.13.0
  stage: lint
  script:
    - DRY_RUN=1 ./.skopeo.sh

sync:
  image: quay.io/skopeo/stable:v1.13.0
  stage: sync
  script:
    - ./.skopeo.sh
  only:
    refs:
      - master</pre>
  <p id="C6Ym">Чуть ниже создаем файл <code>.skopeo.sh</code>:</p>
  <pre id="mBKd" data-lang="bash">#!/usr/bin/env bash

set -o errexit
set -o nounset

for file in *-images.yaml; do
  echo &quot;==&gt; ${file}&quot;
  skopeo sync \
    --scoped \
    --keep-going \
    --src yaml \
    --dry-run=&quot;${DRY_RUN:-0}&quot; \
    --dest docker &quot;${file}&quot; &quot;${REGISTRY_ADDR}&quot; \
    --dest-authfile .docker_config.json
done</pre>
  <p id="w566">Обращу внимание, что для пуша могут потребоваться какие-то реквизиты, через аргумент <code>--dest-authfile</code> задается путь к стандартному для Docker файлу с реквизитами в формате JSON, можно так же использовать <code>--dest-registry-token</code> или <code>--dest-creds</code>, все варианты перечислены в справке.</p>
  <p id="YBEb">Теперь создаем файл, например, <code>team-infra-images.yaml</code> (файлов может быть несколько, главное, чтобы заканчивались на <code>-images.yaml</code>) с перечнем образов, которые нужно перенести к нам:</p>
  <pre id="Kinb" data-lang="yaml">---
quay.io:
  images:
    skopeo/stable:
      - v1.13.0
mirror.gcr.io:
  images:
    golang:
      - &quot;1.21&quot;</pre>
  <p id="FFLN">Теперь, при создании Merge Request у нас будет выполняться dry-run запуск, оставим на всякий случай, чтобы убедиться, что внутри конфигурационных файлов корректный синтаксис. После вливания изменений в основную ветку произойдет проверка источника с получателем, если есть разночтения, то образы будут скопированы в нужный регистри.</p>
  <p id="GGlM">Из флагов в команде sync отмечу следующие:</p>
  <ul id="gNyW">
    <li id="DjFQ"><code>--scoped</code> – для включения хоста источника в имя результирующего образа в нашем регистри (например, образ <code>quay.io/skopeo/stable:v1.13.0</code> будет скопирован не в <code>my.registry/aabbccdd/skopeo/stable:v1.13.0</code>, а в <code>my.registry/aabbccdd/quay.io/skopeo/stable:v1.13.0</code>);</li>
    <li id="YRzE"><code>--keep-going</code> – продолжать синхронизацию образов, даже если получили ошибку.</li>
  </ul>
  <p id="HPk3">Так же можно запланировать периодический запуск, чтобы быть уверенным, что все образы на месте.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/v7qTdsBqLp_</guid><link>https://levaminov.ru/v7qTdsBqLp_?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/v7qTdsBqLp_?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Мои базовые настройки macOS</title><pubDate>Sun, 13 Aug 2023 11:26:42 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/67/0b/670b590e-7c68-4ef2-9811-19d553634ede.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/a9/55/a9558b6f-0c22-43f3-ab7d-742b5bbd605a.png"></img>Каждый раз при смене ноутбука устанавливаю одни и те же настройки, от каких-то избавился, а какие-то со мной уже давно, здесь перечислю, чтобы и самому не забывать и может другим помочь.]]></description><content:encoded><![CDATA[
  <p id="CfmP">Каждый раз при смене ноутбука устанавливаю одни и те же настройки, от каких-то избавился, а какие-то со мной уже давно, здесь перечислю, чтобы и самому не забывать и может другим помочь.</p>
  <h2 id="cYMp">Тапы по тачпаду</h2>
  <p id="ygIv">Помимо физического нажатия на тачпад можно будет просто по нему тапать пальцем, будет считаться за клик. Переходим в настройки, затем включаем нижнюю галочку &quot;Tap on click&quot;:</p>
  <figure id="aLuM" class="m_column">
    <img src="https://img3.teletype.in/files/a9/55/a9558b6f-0c22-43f3-ab7d-742b5bbd605a.png" width="1654" />
  </figure>
  <h2 id="uBJC">Скорость реакции тачпада</h2>
  <p id="swCg">Та же страница, настройку &quot;Tracking speed&quot; выкручиваем на максимум.</p>
  <h2 id="M80S">Скорость повтора нажатия</h2>
  <p id="x3k8">Настройки &quot;Key repeat rate&quot; и &quot;Delay until repeat&quot; выставляем в положение &quot;Fast&quot; и &quot;Short&quot;:</p>
  <figure id="Xjls" class="m_column">
    <img src="https://img3.teletype.in/files/2c/21/2c212217-fe2c-407c-8ec2-9762229774a7.png" width="1654" />
  </figure>
  <h2 id="rhGv">Перетаскивание окон тремя пальцами</h2>
  <p id="aQOm">Переходим в Accessibility, там в раздел Pointer Control, жмем на кнопку Trackpad Options, ставим галочку &quot;Use trackpad for dragging&quot;, а значение &quot;Dragging style&quot; устанавливаем как &quot;Three-Fingers Drag&quot;:</p>
  <figure id="OuIm" class="m_column">
    <img src="https://img2.teletype.in/files/5c/01/5c011dd3-4773-4922-b2be-dbeebd457df9.png" width="1654" />
  </figure>
  <h2 id="R2l8">Блокировка экрана через перемещения курсора в угол экрана</h2>
  <p id="2GLr">Вообще есть сочетание клавиш <code>^⌘Q</code>, но привычка – дело такое. Переходим в раздел настроек Desktop &amp; Dock, в самом низу будет кнопка Hot Corners, выбираем блокировку для нужного угла:</p>
  <figure id="NYpH" class="m_column">
    <img src="https://img2.teletype.in/files/51/3f/513f6d9d-9fcc-494c-82a4-9a5d038c4217.png" width="1654" />
  </figure>
  <h2 id="VluM">Отключение звуковых сигналов в Terminal</h2>
  <p id="Mpdh">Открываем Terminal, переходим в настройки, в настройках текущего профиля переходим во вкладку Advanced, там снимаем флаг &quot;Audible bell&quot;:</p>
  <figure id="gUwt" class="m_column">
    <img src="https://img3.teletype.in/files/29/86/298668a4-66d4-4f99-b5f1-353c7d0c7ddc.png" width="1558" />
  </figure>
  <h2 id="lNj6">Режим разработчика в Safari</h2>
  <p id="Labl">Открываем Safari, далее Настройки и раздел Advanced, в самом низу ставим галочку &quot;Show Develop menu in menu bar&quot;:</p>
  <figure id="pCXG" class="m_column">
    <img src="https://img4.teletype.in/files/b6/ed/b6ed0793-a054-45a4-9ff6-dbecb2c5354c.png" width="1772" />
  </figure>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/JsEwp91lmC-</guid><link>https://levaminov.ru/JsEwp91lmC-?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/JsEwp91lmC-?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Переезд из Slack в Mattermost (3)</title><pubDate>Tue, 28 Mar 2023 07:08:14 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/37/87/37871399-5e35-4903-b521-3cdc07334c91.png"></media:content><description><![CDATA[<img src="https://img1.teletype.in/files/45/0f/450f3070-4d7e-4110-8aae-d44989bddafb.png"></img>Часть 1: https://levaminov.ru/z3TpTpYSK4J]]></description><content:encoded><![CDATA[
  <p id="ByVK">Часть 1: <a href="https://levaminov.ru/z3TpTpYSK4J" target="_blank">https://levaminov.ru/z3TpTpYSK4J</a></p>
  <p id="KIAu">Часть 2: <a href="https://levaminov.ru/Rmks9ZZ7RLl" target="_blank">https://levaminov.ru/Rmks9ZZ7RLl</a></p>
  <p id="kce4">И вот мы опять вернулись к исследованию производительности Mattermost, если не считать дублирующихся сообщений в больших тредах, то все было хорошо и нормально, но эти дубли, в процессе разбора инцидентов, просто выносили всем мозг. При этом у самого сервера никаких серьезных ошибок не фиксируется:</p>
  <figure id="EWSu" class="m_retina">
    <img src="https://img1.teletype.in/files/45/0f/450f3070-4d7e-4110-8aae-d44989bddafb.png" width="1412" />
  </figure>
  <p id="SZLz">Раньше мы, в случае проблем, натыкались на какие-то ошибки подключения к базе, лимитам по коннектам, сейчас только таймату API. При этом график этих самых таймаутов методов совпадал с ошибками закрытия соединений в nginx:</p>
  <figure id="m9uj" class="m_column">
    <img src="https://img1.teletype.in/files/0a/14/0a144637-1002-438e-85af-e07f3ea7fa50.png" width="2030" />
  </figure>
  <p id="7bNW">Тут стало понятно, что есть какие-то таймауты со стороны сервера Mattermost, которые на это влияют, обратились к документации:</p>
  <figure id="5NXv" class="m_column">
    <img src="https://img1.teletype.in/files/c7/b1/c7b14e90-28de-4b95-93a0-325133183fa6.png" width="2026" />
  </figure>
  <p id="wSS6">Проверяем, что у нас:</p>
  <figure id="hogW" class="m_column">
    <img src="https://img1.teletype.in/files/c3/ee/c3ee0292-70bb-4cbc-99ec-f52f366a3642.png" width="1904" />
  </figure>
  <p id="w5Zx">Нам так и не удалось выяснить, откуда взялись эти значения, возможно, мигрировало из нашей инсталляции, которая была развернута и управлялась оператором (там ресурсы на кластер выделяются относительно &quot;размера&quot; кластера в пользователях), возможно неудачная миграция с одного мажора на другой... непонятно. Поменяли значения на дефолтные – ошибки пропали, дубли пропали тоже.</p>
  <figure id="uUYw" class="m_original">
    <img src="https://img4.teletype.in/files/7d/4c/7d4cc28a-afba-4789-9ed9-fabe4d28a147.png" width="2828" />
  </figure>
  <p id="oKHj">После изменений так же упала нагрузка с сервера и базы, поэтому статистику по каналу, которую мы отключали в прошлом заходе, мы вернули обратно.</p>
  <p id="yqYA">Заметил, что это не первая задача за последние пару дней, решение которой происходит в процессе объяснения другому инженеру что делать и с чего начинать исследование проблемы, вот уж точно &quot;правильно заданный вопрос – половина ответа&quot;.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/Rmks9ZZ7RLl</guid><link>https://levaminov.ru/Rmks9ZZ7RLl?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/Rmks9ZZ7RLl?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Переезд из Slack в Mattermost (2)</title><pubDate>Mon, 14 Nov 2022 17:26:26 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/1d/4a/1d4a1657-1b45-4699-af8b-5bbc013f8ff3.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/90/fa/90fa74b0-0979-40f2-8d2b-5ed196a0e916.png"></img>Часть 1: https://levaminov.ru/z3TpTpYSK4J]]></description><content:encoded><![CDATA[
  <p id="gdLu">Часть 1: <a href="https://levaminov.ru/z3TpTpYSK4J" target="_blank">https://levaminov.ru/z3TpTpYSK4J</a></p>
  <p id="8JqU">Часть 3: <a href="https://levaminov.ru/JsEwp91lmC-" target="_blank">https://levaminov.ru/JsEwp91lmC-</a></p>
  <p id="JZut">К нашей инсталляции Mattermost в пике подключено около 350 устройств – не так много, как в истории с мессенджером в Тинькофф, но проблемы бывают и у маленьких инсталляций. Спойлер: наша проблема так и не решена.</p>
  <p id="iOPF">По мере переезда пользователей из Slack мы столкнулись с тем, что в часы пик число коннектов к базе упирается в лимит, ресурсов не хватает, рестарт сервера Mattermost не спасает, лечится только полной остановкой кластера. Да и это могло помочь как на неделю, так и на день, никакой связи с числом подключенных устройств нет.</p>
  <p id="zrQr">Выглядело это так:</p>
  <figure id="rQ5c" class="m_original">
    <img src="https://img2.teletype.in/files/90/fa/90fa74b0-0979-40f2-8d2b-5ed196a0e916.png" width="2830" />
  </figure>
  <p id="OlUk">В какой-то момент метод получения числа файлов закрепленных за конкретным чатом начинает возвращать таймауты, затем появляются ошибки подключения к базе – too many connections. Возникает вопрос – а где проблема – в базе или в самом приложении?</p>
  <p id="1fvf">Время шло, мы занимались очередными переездами, но ситуация с Mattermost надоела окончательно и мы решили перенести базу с конфигурации 4 vCPU/16 GB на 8 vCPU/32 GB.</p>
  <p id="RZPE">Это помогло, но ненадолго...</p>
  <p id="JQqO">В итоге дошло до того, что проблемы начали появляться с началом дня и заканчиваться только поздней ночью, а число рестартов начинало напрягать – даунтайм Mattermost был 15-20 секунд, а после 10-15 минут проблема могла возвращалась и так весь день. Для пользователей это проявлялось в следующем:</p>
  <ul id="7nNX">
    <li id="Agvw">долго загружаются каналы и треды;</li>
    <li id="ck5t">при отправке сообщения оно дублируется, либо не сохраняется;</li>
    <li id="dOsq">при запуске приложения отображаются не все каналы;</li>
    <li id="4Cz5">ну и наконец – даунтайм отключал от общения всех.</li>
  </ul>
  <p id="E1Fa">Еще с начала тестовой установки Mattermost мы писал все логи, в том числе SQL, все это анализировалось в режиме реального времени:</p>
  <figure id="rmpY" class="m_original">
    <img src="https://img3.teletype.in/files/a4/d6/a4d618ae-15b5-4d25-8e2e-860681141f58.png" width="2826" />
  </figure>
  <p id="k27d">Благодаря этому мы всегда видели как система себя ведет в общем, как реагирует на какие-то изменения. На графиках мы заметили, что проблемы с нагрузкой всегда начинаются с ошибок:</p>
  <pre id="tBMT">Unable to get the file count for the channel</pre>
  <p id="zDND">На графике это выглядит так:</p>
  <figure id="sY6G" class="m_column">
    <img src="https://img3.teletype.in/files/64/27/6427e0b2-2601-4207-8851-34654fba624d.png" width="2716" />
  </figure>
  <p id="QRSV">По коду же, так:</p>
  <figure id="707B" class="m_retina">
    <img src="https://img3.teletype.in/files/64/d3/64d39618-e6e4-4ff5-ac72-a05e31750d37.png" width="522" />
  </figure>
  <p id="taZE">И так:</p>
  <figure id="iTvw" class="m_retina">
    <img src="https://img4.teletype.in/files/72/88/728836cb-6fad-443e-8eca-4a39b59e1071.png" width="817" />
  </figure>
  <p id="zWsR">В часы пик хост mySQL, за который мы платим 25000 руб. в месяц, был загружен на 98% – выглядит сомнительно, поэтому мы продолжили капать дальше, метод GetChannelFileCount вызывается только в getChannelStats – статистика по каналу, содержит в себе:</p>
  <ul id="BBEq">
    <li id="NUvf">GetChannelMemberCount</li>
    <li id="6f5d">GetChannelGuestCount</li>
    <li id="xPG7">GetChannelPinnedPostCount</li>
    <li id="MVRQ">GetChannelFileCount</li>
  </ul>
  <p id="iVDX">В какой-то момент у меня возникла мысль, что у нас нет какого-то индекса на таблице Files, но это предположение не оправдалось – свежая инсталляция содержит все то же описание таблиц... ну окей.</p>
  <p id="QFBv">Метод getChannelStats использует только в API:</p>
  <figure id="C5aU" class="m_retina">
    <img src="https://img2.teletype.in/files/57/4d/574d7368-c980-41ca-9b8f-083e86b6904c.png" width="767" />
  </figure>
  <p id="ltkM">Вызывается так:</p>
  <pre id="b72r">GET /api/v4/channels/.+/stats</pre>
  <p id="zxIB">В качестве эксперимента отдаем по этому пути 204:</p>
  <pre id="caCT">nginx.ingress.kubernetes.io/configuration-snippet: |
  location ~ /api/v4/channels/.+/stats {
    return 204;
  }</pre>
  <p id="m0CF">В итоге нагрузка на хост базы резко сократилась:</p>
  <figure id="hLY3" class="m_retina">
    <img src="https://img1.teletype.in/files/00/e0/00e0ba6c-a347-4496-a302-8b5f09d090db.png" width="370" />
  </figure>
  <p id="ZHDb">В конце 11 ноября мы перекрыли этот и еще один роут (на графике он значился как GetTopChannelsForUserSince), график по ошибкам стал таким:</p>
  <figure id="OUSg" class="m_original">
    <img src="https://img2.teletype.in/files/d7/7d/d77d17d4-ceb7-4722-877f-259193b6b061.png" width="2826" />
  </figure>
  <p id="fGH2">Высокие столбцы – это те самые моменты, в которые мы рестартовали кластер Mattermost. Вроде, стало хорошо. Почему я написал, что проблема не решена? Нууу... теперь в шапке канала пользователи видят следующее:</p>
  <figure id="8h3Z" class="m_retina">
    <img src="https://img2.teletype.in/files/14/a9/14a97ed8-8189-48cb-a98f-a75d13f22c31.png" width="150" />
  </figure>
  <p id="IeLn">Так как метод ничего не возвращает, в интерфейсе тоже ничего не отображается, что может запутать наших пользователей, поэтому сейчас мы думаем (в свободное от работы время) над тем, как решить и эту проблему.</p>
  <p id="Dghw"><strong>UPDATE:</strong> Я писал в Тинькофф с вопросами по распространению их переработанной версии (бесплатно или за деньги), мне ответили, что точного решения нет, как и сроков. Так что, на месте тех, кто готовится к переезду со Slack, я бы не расчитывал на их решение и не ждал его в ближайшем будущем.</p>
  <p id="SVyz">P.S. Мы предлагаем свою аналитику по логам, как на графиках выше – <a href="https://anomaly1.ru" target="_blank">https://anomaly1.ru</a>.</p>
  <p id="dMxV"><s>P.P.S. Да и вообще можем по графикам проконсультировать или настроить что-нибудь – <a href="https://arendamozgov.ru" target="_blank">https://arendamozgov.ru</a>.</s></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/UTnryjmsoV0</guid><link>https://levaminov.ru/UTnryjmsoV0?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/UTnryjmsoV0?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Проверка на дублирование Application в ArgoCD с помощью OPA</title><pubDate>Fri, 02 Sep 2022 13:30:35 GMT</pubDate><description><![CDATA[Так уж вышло, что в ArgoCD плоская структура для хранения описания приложений - все они живут в одном namespace, а значит должны иметь уникальные имена, чтобы предостеречь себя от разных непонятных ситуаций есть простое решение - проверять, не дублируются ли эти самые имена.]]></description><content:encoded><![CDATA[
  <p id="WfTG">Так уж вышло, что в ArgoCD плоская структура для хранения описания приложений - все они живут в одном namespace, а значит должны иметь уникальные имена, чтобы предостеречь себя от разных непонятных ситуаций есть простое решение - проверять, не дублируются ли эти самые имена.</p>
  <p id="T7TP">Для реализации проверки будем использовать проверенный инструмент – <a href="https://www.openpolicyagent.org" target="_blank">Open Policy Agent</a>, точнее даже не инструмент, а движок, который это все реализует, для самого запуска проверки будет использоваться <a href="https://www.conftest.dev" target="_blank">conftest</a>.</p>
  <p id="OhzM">Для поиска дублей нужно описать политику (policy), она описывается в синтаксисе rego - не самая понятная штука, поэтому моя заметка служит просто готовым решением, детальное описание синтаксиса можно найти <a href="https://www.openpolicyagent.org/docs/latest/policy-language/" target="_blank">на сайте с документацией</a> (есть даже <a href="https://play.openpolicyagent.org" target="_blank">песочница</a>).</p>
  <p id="8XwQ">В репозитории, где храним спецификации для ArgoCD, создаем каталог policy, в нем файл argocd-applications.rego (например), с таким содержимым:</p>
  <pre id="JVCa" data-lang="java">package main

deny[msg] {
	i != j
	currentFilePath = input[i].path
	input[i].contents.kind == input[j].contents.kind
	input[i].contents.metadata.name == input[j].contents.metadata.name
	msg := sprintf(&quot;Объект с именем %s (kind: %s) из файла %s дублируется в файле %s&quot;,
		[input[i].contents.metadata.name, input[i].contents.kind, currentFilePath, input[j].path])
}</pre>
  <p id="03bb">Ну, а дальше встраиваем вызов в нашем CI:</p>
  <pre id="e18r" data-lang="yaml">---
stages:
  - lint

conftest:
  image:
    name: openpolicyagent/conftest:v0.34.0
    entrypoint: [&quot;&quot;]
  stage: lint
  script:
    - conftest test --combine kubernetes/argocd-apps</pre>
  <p id="Enfi">Где <code>kubernetes/argocd-apps</code> - путь до описания ваших Application для ArgoCD.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/z3TpTpYSK4J</guid><link>https://levaminov.ru/z3TpTpYSK4J?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/z3TpTpYSK4J?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Переезд из Slack в Mattermost</title><pubDate>Wed, 20 Apr 2022 05:33:30 GMT</pubDate><description><![CDATA[<img src="https://img1.teletype.in/files/4b/59/4b591bcc-92f7-4a98-b007-b6da50cdba51.png"></img>Часть 2: https://levaminov.ru/Rmks9ZZ7RLl]]></description><content:encoded><![CDATA[
  <p id="KvQU">Часть 2: <a href="https://levaminov.ru/Rmks9ZZ7RLl" target="_blank">https://levaminov.ru/Rmks9ZZ7RLl</a></p>
  <p id="4u4L">Часть 3: <a href="https://levaminov.ru/JsEwp91lmC-" target="_blank">https://levaminov.ru/JsEwp91lmC-</a></p>
  <p id="BTku">Не нашел живых примеров переездов на Mattermost в красках, чтобы с кровью и болью, поэтому опишу здесь несколько моментов, на которые стоит обратить внимание.</p>
  <h2 id="PFAg">Установка</h2>
  <p id="YGAV">Страница инсталляции <a href="https://mattermost.com/deploy/" target="_blank">https://mattermost.com/deploy/</a> предлагает несколько вариантов, в нем есть вариант установки в Kubernetes – мой вариант, хотя есть описание установки через Docker, документация веред на страницу с 404 ошибкой, проекты из репозитория, которые подходят по смыслу:</p>
  <ul id="0Lo9">
    <li id="kgR4"><a href="https://github.com/mattermost/mattermost-docker" target="_blank">https://github.com/mattermost/mattermost-docker</a> – в архиве;</li>
    <li id="Y6Q4"><a href="https://github.com/mattermost/mattermost-docker-preview" target="_blank">https://github.com/mattermost/mattermost-docker-preview</a> – превью (?);</li>
    <li id="f3x7"><a href="https://github.com/mattermost/docker" target="_blank">https://github.com/mattermost/docker</a> – видимо, оно, под громким названием &quot;Redesigned mattermost-docker&quot;.</li>
  </ul>
  <p id="5Dv5">Ладно. Установка в кластер Kubernetes по статье реализуется через 3 оператора – mysql-operator, minio-operator, mattermost-operator. Последний управляет двумя другими, вот такой вот подход. Ещё в документе есть таблица с тем сколько и на активное количество пользователей потребуется ресурсов.</p>
  <p id="WH95"><a href="https://github.com/mattermost/mattermost-operator/blob/master/docs/examples/mattermost_full.yaml" target="_blank">Описание конфигурации</a> Mattermost, для оператора, состоит из нескольких обязательных полей:</p>
  <ul id="QUnh">
    <li id="o8uA">name;</li>
    <li id="mSYc">size (!?);</li>
    <li id="Gdwz">ingress.</li>
  </ul>
  <p id="syNl">Мой интерес вызвал параметр &quot;size&quot;, вот что о нем пишут:</p>
  <blockquote id="IKMW">The size of your installation.</blockquote>
  <p id="DchW">Так-так-так, и что?</p>
  <blockquote id="SPeD">This can be ‘100users’, ‘1000users, ‘5000users’, ‘10000users’, or ‘25000users’.</blockquote>
  <p id="fmlH">Не очень понятно, открываем сам <a href="https://raw.githubusercontent.com/mattermost/mattermost-operator/master/docs/mattermost-operator/mattermost-operator.yaml" target="_blank">CustomResourceDefinition</a>, там зловещая идея раскрывается более подробно:</p>
  <blockquote id="a0so">Size defines the size of the ClusterInstallation. This is typically specified in number of users. This will override replica and resource requests/limits appropriately for the provided number of users. This is a write-only field - its value is erased after setting appropriate values of resources. Accepted values are: 100users, 1000users, 5000users, 10000users, 250000users. If replicas and resource requests/limits are not specified, and Size is not provided the configuration for 5000users will be applied. Setting &#x27;Replicas&#x27;, &#x27;Resources&#x27;, &#x27;Minio.Replicas&#x27;, &#x27;Minio.Resource&#x27;, &#x27;Database.Replicas&#x27;, or &#x27;Database.Resources&#x27; will override the values set by Size. Setting new Size will override previous values regardless if set by Size or manually.</blockquote>
  <p id="RXyw">Другими словами – от параметра &quot;size&quot; зависит конфигурация инсталляции вашего сервера Mattemost, число его реплик и выделенных ресурсов. Шаманство какое-то, но на практике удалось выяснить следующее:</p>
  <ul id="F5XW">
    <li id="t3xP">100users – 1 реплика Mattermost, Master MySQL;</li>
    <li id="fQl8">1000users – 2 реплики Mattermost, Master/Slave MySQL.</li>
  </ul>
  <p id="V6Jp">Переопределение ресурсов и лимитов, которые упоминаются, невозможно, для каждого значения параметра &quot;size&quot; это свои цифры, а отсутствие значение &quot;size&quot; – это, как выше написано, – &quot;5000users&quot;.</p>
  <p id="Gsyp">Тут следует отметить одно из ограничений бесплатной версии Mattermost – <strong>нет HA-режима</strong>, соответственно вариант работы в несколько реплик отпадает сам собой, при старте сервер ругается, что у него нет лицензии для работы в таком режиме, но вы всё ещё можете это попробовать. У Mattermost нет external кэша в виде какого-нибудь Redis, поддержка кластерного режима реализована внутри сервера, реплики обмениваются данными по gossip-протолоку, без этой связки можно ловить странные вещи – сохранение конфигурации через раз, некорректная загрузка файлов. Разработчики заботливо оставили <a href="https://github.com/mattermost/mattermost-server/blob/master/einterfaces/cluster.go" target="_blank">интерфейс</a>, по которому реализуется поддержка кластерного режима, да и вообще, поговаривают, до какого-то времени собрать Enterprise версию можно было самостоятельно. Поверим на слово.</p>
  <p id="qyFE">Возвращаясь к теме конфигурации и тексту документа, с которого все начинается:</p>
  <blockquote id="6rVA">This document describes installing and deploying a production-ready Mattermost system on a Kubernetes cluster using the Mattermost Kubernetes operator.</blockquote>
  <p id="Dvqe">Ниже можно заметить:</p>
  <blockquote id="Cknp">It is possible to manage MySQL database and MinIO file store using the Mattermost Operator, but it is not recommended for production usage.</blockquote>
  <p id="1z7o">После всех тестов мы решили перенести файловое хранилище в Yandex Object Storage, а MySQL в Managed Service for MySQL, всё в том же Yandex Cloud. Для хранилища создается отдельный секрет:</p>
  <pre id="TzQp" data-lang="yaml">---
apiVersion: v1
kind: Secret
metadata:
  name: mattermost-filestore
type: Opaque
data:
  accesskey: ...
  secretkey: ...</pre>
  <p id="StIX">В переменных окружения сервера задается следующее (все эти настройки можно так же определить в Системной консоли):</p>
  <ul id="Ug0i">
    <li id="mBYt">MM_FILESETTINGS_DRIVERNAME – amazons3</li>
    <li id="UE6Y">MM_FILESETTINGS_AMAZONS3BUCKET – имя бакета</li>
    <li id="NRm8">MM_FILESETTINGS_AMAZONS3ENDPOINT – storage.yandexcloud.net</li>
    <li id="qJ6b">MM_FILESETTINGS_AMAZONS3SSL – true</li>
    <li id="srhc">MM_FILESETTINGS_AMAZONS3SSE – true</li>
    <li id="DeD3">MM_FILESETTINGS_AMAZONS3TRACE – true (если нужны отладочные логи)</li>
    <li id="9uy3">MM_FILESETTINGS_AMAZONS3REGION – ru-central1</li>
  </ul>
  <p id="OMa7">Для базы, по статье, следует создать секрет и задать в нем некоторые параметры, все они содержать реквизиты подключения к базе, но используются в разных частях системы, поэтому записываются немного по-разному, такой вот поворот:</p>
  <ul id="QkUl">
    <li id="Mxeb">DB_CONNECTION_CHECK_URL – http-ссылка, в формате <a href="http://hostname:3306" target="_blank"><code>http://hostname:3306</code></a>, резвизиты не передаются, используется в init-container&#x27;е, чтобы определить, что база доступна;</li>
    <li id="MBLl">DB_CONNECTION_STRING – строка подключения в формате <code>mysql://user:password@tcp(hostname:3306)/mattermost?charset=utf8mb4,utf8&amp;writeTimeout=30s</code>, в переменных передается серверу как MM_CONFIG;</li>
    <li id="mNBT">MM_SQLSETTINGS_DATASOURCEREPLICAS – строка подключения в формате <code>user:password@tcp(hostname:3306)/mattermost?readTimeout=30s&amp;writeTimeout=30s</code>.</li>
  </ul>
  <p id="KUoF">Последнее, предположительно, используется в кластерном режиме, в документации упоминаний об этом нет, но если запускать сервер в 1 реплику с внешней базой – init container успешно отработает, а дальше начинается проверка соединения внутри сервера Mattermost, который пытается подключиться куда-то не туда, куда нам нужно. Опытным путем выяснилось, что нужно задать еще один параметр:</p>
  <ul id="GX8C">
    <li id="iUOV">MM_SQLSETTINGS_DATASOURCE – строка подключения в формате <code>user:password@tcp(hostname:3306)/mattermost?readTimeout=30s&amp;writeTimeout=30s</code>.</li>
  </ul>
  <h2 id="LG76">Настройка</h2>
  <h3 id="Uz5K">Треды</h3>
  <p id="bF00">В настройщее время (я все эксперименты проводил на Mattermost 6.5, хотя уже вышла 6.6), треды как вложенные обсуждения – это экспериментальная фитча:</p>
  <figure id="zWyg" class="m_column">
    <img src="https://img1.teletype.in/files/4b/59/4b591bcc-92f7-4a98-b007-b6da50cdba51.png" width="1832" />
  </figure>
  <p id="A0LZ">Чтобы включить этот функционал, нужно включить Automatically Follow Threads, сделать это можно через переменную окружения:</p>
  <pre id="um0s" data-lang="yaml">- name: MM_SERVICESETTINGS_THREADAUTOFOLLOW
  value: &quot;true&quot;</pre>
  <p id="1Nls">После того, как значение будет задано, тип тредов можно будет переключить.</p>
  <h3 id="nB3b">Пуши</h3>
  <p id="qCYI">По умолчанию, это отключено, можно включить публичный сервис для пушей Mattermost, насколько стабилен и какие ограничения – не выяснялось.</p>
  <figure id="ugXp" class="m_column">
    <img src="https://img4.teletype.in/files/bc/27/bc275ec2-3af4-45fc-ab7d-c7834583a257.png" width="1832" />
  </figure>
  <h3 id="uPMd">Звонки</h3>
  <p id="IScU">Функционал звонков реализован в виде плагина, который ставится из маркетплейса Mattermost:</p>
  <figure id="9mZv" class="m_column">
    <img src="https://img4.teletype.in/files/3d/65/3d65f0fd-8297-405e-b1f8-8e06b0fee90e.png" width="1482" />
  </figure>
  <p id="3gAR">Больши информации можно найти <a href="https://github.com/mattermost/mattermost-plugin-calls/" target="_blank">здесь</a>, функционал в бете, но нам удалось даже созвониться.</p>
  <h3 id="XX3G">Интеграции</h3>
  <p id="l6Wb">Если вы использовали Slack для получения каких-то простых уведомления, то всё нормально, этот функционал сохранен, если только вы не используете какие-то диковиные штуки внутри:</p>
  <figure id="B32r" class="m_retina">
    <img src="https://img2.teletype.in/files/96/98/969840a3-55ae-466c-987e-3277d1f390cc.png" width="369" />
  </figure>
  <p id="1PFB">Для создания типово Slack Webhook ссылки, переходите из меню в Интеграции, далее нажимаете на здоровенную кнопку Входящие вебхуки, создаете вебхук, копируете ссылку, используете вместо то, что была раньше.</p>
  <p id="yHfq">По умолчанию сообщения будут создаваться от имени человека, который эту интеграцию создал, аватарка так же будет от него, чтобы поменять подобное поведение переходим в Системную консоль и в разделе настроек интеграции задаете:</p>
  <figure id="JyOc" class="m_column">
    <img src="https://img2.teletype.in/files/d6/be/d6be5043-a6b2-4ee7-9e9d-e1247a580367.png" width="1808" />
  </figure>
  <p id="S0RN">По итогу получается что-то такое:</p>
  <figure id="7R92" class="m_retina">
    <img src="https://img2.teletype.in/files/9e/59/9e592274-871a-4453-b6d2-b79d2f22dee0.png" width="465" />
  </figure>
  <h3 id="fXRP">SAML, LDAP, OpenID</h3>
  <p id="dy7R">Ничего этого нет, есть только аутентификация через Gitlab.</p>
  <h3 id="7eAB">Полнотекстовый поиск</h3>
  <p id="ogao">Поддержка ElasticSearch есть только в Enterprise версии, в бесплатной по умолчанию используется полнотекстовый поиск по базе, либо предлагается альтернатива в виде поискового движка Bleve, который встроен в сервер (пару раз работа этого движка приводила к падению Mattermost).</p>
  <h2 id="adTq">Заключение</h2>
  <p id="FxRT">Да, в Mattermost многое работает странно, да, часть интеграций отвалилась и нужно переделывать, да, там все выглядеть страшно, но всё же меня радует, что людям предоставлен функционал плагинов, а значит ждем от всех поуехавших функционал, который доведет Mattermost до готового решения коммуникации в компании.</p>
  <h3 id="dHcN">P.S.</h3>
  <p id="mrUY">В Mattermost есть плагин – генератор мемов...</p>
  <figure id="a2Ro" class="m_original">
    <img src="https://chat.qleanlabs.ru/plugins/memes/templates/batman-slapping-robin.jpg?text=slack&text=mattermost" width="400" />
  </figure>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/-GTUP-QWYFV</guid><link>https://levaminov.ru/-GTUP-QWYFV?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/-GTUP-QWYFV?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Метрики Qrator (Qrator Exporter)</title><pubDate>Wed, 23 Mar 2022 04:46:47 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/2e/8e/2e8ec0ad-3e30-4eba-886f-43a2895beef7.png"></media:content><description><![CDATA[<img src="https://miro.medium.com/max/3150/1*nDIJd9iv2xt-LGSEhaCUhA.png"></img>В очередной раз пришлось настраивать сбор метрик с Qrator, прошлая моя заметка на этот счет жила в виде Issue в репозитории StupidScience/qrator-exporter (в проекте используются deprecated-методы), но там она пропала, поэтому опишу здесь, чтобы уж точно не потерялось.]]></description><content:encoded><![CDATA[
  <p id="z0hw">В очередной раз пришлось настраивать сбор метрик с <a href="https://qrator.net/en/" target="_blank">Qrator</a>, прошлая моя заметка на этот счет жила в виде Issue в репозитории <a href="https://github.com/StupidScience/qrator-exporter" target="_blank">StupidScience/qrator-exporter</a> (в проекте используются deprecated-методы), но там она пропала, поэтому опишу здесь, чтобы уж точно не потерялось.</p>
  <figure id="PsOu" class="m_retina" data-caption-align="center">
    <img src="https://img3.teletype.in/files/67/25/67258451-1300-4643-9755-87154e1bd60d.png" width="386" />
    <figcaption>Источник qratorlabs</figcaption>
  </figure>
  <p id="RW1u">Сбор данных будет осуществляться через <a href="https://github.com/influxdata/telegraf" target="_blank">telegraf</a> и, с помощью него же, отдаваться в виде метрик формата Prometheus.</p>
  <p id="Cp9G">Для начала потребуется получить API-токен для получения данных из Qrator, для этого переходим в <a href="https://client.qrator.net/qrator/apitoken/" target="_blank">раздел с ключами</a> в личном кабинете и выпускаем токен.</p>
  <p id="1Loh">Далее переходим в список доменов и сохраняем их идентификаторы, по ним будет обращение к методам API:</p>
  <figure id="wrGN" class="m_retina">
    <img src="https://img1.teletype.in/files/8e/d8/8ed8604b-e833-4dd1-9d5c-7cee466caab6.png" width="244" />
  </figure>
  <p id="04VC">Здесь 11111 и 11222 – как раз те самые идентификаторы доменов, теперь описываем конфигурацию для телеграфа:</p>
  <pre id="Cd9S" data-lang="toml">[[inputs.http]]
	name_prefix = &quot;qrator_blocks_&quot;
	method = &quot;POST&quot;
	urls = [
		&quot;https://api.qrator.net/request/domain/11111&quot;,
		&quot;https://api.qrator.net/request/domain/11222&quot;,
	]
	headers = {&quot;X-Qrator-Auth&quot; = &quot;${QRATOR_API_KEY}&quot;, &quot;Content-Type&quot; = &quot;application/json&quot;}
	body = &#x27;{&quot;method&quot;:&quot;statistics_current_blocks&quot;}&#x27;
	data_format = &quot;json&quot;
	timeout = &quot;30s&quot;

[[inputs.http]]
	name_prefix = &quot;qrator_http_&quot;
	method = &quot;POST&quot;
	urls = [
		&quot;https://api.qrator.net/request/domain/11111&quot;,
		&quot;https://api.qrator.net/request/domain/11222&quot;,
	]
	headers = {&quot;X-Qrator-Auth&quot; = &quot;${QRATOR_API_KEY}&quot;, &quot;Content-Type&quot; = &quot;application/json&quot;}
	body = &#x27;{&quot;method&quot;:&quot;statistics_current_http&quot;}&#x27;
	data_format = &quot;json&quot;
	timeout = &quot;30s&quot;

[[inputs.http]]
	name_prefix = &quot;qrator_ip_&quot;
	method = &quot;POST&quot;
	urls = [
		&quot;https://api.qrator.net/request/domain/11111&quot;,
		&quot;https://api.qrator.net/request/domain/11222&quot;,
	]
	headers = {&quot;X-Qrator-Auth&quot; = &quot;${QRATOR_API_KEY}&quot;, &quot;Content-Type&quot; = &quot;application/json&quot;}
	body = &#x27;{&quot;method&quot;:&quot;statistics_current_ip&quot;}&#x27;
	data_format = &quot;json&quot;
	timeout = &quot;30s&quot;

[[inputs.http]]
	name_prefix = &quot;qrator_locations_&quot;
	method = &quot;POST&quot;
	urls = [
		&quot;https://api.qrator.net/request/domain/11111&quot;,
		&quot;https://api.qrator.net/request/domain/11222&quot;,
	]
	headers = {&quot;X-Qrator-Auth&quot; = &quot;${QRATOR_API_KEY}&quot;, &quot;Content-Type&quot; = &quot;application/json&quot;}
	body = &#x27;{&quot;method&quot;:&quot;statistics_current_locations&quot;}&#x27;
	data_format = &quot;json&quot;
	timeout = &quot;30s&quot;

[[outputs.prometheus_client]]
	listen = &quot;:9273&quot;</pre>
  <p id="vXCi">В поле urls передается массив из ссылок на ресурсы (включают в себя идентификаторы доменов), в поле body – метод, а для передачи API-ключа используется переменная окружения QRATOR_API_KEY, нам нужно будет её дополнительно передать телеграфу, чтобы не хранять напрямую в конфигурации.</p>
  <p id="gVLS">Осталось только запустить. Минифицированный Deployment для kustomize может выглядеть так:</p>
  <pre id="j3VS" data-lang="yaml">---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: qrator-exporter
spec:
  template:
    spec:
      containers:
        - name: telegraf
          image: telegraf:1.21.4
          ports:
            - name: metrics
              containerPort: 9273
          env:
            - name: QRATOR_API_KEY
              value: CHANGE_ME
          securityContext:
            runAsUser: 1001
            capabilities:
              drop:
                - ALL
            readOnlyRootFilesystem: true
            runAsNonRoot: true
          volumeMounts:
            - name: config
              mountPath: &quot;/etc/telegraf&quot;
              readOnly: true
            - name: cache
              mountPath: &quot;/.cache&quot;
      volumes:
        - name: config
          secret:
            secretName: qrator-exporter
        - name: cache
          emptyDir: {}</pre>
  <p id="tbNj">Сам секрет qrator-exporter описывается в файле kustomization.yaml, например:</p>
  <pre id="rVRZ" data-lang="yaml">secretGenerator:
  - name: qrator-exporter
    files:
      - config/telegraf.conf</pre>
  <p id="DCzG">Не забываем описать сервис и Service Monitor:</p>
  <pre id="ku3u" data-lang="yaml">---
apiVersion: v1
kind: Service
metadata:
  name: qrator-exporter
spec:
  type: ClusterIP
  ports:
    - name: metrics
      port: 9273
      targetPort: 9273
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: qrator-exporter
spec:
  endpoints:
    - interval: 30s
      path: /metrics
      port: metrics
  selector:
    matchLabels:
      app.kubernetes.io/name: qrator-exporter
      app.kubernetes.io/component: service
      app.kubernetes.io/part-of: monitoring
  namespaceSelector:
    any: true</pre>
  <p id="pOjy">Селектор по лейблам, которые заданы в kustomization.yaml:</p>
  <pre id="NCOv" data-lang="yaml">---
commonLabels:
  app.kubernetes.io/name: qrator-exporter
  app.kubernetes.io/component: service
  app.kubernetes.io/part-of: monitoring</pre>
  <p id="yp7i">После этого мы начнем собирать метрики, однако в качестве url в метриках будет непонятный адрес ресурса Qrator, поэтому добавляем релейбл:</p>
  <pre id="9wrb" data-lang="yaml">---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: qrator-exporter
spec:
  endpoints:
    - interval: 30s
      path: /metrics
      port: metrics
      metricRelabelings:
        - sourceLabels: [&quot;url&quot;]
          regex: https://api.qrator.net/request/domain/(.+)
          replacement: $1
          targetLabel: domain_id
          action: replace
        - sourceLabels: [&quot;url&quot;]
          regex: https://api.qrator.net/request/domain/11111
          replacement: domain.ru
          targetLabel: domain_name
          action: replace
        - sourceLabels: [&quot;url&quot;]
          regex: https://api.qrator.net/request/domain/11222
          replacement: super-domain.ru
          targetLabel: domain_name
          action: replace
  selector:
    matchLabels:
      app.kubernetes.io/name: qrator-exporter
      app.kubernetes.io/component: service
      app.kubernetes.io/part-of: monitoring
  namespaceSelector:
    any: true</pre>
  <p id="Tens">Теперь в domain_name будет читаемый параметр, который можно использовать для селекторов в Grafana или в алертах.</p>
  <p id="f6aM">Примеры алертов для Prometheus Operator:</p>
  <pre id="7H15" data-lang="yaml">---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: qrator-exporter
  labels:
    app: prometheus-operator
    release: &quot;monitoring&quot;
spec:
  groups:
    - name: QratorExporter
      rules:
        - alert: QratorHighBandwidthInput
          expr: qrator_ip_http_result_bandwidth_input &gt; 5000000
          for: 5m
          labels:
            severity: warning
            domain: &quot;{{ $labels.domain_name }}&quot;
          annotations:
            summary: Большой входящий трафик на {{ $labels.domain_name }}
            description: На домене {{ $labels.domain_name }} в Qrator фиксируется повышенный входящий трафик, более 5Мбит/с
        - alert: QratorHighBandwidthOutput
          expr: qrator_ip_http_result_bandwidth_input &gt; 5000000
          for: 5m
          labels:
            severity: warning
            domain: &quot;{{ $labels.domain_name }}&quot;
          annotations:
            summary: Большой исходящий трафик на {{ $labels.domain_name }}
            description: На домене {{ $labels.domain_name }} в Qrator фиксируется повышенный исходящий трафик, более 5Мбит/с
        - alert: QratorHigh5xxRate
          expr: qrator_http_http_result_errors_total &gt;= 0.1
          for: 5m
          labels:
            severity: critical
            domain: &quot;{{ $labels.domain_name }}&quot;
          annotations:
            summary: В Qrator на {{ $labels.domain_name }} фиксируется рост числа ошибок
            description: В Qrator на домене {{ $labels.domain_name }} в течении 5 минут фиксируется рост числа 50x ошибок</pre>
  <p id="wH7W">Перед добавлением алертов стандартная рекомендация – пособирайте некоторые время метрики, чтобы определить для себя граничные значения, удобнее всего за этим наблюдать в Grafana, поэтому в качестве базового можно взять этот дашборд:</p>
  <pre id="3h4J" data-lang="javascript">{
  &quot;annotations&quot;: {
    &quot;list&quot;: [
      {
        &quot;builtIn&quot;: 1,
        &quot;datasource&quot;: &quot;-- Grafana --&quot;,
        &quot;enable&quot;: true,
        &quot;hide&quot;: true,
        &quot;iconColor&quot;: &quot;rgba(0, 211, 255, 1)&quot;,
        &quot;name&quot;: &quot;Annotations &amp; Alerts&quot;,
        &quot;target&quot;: {
          &quot;limit&quot;: 100,
          &quot;matchAny&quot;: false,
          &quot;tags&quot;: [],
          &quot;type&quot;: &quot;dashboard&quot;
        },
        &quot;type&quot;: &quot;dashboard&quot;
      }
    ]
  },
  &quot;editable&quot;: true,
  &quot;fiscalYearStartMonth&quot;: 0,
  &quot;gnetId&quot;: null,
  &quot;graphTooltip&quot;: 1,
  &quot;id&quot;: 106,
  &quot;iteration&quot;: 1647973063127,
  &quot;links&quot;: [],
  &quot;liveNow&quot;: false,
  &quot;panels&quot;: [
    {
      &quot;datasource&quot;: null,
      &quot;description&quot;: &quot;Alerts:\n\n* QratorHighBandwidthInput\n&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisLabel&quot;: &quot;&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;barAlignment&quot;: 0,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 0,
            &quot;gradientMode&quot;: &quot;none&quot;,
            &quot;hideFrom&quot;: {
              &quot;legend&quot;: false,
              &quot;tooltip&quot;: false,
              &quot;viz&quot;: false
            },
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 1,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;auto&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            },
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;off&quot;
            }
          },
          &quot;mappings&quot;: [],
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 80
              }
            ]
          },
          &quot;unit&quot;: &quot;bits&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 9,
        &quot;w&quot;: 12,
        &quot;x&quot;: 0,
        &quot;y&quot;: 0
      },
      &quot;id&quot;: 2,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;max&quot;
          ],
          &quot;displayMode&quot;: &quot;list&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;exemplar&quot;: true,
          &quot;expr&quot;: &quot;sum(qrator_ip_http_result_bandwidth_input{domain_name=\&quot;$domain\&quot;})&quot;,
          &quot;interval&quot;: &quot;&quot;,
          &quot;legendFormat&quot;: &quot;input&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;exemplar&quot;: true,
          &quot;expr&quot;: &quot;sum(qrator_ip_http_result_bandwidth_output{domain_name=\&quot;$domain\&quot;})&quot;,
          &quot;hide&quot;: false,
          &quot;interval&quot;: &quot;&quot;,
          &quot;legendFormat&quot;: &quot;output&quot;,
          &quot;refId&quot;: &quot;B&quot;
        }
      ],
      &quot;title&quot;: &quot;Traffic&quot;,
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: null,
      &quot;description&quot;: &quot;Alerts:\n\n* QratorHigh5xxRate&quot;,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisLabel&quot;: &quot;&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;barAlignment&quot;: 0,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 0,
            &quot;gradientMode&quot;: &quot;none&quot;,
            &quot;hideFrom&quot;: {
              &quot;legend&quot;: false,
              &quot;tooltip&quot;: false,
              &quot;viz&quot;: false
            },
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 1,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;auto&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            },
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;off&quot;
            }
          },
          &quot;decimals&quot;: 2,
          &quot;mappings&quot;: [],
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 80
              }
            ]
          },
          &quot;unit&quot;: &quot;reqps&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 9,
        &quot;w&quot;: 12,
        &quot;x&quot;: 12,
        &quot;y&quot;: 0
      },
      &quot;id&quot;: 4,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [],
          &quot;displayMode&quot;: &quot;list&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;exemplar&quot;: true,
          &quot;expr&quot;: &quot;sum({__name__=~\&quot;qrator_http_http_result_errors_.+\&quot;, domain_name=\&quot;$domain\&quot;})by(__name__)&quot;,
          &quot;interval&quot;: &quot;&quot;,
          &quot;legendFormat&quot;: &quot;{{ __name__ }}&quot;,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Errors&quot;,
      &quot;transformations&quot;: [
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_errors_(.*)&quot;,
            &quot;renamePattern&quot;: &quot;$1&quot;
          }
        }
      ],
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: null,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisLabel&quot;: &quot;&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;barAlignment&quot;: 0,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 0,
            &quot;gradientMode&quot;: &quot;none&quot;,
            &quot;hideFrom&quot;: {
              &quot;legend&quot;: false,
              &quot;tooltip&quot;: false,
              &quot;viz&quot;: false
            },
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 1,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;auto&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            },
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;off&quot;
            }
          },
          &quot;mappings&quot;: [],
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 80
              }
            ]
          },
          &quot;unit&quot;: &quot;reqps&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 9,
        &quot;w&quot;: 12,
        &quot;x&quot;: 0,
        &quot;y&quot;: 9
      },
      &quot;id&quot;: 7,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [],
          &quot;displayMode&quot;: &quot;list&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;single&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;exemplar&quot;: true,
          &quot;expr&quot;: &quot;sum(qrator_http_http_result_requests{domain_name=\&quot;$domain\&quot;})&quot;,
          &quot;interval&quot;: &quot;&quot;,
          &quot;legendFormat&quot;: &quot;total&quot;,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Requests&quot;,
      &quot;transformations&quot;: [
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0000_0(.*)&quot;,
            &quot;renamePattern&quot;: &quot;Less $1 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0(.*)_0(.*)&quot;,
            &quot;renamePattern&quot;: &quot;$1 - $2 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0(.*)_(.*)&quot;,
            &quot;renamePattern&quot;: &quot;$1 - $2 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_1000_1500&quot;,
            &quot;renamePattern&quot;: &quot;1 - 1.5 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_1500_2000&quot;,
            &quot;renamePattern&quot;: &quot;1.5 - 2 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_2000_5000&quot;,
            &quot;renamePattern&quot;: &quot;2 - 5 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_5000_inf&quot;,
            &quot;renamePattern&quot;: &quot;More 5 s&quot;
          }
        }
      ],
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: null,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisLabel&quot;: &quot;&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;barAlignment&quot;: 0,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 0,
            &quot;gradientMode&quot;: &quot;none&quot;,
            &quot;hideFrom&quot;: {
              &quot;legend&quot;: false,
              &quot;tooltip&quot;: false,
              &quot;viz&quot;: false
            },
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 1,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;auto&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            },
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;off&quot;
            }
          },
          &quot;mappings&quot;: [],
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 80
              }
            ]
          },
          &quot;unit&quot;: &quot;reqps&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 9,
        &quot;w&quot;: 12,
        &quot;x&quot;: 12,
        &quot;y&quot;: 9
      },
      &quot;id&quot;: 10,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [],
          &quot;displayMode&quot;: &quot;list&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;single&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;exemplar&quot;: true,
          &quot;expr&quot;: &quot;sum({__name__=~\&quot;qrator_http_http_result_responses_.+\&quot;, domain_name=\&quot;$domain\&quot;})by(__name__)&quot;,
          &quot;interval&quot;: &quot;&quot;,
          &quot;legendFormat&quot;: &quot;{{ __name__ }}&quot;,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Requests by response time&quot;,
      &quot;transformations&quot;: [
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0000_0(.*)&quot;,
            &quot;renamePattern&quot;: &quot;Less $1 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0(.*)_0(.*)&quot;,
            &quot;renamePattern&quot;: &quot;$1 - $2 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0(.*)_(.*)&quot;,
            &quot;renamePattern&quot;: &quot;$1 - $2 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_1000_1500&quot;,
            &quot;renamePattern&quot;: &quot;1 - 1.5 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_1500_2000&quot;,
            &quot;renamePattern&quot;: &quot;1.5 - 2 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_2000_5000&quot;,
            &quot;renamePattern&quot;: &quot;2 - 5 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_5000_inf&quot;,
            &quot;renamePattern&quot;: &quot;More 5 s&quot;
          }
        }
      ],
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: null,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisLabel&quot;: &quot;&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;barAlignment&quot;: 0,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 0,
            &quot;gradientMode&quot;: &quot;none&quot;,
            &quot;hideFrom&quot;: {
              &quot;legend&quot;: false,
              &quot;tooltip&quot;: false,
              &quot;viz&quot;: false
            },
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 1,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;auto&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            },
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;off&quot;
            }
          },
          &quot;mappings&quot;: [],
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 80
              }
            ]
          },
          &quot;unit&quot;: &quot;pps&quot;
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 9,
        &quot;w&quot;: 12,
        &quot;x&quot;: 0,
        &quot;y&quot;: 18
      },
      &quot;id&quot;: 11,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [],
          &quot;displayMode&quot;: &quot;list&quot;,
          &quot;placement&quot;: &quot;bottom&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;multi&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;exemplar&quot;: true,
          &quot;expr&quot;: &quot;sum(qrator_ip_http_result_packets_input{domain_name=\&quot;$domain\&quot;})&quot;,
          &quot;interval&quot;: &quot;&quot;,
          &quot;legendFormat&quot;: &quot;input&quot;,
          &quot;refId&quot;: &quot;A&quot;
        },
        {
          &quot;exemplar&quot;: true,
          &quot;expr&quot;: &quot;sum(qrator_ip_http_result_packets_output{domain_name=\&quot;$domain\&quot;})&quot;,
          &quot;hide&quot;: false,
          &quot;interval&quot;: &quot;&quot;,
          &quot;legendFormat&quot;: &quot;output&quot;,
          &quot;refId&quot;: &quot;B&quot;
        }
      ],
      &quot;title&quot;: &quot;Packets&quot;,
      &quot;transformations&quot;: [
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0000_0(.*)&quot;,
            &quot;renamePattern&quot;: &quot;Less $1 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0(.*)_0(.*)&quot;,
            &quot;renamePattern&quot;: &quot;$1 - $2 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_0(.*)_(.*)&quot;,
            &quot;renamePattern&quot;: &quot;$1 - $2 ms&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_1000_1500&quot;,
            &quot;renamePattern&quot;: &quot;1 - 1.5 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_1500_2000&quot;,
            &quot;renamePattern&quot;: &quot;1.5 - 2 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_2000_5000&quot;,
            &quot;renamePattern&quot;: &quot;2 - 5 s&quot;
          }
        },
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_http_http_result_responses_5000_inf&quot;,
            &quot;renamePattern&quot;: &quot;More 5 s&quot;
          }
        }
      ],
      &quot;type&quot;: &quot;timeseries&quot;
    },
    {
      &quot;datasource&quot;: null,
      &quot;fieldConfig&quot;: {
        &quot;defaults&quot;: {
          &quot;color&quot;: {
            &quot;mode&quot;: &quot;palette-classic&quot;
          },
          &quot;custom&quot;: {
            &quot;axisLabel&quot;: &quot;&quot;,
            &quot;axisPlacement&quot;: &quot;auto&quot;,
            &quot;barAlignment&quot;: 0,
            &quot;drawStyle&quot;: &quot;line&quot;,
            &quot;fillOpacity&quot;: 0,
            &quot;gradientMode&quot;: &quot;none&quot;,
            &quot;hideFrom&quot;: {
              &quot;legend&quot;: false,
              &quot;tooltip&quot;: false,
              &quot;viz&quot;: false
            },
            &quot;lineInterpolation&quot;: &quot;linear&quot;,
            &quot;lineWidth&quot;: 1,
            &quot;pointSize&quot;: 5,
            &quot;scaleDistribution&quot;: {
              &quot;type&quot;: &quot;linear&quot;
            },
            &quot;showPoints&quot;: &quot;auto&quot;,
            &quot;spanNulls&quot;: false,
            &quot;stacking&quot;: {
              &quot;group&quot;: &quot;A&quot;,
              &quot;mode&quot;: &quot;none&quot;
            },
            &quot;thresholdsStyle&quot;: {
              &quot;mode&quot;: &quot;off&quot;
            }
          },
          &quot;mappings&quot;: [],
          &quot;thresholds&quot;: {
            &quot;mode&quot;: &quot;absolute&quot;,
            &quot;steps&quot;: [
              {
                &quot;color&quot;: &quot;green&quot;,
                &quot;value&quot;: null
              },
              {
                &quot;color&quot;: &quot;red&quot;,
                &quot;value&quot;: 80
              }
            ]
          }
        },
        &quot;overrides&quot;: []
      },
      &quot;gridPos&quot;: {
        &quot;h&quot;: 9,
        &quot;w&quot;: 12,
        &quot;x&quot;: 12,
        &quot;y&quot;: 18
      },
      &quot;id&quot;: 5,
      &quot;options&quot;: {
        &quot;legend&quot;: {
          &quot;calcs&quot;: [
            &quot;max&quot;,
            &quot;last&quot;
          ],
          &quot;displayMode&quot;: &quot;table&quot;,
          &quot;placement&quot;: &quot;right&quot;
        },
        &quot;tooltip&quot;: {
          &quot;mode&quot;: &quot;single&quot;
        }
      },
      &quot;targets&quot;: [
        {
          &quot;exemplar&quot;: true,
          &quot;expr&quot;: &quot;sum({__name__=~\&quot;qrator_locations_http_result_locations_.+\&quot;, domain_name=\&quot;$domain\&quot;}&gt;0)by(__name__)&quot;,
          &quot;interval&quot;: &quot;&quot;,
          &quot;legendFormat&quot;: &quot;{{ __name__ }}&quot;,
          &quot;refId&quot;: &quot;A&quot;
        }
      ],
      &quot;title&quot;: &quot;Black list&quot;,
      &quot;transformations&quot;: [
        {
          &quot;id&quot;: &quot;renameByRegex&quot;,
          &quot;options&quot;: {
            &quot;regex&quot;: &quot;qrator_locations_http_result_locations_(.*)&quot;,
            &quot;renamePattern&quot;: &quot;$1&quot;
          }
        }
      ],
      &quot;type&quot;: &quot;timeseries&quot;
    }
  ],
  &quot;schemaVersion&quot;: 32,
  &quot;style&quot;: &quot;dark&quot;,
  &quot;tags&quot;: [
    &quot;WIP&quot;
  ],
  &quot;templating&quot;: {
    &quot;list&quot;: [
      {
        &quot;allValue&quot;: null,
        &quot;current&quot;: {
          &quot;selected&quot;: false,
          &quot;text&quot;: &quot;qlean.ru&quot;,
          &quot;value&quot;: &quot;qlean.ru&quot;
        },
        &quot;datasource&quot;: null,
        &quot;definition&quot;: &quot;label_values(qrator_http_http_id, domain_name)&quot;,
        &quot;description&quot;: null,
        &quot;error&quot;: null,
        &quot;hide&quot;: 0,
        &quot;includeAll&quot;: false,
        &quot;label&quot;: &quot;Domain&quot;,
        &quot;multi&quot;: false,
        &quot;name&quot;: &quot;domain&quot;,
        &quot;options&quot;: [],
        &quot;query&quot;: {
          &quot;query&quot;: &quot;label_values(qrator_http_http_id, domain_name)&quot;,
          &quot;refId&quot;: &quot;StandardVariableQuery&quot;
        },
        &quot;refresh&quot;: 1,
        &quot;regex&quot;: &quot;&quot;,
        &quot;skipUrlSync&quot;: false,
        &quot;sort&quot;: 1,
        &quot;type&quot;: &quot;query&quot;
      }
    ]
  },
  &quot;time&quot;: {
    &quot;from&quot;: &quot;now-12h&quot;,
    &quot;to&quot;: &quot;now&quot;
  },
  &quot;timepicker&quot;: {},
  &quot;timezone&quot;: &quot;&quot;,
  &quot;title&quot;: &quot;Qrator&quot;,
  &quot;uid&quot;: &quot;gM2arMHnk&quot;,
  &quot;version&quot;: 23
}</pre>
  <p id="UAo4">TODO: Перенести дашборд в <a href="https://grafana.com/grafana/dashboards/" target="_blank">https://grafana.com/grafana/dashboards/</a>.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/azHWMc4Vv3E</guid><link>https://levaminov.ru/azHWMc4Vv3E?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/azHWMc4Vv3E?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>&quot;Нейронка&quot; для определения ответственного за сервис</title><pubDate>Tue, 22 Mar 2022 18:47:08 GMT</pubDate><category>юмор</category><description><![CDATA[Недавно в одном из разговоров я упоминал, что на Prometheus-based стеке смог каждый алерт ассоциировать с командой, которая обслуживает сервис и в случае критических событий эта самая команда меншенится в Slack, чтобы привлечь её внимание. Что ж, показываю как это выглядит в реальности.]]></description><content:encoded><![CDATA[
  <p id="TMKn">Недавно в одном из разговоров я упоминал, что на Prometheus-based стеке смог каждый алерт ассоциировать с командой, которая обслуживает сервис и в случае критических событий эта самая команда меншенится в Slack, чтобы привлечь её внимание. Что ж, показываю как это выглядит в реальности.</p>
  <p id="XGSb">Суровая реальность такова – заводится вот такая вот структура (крутите по горизонтали, там 500+ символов):</p>
  <pre id="jlnj" data-lang="yaml">owner_team:
  - &amp;owner_team_by_labels_exported_namespace |
      {{if (match &quot;finance&quot; $labels.exported_namespace)}}finance_duty{{else if (match &quot;storage|warehouse&quot; $labels.exported_namespace)}}warehouse_duty{{else if (match &quot;sso&quot; $labels.exported_namespace)}}sso_duty{{else if (match &quot;crm|bff-self-service-form|bff-offers&quot; $labels.exported_namespace)}}crm_duty{{else if (match &quot;loyalty-bonus&quot; $labels.exported_namespace)}}finance_duty{{else if (match &quot;loyalty&quot; $labels.exported_namespace)}}crm_duty{{else if (match &quot;communications-gateway|sms-sender|contractor-app-bff&quot; $labels.exported_namespace)}}infogate_duty{{else}}-{{end -}}</pre>
  <p id="yLFe">В массиве, на самом деле, множество записей, чтобы определять команду по тому или иному признаку. После этого в лейблы алерта добавляем признак команды:</p>
  <pre id="VNej" data-lang="yaml">owner_team: *owner_team_by_labels_exported_namespace</pre>
  <p id="n6Lo">В конфигурации ресивера включаем линк по именам:</p>
  <pre id="pWuF" data-lang="yaml">slack_configs:
  - channel: &quot;#alerts&quot;
    link_names: true</pre>
  <p id="fwxZ">В качестве шаблона футера определяем следующее:</p>
  <pre id="2ILn">{{ define &quot;slack.default.footer&quot; }}{{ if .CommonLabels.owner_team }}{{ if not (eq .CommonLabels.owner_team &quot;-&quot;) }}Команда: @{{ .CommonLabels.owner_team }}{{ end }}{{ end }}{{ end }}</pre>
  <p id="NrEa">Easy-peasy.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://levaminov.ru/V4cQiyviW4J</guid><link>https://levaminov.ru/V4cQiyviW4J?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov</link><comments>https://levaminov.ru/V4cQiyviW4J?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=levaminov#comments</comments><dc:creator>levaminov</dc:creator><title>Тестирование Helm Chart. Часть 2</title><pubDate>Tue, 22 Mar 2022 08:00:14 GMT</pubDate><description><![CDATA[В первой части мы проверяли чарты на соответствие нашим ожиданиям по входящим параметрам, хранящимся в values-файлах. В этой части я расскажу, как мы валидируем пользовательские values-файлы к этом чарту, которые правят разработчики, так как проблема остается той же – недопустимый набор входных параметров порождает неприменимые спецификации Kubernetes, что приводит к ошибкам публикации.]]></description><content:encoded><![CDATA[
  <p id="Ewck">В <a href="https://levaminov.ru/v2FJJF1UrSx" target="_blank">первой части</a> мы проверяли чарты на соответствие нашим ожиданиям по входящим параметрам, хранящимся в values-файлах. В этой части я расскажу, как мы валидируем пользовательские values-файлы к этом чарту, которые правят разработчики, так как проблема остается той же – недопустимый набор входных параметров порождает неприменимые спецификации Kubernetes, что приводит к ошибкам публикации.</p>
  <h2 id="y9ck">Валидация входных параметров</h2>
  <p id="OP5f">По аналогии с <a href="https://github.com/instrumenta/kubeval" target="_blank">kubeval</a> хотелось бы иметь какой-то инструмент, который способен валидировать файлы на соответствие нашей собственной схеме, сам kubeval использует схемы в JSON:</p>
  <pre id="NMQq" data-lang="javascript">{
  &quot;description&quot;: &quot;Binding ties one object to another; for example, a pod is bound to a node by a scheduler. Deprecated in 1.7, please use the bindings subresource of pods instead.&quot;,
  &quot;properties&quot;: {
    &quot;apiVersion&quot;: {
      &quot;description&quot;: &quot;APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources&quot;,
      &quot;type&quot;: [
        &quot;string&quot;,
        &quot;null&quot;
      ]
    },
    &quot;kind&quot;: {
      &quot;description&quot;: &quot;Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds&quot;,
      &quot;type&quot;: [
        &quot;string&quot;,
        &quot;null&quot;
      ],
      &quot;enum&quot;: [
        &quot;Binding&quot;
      ]
    },
    &quot;metadata&quot;: {
      &quot;$ref&quot;: &quot;https://kubernetesjsonschema.dev/master/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta&quot;,
      &quot;description&quot;: &quot;Standard object&#x27;s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata&quot;
    },
    &quot;target&quot;: {
      &quot;$ref&quot;: &quot;https://kubernetesjsonschema.dev/master/_definitions.json#/definitions/io.k8s.api.core.v1.ObjectReference&quot;,
      &quot;description&quot;: &quot;The target object that you want to bind to the standard object.&quot;
    }
  },
  &quot;required&quot;: [
    &quot;target&quot;
  ],
  &quot;type&quot;: &quot;object&quot;,
  &quot;x-kubernetes-group-version-kind&quot;: [
    {
      &quot;group&quot;: &quot;&quot;,
      &quot;kind&quot;: &quot;Binding&quot;,
      &quot;version&quot;: &quot;v1&quot;
    }
  ],
  &quot;$schema&quot;: &quot;http://json-schema.org/schema#&quot;
}</pre>
  <p id="4dmO">Не самый удобный формат, другой нюанс – kubeval ориентируется по полям kind и apiVersion для получения схем:</p>
  <pre id="Ulhh" data-lang="go">// We haven&#x27;t cached this schema yet; look for one that works
primarySchemaBaseURL := determineSchemaBaseURL(config)
primarySchemaRef := determineSchemaURL(primarySchemaBaseURL, resource.Kind, resource.APIVersion, config)
schemaRefs := []string{primarySchemaRef}

for _, additionalSchemaURLs := range config.AdditionalSchemaLocations {
  additionalSchemaRef := determineSchemaURL(additionalSchemaURLs, resource.Kind, resource.APIVersion, config)
  schemaRefs = append(schemaRefs, additionalSchemaRef)
}</pre>
  <p id="7nqm">Решаемо. Так может не будем придумывать велосипедов, а попробуем воспользоваться готовым инструментом?</p>
  <p id="5Y3R">Пробуем запустить валидацию values-файла из репозитория с кодом сервиса как есть:</p>
  <pre id="Cf13" data-lang="bash">% kubeval values.yaml
ERR  - values.yaml: Missing &#x27;kind&#x27; key</pre>
  <p id="i5AK">Добавляем kind и apiVersion, и так знаем, что они нужны:</p>
  <pre id="DKeS" data-lang="yaml">---
apiVersion: qlean/v1
kind: values

# Базовые настройки чарта
app-template:
  app:
    name: web-widget-navbar
    component: frontend
    project: platform</pre>
  <p id="ItrA">Проверяем еще раз:</p>
  <pre id="jKfj" data-lang="bash">% kubeval values.yaml
..https://kubernetesjsonschema.dev/master-standalone/values-qlean-v1.json: Could not read schema from HTTP, response status is 404 Not Found</pre>
  <p id="qn0K">Для переопределения адреса источника схем в kubeval есть параметр --schema-location (либо переменная окружения KUBEVAL_SCHEMA_LOCATION), в качестве значения можно задавать как ссылку, так и локальный каталог:</p>
  <pre id="5fUY" data-lang="bash">export KUBEVAL_SCHEMA_LOCATION=file://./schemas</pre>
  <p id="pnD3">Запускаем:</p>
  <pre id="1cKy" data-lang="bash">% kubeval values.yaml                            
..open ./schemas/master-standalone/values-qlean-v1.json: no such file or directory</pre>
  <p id="0V7A">В качестве дочернего каталога выступает значение флага --kubernetes-version (по умолчанию master) и префикс standalone. Создаем в нужном каталоге файл values-qlean-v1.json со следующим тестовым содержимым:</p>
  <pre id="wfeJ" data-lang="javascript">{
  &quot;title&quot;: &quot;Helm Values&quot;,
  &quot;additionalProperties&quot;: false
}</pre>
  <p id="QYUb">Здесь мы определяем additionalProperties, который говорит, что на текущем уровне (в корне) запрещены любые неописанные поля, проверяем:</p>
  <pre id="hKgb" data-lang="bash">% kubeval values.yaml 
..kind: Additional property kind is not allowed
..apiVersion: Additional property apiVersion is not allowed
..app-template: Additional property app-template is not allowed</pre>
  <p id="QZNR">Обновляем схему:</p>
  <pre id="TnTM" data-lang="javascript">{
  &quot;title&quot;: &quot;Helm Values&quot;,
  &quot;additionalProperties&quot;: false,
  &quot;properties&quot;: {
    &quot;apiVersion&quot;: {
      &quot;type&quot;: &quot;string&quot;
    },
    &quot;kind&quot;: {
      &quot;type&quot;: &quot;string&quot;
    },
    &quot;app-template&quot;: {
      &quot;type&quot;: &quot;object&quot;
    }
  }
}</pre>
  <p id="ICgU">Проверяем:</p>
  <pre id="CVug" data-lang="bash">% kubeval values.yaml
PASS - values.yaml contains a valid values (unknown)</pre>
  <p id="GP1m">Ну, а далее идем изучать <a href="https://json-schema.org" target="_blank">https://json-schema.org</a> и, если хочется, схемы ресурсов Kubernetes, и формируем свою собственную схему, на основе которой будут проверяться пользовательские настройки чарта. Ниже несколько примеров из реальной жизни.</p>
  <p id="iteu">1. Поле &#x60;app&#x60; должно содержать ключ-значение, есть обязательные ключи, значения должны быть строкой:</p>
  <pre id="HF5Q" data-lang="javascript">&quot;app&quot;: {
  &quot;type&quot;: &quot;object&quot;,
  &quot;additionalProperties&quot;: {
    &quot;type&quot;: &quot;string&quot;
  },
  &quot;required&quot;: [
    &quot;name&quot;,
    &quot;component&quot;,
    &quot;project&quot;
  ]
}</pre>
  <p id="Kuo2">2. Поле &#x60;image&#x60; должно содержать два параметра – repository и tag, и больше никаких других:</p>
  <pre id="bixX" data-lang="javascript">&quot;image&quot;: {
  &quot;type&quot;: &quot;object&quot;,
  &quot;additionalProperties&quot;: false,
  &quot;properties&quot;: {
    &quot;repository&quot;: {
      &quot;type&quot;: &quot;string&quot;
    },
    &quot;tag&quot;: {
      &quot;type&quot;: &quot;string&quot;
    }
  }
}</pre>
  <p id="qq8U">3. Поле &#x60;annotations&#x60; должно содержать ключ-значение, но есть ключи, которые использовать нельзя:</p>
  <pre id="7JPQ" data-lang="javascript">&quot;annotations&quot;: {
  &quot;type&quot;: &quot;object&quot;,
  &quot;properties&quot;: {
    &quot;pltf_ttl&quot;: false,
    &quot;ci_project_id&quot;: false
  },
  &quot;additionalProperties&quot;: {
    &quot;type&quot;: &quot;string&quot;
  }
}</pre>
  <p id="dn3Y">4. Значение поля &#x60;type&#x60; должно быть одним из допустимых:</p>
  <pre id="2mJ2" data-lang="javascript">&quot;type&quot;: {
  &quot;type&quot;: &quot;string&quot;,
  &quot;enum&quot;: [
    &quot;http&quot;,
    &quot;app&quot;
  ]
}</pre>
  <p id="Aoen">5. Значение &#x60;сount&#x60; должно быть целым числом и не меньше нуля:</p>
  <pre id="TCIZ" data-lang="javascript">&quot;сount&quot;: {
  &quot;type&quot;: &quot;integer&quot;,
  &quot;minimum&quot;: 0
}</pre>
  <p id="wXw8">6. Поле &#x60;for&#x60; должно содержать строку в формате duration:</p>
  <pre id="MTVw" data-lang="javascript">&quot;for&quot;: {
  &quot;type&quot;: &quot;string&quot;,
  &quot;format&quot;: &quot;duration&quot;
}</pre>
  <p id="fEvT">JSON-формат не так удобен для чтения, по сравнению с HCL или YAML, поэтому можно задуматься о том, чтобы конвертировать из подходящего формата в JSON при сборке образа.</p>
  <p id="qN4Y">Чем детальнее будет описана схема, тем меньше возможностей сделать что-то не так, а само внедрение валидации позволит с меньшими трудозатратами мигрировать на новый формат в дальнейшем.</p>
  <h2 id="LGdU">Со звездочкой</h2>
  <p id="NiBp">Если стойкое желание сделать свой велосипед к этому моменту сохранилось, полезным будет посмотреть в сторону имплементации <a href="https://github.com/xeipuuv/gojsonschema" target="_blank">JSON Schema на Go</a>, пакет позволяет, в числе прочего, описывать собственные форматы данных, например, для крона может выглядеть так:</p>
  <pre id="i2Xl" data-lang="go">package main

import &quot;github.com/gorhill/cronexpr&quot;

type CronFormat struct{}

func init() {
	gojsonschema.FormatCheckers.Add(&quot;cron&quot;, CronFormat{})
}

func (c CronFormat) IsFormat(cron interface{}) bool {
	cronString, ok := cron.(string)
	if !ok {
		return false
	}
	_, err := cronexpr.Parse(cronString)
	return err == nil
}</pre>
  <p id="UNZV">С последующим применением в таком виде:</p>
  <pre id="XTbr" data-lang="javascript">&quot;cron&quot;: {
  &quot;type&quot;: &quot;string&quot;,
  &quot;format&quot;: &quot;cron&quot;
}</pre>
  <p id="BSdD">Ещё один плюс – чтение схем с диска можно описать самому и использовать любой формат, вот, например, YAML:</p>
  <pre id="1e58" data-lang="go">var bodySchema map[string]interface{}
_ = yaml.Unmarshal(schemaBytes, &amp;bodySchema)
schemaLoader := gojsonschema.NewGoLoader(bodySchema)
return gojsonschema.NewSchema(schemaLoader)</pre>
  <p id="7mbV">Детали реализации можно посмотреть <a href="https://gitlab.com/leominov/helm-values-linter" target="_blank">в коде</a>, он открыт.</p>
  <h2 id="e8eP">Полезные ссылки</h2>
  <ol id="qj8Y">
    <li id="08dO"><a href="https://www.kubeval.com" target="_blank">https://www.kubeval.com</a></li>
    <li id="O0GO"><a href="https://json-schema.org" target="_blank">https://json-schema.org</a></li>
    <li id="ZrXS"><a href="https://json-schema.org/draft/2020-12/json-schema-validation.html" target="_blank">https://json-schema.org/draft/2020-12/json-schema-validation.html</a></li>
    <li id="YLJ6"><a href="https://github.com/instrumenta/kubernetes-json-schema" target="_blank">https://github.com/instrumenta/kubernetes-json-schema</a></li>
    <li id="EcJ2"><a href="https://json-schema-everywhere.github.io/yaml" target="_blank">https://json-schema-everywhere.github.io/yaml</a></li>
  </ol>

]]></content:encoded></item></channel></rss>