← назад к разделу

Когда образ контейнера собран и загружен в registry, самое интересное только начинается. Kubernetes нужно объяснить: сколько копий запустить, когда считать под готовым, как обновлять без простоя и куда смотреть, если что-то пошло не так. Всё это описывается в файлах — и файлы живут в git.

Главная зависимость всей выкатки — readiness probe: именно она решает, попадёт ли новая версия под трафик, и вот как это выглядит такт за тактом при replicas: 3, maxSurge: 1 и maxUnavailable: 0.

выкатка 1.42.0 → 1.43.0 при maxSurge 1 и maxUnavailable 0 deployment order-service · replicas: 3 под-1 · 1.42.0в endpointsпод-2 · 1.42.0в endpointsпод-3 · 1.42.0в endpointsслот maxSurge: 1 свободентрафик: 100% на 1.42.0 · 3 пода из 3 шаг 1 · установившееся состояниеreplicas: 3 · maxSurge: 1maxUnavailable: 0все три прошли readiness probeёмкость: 3 из 3Service шлёт запросы в готовые подычетвёртый слот свободентак выглядит прод до выкатки под-1 · 1.42.0в endpointsпод-2 · 1.42.0в endpointsпод-3 · 1.42.0в endpointsпод-4 · 1.43.0probe ещё идёттрафик: 100% на 1.42.0 · 1.43.0 — 0% шаг 2 · kubectl apply: образ 1.43.0maxSurge: 1 → создан 4-й подmaxUnavailable: 0 → старые не гасимновый под ещё не в endpointsёмкость: 3 из 3, простоя нетreadiness probe ещё не ответиладо 1.43.0 дошло 0 запросовновый под поднялся, трафика не получил под-1 · 1.42.0в endpointsпод-2 · 1.42.0в endpointsпод-3 · 1.42.0в endpointsпод-4 · 1.43.0probe не прошлатрафик: 100% на 1.42.0 · выкатка стоит шаг 3 · probe честная: проверяет базупод-4 не смог открыть соединениеprobe не прошла → вне endpointsmaxUnavailable: 0 → старый живвыкатка замерла на 3 + 1kubectl rollout status покажет стопкривая версия не увидела запросовчестная probe останавливает выкатку под-1 · 1.43.0в endpointsпод-2 · 1.43.0в endpointsпод-3 · 1.43.0в endpointsслот снова свободентрафик: 100% на 1.43.0 · нужен откат шаг 4 · probe формальная: 200 всегдапод-4 «готов» сразу → в endpointsстарый под гаснет, цикл ×3выкатка дошла до конца100% трафика на кривой версииrollout status: успех, а в проде сбойспасает kubectl rollout undoта же настройка, другой исход — из-за probe

Пока новый под не ответил «готов», Service не шлёт в него ни одного запроса. При maxUnavailable: 0 честная probe просто замораживает выкатку на трёх старых подах и одном новом, а формальная «200 всегда» доводит её до конца — и весь трафик оказывается на кривой версии.

Обязательно

Манифест: один файл — одно намерение

Раньше сервисы раскладывали по серверам вручную или через скрипты. Kubernetes устроен иначе: вы описываете, что хотите, а кластер сам разбирается, как это обеспечить.

Описание пишется в YAML-файлах, которые называют манифестами. Типичная связка для одного сервиса — два файла:

# deployment.yaml — сколько запустить и какой образ
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  strategy:
    rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: app
          image: order-service:1.42.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet: { path: /actuator/health/readiness, port: 8080 }
            periodSeconds: 5
            timeoutSeconds: 3
          resources:
            requests: { cpu: "500m", memory: "768Mi" }
            limits: { memory: "768Mi" }

Три блока здесь легко принять за необязательные украшения — на самом деле без них манифест в прод не годится. resources говорят планировщику, сколько места занимает под: без них кластер расставляет поды наугад. strategy задаёт, как именно обновляться. А readinessProbe — та самая проверка, вокруг которой построена вся статья: без неё Kubernetes считает под готовым сразу, как стартовал процесс, и шлёт туда трафик, пока приложение ещё поднимается. Deployment без readiness probe даёт ровно ту формальную выкатку, о которой речь пойдёт ниже.

