В прошлой главе мы договорились разрабатывать myapp (HTTP API на Python 3.12 + FastAPI, порт 8080, зависит от PostgreSQL) внутри настоящего Kubernetes-кластера прямо на ноутбуке. Но прежде чем поднимать кластер, важно понять, какую именно локалку мы строим и зачем. Эта глава — про идею «похоже на продакшн» (англ. production-like): что мы воспроизводим осознанно, что упрощаем сознательно и где проходит граница, за которой имитировать прод локально становится вредно.

Что значит «похоже на продакшн» и зачем это новичку

«Production-like окружение» — это не «копия прода один в один». Скопировать прод целиком на ноутбук невозможно (об этом ниже), да и не нужно. Цель скромнее и полезнее: сознательно сжать разрыв между тем, как сервис ведёт себя у вас на машине, и тем, как он поведёт себя в проде — чтобы баги, связанные с упаковкой и деплоем, ловились на вашей машине, а не на боевом кластере под трафиком.

Откуда вообще берётся этот разрыв? Классическую формулировку даёт методология The Twelve-Factor App (двенадцать факторов — набор практик для проектирования сервисов, к которому мы ещё не раз вернёмся). Её десятый фактор, «Dev/prod parity» (паритет разработки и продакшна), выделяет три разрыва между dev и prod:

  • Разрыв во времени (time): код от написания до прода доходит дни или недели — цель сократить это до часов и минут.
  • Разрыв в людях (personnel): код пишут одни люди, а деплоят и эксплуатируют — другие.
  • Разрыв в инструментах (tools): локальный стек разработчика не совпадает с продовым.

Именно из третьего разрыва растёт коварная проблема. The Twelve-Factor App формулирует её прямо: «накапливаются крошечные несовместимости, из-за которых код, который работал и проходил тесты в разработке, отказывает в продакшне» (tiny incompatibilities crop up, causing code that worked and passed tests in development or staging to fail in production). Та же мысль разобрана для начинающих в конспекте KodeKloud по Dev/Prod Parity: если локально вы пишете под SQLite, а в проде стоит PostgreSQL, рано или поздно вы наступите на различие в их поведении — и узнаете об этом в самый неподходящий момент.

Для новичка, который впервые готовит сервис к Kubernetes, ценность локального кластера именно в этом. Целый класс проблем существует только на уровне Kubernetes-манифестов и самого деплоя: неправильно заданные пробы (healthchecks), нехватка прав (RBAC), ошибки в service discovery (обнаружении сервисов по DNS-имени внутри кластера), забытые лимиты ресурсов. Запуская myapp локально в настоящем кластере, вы прогоняете ровно тот путь упаковки и выкатки, что и в проде, — и ловите эти ошибки до того, как они дойдут до боевого окружения.

Паритет окружений: та же версия Kubernetes, аддоны, реальные манифесты

Паритет на практике складывается из трёх вещей: та же minor-версия Kubernetes, те же backing services (внешние зависимости — БД, очереди, кэши) по типу и версии и те же реальные манифесты, которыми сервис выкатывается в проде.

Начнём с версии. Kubernetes — быстро движущийся проект, и поведение между minor-версиями (например, 1.30 и 1.33) может меняться: устаревают и удаляются API, меняются дефолты. Поэтому версию кластера нужно пинить явно, а не полагаться на latest, который дрейфует со временем.

В k3d (наш основной инструмент, см. главу 5) версия задаётся образом k3s — флагом --image. Здесь мы показываем сам принцип пина, а «вживую» — с разбором флагов и реальным k3d cluster create dev — вы увидите его в главе 5:

bash
1# Один важный нюанс с тегом: в Docker-тегах нельзя использовать '+',
2# поэтому пишем 'v1.31.5-k3s1', а НЕ 'v1.30.2+k3s1'
3k3d cluster create dev --image rancher/k3s:v1.31.5-k3s1

То же самое аккуратнее — через config-файл k3d (его удобно держать в репозитории рядом с кодом):

yaml
1apiVersion: k3d.io/v1alpha5
2kind: Simple
3metadata:
4  name: dev
5servers: 1
6agents: 2
7image: rancher/k3s:v1.31.5-k3s1

Если вы предпочтёте kind (Kubernetes IN Docker, альтернатива из главы 5), версия пинуется образом ноды, причём в документации kind рекомендуется указывать не только тег, но и sha256-дайджест (его берут из release notes соответствующего релиза kind) — так образ фиксируется однозначно:

yaml
1kind: Cluster
2apiVersion: kind.x-k8s.io/v1alpha4
3nodes:
4- role: control-plane
5  image: kindest/node:v1.33.0@sha256:<digest-из-release-notes>

