Когда переходить на Kubernetes

От Docker Compose к оркестрации

20 апреля 2024

Главный вопрос про Kubernetes - не «что это» и не «как поставить», а «в какой момент текущий способ управления контейнерами перестаёт работать и какая ментальная модель нужна для следующего шага». Compose описывает запуск процессов на одной машине: вы императивно говорите «запусти эти контейнеры с этими портами и этими volume». Kubernetes описывает желаемое состояние кластера и поручает control plane непрерывно его поддерживать. Это разные модели мышления, и переход между ними - не апгрейд инструмента, а смена контракта между разработчиком и инфраструктурой. Статья разбирает, когда этот контракт становится необходим, откуда взялись его примитивы, на чём ломаются типичные миграции и какие реальные истории стоит читать перед тем, как принимать решение.

Когда Compose становится узким местом

У Compose есть один молчаливый контракт с пользователем: всё, что вы описали, живёт на одной машине, и если эта машина падает, падает и сервис. Пока команда маленькая, продукт переживает короткие окна downtime, а нагрузка укладывается в один сервер с запасом, этот контракт не мешает - наоборот, он экономит обучение, инфраструктуру и операционную сложность. Compose был придуман Docker Inc. как локальный orchestrator для разработчика, и в этой роли он по-прежнему один из лучших инструментов, которые отрасль придумала за десятилетие.

Узкое место появляется не там, где «нагрузка выросла», а там, где меняется тип риска. Пока сервис лежит в одном процессе на одном хосте, риск управляется людьми: дежурный получает алерт, заходит по SSH, перезапускает контейнер, разбирается утром. Когда продукт начинает обслуживать клиентов, у которых SLA измеряется в девятках, или когда ночной перезапуск означает потерянную выручку и обиженных пользователей, риск перестаёт быть управляем людьми по скорости реакции. Нужна машина, которая умеет за миллисекунды детектировать падение и поднимать новый экземпляр сама. Это и есть водораздел между Compose и оркестратором: автоматизированное восстановление желаемого состояния, а не ручное.

Второй типичный триггер - рост числа окружений. Пока есть «прод и мой ноутбук», docker-compose.yml в репозитории закрывает оба случая. Когда появляются dev, staging, demo, prod-eu, prod-us, набор YAML-файлов начинает расходиться: где-то RAM меньше, где-то другой API ключ, где-то Postgres внешний, где-то контейнером. Compose не умеет аккуратно параметризовать конфигурацию по окружениям; начинаются хаки с .env, override-файлами, симлинками. Каждый такой хак работает, но усложняет онбординг и увеличивает шанс «у нас на staging работает, на prod нет». Kubernetes решает эту проблему за счёт ConfigMap, Secret и Helm, но решает её только как побочный эффект общей идеи декларативной конфигурации.

Третий триггер - команда. Пока DevOps делает один человек, который держит в голове, на каком сервере что крутится, Compose работает как расширение этой памяти. Когда команда растёт до точки, где нужен RBAC, audit log, namespaces для изоляции по проектам, разделение прав между разработчиками и SRE - Compose упирается в потолок не функциональности, а коллаборативной работы. У него нет встроенного понятия «у этого человека права только на namespace dev».

Главное правило: переход на Kubernetes должен решать проблему, которую вы можете назвать в одном предложении. «Нам нужна доступность 99.95% и rolling deploy без downtime» - валидная причина. «Нам нужно как у больших» - не валидная.

Kelsey Hightower, один из самых известных голосов в community Kubernetes и автор «Kubernetes the Hard Way», много лет публично повторяет ту же мысль: Kubernetes - это infrastructure platform, не silver bullet. Большинство компаний приходят к нему слишком рано, тратят 6-12 месяцев на освоение и обнаруживают, что их реальная проблема - в архитектуре приложения, а не в способе его запуска. Его формулировка - «No, you don't need Kubernetes» - стала почти мемом, но за ней стоит инженерный аргумент: если ваше приложение не пишется как stateless горизонтально-масштабируемое, оркестратор не превратит его в такое.

История оркестрации: Borg, Swarm, Mesos и почему победил Kubernetes