# service.yaml — как достучаться до подов изнутри кластера
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080

Применить манифест — одна команда:

kubectl apply -f deployment.yaml

Kubernetes прочитает файл и приведёт кластер в описанное состояние. Если под упал — кластер поднимет новый. Если вы поменяли количество реплик и применили файл заново — кластер перестроится сам. Это и называют декларативным подходом.

Kustomize и Helm: когда одного файла мало

Один файл работает хорошо, пока у вас одна среда. Как только появляются staging и prod — они отличаются числом реплик, лимитами ресурсов, адресами баз. Копировать манифест и менять несколько строк — это путь к расхождениям.

Есть два инструмента, которые решают эту задачу по-разному.

Kustomize работает без шаблонов: есть базовый вариант и «наложения» (overlays) для каждой среды. Накладывать можно что угодно: только replicas, только ресурсы, только адрес образа. Kustomize встроен прямо в kubectl — отдельно ставить нечего (с оговоркой: встроенная версия обычно отстаёт от самостоятельного бинаря на несколько релизов, и если нужна свежая возможность, манифесты собирают командой kustomize build, а результат отдают в kubectl apply -f -):

deploy/
  base/                 # deployment.yaml, service.yaml — общая основа
  overlays/
    staging/            # replicas: 1, меньше памяти
    prod/               # replicas: 3, HPA, PodDisruptionBudget
kubectl apply -k overlays/prod/

Helm — это уже шаблонизатор с пакетированием. Из одного чарта можно развернуть сервис десятки раз с разными параметрами. Именно так работает весь сторонний софт (базы, мониторинг, ингресс-контроллеры) — скачиваете чарт, передаёте values.yaml под свою среду.

helm upgrade --install order-service ./chart -f values-prod.yaml

Одна вещь тут удивляет почти каждого: значения, переданные команде, между запусками не запоминаются. Забыли -f values-prod.yaml на следующем обновлении — Helm возьмёт значения из самого чарта, и прод спокойно уедет на настройки по умолчанию. Поэтому файл значений держат в git и передают каждый раз, а не полагаются на память инструмента.

Недостаток Helm — шаблоны пишутся на Go-templates поверх YAML. Это читается хуже, и для отладки часто нужно смотреть, что Helm сгенерировал:

helm template order-service ./chart -f values-prod.yaml

Практический выбор: kustomize удобен для своих сервисов, где изменений немного; Helm нужен для стороннего ПО и когда конфигурация действительно сильно варьируется между средами.

Теги образов: как не потерять возможность откатиться

Маленькая деталь с большими последствиями. Тег :latest на образе выглядит удобно — загрузил новый образ под тем же тегом, и всё. Но это ломает три важные вещи:

  • откат: на что возвращаться, если тег тот же?
  • воспроизводимость: что именно сейчас в проде?
  • выкатку: kubectl apply сравнивает манифесты, а не содержимое образа. Строка image: order-service:latest не изменилась — значит, шаблон пода тот же, новой ревизии нет, и Deployment просто не начинает выкатку. Поды крутятся на старом образе, а команда уверена, что задеплоила.

Отдельная ловушка рядом — перезалитый фиксированный тег. Для :latest Kubernetes подставляет imagePullPolicy: Always, то есть образ хотя бы перекачивается; а вот у тега вида :1.42.0 политика по умолчанию IfNotPresent — если такой образ уже лежит на ноде, новую версию оттуда никто не заберёт. Перезаливать поверх существующего тега поэтому не стоит вдвойне.

Правило одно: тег уникален и неизменяем. Хорошие варианты — версия (order-service:1.42.0) или SHA коммита (order-service:a1b2c3d). Тогда в любой момент видно, что задеплоено, и можно вернуться к любому предыдущему состоянию.

Rolling update: как версия попадает в прод без простояспросят на собеседовании

По умолчанию Kubernetes обновляет поды постепенно, не выключая сервис целиком. Это называют rolling update: новые поды поднимаются, старые гасятся порциями.

Поведение настраивается:

strategy:
  rollingUpdate:
    maxSurge: 1        # сколько дополнительных подов можно создать сверх replicas
    maxUnavailable: 0  # сколько подов может не работать в процессе

maxUnavailable: 0 означает: ёмкость никогда не проседает — новый под стартует раньше, чем остановится старый. Это консервативный вариант для прода.

Здесь появляется главная зависимость: новый под получает трафик только после того, как пройдёт readiness probe. Это специальная проверка, которую Kubernetes делает перед тем, как пустить запросы на под. Пока под не ответил «готов» — он трафик не получает.

Если probe честная (проверяет соединение с базой, очередью, нужными зависимостями) — кривая версия просто не пройдёт проверку, и выкатка остановится. kubectl rollout status покажет проблему.

Ждать он будет не вечно, и это важно знать заранее. У Deployment есть срок progressDeadlineSeconds — по умолчанию 600 секунд. Если за это время выкатка не сдвинулась, Deployment получает условие ProgressDeadlineExceeded, а kubectl rollout status выходит с ненулевым кодом. Пока срок не вышел, команда просто висит и печатает «waiting for rollout to finish» — и отличить «выкатка идёт медленно» от «выкатка встала намертво» по этому выводу нельзя. Именно на ненулевой код и завязывают конвейер: он превращает зависшую выкатку в упавший шаг, а не в десять минут молчания.

Если probe формальная («отвечаю 200 всегда») — кривая версия сразу примет трафик, и у вас инцидент.

Откат при проблеме — штатная операция:

kubectl rollout undo deployment/order-service
kubectl rollout history deployment/order-service   # история ревизий

Кроме rolling update есть стратегия Recreate — сначала выключить все старые поды, потом поднять новые. Это вызывает кратковременный простой, но нужна для фоновых обработчиков, которым нельзя работать в двух версиях одновременно.

Посмотреть, что уедет: diff и --dry-run=server

kubectl apply -f . применяет всё, что нашёл, и обратной кнопки у него нет. Две команды, которые стоит выполнять до него — особенно на промышленном кластере.

kubectl diff -f k8s/                     # что изменится по сравнению с тем, что уже в кластере
kubectl apply -f k8s/ --dry-run=server   # проверить, но не применять

kubectl diff отправляет манифесты в api-server, получает результат слияния и показывает построчную разницу с текущим состоянием. Это не сравнение файлов между собой — это ответ на вопрос «что именно поменяется в кластере». Здесь и обнаруживаются неприятные сюрпризы: вместе с версией образа уезжает изменённый лимит памяти, из манифеста пропало поле, которое кто-то добавил руками, или в файл случайно попал другой namespace.

--dry-run=server (именно server, не client) прогоняет объект через api-server целиком: проверяются схема, права, значения по умолчанию и допускающие вебхуки, но ничего не записывается. Отличие от client существенное: клиентская проверка видит только опечатки в YAML, серверная — ещё и то, что объект не пройдёт политику кластера или что у вас нет прав.

Практический порядок для промышленного кластера: diff, глазами прочитать разницу, --dry-run=server, затем apply. Тридцать секунд, которые отделяют «обновил тег образа» от «заодно снёс лимиты у трёх сервисов».

И то, что делает diff особенно полезным: он показывает последствия чужих ручных правок. Если кто-то поправил объект в кластере напрямую, ваш следующий apply вернёт его к состоянию из файлов — и вы об этом узнаете заранее, а не по инциденту. Это, кстати, главный практический аргумент за подход с репозиторием как источником правды из раздела ниже.

Другие способы: canary и blue-green

Плавное обновление — не единственная стратегия, а ещё две упоминают так часто, что стоит знать, чем они отличаются и чего требуют.

Canary («канарейка») — новая версия получает малую долю трафика, например пять процентов, и её наблюдают: ошибки, задержки, бизнес-показатели. Если всё хорошо, долю увеличивают, пока не станет сто; если плохо — возвращают в ноль, и пострадали немногие.

Дешёвый вариант делается без всяких дополнительных средств: поднять рядом второй Deployment с новой версией и тем же набором меток, которые выбирает Service. Пять подов старой версии и один новой означают примерно шестую часть трафика. Точности здесь нет (доля задаётся числом подов), но работает везде.