Совпадение версии можно (и нужно) проверить — сравните вывод с тем, что стоит в проде:

bash
1kubectl version --output=yaml

Второй элемент паритета — реальные манифесты. Это значит: локально вы выкатываете myapp теми же декларативными описаниями, что и в проде, а не каким-то отдельным «локальным» способом. Официальная документация Kubernetes Managing Workloads показывает штатные приёмы работы с такими манифестами: группировку нескольких ресурсов в одном файле через разделитель ---, рекурсивное применение целой директории и встроенный в kubectl Kustomize (инструмент наложения настроек поверх базовых манифестов):

bash
1# применить все манифесты из директории, включая вложенные
2kubectl apply -f k8s/overlays/dev --recursive
3
4# или через Kustomize, встроенный прямо в kubectl
5kubectl apply -k k8s/overlays/dev

Помимо kubectl и Kustomize, для упаковки прод-конфигурации часто используют Helm — пакетный менеджер для Kubernetes (он тоже упомянут в Managing Workloads как способ управления приложениями). Конкретно манифесты myapp мы напишем в главе 7, а раскладывание по окружениям с Kustomize/Helm разберём в главе 13. Здесь важен принцип: локалка использует те же артефакты выката, что и прод, — иначе паритет нарушен на самом важном уровне.

Что воспроизводим, а что осознанно упрощаем

Production-like — это всегда баланс. Часть прода мы воспроизводим, потому что именно там прячутся баги; часть — упрощаем, потому что точная имитация невозможна или вредна.

Воспроизводим обязательно:

  • Версию Kubernetes и тип/версию backing services. Для myapp это значит: локально PostgreSQL, а не SQLite (прямое следствие десятого фактора — не подменять backing services). Как поднять зависимости локально — в главе 9.
  • requests и limits ресурсов (запрошенные и предельные CPU/память для контейнера) — чтобы поведение планировщика и ограничений было реалистичным.
  • Пробы (healthchecks) — liveness, readiness и при необходимости startup.
  • ConfigMap и Secret для конфигурации и секретов вместо хардкода — об этом глава 10.
  • Ingress и service discovery — маршрутизацию извне и обнаружение сервисов по DNS внутри кластера (глава 11).

Пробы новички регулярно путают, но детальный разбор трёх типов уместнее там, где мы их и настраиваем, — в главе 13. Здесь достаточно главного: три пробы ведут себя по-разному — liveness перезапускает контейнер, readiness лишь убирает под из endpoints Service (трафик не идёт, но рестарта нет), startup прикрывает медленный старт. Это стабильное поведение из официальной документации Kubernetes, и воспроизвести его локально — часть паритета с продом.

Практический вывод: перепутанные пробы дают либо петли рестартов (если на медленном старте навесить агрессивный liveness вместо startup), либо «мёртвые» эндпоинты под трафиком (если перепутать readiness и liveness). Для myapp логика простая: /healthz как liveness отвечает, пока процесс жив; /ready как readiness проверяет, что соединение с PostgreSQL установлено, — и пока БД недоступна, под не получает запросов. Эту пару эндпоинтов (/healthz + /ready) мы используем в статье сквозным образом, а сами манифесты проб напишем в главе 13.

Осознанно упрощаем:

  • Масштаб и продовую топологию. Ваша машина ограничена по CPU, памяти и диску. Как отмечает обзор Local Kubernetes от Plural, локальные окружения идеальны для прототипирования, но переход к продакшну и мульти-региону приносит новые вызовы, которые на ноутбуке просто не воспроизвести. Десятки реплик, несколько зон доступности, продовый автоскейлинг — не локальная задача.
  • Managed-сервисы облака. Amazon RDS, облачные очереди и кэши локально не реплицируются — вы поднимаете их open-source аналоги (PostgreSQL в контейнере вместо RDS). Поведение близко, но не идентично; это сознательный компромисс.
  • Нагрузочное тестирование. Запускать серьёзный load-test на тех же нодах, где крутится приложение, — плохая идея: нагрузочные поды конкурируют за CPU и память с самим myapp на той же машине, искажая результаты. Нагрузку гоняют в отдельном окружении, а не на локалке.

Полезный приём экономии ресурсов — поднимать на сессию только то, что реально нужно: отдельный namespace под задачу и лишь те сервисы, с которыми вы сейчас работаете, вместо того чтобы тащить весь зоопарк прода.

Антипаттерн: docker-compose как «замена» прода