Kubernetes возник не на пустом месте. Его прямой предок - Borg, внутренняя система Google, на которой с начала 2000-х запускаются практически все production сервисы компании - от поиска до Gmail. В 2015 году Verma, Pedrosa, Korupolu, Oppenheimer, Tune и Wilkes опубликовали на EuroSys статью «Large-scale cluster management at Google with Borg», где впервые подробно описали, как устроена эта система: единый пул машин, declarative job specifications, автоматический scheduler, который размещает workloads с учётом ресурсов, приоритетов и affinity. Большинство концепций, которые сегодня кажутся естественными в Kubernetes - Pod как группа co-located контейнеров, declarative reconciliation, labels как универсальный механизм selection - пришли оттуда.

Brendan Burns - один из трёх создателей Kubernetes внутри Google (вместе с Joe Beda и Craig McLuckie). Он прямо описывает Kubernetes как «Borg-3»: попытку взять накопленный за 15 лет опыт эксплуатации Borg и переписать систему с нуля для open-source мира, исправив архитектурные решения, которые в Borg оказались болезненными. После ухода в Microsoft Burns написал книгу «Designing Distributed Systems» (O'Reilly, 2018), где формализовал паттерны, лежащие в основе Kubernetes: sidecar, ambassador, adapter, leader election, work queue. Эта книга важна не как руководство по K8s, а как объяснение, почему примитивы сделаны именно такими, и я ссылаюсь на неё дальше в тексте, когда мотивирую отдельные решения платформы.

Конкуренция за стандарт оркестрации шла в 2014-2017 годах между несколькими системами. Docker Swarm от Docker Inc. ставил на простоту и встроенность в существующий toolchain: тот же CLI, тот же compose-формат, минимальный порог входа. Apache Mesos с Marathon от Mesosphere делал ставку на масштаб и зрелость - Mesos был проверен в Twitter и Airbnb на тысячах нод. Nomad от HashiCorp предлагал лёгкость и unified workflow с Terraform и Consul. Kubernetes был самым сложным из всех, но имел три преимущества, которые в итоге оказались решающими.

Первое - Google как основатель и постоянный донор кода. Это означало не только инженерную репутацию, но и долгосрочное обязательство: компания, у которой Borg крутится в production 15 лет, понимает, что нужно cluster manager-у. Второе - передача проекта в Cloud Native Computing Foundation в 2015 году, что сняло страх vendor lock-in: Kubernetes перестал быть «продуктом Google» и стал нейтральной платформой, в которую готовы были вкладываться Red Hat, IBM, AWS, Microsoft. Третье - открытая модель расширения через CRD и operators, благодаря которой вокруг ядра вырос целый ecosystem (Helm, Istio, ArgoCD, Prometheus operator, cert-manager), который сами эти компании поддерживать бы не смогли.

К 2018 году конкуренция в основном закончилась. Docker Inc. публично перестала позиционировать Swarm как production-альтернативу и встроила Kubernetes в Docker Desktop. Mesos постепенно уступил позиции, и в 2021 году Apache Mesos перешёл в Attic. CNCF Annual Survey 2023 фиксирует Kubernetes в production у 84% респондентов (см. ссылку в разделе «Источники»), и большинство production-кластеров сегодня живут не на self-hosted control plane, а на managed-сервисах (EKS, GKE, AKS). Это не значит, что K8s - правильный выбор для всех; это значит, что ecosystem, документация, экспертиза и tooling сконцентрированы вокруг него и любая альтернатива требует от вас плыть против течения.

Полезный исторический урок: победил не самый простой и не самый быстрый orchestrator, а тот, у которого оказалась самая расширяемая архитектура и самая широкая коалиция вендоров. Это типичный паттерн для платформенных стандартов - тот же, что сделал в своё время Linux дефолтом для серверов, а не более простую BSD или более изящный Plan 9.

Архитектурный паттерн: объявление желаемого состояния vs. императивные команды

Самая важная идея Kubernetes - не контейнеры и не масштабирование, а declarative reconciliation. В Compose вы говорите системе, что сделать: «запусти этот контейнер», «останови тот», «пересобери образ». В Kubernetes вы говорите системе, как должно выглядеть состояние мира: «здесь всегда должно работать 3 реплики этого Deployment, привязанные к Service my-app, доступные снаружи через Ingress на my-app.example.com». Дальше control plane непрерывно сравнивает реальное состояние кластера с желаемым и принимает действия, чтобы их свести.

Этот паттерн называется reconciliation loop, и он повторяется на всех уровнях Kubernetes. Deployment controller следит, чтобы желаемое количество ReplicaSet было таким, как описано. ReplicaSet controller следит, чтобы желаемое количество Pod-ов работало. kubelet на каждой ноде следит, чтобы реальное состояние контейнеров на хосте соответствовало тому, что control plane назначил этой ноде. Если на любом уровне реальность расходится с желанием - падает Pod, перегружается нода, истекает образ - соответствующий controller принимает действия. Это и есть «self-healing»: не магическое свойство, а следствие архитектурного решения отделить декларацию состояния от его поддержания.

Контраст с Compose нагляднее всего виден на сценарии падения. Если в Compose контейнер упал из-за OOM, Compose может его перезапустить (если настроен restart: always), но не более того. Если упал хост - сервис лежит до тех пор, пока человек не зайдёт и не починит. Если кто-то вручную остановил контейнер - он остановлен, и compose-файл это никак не оспорит. В Kubernetes это работает иначе: kubelet увидит, что Pod не запущен, и попробует поднять его снова. Если нода отвалилась - scheduler перенесёт Pod на здоровую ноду. Если кто-то вручную удалил Pod - ReplicaSet controller заметит расхождение с replicas: 3 и создаст новый Pod взамен удалённого. Состояние оказывается самовосстанавливающимся - его не нужно поддерживать вручную, оно само возвращается к описанному в манифесте.

Цена этой модели - сложность mental model. В Compose вы знаете, какие команды вы выполнили и в каком порядке. В Kubernetes вы знаете, какое состояние вы описали, но не контролируете, в какой момент и как именно reconciler примет действия. Отсюда классическое замешательство новичков: «я применил манифест, но Pod ещё не запустился, что не так?» - чаще всего ничего, просто scheduler ещё не нашёл подходящую ноду или image pull занимает 30 секунд. Чтобы продуктивно работать с Kubernetes, нужно отказаться от ментальной модели «я отдаю команды» и принять модель «я описываю инвариант, а система его поддерживает».

Декларативная модель не уникальна для K8s. Те же принципы лежат в основе Terraform (желаемое состояние инфраструктуры), Ansible с idempotent tasks, баз данных с MERGE/UPSERT, систем сборки с tracking зависимостей. Освоив этот образ мышления один раз, вы переносите его на множество других инструментов.

Примитивы как словарь: Pod, Deployment, Service, Ingress

Когда документация Kubernetes говорит «Pod - это группа из одного или нескольких контейнеров с общим network namespace и общими volumes», она описывает реализацию. Полезнее смотреть на примитивы как на словарь, которым команда описывает контракт между приложением и платформой. Каждый примитив отвечает на конкретный вопрос: кто запускается, в каком количестве, как доступен внутри кластера, как доступен снаружи, как конфигурируется, как мониторится. Знать примитивы - значит знать вопросы, которые платформа за вас не решает и которые приходится решать самостоятельно.

Ниже - сопоставление примитивов с теми задачами, которые они решают. Таблица служит словарём; в реальном дизайне последовательность обычно обратная - сначала возникает вопрос, потом подбирается примитив.

Вопрос Примитив Что именно делает
Что вообще запускается? Pod Минимальная единица планирования: группа контейнеров с общим network и volume.
Сколько копий и как обновлять? Deployment Декларация количества Pod-ов и стратегии rolling update / rollback.
Как другие сервисы внутри кластера находят это приложение? Service Стабильный DNS-имя и cluster IP; load balancing по живым Pod-ам.
Как пользователь снаружи попадает в приложение? Ingress Маршрутизация HTTP по hostname/path + TLS termination.
Где живут конфиги и секреты? ConfigMap / Secret Декларативно описанные key-value, монтируемые в Pod как env или файлы.
Как сохраняется состояние? PersistentVolumeClaim Запрос на storage с заданным размером и StorageClass.
Как платформа понимает, что Pod жив и готов к трафику? Liveness / Readiness probes HTTP/TCP/exec проверки, на основе которых kubelet перезапускает или скрывает Pod.
Как масштабироваться под нагрузку? HorizontalPodAutoscaler Изменение количества реплик Deployment по метрикам CPU, памяти или custom metrics.
Как изолировать команды и окружения внутри одного кластера? Namespace + RBAC Логическое разделение объектов и контроль доступа по ролям.

Pod как совместная единица планирования - не очевидное решение. Можно было бы сделать минимальной единицей контейнер, как в Docker. Команда Borg специально пошла на введение группы: контейнеры внутри Pod гарантированно живут на одной машине, делят сеть и могут общаться через localhost, делят volumes без сетевой задержки. Это позволяет делать sidecar-pattern: основной контейнер с приложением и вспомогательный контейнер, который, например, забирает логи или обновляет TLS-сертификаты, без того чтобы встраивать эту логику в основной образ. Sidecar - это организационный механизм отделения platform-кода от product-кода в одном Pod.

Deployment как декларация версии ценен не количеством реплик, а тем, что он управляет переходом между версиями. Когда вы меняете image: my-app:v1.2 на my-app:v1.3, Deployment controller создаёт новый ReplicaSet, постепенно переводит трафик на него и убирает старый. Если новая версия не проходит readiness-проверку - rollout останавливается. Если приходит команда rollback - ReplicaSet с предыдущей версией поднимается обратно. Это и есть rolling deploy без downtime, который в Compose требует написать собственный shell-скрипт с blue-green логикой.

Service - это уровень indirection между потребителем и поставщиком. Pod-ы приходят и уходят (rolling update меняет их IP-адреса каждые несколько минут), а Service - это стабильное DNS-имя, за которым прячется текущий набор живых Pod-ов. Внутри кластера приложения обращаются к my-app.namespace.svc.cluster.local, и kube-proxy балансирует запросы по реальным IP. Это маленький, но критичный элемент: без него любое падение или передеплой Pod-а превращался бы в сетевой сбой для его клиентов.

Ingress - это L7-фронт кластера. Service по умолчанию доступен только внутри. Чтобы выпустить трафик наружу, нужен Ingress controller (типичные реализации - nginx-ingress, Traefik, HAProxy, AWS ALB), который слушает 80/443, terminate TLS, маршрутизирует по hostname или path к нужному Service. Это позволяет, например, на одном кластере держать api.example.com, app.example.com, admin.example.com - все за общим IP, разводимые по правилам Ingress. В Compose эту роль выполнял отдельно поднятый nginx с вручную написанным конфигом.

Ниже - типичный минимальный комплект манифестов для одного веб-приложения. Он не нужен для понимания, его легко найти в документации; он приведён, чтобы видеть, как примитивы складываются в работающую конфигурацию.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: my-app:v1.3.0
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
            limits:   { cpu: "500m", memory: "256Mi" }
          readinessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /alive, port: 8080 }
            initialDelaySeconds: 30
            periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector: { app: my-app }
  ports:
    - port: 80
      targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