Точный вариант требует прокси, который умеет делить трафик по весам: контроллер входящего трафика с соответствующими настройками, сервисная сетка или готовые инструменты выката (Argo Rollouts, Flagger). Они же умеют главное — автоматически останавливать выкат, глядя на метрики, и откатывать без человека.

Blue-green — рядом со старой версией (синей) поднимается полная копия новой (зелёной), проверяется, и трафик переключается целиком одним движением: у Service меняется селектор. Откат — обратное переключение, и он мгновенный, в этом главный плюс. Минус очевиден: на время выката нужна двойная ёмкость, а если версии работают с одной базой, «мгновенное переключение» всё равно требует совместимости схемы — о чём следующий раздел.

Что выбирают на практике. Плавное обновление — по умолчанию, для почти всего. Blue-green — когда нужен мгновенный откат и ёмкость не проблема (обычно для чего-то критичного и редко меняющегося). Canary — когда риск ошибки высок, а её последствия видны только на реальном трафике; и брать его стоит вместе с автоматической проверкой метрик, иначе это просто медленный выкат, за которым никто не следит.

Миграции БД: когда в проде одновременно работают две версииспросят на собеседовании

Во время rolling update в кластере несколько минут сосуществуют старая и новая версии приложения. Это означает: обе версии работают с одной базой данных одновременно.

Отсюда правило: каждая миграция должна быть совместима со старой версией приложения. Переименовать колонку через RENAME COLUMN — нельзя: старая версия её не найдёт. Вместо этого используют подход expand-contract на несколько релизов:

  1. Релиз 1: добавить новую колонку, старая остаётся. Читаем и пишем по-прежнему в старую.
  2. Релиз 2: код пишет сразу в обе колонки, читает всё ещё из старой.
  3. Перенос данных: отдельным шагом копируем значения из старой колонки в новую по всем старым строкам. Шаг, который пропускают чаще всего, — и зря: без него «читать из новой» вернёт NULL по всем записям, созданным до релиза 1. На большой таблице перенос делают порциями, чтобы не держать долгую блокировку.
  4. Релиз 3: код читает из новой колонки; убедились, что всё на месте — старая удаляется.
релиз 1 новая колонка пустая релиз 2 пишем в обе перенос копируем старые строки релиз 3 читаем новую уборка старая колонка удалена

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

Где запускать миграцию — отдельный вопрос. Миграция при старте пода удобна на одном экземпляре, но в кластере несколько подов стартуют одновременно и конкурируют за блокировку миграции, упавшая миграция отправляет под в CrashLoopBackOff, а долгая — срывает startup probe. Надёжнее выполнять миграцию отдельным шагом конвейера или Kubernetes Job перед выкаткой новой версии: один раз, под контролем, с понятным логом — и только потом новый код получает трафик.

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

PodDisruptionBudget: сколько подов можно гасить одновременно

В дереве настроек выше есть строка про PodDisruptionBudget, и без объяснения она выглядит необязательной. На самом деле это единственное, что защищает сервис от планового обслуживания кластера.

Ситуация такая. Администратор выводит узел из работы (обновление ядра, замена машины) командой kubectl drain. Она выселяет с узла все поды разом. Если на этом узле оказались все три реплики вашего сервиса — сервис исчезает целиком, хотя ничего не сломалось. Плавное обновление тут не помогает: это не выкат, а выселение.

PodDisruptionBudget описывает правило «сколько подов сервиса должно оставаться живыми при добровольном выселении»:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: orders
spec:
  minAvailable: 2          # или maxUnavailable: 1
  selector:
    matchLabels:
      app: orders

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

Три вещи, которые надо знать, чтобы это работало.

Правило действует только на добровольные выселения. Падение узла, OOMKilled, удаление пода вручную — это не выселение, и бюджет тут ни при чём. Он про обслуживание, а не про аварии.

minAvailable: 1 при одной реплике — ловушка. Такое правило означает «нельзя выселять вообще», и обслуживание узла встанет. Для одной реплики либо не заводят бюджет, либо пишут maxUnavailable: 1 и соглашаются на короткий перерыв.

