Дома у меня три ноды kubeadm на мини-ПК. На них живёт медиасервер: Jellyfin, торрент-клиент и сервисы, которые сами находят фильмы и раскладывают их по папкам. Сверху Istio с mTLS, Jenkins, который всё это раскатывает, Prometheus с Grafana, свой Nexus и Vault.

Начиналось всё не с медиасервера. Начиналось с демо-магазина от Google — и полтора месяца я на него потратил, прежде чем понял, что учусь вхолостую.

Про это и статья.

Зачем я вообще полез

Четыре месяца назад я перешёл в девопс. До этого год работал в сопровождении с элементами SRE — поднимал репликацию PostgreSQL через Debezium и Kafka в Hadoop, чтобы понимать рабочий стек. Ещё раньше сидел в центре мониторинга сетей у телеком-оператора. Прошёл собеседование — и пошёл разбираться с Kubernetes, Istio и Groovy.

По курсам разбираться смысла было мало. В курсах всё собирают с чистого листа: вот кластер, вот ingress, вот деплой, все компоненты на месте и ни одного лишнего. Настоящий прод так не выглядит, потому что его никто не проектировал целиком. Он нарастал слоями. Что-то появилось после аварии, что-то по требованию безопасности, что-то потому, что в компании принят Jenkins и никаких GitHub Actions.

Простой пример. Бывает, что в кластере одновременно стоят nginx ingress controller и Istio Ingress Gateway. Оба сразу. Istio пришёл позже, его добавили по требованию безопасности, а nginx убирать не стали — работает же, и трогать боязно. Хотя с Istio он уже почти не нужен, а с марта 2026 community-версия ingress-nginx ещё и закрыта: репозиторий в архиве, патчей безопасности не будет.

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

Был и личный толчок. Коллега много лет пилит дома свой проект, и у него весь трафик между подами ходит через ingress. Я на это посмотрел и не понял, зачем так. Обычно вход в кластер идёт через ingress, выход через egress, а между собой сервисы общаются напрямую. И вот это «не понял, зачем» — штука неприятная. Можно пожать плечами и забыть. А можно собрать похожее у себя и разобраться.

Отсюда правило, которое я поставил себе сразу: инфраструктуру дома собираю так, как её собирают в проде. Не kubectl apply, а чарт. Не «Istio потом», а сразу. Есть продовый способ и есть способ срезать угол — беру продовый.

Правило про то, что я изучаю. Заглушки я себе позволяю, но на время: реестр у меня сейчас работает по HTTP без TLS, и это лежит записанным в техдолге. Записанная заглушка и забытый срезанный угол — разные вещи.

При этом я прагматичен. Устроился я в девопс, а не в автотестеры. Поэтому изучаю девопс, а не пишу автотесты для проверки учебного гугл сервиса, чтобы понимать сломал я что-то с истио или нет. Знакомый вон вечерами меняет цвета в Jellyfin через CSS патчи. Зачем мне тратить вечер на цвет кнопки, если я могу поднять ingress и разобрать как работает в этом плане Istio? Одно даёт рост — в знаниях, в опыте, в зарплате. Второе не даёт ничего (или кнопку другого цвета, которую на работе меняют фронтендеры).

Полтора месяца на демо-магазине

Начал я с Online Boutique — демо-магазина от Google. Десять сервисов, каждый на своём языке, и это было главным плюсом: джоба сборки получалась разношерстная. Появился yaml-конфиг, где под каждый язык свои команды билда. Часть сервисов пришлось проксировать — зависимости не тянулись, до pypi таймаут прямо из контейнера. Под Python собрал свой образ с уже вшитыми зависимостями. Тогда я ещё не знал, что с блокировками провожусь потом всерьёз — но это отдельная история.

Дальше чарты. Вместо чарта на каждый сервис я сделал один универсальный и подключил к нему helpers с общими кусками шаблонов, чтобы не копипастить одно и то же в десять мест. Values разнёс: дефолты лежат в самом чарте, а то, что меняется от среды к среде, — отдельными файлами.

Потом джобы, три штуки, каждая под свой момент жизни кода:

  • pr-check — на пул-реквест. Гоняет тесты и отдаёт результаты в Sonar. Разработчику нужен быстрый ответ: собирается или нет, тесты прошли или нет.

  • build-on-merge — после мержа. Собирает образ, кладёт в Nexus, раскатывает на DEV и двигает барьерные теги.

  • сборка дистрибутива — складывает в Nexus архив с манифестом: какие именно версии образов входят в этот релиз. Джоба деплоя потом катит фиксированные образы на тестовые и прод стенды.

Барьерные теги поясню сразу, дальше они всплывут ещё не раз. Идея простая: на образ вешаются три тега-указателя. new — то, что только что собралось и ещё не проверено. work — последнее, что заведомо работает. oldwork — предыдущее рабочее, страховка. Раскатили new, он поднялся — двигаем указатели. Не поднялся — есть куда откатываться.

