Команда VK Cloud перевела статью для тех, кто разбирает инциденты в Kubernetes с помощью kubectl debug. Автор рассказывает про незаметный пробел в данных: после завершения debug-сессии API Kubernetes не сохраняет контекст ее завершения — код возврата, длительность сессии и целевой контейнер исчезают при первом же изменении состояния пода. В статье как воспроизвести это тремя командами, почему так устроено на уровне спецификации API, чем это грозит при разборе инцидентов и комплаенсе и что можно сделать уже сегодня. 

Сессия, не оставившая следа

Сессия kubectl debug может содержать единственное прямое наблюдение сбойного состояния системы. Но как только сессия завершилась, Kubernetes не сохраняет в своём API контекст её завершения. Это не баг kubectl, так намеренно устроен API Kubernetes для ephemeral-контейнеров (временных контейнеров, которые запускают для отладки, не затрагивая жизненный цикл пода).

Стоит состоянию пода измениться и API Kubernetes больше не отдаёт контекст завершения той debug-сессии. Код возврата с вашей находкой, длительность сессии, контейнер, на который вы нацелились, после следующего обновления пода API Kubernetes ничего из этого не хранит.

Вот как это выглядит и что значит для процесса разбора инцидентов.

Воспроизведите это тремя командами

Чтобы это увидеть, специальный кластер не нужен. Подойдёт любой кластер Kubernetes 1.25+. Три команды подтверждают пробел.

Шаг 1. Разверните стабильный целевой под:

kubectl run debug-target --image=nginx:alpine -n default
kubectl wait --for=condition=Ready pod/debug-target -n default

Шаг 2. Подключите debug-сессию, поработайте 10 секунд, выйдите с отличительным кодом:

kubectl debug debug-target -n default \
  --image=busybox:1.36 \
  --target=nginx \
  -it -- sh -c "echo 'finding: connection pool exhausted'; sleep 10; exit 42"

Примечание: --target — это возможность CLI kubectl, которая направляет debug-контейнер в пространство имён процессов целевого контейнера. Имя целевого контейнера API не хранит как поле в объекте пода.

Шаг 3. Сразу после выхода проверьте статус ephemeral-контейнера:

kubectl get pod debug-target -n default \
  -o jsonpath='{.status.ephemeralContainerStatuses[*]}' | jq .

Что вы получаете:

{
  "containerID": "containerd://...",
  "image": "busybox:1.36",
  "name": "debugger-xxxxx",
  "ready": false,
  "state": {
    "terminated": {
      "exitCode": 42,
      "finishedAt": "2026-04-17T16:43:56Z"
    }
  }
}

Код возврата здесь виден, но только до тех пор, пока сохраняется запись State.Terminated. Как только любое другое событие меняет под, перезапускается другой контейнер, подключается вторая debug-сессия, под переразмещается и этот контекст завершения заменяется. Кода возврата предыдущей сессии уже не увидеть. Проверьте логи после завершения сессии:

kubectl logs debug-target -c debugger-xxxxx -n default
Error from server (NotFound): container "debugger-xxxxx" not found

Нет lastState, на который можно опереться. Как только текущее состояние меняется, контекст завершения через API Kubernetes уже недоступен.

Почему так происходит: решение в спецификации API

Это не баг и не пропущенная возможность, это явное проектное решение API. Тип EphemeralContainerStatus в API Kubernetes (v1.32) не включает поле lastState:

EphemeralContainerStatus:
  containerID    string
  image          string
  name           string
  ready          boolean
  state          ContainerState
  # lastState:   отсутствует намеренно
  # restartCount: отсутствует намеренно

Сравните с ContainerStatus для обычного контейнера:

ContainerStatus:
  containerID    string
  image          string
  lastState      ContainerState    ← хранит предыдущее завершение
  name           string
  ready          boolean
  restartCount   integer           ← считает перезапуски
  state          ContainerState

Ключевое отличие: ContainerStatus.lastState хранит запись о предыдущем завершении — код возврата, время старта, время финиша, причину — и переживает перезапуски. У EphemeralContainerStatus эквивалентного поля нет.

Ephemeral-контейнеры ввели для отладки, не затрагивая гарантии жизненного цикла пода. Их устройство намеренно избегает семантики перезапуска, и это влияет на то, как их состояние отдаётся в API Kubernetes. Спецификация Kubernetes определяет ephemeral-контейнеры как «не перезапускаемые при сбое». Поэтому механизмы, которые отслеживают перезапуски и сохраняют последнее состояние для обычных контейнеров, здесь исключены намеренно. См. документацию Kubernetes по ephemeral-контейнерам и справочник по kubectl debug для upstream-спецификации.

Сравнение ContainerStatus и EphemeralContainerStatus бок о бок: lastState присутствует/отсутствует
Изображение: сравнение ContainerStatus и EphemeralContainerStatus бок о бок, показывающее наличие/отсутствие lastState

Что теряется на практике

Сигнал для расследования

Виден после изменения состояния пода?

Код возврата

Только пока на месте текущий State.Terminated