Числа согласуют с репликами. Три реплики и minAvailable: 2 — разумно: узлы обслуживаются по одному. Три реплики и minAvailable: 3 — то же, что запрет.

Практический минимум: у каждого сервиса, у которого больше одной реплики и который кому-то нужен, есть такой объект. Он стоит десять строк один раз и снимает целый класс «сервис лёг, а мы ничего не выкатывали».

GitOps: git как источник правды о состоянии кластераспросят на собеседовании

Обычный CI/CD-конвейер работает так: тесты → сборка образа → push в registry → kubectl apply из pipeline. Это называют push-подходом: CI сам толкает изменения в кластер. Минус — кластер доступен из CI, и фактическое состояние нигде не зафиксировано.

GitOps переворачивает это: в git хранится «желаемое состояние» кластера, а специальный оператор в кластере (Argo CD, Flux) постоянно сверяет реальное состояние с git и применяет разницу сам.

Деплой новой версии в GitOps — это pull request с новым тегом образа. Откат — это git revert. Если кто-то вручную поменял что-то в кластере — оператор это заметит как «отклонение от желаемого» (drift) и вернёт обратно. Как это выглядит на объекте Application и что происходит при дрейфе — в статье про Argo CD.

разработчик git push с новым тегом Argo CD видит разницу применяет к кластеру

Состояние кластера описано в репозитории, а не в чьей-то консоли. Что выкачено — видно по истории git, откат — это возврат коммита.

GitOps хорошо работает, когда сервисов больше двух-трёх: история изменений в git, аудит, откаты через стандартные git-инструменты.

Три вопроса встают сразу же, как только это решают внедрять.

Кто меняет тег образа после сборки. Сам конвейер сборки в кластер больше не ходит — значит, кто-то должен положить новый тег в репозиторий манифестов. Вариантов три. Шаг в конвейере после публикации образа делает коммит в репозиторий манифестов (пять строк на sed и git commit, самый распространённый способ). Отдельный компонент в кластере следит за реестром и сам обновляет тег в репозитории — так работает Argo CD Image Updater. Или тег не меняется вовсе: развёртывание ссылается на дайджест, который подставляет тот же шаг конвейера. Выбор влияет на то, кому нужны права на запись в репозиторий манифестов, и это стоит решить до внедрения, а не после.

Где брать секреты. Пароли нельзя положить в репозиторий открытым текстом, а объект Secret с base64 — это открытый текст. Рабочих подходов три: зашифрованный секрет, который расшифровывает только оператор в кластере (Sealed Secrets); файл, зашифрованный ключом из внешнего хранилища (SOPS); или объект, который ссылается на секрет во внешнем хранилище, а оператор синхронизирует его в кластер (External Secrets Operator с Vault или хранилищем облака). Последний вариант обычно правильный, если хранилище секретов уже есть.

Чем за это платят. В кластере появляется компонент с широкими правами — по сути тот, кто может менять всё. Аварийная ручная правка перестаёт работать: поправили объект руками, оператор вернул как в репозитории (и это ровно то, чего от него хотели, но в три часа ночи это неожиданно). Появляется новый класс задач «почему не синхронизируется» — недоступный репозиторий, неверная ветка, ошибка шаблона, зависшая операция. И скорость выката теперь зависит от того, как часто оператор проверяет репозиторий.

Отсюда и граница применимости: пока сервис один и выкатывает его один человек, kubectl apply из конвейера проще и понятнее. Подробный разбор объектов, состояний и связки с миграциями — в статье про Argo CD.

Дополнительно: при первом чтении можно пропустить

Глубже: ReplicaSet и ревизии: что на самом деле делает rollout undoрасширенное

Deployment сам подов не создаёт. Под ним живёт объект, который в статьях обычно пропускают, а в kubectl get all он на виду: ReplicaSet. Его задача одна, держать заданное число подов по шаблону; всё, что умеет Deployment сверх этого, это управление несколькими ReplicaSet.