spec:
  rules:
    - host: my-app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port: { number: 80 }

Этот пример показывает важное: даже для самого простого приложения Kubernetes требует от вас явно описать probe-эндпоинты, ресурсные лимиты, селекторы и стратегию доступа. В Compose всё это либо отсутствует, либо принимается по умолчанию. K8s превращает неявные предположения в явные декларации - это и сила, и стоимость входа.

Реальные случаи миграции: Monzo, Shopify, GitHub

Абстрактные аргументы за и против Kubernetes стоят меньше, чем разобранные истории компаний, которые прошли путь миграции и публично об этом написали. Ниже - три кейса с разным контекстом и разными уроками. Источники приведены в разделе «Источники»; если факт указан без ссылки, это стилизация на основе типичных паттернов.

Monzo - британский онлайн-банк, известный в инженерном сообществе радикальной приверженностью микросервисам. К 2019 году у них было около 1500 микросервисов на одном Kubernetes-кластере, и инженерный блог банка регулярно публиковал детальные посты о том, как эта архитектура устроена и где она ломается. Самый разобранный материал - пост «The Anatomy of an Incident: 29th July 2019» (monzo.com) о сбое, при котором плановое обновление Cassandra (rolling upgrade) совпало с особенностью драйвера, который при частичном состоянии кластера начал генерировать ошибки в одном из ключевых сервисов; платёжная функциональность лежала несколько часов. Урок Monzo не «Kubernetes виноват», а противоположный - оркестрация позволила им управлять огромным количеством мелких сервисов, которое в монолитной архитектуре было бы невозможно. Но та же архитектура повышает blast radius любого misconfiguration в shared infrastructure: когда у вас 1500 сервисов смотрят на одну Cassandra и одну сеть, проблема в этом слое затрагивает всех сразу.

