Local Kubernetes Dev — Part 2: Production-like environments — what and why
Что на самом деле значит production-like локалка: что воспроизводим осознанно, что упрощаем сознательно и где имитировать прод локально становится вредно.
В прошлой главе мы договорились разрабатывать 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:
1# Один важный нюанс с тегом: в Docker-тегах нельзя использовать '+',
2# поэтому пишем 'v1.31.5-k3s1', а НЕ 'v1.30.2+k3s1'
3k3d cluster create dev --image rancher/k3s:v1.31.5-k3s1То же самое аккуратнее — через config-файл k3d (его удобно держать в репозитории рядом с кодом):
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) — так образ фиксируется однозначно:
1kind: Cluster
2apiVersion: kind.x-k8s.io/v1alpha4
3nodes:
4- role: control-plane
5 image: kindest/node:v1.33.0@sha256:<digest-из-release-notes>Совпадение версии можно (и нужно) проверить — сравните вывод с тем, что стоит в проде:
1kubectl version --output=yamlВторой элемент паритета — реальные манифесты. Это значит: локально вы выкатываете myapp теми же декларативными описаниями, что и в проде, а не каким-то отдельным «локальным» способом. Официальная документация Kubernetes Managing Workloads показывает штатные приёмы работы с такими манифестами: группировку нескольких ресурсов в одном файле через разделитель ---, рекурсивное применение целой директории и встроенный в kubectl Kustomize (инструмент наложения настроек поверх базовых манифестов):
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 k3dk3d-registry.localhost:5000, см. главу 6).network_mode: hostи кастомные сети ложатся на сетевую модель Kubernetes плохо или игнорируются.- Bind-mounts не сохраняются — вместо монтирования локальных файлов рекомендуется ConfigMap/Secret.
И главное, ради чего вся глава: сгенерированные из Compose манифесты по умолчанию идут без requests/limits, без health-проб и с переменными окружения в открытом виде вместо Secret. То есть Kompose-вывод — это стартовая точка, а не готовый прод-манифест; именно те самые production-like аспекты, ради которых мы строим локалку, Compose и пропускает.
Если хочется быстро посмотреть, во что превратится ваш Compose-файл, конвертацию запустить можно — но только как черновик для дальнейшей доработки:
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 окружение — это не то, где «всё как в проде», а то, где вы точно знаете, что совпадает, что нет и почему. Дальше мы как раз и будем по шагам закрывать пункты этого чек-листа: со следующей главы разберём, какой инструмент за что отвечает.
Источники
- The Twelve-Factor App — X. Dev/prod parity
- Dev/Prod Parity — KodeKloud Notes
- kind — Configuration
- k3d — Config File
- Managing Workloads — Kubernetes
- Configure Liveness, Readiness and Startup Probes — Kubernetes
- Kompose — Convert Docker Compose to Kubernetes
- How to Convert Docker Compose Files to Kubernetes Manifests with Kompose — oneuptime (2026)
- Docker Compose vs Kubernetes — Spacelift
- Local Kubernetes: A Comprehensive Guide — Plural