При каждом изменении шаблона пода (образ, переменные, пробы) Deployment создаёт новый ReplicaSet с хешем шаблона в имени, order-service-7d9f8c6b, и ведёт rolling update как перекладывание реплик: новый набор растёт на maxSurge, старый уменьшается на maxUnavailable, пока в старом не останется ноль. Старый ReplicaSet при этом не удаляется, а остаётся с нулём реплик, и это и есть ревизия. kubectl rollout history deployment/order-service показывает их список, kubectl get rs показывает те же объекты с числами реплик.

kubectl rollout undo не хранит «старый YAML»: он берёт шаблон из предыдущего ReplicaSet и делает его текущим, то есть это обычный rolling update в обратную сторону, с теми же пробами и тем же временем. --to-revision=3 откатывает к конкретной. Сколько ревизий хранится, задаёт revisionHistoryLimit, десять по умолчанию; поставили один, и откатываться некуда. Изменение только числа реплик или аннотаций ревизию не создаёт: ревизия это шаблон пода.

Что из этого следует на практике. Откат через undo работает, только если старый образ ещё есть в реестре, о чём раздел про теги. В GitOps откат делают не undo, а возвратом коммита, иначе кластер разойдётся с репозиторием и Argo CD вернёт всё обратно. И при разборе «под пересоздался» полезно посмотреть, тот ли ReplicaSet его породил: под из старого набора после отката выглядит как новый, хотя это версия недельной давности.

Глубже: Job и CronJob: миграция как объектрасширенное

«Kubernetes Job перед выкатом» из раздела про миграции стоит разобрать как объект, потому что у него своё поведение, отличное от пода.

Job запускает под и следит, чтобы тот завершился успешно, кодом ноль. Упал с ошибкой, Job создаёт новый под, пока не исчерпает backoffLimit (шесть по умолчанию), с растущей паузой между попытками. activeDeadlineSeconds ограничивает общее время, ttlSecondsAfterFinished удаляет завершённый Job вместе с подом через заданное время, иначе они копятся. Для миграции это выглядит так:

apiVersion: batch/v1
kind: Job
metadata: { name: order-service-migrate-1-4-2 }
spec:
  backoffLimit: 1
  activeDeadlineSeconds: 600
  ttlSecondsAfterFinished: 3600
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: registry/order-service:1.4.2
          args: ["--spring.main.web-application-type=none", "--spring.liquibase.enabled=true"]

Тот же образ, что у приложения, запущенный без веб-сервера, применяет миграции и выходит; backoffLimit: 1 потому, что повторять упавшую миграцию автоматически не стоит, её смотрят глазами. Имя Job уникально на версию, потому что Job неизменяем: второй apply с тем же именем и другим образом отвергнется. Конвейер ждёт результата, kubectl wait --for=condition=complete job/order-service-migrate-1-4-2 --timeout=600s, и только после успеха выкатывает Deployment; журнал читают через kubectl logs job/.... В Helm то же оформляют хуком pre-upgrade, в Argo CD хуком синхронизации PreSync.

CronJob создаёт Job по расписанию: ночная пересборка витрины, очистка отметок идемпотентности. Ключевые поля: schedule в формате cron с часовым поясом timeZone, concurrencyPolicy: Forbid, чтобы затянувшийся прогон не запускался вторым параллельно, startingDeadlineSeconds на случай, когда контроллер пропустил время, и лимиты истории успешных и неудачных Job. @Scheduled внутри сервиса и CronJob решают одну задачу по-разному: первый живёт в каждой реплике и требует распределённой блокировки, второй запускается один раз в кластере, и для задач, которым не нужен контекст сервиса, CronJob проще.

Глубже: ConfigMap и Secret как объекты: файл против переменных, пределы и immutableрасширенное

envFrom из примеров это один из двух способов отдать конфигурацию поду, и у каждого своё поведение при изменении.

Переменные окружения читаются при создании контейнера и больше не меняются: правка ConfigMap работающий под не видит, и его перезапускают, о чём статья про обновление конфигурации. Зато переменные просты и работают с любым приложением. Файл в томе обновляется у живого пода: kubelet перечитывает ConfigMap раз в минуту или около того и подменяет файл, и приложение, которое умеет перечитывать конфигурацию, увидит изменение без перезапуска. Исключение, о котором узнают из инцидента: монтирование одного ключа через subPath не обновляется никогда, потому что kubelet подменяет каталог целиком, а subPath держит ссылку на старый файл.