Shopify мигрировал свою основную платформу на Kubernetes начиная с 2017 года. У них уникальная инженерная сложность: один из крупнейших Rails-монолитов в мире (внутреннее имя Shopify Core) обслуживает миллионы магазинов, и спайки трафика на Black Friday / Cyber Monday кратно превосходят обычную нагрузку. Их проектная команда подробно описывала путь в публикациях на shopify.engineering и в выступлениях на KubeCon. Ключевой урок Shopify - инвестиция в внутреннюю платформу. Они не просто переехали на K8s, а построили поверх него Cloud Buildpacks, ChatOps tooling, собственные CRD для описания «приложения» в Shopify-смысле. Без этого слоя команды разработчиков утонули бы в YAML и не могли бы быстро деплоить. Это типовой паттерн: для большой организации Kubernetes сам по себе - не платформа, а сырьё, из которого строится платформа. Источник: https://shopify.engineering/.

GitHub MySQL outage 21 октября 2018 года - кейс про обратную сторону любой автоматики над распределённым состоянием. У GitHub 43-секундный network partition между восточным и западным дата-центрами разорвал связь между MySQL-кластерами и автоматикой топологии (Orchestrator). На обеих половинах Orchestrator независимо переизбрал собственный primary; после восстановления связи обе стороны успели принять записи в свои новые primary, и кластер потребовал ручной реконсиляции, чтобы решить, чьи данные авторитетны. Сервис восстановился полностью только через 24 часа. Технический post-mortem (github.blog/2018-10-30-oct21-post-incident-analysis) подробно разбирает, как именно произошёл split-brain. Это не история про конкретный продукт, а про класс проблем: любая распределённая control plane (что MySQL Orchestrator, что etcd Kubernetes) имеет режимы отказа, в которых автоматическое восстановление невозможно или опасно. Kubernetes наследует этот класс: если у вас падает etcd, никакие декларации в манифестах не помогут, control plane остаётся без памяти. Поэтому управляемые сервисы (EKS, GKE, AKS), которые берут на себя эксплуатацию control plane и его восстановление, для большинства команд оказываются разумным компромиссом - вы платите за то, чтобы не разбираться лично с тем, как восстанавливать кворум после сетевого разрыва.