Грабли шли примерно по одной на вечер. Helm с --wait молча висел, пока я не выдал в Role права на replicasets, — причём не Forbidden, а именно таймаут, потому что helm читает цепочку Deployment → ReplicaSet → Pod. До этого jenkins-агент не видел API minikube: тот крутился на docker-драйвере внутри VM и снаружи был недоступен, прокинул через socat юнитом systemd.

По часу в будни, до четырёх в выходные. Так прошло полтора месяца.

Очки за 8 долларов

Потом я как-то открыл этот онлайн бутик и прогнал покупку каких-то очков за 8 долларов. Заказ оформился.

Но так ли, как нужно? А вдруг слева должен быть баннер, за него отвечает микросервис X, и он не работал с самого начала — ещё когда я всё раскатывал без чартов. Я ж не знаю, как оно должно выглядеть.

Написать тесты? Это сворачивать в сторону, я вообще не за этим сюда шёл. Найти видео, где показывают рабочий магазин, и сверяться с ним? Ещё тупее, странно как-то.

Тогда я и понял, что это не то.

Чего мне не хватало

Дело было не в Kubernetes и не в Istio. Дело было в том, что у меня не было способа за две секунды понять, живо оно или нет.

Открыл — работает. Открыл — не работает, значит сломал вчера вечером. Всё.

В демо-магазине такого нет и быть не может. Я не покупатель этого магазина. У меня нет сценария, в котором я им пользуюсь и что-то замечаю. Я могу только смотреть на страницу и гадать, всё ли на месте.

А без этого обучение превращается в чтение. Раскатил, поды в Running, kubectl get all показывает что-то зелёное. Дальше что? Дальше ничего. Идёшь читать следующую главу документации.

Собирать оказалось нечего

Первая мысль после этого была простая: раз магазин не годится, беру вместо него своё и переезжаю целиком.

Так не вышло бы. Точнее, вышло бы, но я не захотел.

Исходники у Sonarr и Radarr открытые, собрать их технически можно. Только это .NET, и разбираться пришлось бы с чужим билдом на C#. А я в тот момент выбирал, на что тратить вечера: на сборку сервисов, которые и так собраны, или на деплой в Kubernetes, Istio и Groovy — то, за чем я в девопс и пришёл. Выбор был очевидный.

Значит, джобы сборки остаются ждать своего проекта. Никуда они не денутся.

А всё остальное переезжает. Кластеру, Helm, Istio, Vault, мониторингу и деплою совершенно всё равно, что раскатывать. Хоть магазин, хоть Jellyfin.

«Я не должен уметь это делать»

Уже потом, когда медиасервер работал, мне попалась статья. Человек без девопс-опыта поднял дома Nextcloud и NetBird в Kubernetes, разбираясь с Cursor. Написана честно: он прямо говорит, что не может оценить, что именно сделал за него Cursor, и не знает, что там может пойти не так.

И выводит формулу: «Я не должен уметь это делать. Я должен понимать, что происходит».

Я на ней застрял, потому что полтора месяца был уверен, что понимаю, что происходит.

Возьмём тот же дистрибутив. Я его собирал, у меня была джоба, она работала. И я был уверен, что собрать дистрибутив — это и есть собрать: сбилдить код, получить образ. Так ведь и звучит. Что на самом деле речь про сложить уже готовые образы по тегам в один файл и зафиксировать, какие версии входят в релиз, — из слова никак не следует. Мне это рассказали позже, на работе.

Дело не в том, что я поленился разобраться. Я не знал, что там есть что разбирать. Вопрос про дистрибутив я не мог задать, потому что не подозревал о его существовании.

Вот тут формула и не работает. Понимать, что происходит, звучит хорошо, пока не спросишь, как проверить, что ты действительно понимаешь. Изнутри никак. Ощущение понимания есть всегда, и дырки в нём не видно — они как раз в тех местах, о которых ты не знаешь.

А когда делаешь руками, дырки вылезают сами. У меня всё падало с правами: Radarr отказывался писать в папку, под при этом зелёный, в логах пусто. Про root_squash в NFS я прочитал за пять минут и решил, что разобрался. Понял позже, когда сам увидел — шару монтирует kubelet от имени ноды, а не под, и UID внутри контейнера с UID на диске просто не совпадает.

До этого у меня было название проблемы вместо понимания.

Как я теперь узнаю о поломках

Сейчас всё это работает: три ноды, восемь сервисов, Istio, Jenkins, который раскатывает. Но интереснее не это, а то, как я узнаю о поломках.

Вечер, сажусь смотреть новый сериал. Sonarr его скачал и разложил, всё по сезонам и сериям. Открываю Jellyfin — сериала нет.

Причина оказалась не в кластере. У меня две библиотеки: старая, куда я ещё до кластера накидал сериалы руками через шару, и новая, куда складывает Sonarr. У новой я указал путь слишком глубоко — прямо на папку конкретного шоу. А библиотека типа Shows ждёт, что на верхнем уровне лежат папки шоу, а не сразу серии. Поднялся на уровень выше — всё нашлось.