Пределы. ConfigMap и Secret хранятся в etcd, и у каждого объекта потолок в один мегабайт. Большой справочник или сертификатная цепочка со всеми промежуточными в него не влезают, и это признак, что данные лежат не там: большое в объектное хранилище или в образ, в ConfigMap только настройки. Secret отличается от ConfigMap только пометкой и кодированием base64 на входе; шифрование в покое включается отдельно на уровне кластера, а доступ ограничивают RBAC, о чём статья про эксплуатацию.

immutable: true запрещает менять объект после создания и снимает с kubelet обязанность следить за его изменениями; для кластера с тысячами подов это заметная экономия, а для GitOps это естественно: новая версия конфигурации это новый объект с хешем в имени и новая ревизия Deployment, откат которой возвращает и конфигурацию. Так конфигурация становится частью выката, а не отдельной правкой, которую никто не заметил.

Коротко

  • Манифест — YAML-файл с декларацией: «хочу 3 копии этого образа». Kubernetes сам обеспечивает это состояние. Kustomize — overlays без шаблонов, для своих сервисов с несколькими средами. Helm — шаблонизатор с пакетированием, нужен для стороннего ПО.
  • Тег образа должен быть уникальным и неизменяемым — версия или SHA коммита. Тег :latest ломает откаты и воспроизводимость.
  • Rolling update обновляет поды порциями без остановки сервиса. Трафик попадает на новый под только после зелёной readiness probe.
  • kubectl rollout undo откатывает к предыдущей ревизии — штатная операция, не подвиг. Под Deployment живёт ReplicaSet на каждую версию шаблона; rollout undo это rolling update к предыдущему ReplicaSet, ревизий хранится revisionHistoryLimit, в GitOps откатывают коммитом.
  • Во время rolling update в кластере одновременно работают две версии, поэтому миграции БД должны быть совместимы со старым кодом.
  • GitOps: git хранит желаемое состояние кластера, оператор (Argo CD, Flux) применяет его автоматически. Деплой = PR с новым тегом. В подходе с репозиторием как источником правды сразу решают три вопроса: кто меняет тег после сборки, где живут секреты (Sealed Secrets, SOPS, External Secrets) и что аварийная ручная правка перестаёт работать.
  • Job запускает под до успешного выхода с backoffLimit и ttlSecondsAfterFinished, имя уникально на версию, конвейер ждёт kubectl wait; CronJob с concurrencyPolicy: Forbid заменяет @Scheduled для задач без контекста сервиса. Переменные окружения не обновляются, файл в томе обновляется, кроме subPath; предел объекта один мегабайт; immutable с хешем в имени делает конфигурацию частью ревизии.
  • До apply на промышленном кластере — kubectl diff (что именно изменится, включая чужие ручные правки) и --dry-run=server (схема, права, политики), а не только проверка YAML.
  • Кроме плавного обновления есть canary (доля трафика числом подов или прокси с весами плюс автоматическая проверка метрик) и blue-green (двойная ёмкость, мгновенный откат переключением селектора).
  • PodDisruptionBudget защищает от планового выселения при обслуживании узла, но не от аварий; minAvailable согласуют с числом реплик, иначе обслуживание блокируется.

Что почитать дальше

  • Spring Boot в Kubernetes — readiness probe через Actuator, graceful shutdown, ресурсы JVM, на примере Spring Boot; для Go, Node и Python то же в статьях про остановку в Kubernetes (Go, Node, Python).
  • Эксплуатация и отладка — что делать, когда rollout застрял.
  • Основы Kubernetes — поды, узлы, контроллеры и желаемое состояние.
  • Helm - чарт, values и релизы: как разложить один сервис по средам, не копируя манифесты.
  • Argo CD - объект Application, дрейф и синхронизация: что происходит в кластере, когда состояние описано в репозитории.