Длительность сессии

Недоступна

Целевой контейнер (--target)

Не записан как поле API

Логи debug-контейнера

Недоступны после завершения

Эти сигналы живут лишь временно и не сохраняются при переходах состояния пода.

Соглашение о кодах возврата особенно распространено в практике SRE: exit 42 для «connection pool exhausted», exit 1 для «config file missing». Стоит состоянию пода измениться и этих сигналов через API Kubernetes уже не увидеть.

Частичный обходной путь: перенаправьте находки в файл на общем volume перед выходом или держите kubectl logs -f в параллельном терминале, чтобы захватывать stdout в реальном времени. Учтите, что kubectl logs -f сработает, только если сессия активно пишет в stdout, а вы захватываете вывод до выхода — во время живого инцидента это возможно не всегда.

Влияние на реагирование на инциденты

Рассмотрим реалистичную последовательность передачи смены:

  1. Дежурный инженер подключает kubectl debug к проблемному поду

  2. Тратит 12 минут на исследование внутри контейнера

  3. Выявляет проблему, выходит с кодом 42

  4. Записывает заметки по инциденту: «нашёл исчерпание пула соединений, exit 42»

  5. Передаёт смену следующему инженеру

Следующий инженер пытается проверить:

# Сколько длилась debug-сессия?
kubectl get pod my-pod -o jsonpath='{.status.ephemeralContainerStatuses[*]}' | jq .
# Длительность: недоступна


# На какой контейнер нацеливались?
# --target — флаг CLI kubectl, в API не хранится


# Можно посмотреть, что они запускали?
kubectl logs my-pod -c debugger-xxxxx
# Ошибка: контейнер не найден

Передача смены полностью зависит от заметок первого инженера. А если заметки неполны, во время живого инцидента так и бывает, диагностический контекст через API Kubernetes можно уже не увидеть. Второй инженер начинает с нуля.

В некоторых регулируемых средах у этого есть последствия для комплаенса. Фреймворки, которым нужна отслеживаемость операционных действий, например, требование PCI-DSS 10.3 к журналированию аудита или требования SOC 2 к активности доступа, одним лишь API Kubernetes не выполнить для сессий ephemeral-контейнеров.

API Kubernetes не записывает имя контейнера --target и длительность сессии как поля API. Поэтому ответить на вопрос «кто на какой контейнер смотрел и как долго», опираясь на одни лишь стандартные журналы аудита Kubernetes для сессий ephemeral-контейнеров, сейчас невозможно.

Что можно сделать сегодня

Логирование на уровне приложения: договоритесь в команде записывать находки в общий volume или внешнюю систему перед выходом. Просто, но требует дисциплины под давлением инцидента.

Захват в реальном времени через watch API: событийный захват в момент перехода в Terminated сохранит блок State.Terminated до того, как его заменит любая следующая модификация пода. Такой подход сохраняет состояние завершения ephemeral-контейнера в момент его возникновения, пока его не заменили последующие обновления пода. Для этого нужен watch на модификации пода, и расчёт на то, что никакой другой контроллер не обновит под раньше захвата.

Внешние системы наблюдаемости: направляйте находки debug-сессии во внешний журнал аудита или SIEM на уровне приложения перед выходом.

Пример реализации, который демонстрирует этот подход, доступен на github.com/opscart/k8s-causal-memory, с воспроизводимым сценарием в scenarios/05-ephemeral-exit/. Захваченная запись для 10-секундной сессии, вышедшей с кодом 42:

container_name:     debugger-1776446626
target_container:   nginx
exit_code:          42
exit_class:         ERROR
duration_seconds:   10.0
node_name:          opscart-m02

Всё, что API Kubernetes уже не отдаёт после изменения состояния пода, сохранено в момент выхода.

Стоит ли это KEP?

Здесь есть что улучшить в Kubernetes и формализовать это можно через KEP (Kubernetes Enhancement Proposal, формальное предложение по развитию проекта). Один из вариантов ввести для ephemeral-контейнеров минимальную историю завершения, похожую на lastState в ContainerStatus. Поле lastState можно добавить в EphemeralContainerStatus с минимальными ломающими изменениями: ephemeral-контейнеры никогда не перезапускаются. Оно хранило бы только самую последнюю запись о завершении.

Исключить lastState имело смысл, когда ephemeral-контейнеры вводили в alpha (Kubernetes v1.16). Но kubectl debug становится стандартным инструментом разбора инцидентов во всей экосистеме, и отсутствие записи о завершении даёт реальные операционные последствия.

По мере того как ephemeral-контейнеры становятся стандартным механизмом отладки, отсутствие даже минимальной истории завершения поднимает более широкие вопросы о наблюдаемости и аудируемости в самом Kubernetes.

Естественные владельцы такого предложения — SIG Node, которая отвечает за kubelet и жизненный цикл контейнеров, или SIG Instrumentation, которая отвечает за примитивы наблюдаемости.

Воспроизведите сценарий: github.com/opscart/k8s-causal-memory/scenarios/05-ephemeral-exit

Комментарии (0)