Другой вечер, хочу посмотреть фильм. Radarr показывает, что скачано, но в очереди висит «Downloaded — Waiting to Import», а ниже «Unable to parse file». Иду на NFS-сервер смотреть, что там лежит:

ls downloads/
'The Lord of the Rings_The Fellowship of the Ring_2001г_Extended Edition_Sir Peter Robert Jackson.mkv'

Парсер такого не ждёт. Он ждёт Spider.Man.Homecoming.2017.BDRemux.1080p.mkv: точки вместо пробелов, год отдельным полем, дальше качество и источник. Тут подчёркивания, в конце зачем-то имя режиссёра, и главное — год записан как 2001г, с русской буквой на хвосте. Одной буквы хватило.

Разбирался по контрасту. У сериала файлы переименованы по схеме Sonarr — значит Sonarr про загрузку знал, отследил её через API qBittorrent и импорт отработал. У фильма имя осталось ровно таким, каким пришло с раздачи, — значит импорт не сработал вообще. Дальше понятно, куда смотреть.

А в самом начале вообще всё падало. Prowlarr улетал в OOMKilled стабильно: дефолтных 128 мегабайт .NET-рантайму мало. За ним bazarr, jellyseerr и сам Jellyfin.

И вот тут любопытная вещь. На магазине лимиты и реквесты уже лежали в готовых манифестах в репозитории. Я их переносил в свой чарт, но подобрать сам не мог — они были подобраны до меня. Я даже не знал, что это задача. А тут пришлось сидеть и выяснять опытным путём, сколько кому нужно.

Ни про один из этих случаев я не узнал из мониторинга. Каждый раз я садился посмотреть кино и упирался.

И не только я. Jellyfin у меня жил ещё до всей этой истории, просто в LXC-контейнере, и кино дома смотрят каждый день. Так что если я что-то сломаю вечером, мне об этом сообщат, и довольно быстро.

Что я из этого забрал

Скажут, наверное: медиасервер — это не микросервисы, ты взял задачу попроще.

По части сборки — да. Поднять пайплайн для десяти сервисов на разных языках объективно тяжелее, чем раскатывать готовые образы. Спорить не буду, и джобы сборки у меня действительно стоят без дела.

Зато освободившееся время ушло в Istio, mTLS и egress. И в настоящий кластер: на магазине всё жило в minikube, до трёх нод я там так и не дошёл.

А планы для джоб есть. Часть функций переписать на Java, потому что на работе весь бэкенд на Spring Boot. Прикрутить LDAP-аутентификацию. Вынести состояние arr-сервисов из SQLite в PostgreSQL, добавить Kafka. У каждого сервиса есть UI — значит рано или поздно можно разделить сборку фронта и бэка разными джобами.

Форки понадобятся в любом случае, потому что часть вещей не работает. Удаление, например. Скачивание собрано хорошо: в Jellyseerr нажал request, запрос ушёл в Radarr или Sonarr, оттуда через Prowlarr к индексерам, если надо — через FlareSolverr, и дальше в торрент-клиент.

А удалять приходится в трёх местах. В торренте — чтобы освободить диск, он не резиновый. В Radarr или Sonarr — чтобы не сыпали ошибками. И в Jellyseerr, потому что он синхронизируется с Jellyfin и тоже начинает ругаться.

Дойду ли до всего этого — не знаю. Сначала Istio, Kubernetes и Groovy.

Что я из этой истории забрал.

Первое. Учебный проект должен проверяться за две секунды. Не мониторингом и не тестами — сел пользоваться, и сразу видно. Без этого ты не учишься, а читаешь документацию с ощущением, что учишься.

Второе. Проект должен быть тот, которым пользуешься сам. Пока ты не пользователь, проверять тебе нечего.

Третье, про себя. Я себя знаю: где можно съехать, я съеду. Не из принципа, а потому что вечером устал. Поэтому мне нужен был проект, который сам не даст: схалтурил — вечером нет кино.

Побочный эффект оказался неожиданным. Когда я поставил Istio Ingress Gateway и он что-то сломал, разбираться было некогда — все уже сели смотреть. Я сделал helm rollback, а причину искал потом. Это ровно то, что делают на работе: хотфикс сломал прод — откатываем ревизию, баг вернётся, но недоступность хуже. На учебном стенде такому не научишься, там ничего не горит и можно ковыряться неделю.

Честно про состояние дел. Закрыто не всё. Ingress Gateway я поставил через istioctl вместо Helm-чарта, о чём сразу пожалел и записал в техдолг. Egress в работе, там половина вопросов открыта. FlareSolverr до сих пор ловит OOM, и я не разобрался почему.

Но фильм играет. А значит, я узнаю, если сломаю.


Та самая статья про Nextcloud и Cursor если кому интересно: Kubernetes дома? Ты не в себе?