Полезно сравнить три истории. Monzo показывает, что Kubernetes масштабирует количество сервисов; Shopify - что для этого нужна вторичная платформа поверх него; GitHub - что само существование control plane создаёт новый класс рисков, к которым команда должна быть готова. Все три компании остались на Kubernetes - это валидный выбор - но ни одна из них не утверждает, что путь был дешёвым.

Стилизованный пример контекста, где переход не оправдался: small SaaS, 5 разработчиков, один Rails-монолит, два-три фоновых воркера, Postgres, Redis. Команда переехала с Compose на managed EKS «потому что так делают взрослые». За первые полгода - три инцидента, связанных с misconfigured liveness probes (приложение убивалось во время legitimate долгих запросов), один сетевой сбой из-за неправильно настроенного Calico, один эпизод с заблокированным rollback из-за конфликта в Helm values. Time-to-resolve вырос, потому что в команде нет K8s-эксперта. Через год вернулись на Compose + systemd на двух больших серверах. Это стилизованный, а не задокументированный кейс, но шаблон такой миграции в обратную сторону хорошо знаком тем, кто работает с командами уровня Series-A: цена K8s оплачивается ростом организации, и если этого роста нет, инвестиция не возвращается.

Антипаттерны при переходе

Большинство неудачных миграций на Kubernetes ломаются не на этапе «не получилось установить кластер», а на этапе «работает, но не так, как мы ожидали». Ниже - типичные failure modes; каждый из них стоит пройти как чеклист перед production-запуском.