Очень распространённое заблуждение новичка: «у меня же есть docker-compose.yaml, он и так поднимает сервис с базой — зачем мне Kubernetes локально?». Проблема в том, что Compose-файл структурно не равен Kubernetes-манифестам (Helm/Kustomize-чартам), и «работает в Compose» вовсе не гарантирует «работает в k8s».

Первое различие — архитектурное. Docker Compose — это, как отмечает разбор Docker Compose vs Kubernetes от Spacelift, по сути инструмент для одного хоста, тогда как Kubernetes — оркестратор кластера. У них разный замысел и разный масштаб ответственности.

Второе различие — в зернистости абстракций. Один сервис в Compose в Kubernetes разворачивается в несколько отдельных объектов: Deployment (как запускать поды), Service (как до них достучаться), ConfigMap и Secret (конфигурация и секреты), Ingress (внешний доступ). Это не косметика — каждый объект несёт свой кусок производственного поведения.

Насколько неполно одно переводится в другое, видно по официальному инструменту конвертации — Kompose (проект под зонтиком Kubernetes). Его авторы честно предупреждают: «наши конвертации не всегда один-к-одному... но проведут вас 99% пути» (our conversions are not always 1-1). Разбор конвертации Compose → Kubernetes через Kompose (oneuptime, 2026) перечисляет конкретные места, где маппинг проваливается:

  • depends_on игнорируется — в Kubernetes нет прямого эквивалента «запусти Б после А». Порядок старта решают иначе: init-контейнерами или ретраями на уровне приложения (для myapp — повторное подключение к PostgreSQL, пока БД не поднимется).
  • build: не собирает образ — Kubernetes не умеет собирать образы из исходников; образ нужно собрать и запушить в registry заранее (у нас — встроенный registry k3d k3d-registry.localhost:5000, см. главу 6).
  • network_mode: host и кастомные сети ложатся на сетевую модель Kubernetes плохо или игнорируются.
  • Bind-mounts не сохраняются — вместо монтирования локальных файлов рекомендуется ConfigMap/Secret.

И главное, ради чего вся глава: сгенерированные из Compose манифесты по умолчанию идут без requests/limits, без health-проб и с переменными окружения в открытом виде вместо Secret. То есть Kompose-вывод — это стартовая точка, а не готовый прод-манифест; именно те самые production-like аспекты, ради которых мы строим локалку, Compose и пропускает.

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

bash
1# только как отправная точка, НЕ готовый прод-манифест
2kompose convert -f compose.yaml

Вывод: Compose отлично подходит как быстрый локальный запуск для самых ранних шагов, но он не проверяет ваши Kubernetes-манифесты. Проблемы манифестов всплывают только после реальной выкатки в k8s — поэтому, готовя сервис к Kubernetes, и проверять его стоит в Kubernetes.

Чек-лист «насколько моя локалка похожа на прод»

Сведём всё в практический список. Чем больше галочек — тем меньше сюрпризов при выкатке myapp в прод.

  • Та же minor-версия Kubernetes, что и в проде — закреплена явно через --image у k3d/kind (для kind — ещё и sha256-дайджест). Проверено через kubectl version.
  • Деплой реальными манифестами (Kustomize/Helm), а не отдельным docker-compose или ручными командами.
  • Заданы requests и limits ресурсов для контейнера.
  • Настроены пробы: liveness и readiness (плюс startup, если старт небыстрый), причём осознанно — readiness не перезапускает контейнер, liveness перезапускает.
  • Конфигурация в ConfigMap, секреты в Secret — не хардкод и не plain-text env в манифесте.
  • Воспроизведены Ingress и маршрутизация — внешний доступ к myapp идёт тем же путём, что и в проде.
  • Те же backing services по типу и версии — для myapp это PostgreSQL, а не SQLite.
  • Критичные аддоны/контроллеры (ingress-controller, при необходимости cert-manager) либо подняты, либо упрощение явно отмечено.
  • Осознанные упрощения задокументированы — что именно вы НЕ воспроизводите (масштаб, managed-сервисы, нагрузочное тестирование) и почему.

Последний пункт недооценивают, а он важен: production-like окружение — это не то, где «всё как в проде», а то, где вы точно знаете, что совпадает, что нет и почему. Дальше мы как раз и будем по шагам закрывать пункты этого чек-листа: со следующей главы разберём, какой инструмент за что отвечает.

Источники

Хотите по-настоящему production-like локальное окружение Kubernetes?
Нужна помощь решить, что воспроизводить локально, а что упростить, чтобы локальный Kubernetes реально ловил баги прода? Помогу спроектировать production-like локальное окружение.