Игнорирование multi-container паттернов. Один контейнер на Pod - нормальный default; проблема возникает, когда команда так и не приходит к sidecar/ambassador/init-container, даже там, где они снимают реальную боль: sidecar для сбора логов, ambassador для прокси к внешнему сервису, init-container для миграций. В результате вся вспомогательная логика встраивается в основной образ, что увеличивает его размер, связывает его с инфраструктурой и делает повторное использование невозможным. Burns специально подчёркивает: Pod как multi-container - это организационный примитив, и его игнорирование обнуляет одну из ключевых возможностей платформы.

Misuse Namespace. Команды часто используют namespace как «папку для группировки» - все сервисы продакшна в namespace prod, все сервисы стейджинга в namespace staging. Это работает, пока компания маленькая, но ломается на двух сценариях: во-первых, вы не можете разделить квоты по командам; во-вторых, RBAC становится очень грубым - либо у разработчика есть доступ ко всему prod, либо нет. Правильное использование namespace - это границы команд и tenants, а окружения разделять кластерами или, по крайней мере, явно проектировать NetworkPolicy между ними. Иначе любая ошибка в квоте одной команды превращается в инцидент для всех её соседей.

Отсутствие resource requests и limits. Это, пожалуй, самая частая причина «странных» инцидентов в молодых K8s-кластерах. Без requests scheduler не знает, сколько ресурсов нужно Pod-у, и может разместить десять «голодных» Pod-ов на одной ноде, перегрузив её. Без limits один Pod, у которого утёкла память, съедает всю доступную память ноды, и kubelet начинает по OOM убивать сторонние Pod-ы, выбирая их по эвристике. Внешне это выглядит как «у нас падают сервисы, которые мы не трогали». Решение тривиальное - всегда задавать requests и limits для CPU и памяти - но без него платформа работает как лотерея.

Пропущенные или неправильно настроенные probes. Liveness probe говорит kubelet, когда перезапустить контейнер; readiness probe - когда направлять на него трафик. Без readiness probe Service сразу после rollout направит трафик на Pod, который ещё не прогрел кэш, не открыл соединения к БД, не загрузил конфигурацию - пользователи получат 502. Без liveness probe зависший Pod (deadlock в потоках, забитый event loop) будет годами числиться живым, не обслуживая запросы. С чрезмерно агрессивной liveness probe платформа будет убивать здоровый Pod во время legitimate долгих операций - типичная история «у нас по понедельникам падает batch job, потому что probe не учитывает, что миграция идёт 5 минут». Probes - это контракт между приложением и платформой, и его нужно проектировать осознанно, а не копировать примеры из туториалов.

Stateful workloads без StatefulSet и без понимания storage. Соблазн «давайте просто запустим Postgres в Deployment с PVC» очень велик: технически работает, в туториалах есть. Проблема в том, что Deployment - это набор взаимозаменяемых Pod-ов, а база данных взаимозаменяемой не является. При rolling update Deployment может убить master до того, как replica догонит binlog; PVC может быть привязан к ноде, на которой Pod больше не запустится; backup-стратегия живёт в голове разработчика, а не в манифестах. Для stateful workloads существует StatefulSet с гарантиями ordered deployment и stable network identity, и для большинства случаев managed-сервис (RDS, Cloud SQL) обходится дешевле, чем самостоятельная эксплуатация базы в кластере.

Секреты в git. Соблазн закоммитить Secret YAML в репозиторий вместе с остальными манифестами - постоянный. Kubernetes Secret хранится в etcd в base64 (не зашифрованном!), и git-репозитории, особенно публичные, регулярно сканируются ботами на утечки. Правильные подходы - Sealed Secrets от Bitnami (asymmetric encryption, расшифровывается только в кластере), External Secrets Operator (Secret тянется из Vault/AWS Secrets Manager в момент применения), SOPS (encrypted files, расшифровываются ключом). Любой из них работает; никакой - не работает, и команда платит за это утекшими credentials.

Реорг-театр на платформу. Менее технический, но не менее важный антипаттерн: компания решает «мы переходим на Kubernetes», создаёт DevOps Transformation Team, рисует roadmap на год, но не меняет ни ответственности команд, ни on-call процессов, ни инвестиций в обучение. Через год кластер существует, но используется одной-двумя командами; остальные продолжают деплоить через старые процессы. Внешне выглядит как успешная миграция (есть CI/CD на K8s, есть production-кластер), фактически - параллельная инфраструктура с двойными costs. Это аналог «менять только Structure» из системного менеджмента - реорг без согласования processes, rewards и people.

Эвристика для self-check: возьмите свой production-манифест и пройдитесь по списку выше. Если на любом пункте ответ «у нас этого нет, потому что не задумались» - это техдолг с понятной стоимостью первого инцидента.

Рамка решения: переходить или нет

Любая миграция на Kubernetes - это инвестиция, которая возвращается только при определённом размере организации и определённых требованиях к доступности. Полезно явно сформулировать, при каких условиях эта инвестиция оправдана, а при каких - нет. Ниже - набор практических вопросов, на которые стоит честно ответить до того, как выделять бюджет на проект миграции.

Сколько стоит downtime? Если 30 минут простоя в неделю обходятся в 100 евро упущенной выручки - K8s не окупится. Если те же 30 минут - это пропущенный платёжный шлюз банка с штрафами от регулятора - окупится первой же предотвращённой ночной возней с runbook. Граница не в проценте uptime, а в долларах разницы между ручным и автоматическим восстановлением.

Есть ли у команды экспертиза или бюджет на её приобретение? Kubernetes - сложная экосистема, и человек, способный диагностировать проблему в kube-proxy или etcd, на рынке стоит дорого. Если в команде такого человека нет и нанимать вы не готовы - либо берите managed Kubernetes (EKS, GKE, AKS) и принципиально не лезьте в control plane, либо оставайтесь на Compose. Self-hosted K8s без экспертизы - надёжный способ оплатить первый production-инцидент собственным репутационным ущербом.

Действительно ли вам нужно горизонтальное масштабирование? Современные виртуальные машины легко берут 64-128 vCPU и сотни ГБ памяти. Большинство SaaS-приложений до выручки в десятки миллионов долларов умещаются в один такой узел с запасом, и горизонтальное масштабирование им не нужно вообще - им нужен честный health check и autoreboot, который умеют systemd и Compose со restart: always. Если ваш план роста - x2 за год, а не x100, цена K8s превышает выгоду.

Есть ли у вас несколько окружений с расходящейся конфигурацией? Если конфигурация одинакова или почти одинакова, Compose с .env закрывает задачу. Если у вас 5+ окружений с разной топологией (другие зависимости, другие квоты, другие региональные правила compliance) - конфигурационный аппарат K8s начинает экономить время, особенно вместе с Helm или Kustomize.

Если на большинство вопросов выше ответ «нет» - оставайтесь на Compose. Это не «недостаточно зрелое решение», это разумная экономия. Если на большинство ответ «да» - начинайте с managed K8s (EKS/GKE/AKS), берите внешнюю консультацию или нанимайте человека с production-опытом, инвестируйте в платформу до того, как продуктовые команды начнут массово деплоить, и обязательно описывайте Team API для платформы (см. Системный менеджмент) - иначе платформа превратится в очередной silo.

Связанные материалы:
  • Service Mesh - следующий уровень: Istio, Linkerd, mTLS и observability на уровне сетевого взаимодействия.
  • Монолит vs Микросервисы - архитектурный вопрос, который должен быть решён до выбора оркестратора.
  • Системный менеджмент - почему платформенная команда без Team API превращается в silo.

Источники и смежные статьи

Первоисточники и книги

Реальные кейсы и инциденты

Документация и глубокое погружение