Основы работы с kubectl: Практический чекпоинт

Курс посвящен профессиональному владению основным инструментом DevOps-инженера — kubectl. Вы перейдете от простого запуска команд к глубокой диагностике, чтению логов и анализу внутреннего состояния объектов Kubernetes.

Философия kubectl: контексты, конфигурация и анатомия запроса

Философия kubectl: контексты, конфигурация и анатомия запроса

Вы набираете в терминале kubectl get pods, нажимаете Enter, и перед вами появляется список запущенных контейнеров. Кажется, что вы напрямую управляете кластером. Но вот первый инсайт уровня Senior: утилита kubectl абсолютно ничего не знает о Kubernetes.

В ней нет сложной логики оркестрации, она не управляет контейнерами и не знает, что такое Pod. По своей сути kubectl — это просто очень умная обертка над утилитой curl. Это REST-клиент, чья единственная задача — перевести вашу человекочитаемую команду в HTTP-запрос, отправить его в kube-apiserver (о котором мы говорили в первом курсе) и красиво отформатировать полученный JSON-ответ.

Чтобы стать профессионалом в отладке, нужно перестать воспринимать kubectl как «черный ящик» и научиться видеть, что именно он отправляет по сети.

Анатомия ~/.kube/config: Как клиент находит дорогу

Поскольку kubectl — это лишь HTTP-клиент, для работы ему нужны две базовые вещи: куда отправлять запрос (адрес сервера) и как представиться (сертификаты или токены).

Все эти данные хранятся в конфигурационном файле, который по умолчанию ищется по пути ~/.kube/config. Этот файл часто называют просто kubeconfig.

Файл состоит из трех главных сущностей, которые собираются в единую картину:

  1. Clusters (Кластеры) — это физические эндпоинты kube-apiserver. Здесь хранится URL сервера и корневой сертификат (CA), чтобы клиент понимал, что общается с настоящим сервером, а не с подделкой.
  2. Users (Пользователи) — это ваши учетные данные. Kubernetes не имеет собственной базы пользователей, поэтому здесь хранятся клиентские TLS-сертификаты, токены (Bearer Token) или настройки интеграции с внешними провайдерами (OIDC, AWS IAM).
  3. Contexts (Контексты) — это клей. Контекст жестко связывает один Кластер и одного Пользователя. Опционально он задает Namespace по умолчанию.

Контекст — это ответ на вопрос: «К какому кластеру я сейчас обращаюсь и под чьим именем я это делаю?»

Как это выглядит в YAML

Давайте заглянем внутрь типичного ~/.kube/config:

apiVersion: v1
kind: Config
current-context: prod-admin-context # Указывает, какой контекст активен прямо сейчас

clusters:
- name: production-cluster
  cluster:
    server: https://10.100.0.1:6443
    certificate-authority-data: LS0tLS1C... # Урезанный base64 сертификата CA

users:
- name: admin-user
  user:
    client-certificate-data: LS0tLS1C... # Сертификат пользователя
    client-key-data: LS0tLS1C...         # Приватный ключ пользователя

contexts:
- name: prod-admin-context
  context:
    cluster: production-cluster
    user: admin-user
    namespace: kube-system # Все команды без ключа -n полетят сюда

Обратите внимание на поле current-context. В крупных компаниях у вас в одном файле будут описаны десятки кластеров (dev, stage, prod) и разные пользователи. Переключение между ними — это просто смена активного контекста.

Практика: Управление контекстами

Ошибиться контекстом и удалить базу данных в Production вместо Dev-среды — классическая ошибка новичка. Чтобы этого избежать, используйте базовые команды интроспекции:

  • kubectl config view — выведет содержимое вашего kubeconfig (скрыв сами секреты и сертификаты).
  • kubectl config get-contexts — покажет таблицу всех доступных контекстов. Звездочкой * будет отмечен текущий.
  • kubectl config use-context <имя> — переключит вас на другой контекст.
  • kubectl config set-context --current --namespace=monitoring — закрепит за текущим контекстом дефолтный неймспейс, чтобы не писать -n monitoring в каждой команде.

Рентген запроса: уровни детализации (Verbosity)

Если kubectl — это просто генератор HTTP-запросов, мы можем заставить его показать нам эти запросы. Для этого используется флаг -v (verbosity) с указанием уровня детализации от 0 до 9.

Это мощнейший инструмент отладки. Если команда зависает или выдает неочевидную ошибку, повышение уровня детализации покажет, на каком этапе сети происходит сбой.

Уровень Что показывает Когда использовать
-v=6 URL запроса и HTTP-статус ответа Чтобы понять, к какому именно API-эндпоинту обращается команда (полезно при изучении структуры API).
-v=7 + HTTP-заголовки (Headers) запроса Проверка, правильный ли токен авторизации отправляется на сервер.
-v=8 + Тело запроса (Body) и ответа в JSON Глубокий дебаг. Вы увидите сырой JSON, который возвращает apiserver до того, как kubectl превратит его в таблицу.
-v=9 + Эквивалентная команда curl Если вам нужно скопировать запрос и выполнить его с сервера, где нет kubectl.

Попробуйте выполнить в своем терминале: kubectl get pods -v=8

Вы увидите, что вместо магии происходит банальный GET https://<ip>:6443/api/v1/namespaces/default/pods. Компонент kube-apiserver (проверяющий AuthN/AuthZ, как мы учили во втором курсе) возвращает массив JSON-объектов, а kubectl просто рисует из них колонки NAME, READY, STATUS.

Типичные ошибки и их диагностика

Понимание того, как kubectl читает конфиг и отправляет запросы, позволяет мгновенно диагностировать ошибки подключения.

1. Ошибка: The connection to the server localhost:8080 was refused Это самая частая ошибка. Она означает одно из двух:

  • У вас нет файла ~/.kube/config (или переменная окружения KUBECONFIG указывает в пустоту). Не найдя конфига, kubectl по умолчанию пытается постучаться на http://localhost:8080, где, естественно, никого нет.
  • Конфиг есть, в нем прописан localhost:8080, но локальный кластер (например, minikube или kind) не запущен.

2. Ошибка: error: You must be logged in to the server (Unauthorized) Сетевое соединение установлено, kube-apiserver ответил, но на этапе Authentication (AuthN) ваш запрос был отклонен (HTTP 401). Решение: Проверьте блок users в вашем контексте. Срок действия сертификата истек, либо токен отозван.

3. Ошибка: Unable to connect to the server: x509: certificate signed by unknown authority Вы пытаетесь подключиться к серверу по HTTPS, но kubectl не доверяет сертификату сервера. Решение: В блоке clusters неверно указан certificate-authority-data. Клиент не может криптографически подтвердить, что сервер подлинный.

В следующих главах мы перейдем от устройства самой утилиты к эффективной работе с данными: научимся фильтровать огромные простыни JSON-ответов с помощью JSONPath и кастомных колонок, не прибегая к внешним утилитам вроде grep или awk.

Эффективная навигация: фильтрация, кастомные колонки и JSONPath

Эффективная навигация: фильтрация, кастомные колонки и JSONPath

Выполните kubectl get pods -A в production-кластере средних размеров, и терминал захлебнется, выведя тысячи строк. Среди них есть один Pod, который за последние сутки перезапустился 50 раз из-за утечки памяти. Найти его глазами невозможно. Использовать grep — значит потерять заголовки колонок и контекст.

Поскольку kubectl — это лишь REST-клиент, любой запрос get возвращает от kube-apiserver массивный JSON-объект. Знакомая нам таблица с колонками NAME, READY, STATUS — это лишь жестко зашитый в код kubectl шаблон, который отбрасывает 95% полезной информации из этого JSON.

Чтобы находить иголку в стоге сена, нужно научиться управлять тем, какие данные сервер отправляет клиенту, и тем, как клиент их форматирует.

Анатомия фильтрации: Server-side против Client-side

Когда вы ищете конкретные объекты, у вас есть два принципиально разных пути: отфильтровать данные на стороне сервера (внутри Control Plane) или запросить всё и отфильтровать на стороне клиента (внутри вашего терминала).

Серверная фильтрация (Server-side)

При серверной фильтрации kubectl передает условия поиска прямо в HTTP-запросе к kube-apiserver. Сервер транслирует этот запрос в базу данных etcd, извлекает только нужные объекты и отправляет их по сети.

Размер сетевого ответа растет как O(K)O(K), где KK — количество совпавших объектов, а не общее количество объектов в кластере. Это единственный безопасный способ поиска в высоконагруженных системах.

Для этого используются два флага:

  1. Label selectors (-l или --selector) — поиск по бизнес-меткам.
  2. Field selectors (--field-selector) — поиск по системным полям объекта.

Пример поиска всех Pod'ов, которые не смогли запуститься:

kubectl get pods --field-selector status.phase=Failed

Ограничение: kube-apiserver поддерживает фильтрацию далеко не по всем полям. Вы можете фильтровать по metadata.name, metadata.namespace, spec.nodeName, status.phase, но попытавшись отфильтровать по количеству рестартов контейнера, вы получите ошибку. Серверная фильтрация работает только с плоскими, заранее проиндексированными полями.

Клиентская фильтрация и форматирование (Client-side)

Если нужного поля нет в списке поддерживаемых для --field-selector, придется запрашивать массив объектов целиком и разбирать его на стороне клиента.

Для этого kubectl предлагает мощные инструменты трансформации JSON-ответа: custom-columns и jsonpath.

Проектирование вывода: custom-columns

Флаг -o custom-columns позволяет собрать собственную таблицу, указав названия колонок и пути к нужным данным внутри JSON-объекта.

Синтаксис: -o custom-columns=<ИМЯ_КОЛОНКИ>:<ПУТЬ_В_JSON>,<ИМЯ_КОЛОНКИ2>:<ПУТЬ2>

Предположим, нам нужно быстро собрать аудит версий образов, запущенных в кластере, и узнать, на каких узлах они работают. В стандартном get pods версии образа нет.

kubectl get pods -o custom-columns="POD:.metadata.name,NODE:.spec.nodeName,IMAGE:.spec.containers[0].image"

Результат будет выглядеть как аккуратная таблица:

POD                     NODE         IMAGE
nginx-7cf567cb7-8x2q9   worker-01    nginx:1.24.0
redis-master-0          worker-02    redis:7.0-alpine

Обратите внимание на путь .spec.containers[0].image. Поскольку Pod может содержать несколько контейнеров (например, с pause-контейнером и sidecar-паттерном), поле containers является массивом. Мы жестко обращаемся к первому элементу [0].

Высший пилотаж: JSONPath

Когда нужно не просто вывести колонки, а применить сложную логику (условия, циклы, фильтрацию по вложенным массивам), используется -o jsonpath. Это язык запросов к JSON-структурам, встроенный прямо в kubectl.

В JSONPath корень документа обозначается символом $. Когда мы запрашиваем список Pod'ов, API возвращает объект типа PodList, внутри которого есть массив items. Поэтому большинство запросов начинаются с обхода этого массива: .items[*].

Извлечение данных с условием

Вернемся к задаче из начала статьи: найти Pod'ы с большим количеством рестартов. Поле restartCount спрятано глубоко: оно находится в статусе каждого отдельного контейнера внутри Pod'а.

Конструкция ?() в JSONPath выполняет роль оператора IF, а символ @ ссылается на текущий элемент итерации.

kubectl get pods -o jsonpath='{range .items[?(@.status.containerStatuses[0].restartCount>10)]}{.metadata.name}{"\n"}{end}'

Разберем этот запрос по частям:

  1. {range ...} — открывает цикл по массиву.
  2. .items[?(...)] — берет массив Pod'ов и отфильтровывает только те, которые соответствуют условию.
  3. @.status.containerStatuses[0].restartCount > 10 — само условие: количество рестартов первого контейнера больше 10.
  4. {.metadata.name} — для каждого совпавшего Pod'а выводит его имя.
  5. {"\n"} — добавляет перенос строки, иначе все имена слепятся в одно слово.
  6. {end} — закрывает цикл.

Сортировка на лету

Часто нужно не просто отфильтровать данные, но и выстроить их по определенному критерию. Флаг --sort-by принимает путь в формате JSONPath и сортирует итоговую таблицу.

Например, чтобы найти самые старые Pod'ы в кластере:

kubectl get pods --sort-by=.metadata.creationTimestamp

Или объединим кастомные колонки и сортировку, чтобы вывести список Pod'ов, отсортированных по количеству рестартов (от меньшего к большему):

kubectl get pods --sort-by=.status.containerStatuses[0].restartCount -o custom-columns="NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount"

Освоив серверные селекторы для грубой фильтрации трафика и JSONPath для ювелирной выборки на клиенте, вы перестанете зависеть от стандартных таблиц kubectl. Вы сможете мгновенно находить проблемные объекты, выгружать данные для bash-скриптов и проводить аудит кластера одной строкой.

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

Диагностика жизненного цикла: глубокий анализ Events и Describe

Диагностика жизненного цикла: глубокий анализ Events и Describe

В прошлой главе мы научились виртуозно фильтровать объекты с помощью JSONPath. Допустим, вы выполнили запрос и нашли Pod, который уже час висит в статусе CrashLoopBackOff или Pending. Что дальше? Вы не можете просто «перезагрузить» его, надеясь на лучшее — в декларативной системе нужно устранять первопричину, а не симптом. Для этого необходимо собрать анамнез.

В этой статье мы разберем главный диагностический инструмент Kubernetes — команду describe, и заглянем под капот механизма событий (Events), чтобы понимать, как кластер документирует свои попытки достичь желаемого состояния.

Анатомия команды describe: иллюзия единого отчета

Начинающие инженеры часто воспринимают вывод kubectl describe pod <name> как некий цельный лог, который хранится где-то внутри узла или контейнера. Это опасное заблуждение.

На самом деле, describe — это исключительно клиентская абстракция. В API Kubernetes нет эндпоинта /describe. Когда вы вызываете эту команду, kubectl выступает в роли следователя, который опрашивает разных свидетелей и собирает для вас красивое досье.

Под капотом kubectl делает как минимум два независимых HTTP-запроса к kube-apiserver:

  1. Запрос состояния самого объекта: GET /api/v1/namespaces/.../pods/<name>. Отсюда берется конфигурация (лимиты, тома, образы) и текущие статусы контейнеров.
  2. Запрос связанных событий: GET /api/v1/namespaces/.../events?fieldSelector=involvedObject.name=<name>. Отсюда берется хронология того, что происходило с объектом.

Понимание этой механики спасает при отладке: если kube-apiserver перегружен, команда describe может «зависнуть» на середине вывода, потому что первый запрос прошел, а второй (за событиями) отвалился по таймауту.

Чтение матрицы: ключевые блоки отчета

Вывод describe огромен. В production-ситуациях у вас нет времени читать его целиком. Смотреть нужно строго в три ключевых блока.

1. Conditions (Условия)

Фаза Pod'а (например, Running или Pending) — это слишком грубая метрика. Реальное состояние описывается массивом Conditions. Это набор логических вентилей (True/False), через которые проходит Pod в своем жизненном цикле.

Condition Значение, если True Кто устанавливает
PodScheduled Планировщик успешно нашел узел для Pod'а. kube-scheduler
Initialized Все Init-контейнеры успешно завершили работу. kubelet
ContainersReady Все основные контейнеры прошли проверки готовности (Readiness probes). kubelet
Ready Pod полностью готов принимать сетевой трафик. kubelet

Если Pod находится в фазе Pending, посмотрите на Conditions. Если PodScheduled равен False, проблема в нехватке ресурсов или правилах affinity (работает kube-scheduler). Если PodScheduled равен True, но Initialized равен False — проблема на самом узле при скачивании образа или запуске (работает kubelet).

2. Containers State (Состояние контейнеров)

Внутри блока Containers для каждого контейнера указано его текущее состояние (State) и предыдущее (Last State). Это критически важно для диагностики рестартов.

Контейнер всегда находится в одном из трех состояний:

  • Waiting: Контейнер еще не запущен. Обязательно ищите поле Reason. Частые причины: ContainerCreating (нормально, идет запуск), ImagePullBackOff (ошибка скачивания образа), CrashLoopBackOff (контейнер падает сразу после старта).
  • Running: Процесс внутри контейнера (PID 1) активен.
  • Terminated: Процесс завершился. Здесь главное — Exit Code и Reason.

Вспомните прошлый курс по ресурсам узла. Если вы видите Exit Code: 137 и Reason: OOMKilled — ядро Linux убило процесс за превышение Memory Limit. Если Exit Code: 1 или Exit Code: 255 — это ошибка внутри самого вашего приложения (неверный конфиг, падение базы данных).

3. Events (События)

Это хронологический журнал в самом низу вывода describe. Он показывает, какие компоненты Control Plane и Worker Nodes взаимодействовали с объектом.

Объект Event: мимолетный свидетель

События в Kubernetes — это не просто строчки текста в файле логов. Event — это полноценный REST-объект внутри кластера, точно такой же, как Pod, Service или Node.

Когда kubelet скачивает образ, или kube-scheduler назначает узел, они формируют JSON-объект типа Event и отправляют его POST-запросом в kube-apiserver, который сохраняет его в etcd.

У этого архитектурного решения есть серьезное следствие. Если бы кластер хранил все события вечно, база данных etcd (которая крайне чувствительна к объему данных из-за алгоритма Raft) переполнилась бы за пару дней. Поэтому в Kubernetes встроен механизм самозащиты:

  1. Жесткий TTL (Time To Live): По умолчанию объекты Event живут всего 1 час. Спустя час они безвозвратно удаляются из etcd. Если ваш Pod упал ночью, а вы пришли разбираться утром — команда describe покажет пустой блок Events.
  2. Дедупликация: Если kubelet не может скачать образ, он будет пытаться сделать это каждые несколько секунд. Чтобы не спамить API тысячами одинаковых событий, Kubernetes обновляет существующий объект Event: он увеличивает счетчик Count и обновляет поле LastTimestamp, не создавая новых записей.

Глобальный мониторинг через get events

Команда describe отлично подходит для точечной диагностики конкретного Pod'а. Но что если проблема масштабнее? Например, узел потерял связь с сетью, и десятки Pod'ов начали переезжать.

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

kubectl get events --sort-by='.lastTimestamp'

Эта команда выведет хронологическую ленту всего, что происходит в текущем namespace прямо сейчас.

Чтобы отфильтровать только проблемы, можно использовать Client-side фильтрацию (так как поле type поддерживается для --field-selector):

kubectl get events --field-selector type=Warning

Типы событий бывают только двух видов: Normal (информационные, вроде успешного старта) и Warning (ошибки, нехватки ресурсов, вытеснения). Поток Warning событий — это пульс здоровья вашего кластера.

Мы разобрались, как Kubernetes документирует изменения состояния объектов на уровне инфраструктуры. Но если describe показывает, что контейнер падает с Exit Code: 1, инфраструктура свою работу выполнила — проблема кроется внутри самого приложения. В следующей главе мы научимся проникать внутрь контейнеров с помощью продвинутых техник работы с логами и командой exec.

Интроспекция процессов: продвинутая работа с logs, exec и cp

Интроспекция процессов: продвинутая работа с logs, exec и cp

В прошлой главе мы научились понимать, что кластер думает о нашем Pod'е. Мы проанализировали Events и Conditions, выяснили разницу между кодами возврата. Но что делать, если describe говорит, что Pod находится в идеальном состоянии Running, а пользователи получают 500-е ошибки? Или приложение упало с Exit Code 1, но Events не содержат деталей самой ошибки?

Инфраструктурный слой закончился. Нам нужно пересечь границу изоляции и заглянуть внутрь самого процесса. Для этого у нас есть три основных инструмента: чтение потоков вывода (logs), запуск новых процессов (exec) и работа с файловой системой (cp).

kubectl logs: куда на самом деле пишет приложение

Новички часто думают, что Kubernetes обладает неким магическим механизмом перехвата логов из любых файлов внутри контейнера. Это не так. Kubernetes опирается на стандартный контракт Unix: приложение должно писать логи строго в стандартные потоки вывода — stdout (стандартный вывод) и stderr (стандартная ошибка).

Если ваше приложение пишет логи в файл /var/log/app.log внутри контейнера, команда kubectl logs ничего не покажет.

Как это работает под капотом:

  1. Процесс внутри контейнера пишет строку в stdout.
  2. Container Runtime (например, containerd) перехватывает этот поток.
  3. CRI сохраняет эти данные в физический файл на Worker Node, обычно в директорию /var/log/pods/.
  4. Когда вы выполняете kubectl logs, запрос идет в kube-apiserver, тот обращается к kubelet на нужном узле, а kubelet просто читает этот файл с диска и отдает его содержимое по сети.

Продвинутые флаги для логов

Читать логи целиком — плохая идея, если приложение работает неделями. Файл может весить гигабайты.

  • Ограничение объема: kubectl logs <pod-name> --tail=50 покажет только последние 50 строк.
  • Слежение в реальном времени: kubectl logs <pod-name> -f (follow) привяжет ваш терминал к потоку логов.
  • Логи нескольких Pod'ов: Если у вас Deployment из 5 реплик, вам не нужно читать логи каждого отдельно. Используйте селектор: kubectl logs -l app=backend --tail=10.
  • Мультиконтейнерные Pod'ы: Если в Pod'е есть Sidecar-контейнер, Kubernetes попросит указать имя конкретного контейнера через флаг -c. Если нужны логи всех сразу: kubectl logs <pod-name> --all-containers.

Чтение логов из прошлого (--previous)

Представьте ситуацию: Pod упал с ошибкой (например, OOMKilled или паника в коде), kubelet его перезапустил. Вы делаете kubectl logs и видите... чистый и красивый лог успешного старта нового процесса. Как узнать, почему упал предыдущий?

Здесь спасает флаг -p или --previous:

kubectl logs <pod-name> --previous

Этот флаг заставляет kubelet прочитать файл логов предыдущего, уже завершенного контейнера (CRI сохраняет файл логов от прошлой попытки запуска). Это главный инструмент разбора циклических перезагрузок (CrashLoopBackOff).

kubectl exec: это не SSH

Если логов недостаточно, нам нужно выполнить команды внутри окружения приложения.

kubectl exec часто воспринимают как аналог SSH. Это опасное заблуждение. SSH — это демон (sshd), который постоянно работает внутри ОС, слушает порт, проводит аутентификацию и создает сессию. В контейнере нет (и не должно быть) sshd.

Когда вы выполняете kubectl exec, происходит следующее:

  1. kubectl отправляет HTTP POST запрос в kube-apiserver.
  2. apiserver находит, на каком узле работает Pod, и открывает постоянное соединение с kubelet этого узла.
  3. kubelet обращается к Container Runtime (CRI) через gRPC.
  4. CRI просит ядро Linux запустить новый процесс, но поместить его в уже существующие Linux Namespaces (Network, Mount, PID) целевого контейнера.

Вы не подключаетесь к контейнеру, вы просите ядро запустить новую программу рядом с вашем приложением, в той же изолированной комнате.

Синтаксис и подводные камни

Базовая команда для получения интерактивного терминала выглядит так:

kubectl exec -it <pod-name> -- /bin/sh

Разберем магические символы:

  • -i (stdin): перенаправляет ваш ввод с клавиатуры в процесс внутри контейнера.
  • -t (tty): выделяет псевдотерминал, чтобы корректно работали интерактивные программы (например, чтобы вы видели приглашение командной строки и работали стрелочки).
  • --: это разделитель. Он говорит kubectl: «мои флаги закончились, всё, что идет дальше — это команда для запуска внутри контейнера». Без него kubectl попытается интерпретировать флаги вашей внутренней команды как свои собственные.

Главное ограничение exec Чтобы kubectl exec ... -- /bin/sh сработал, бинарный файл /bin/sh обязан физически существовать внутри файловой системы контейнера (в его образе).

kubectl cp: транспорт файлов без магии

Иногда нам нужно скачать из контейнера дамп памяти, сгенерированный отчет или, наоборот, закинуть туда временный конфигурационный файл для отладки.

Синтаксис похож на утилиту scp:

kubectl cp <namespace>/<pod-name>:/path/to/remote/file ./local-file

Как это работает? В Kubernetes нет специального API для передачи файлов. Команда kubectl cp — это элегантный хак. Под капотом она использует тот же самый механизм exec!

Когда вы делаете cp, kubectl незаметно для вас выполняет команду tar внутри контейнера, упаковывает нужный файл, передает поток байтов через стандартный вывод exec по сети и распаковывает tar на вашей локальной машине.

Практический сценарий: собираем всё вместе

Представьте реальный кейс. У вас есть Pod с legacy-приложением на Java. Оно периодически падает.

Шаг 1: Ищем причину. Мы видим, что счетчик рестартов растет. Смотрим логи прошлого падения:

kubectl logs backend-pod-xyz --previous --tail=100

В логах видим: java.lang.OutOfMemoryError: Java heap space. Dumping heap to /tmp/heapdump.hprof.

Шаг 2: Интроспекция. Новый процесс уже работает, но нам нужен дамп от старого. К счастью, директория /tmp в этом Pod'е примонтирована как emptyDir (том, переживающий рестарт контейнера внутри живого Pod'а). Заходим внутрь текущего контейнера, чтобы проверить файл:

kubectl exec -it backend-pod-xyz -- ls -lh /tmp

Видим файл heapdump.hprof размером 2 гигабайта.

Шаг 3: Извлечение. Скачиваем дамп на локальную машину для анализа в профайлере:

kubectl cp backend-pod-xyz:/tmp/heapdump.hprof ./heapdump.hprof

Мы успешно диагностировали проблему, не нарушая работу текущего процесса.

Но что делать, если современные стандарты безопасности требуют использовать Distroless образы? В таких образах нет ни /bin/sh, ни tar, ни ls. Команды вроде exec ... -- /bin/sh и cp просто вернут ошибку executable file not found in $PATH. О том, как отлаживать такие «глухие» контейнеры с помощью эфемерных контейнеров, мы поговорим в следующей главе.

Интерактивная отладка: использование kubectl debug и эфемерных контейнеров

Интерактивная отладка: использование kubectl debug и эфемерных контейнеров

Вы развернули в production микросервис. Следуя лучшим практикам безопасности, вы использовали минималистичный Distroless-образ: в нём есть только скомпилированный бинарный файл на Go и сертификаты. Никакого пакетного менеджера, утилит curl или ip, и даже нет оболочки sh.

Внезапно сервис перестает подключаться к базе данных. Вы по привычке пишете kubectl exec -it my-app -- sh и получаете закономерную ошибку: executable file not found in $PATH. Контейнер работает, но он абсолютно «глухой». Вы не можете зайти внутрь, не можете проверить сеть, не можете прочитать локальные сокеты.

В предыдущей главе мы научились извлекать данные снаружи через логи и kubectl cp. Но как провести активную сетевую диагностику внутри изолированного пространства, если в самом контейнере нет инструментов?

Анатомия Ephemeral Containers

В Kubernetes Pod по своей природе иммутабелен (неизменяем). Как только kube-apiserver принял манифест и kube-scheduler назначил Pod на узел, вы не можете добавить новый контейнер в массив spec.containers. Это фундаментальное правило: изменение количества контейнеров потребовало бы пересчёта Requests и Limits, что могло бы нарушить гарантии QoS и привести к вытеснению (Eviction) Pod'а с узла.

Однако для отладки был создан специальный механизм — Ephemeral Containers (эфемерные контейнеры).

Эфемерный контейнер — это временный контейнер, который добавляется в уже работающий Pod через специальный подресурс API /ephemeralcontainers.

Эфемерные контейнеры созданы исключительно для интроспекции. Они лишены гарантий выполнения и не могут иметь портов, проб (Liveness/Readiness) или резервирования ресурсов (Requests/Limits).

Как это работает на уровне узла (CRI)

Вспомним архитектуру из второй главы: логический хост Pod'а удерживается pause-контейнером, который создаёт Linux Namespaces (Network, IPC).

Когда вы запрашиваете создание эфемерного контейнера, kube-apiserver обновляет состояние Pod'а. kubelet на целевом узле замечает это изменение и отдаёт команду Container Runtime (например, containerd) запустить новый контейнер, но поместить его в ту же самую песочницу (PodSandbox).

Эфемерный контейнер принесёт с собой собственную файловую систему (свой образ, например, швейцарский нож сетевика nicolaka/netshoot), но при этом он будет использовать сетевой стек приложения. Запустив tcpdump внутри эфемерного контейнера, вы увидите трафик Distroless-приложения, потому что они делят один Network Namespace.

Практика: три режима kubectl debug

Утилита kubectl debug — это высокоуровневая обёртка, которая автоматизирует создание эфемерных контейнеров и копирование объектов. Она работает в трёх принципиально разных режимах.

Режим 1: Внедрение в работающий Pod (Attach)

Это решение нашей проблемы с Distroless-образом. Приложение работает, но нужно проверить сеть.

kubectl debug -it my-app-pod \
  --image=nicolaka/netshoot \
  --target=my-app-container

Разберём, что делает эта команда под капотом:

  1. Создаёт эфемерный контейнер на базе образа nicolaka/netshoot.
  2. Подключает ваш терминал к его stdin/stdout (флаг -it).
  3. Флаг --target делает магию с Process Namespace Sharing.

По умолчанию контейнеры в Pod'е делят сеть, но имеют изолированные пространства PID (Process ID). Это значит, что из эфемерного контейнера вы не увидите процессы основного приложения через команду ps. Флаг --target указывает kubelet настроить эфемерный контейнер так, чтобы он разделил PID Namespace с целевым контейнером.

В результате, находясь в эфемерном контейнере netshoot, вы сможете:

  • Выполнить ps aux и увидеть процесс вашего Go-приложения (он не будет иметь PID=1PID = 1, так как PID=1PID = 1 останется за pause-контейнером или инициализатором песочницы).
  • Прочитать файловую систему целевого контейнера через абстракцию ядра: ls /proc/<PID_приложения>/root/. Это позволяет просматривать конфигурационные файлы Distroless-контейнера, используя утилиты эфемерного контейнера.

Режим 2: Копирование Pod'а с изменением команды

Что делать, если Pod падает с ошибкой CrashLoopBackOff сразу при старте? Внедрить эфемерный контейнер не получится: к моменту, когда вы введёте команду, целевой контейнер уже будет мёртв, а песочница может перезапускаться.

В этом случае kubectl debug использует стратегию копирования:

kubectl debug my-crashing-pod -it \
  --copy-to=my-pod-debug \
  --container=my-app-container \
  -- sh

Здесь не используются эфемерные контейнеры. Вместо этого kubectl:

  1. Скачивает манифест my-crashing-pod.
  2. Создаёт новый независимый Pod с именем my-pod-debug.
  3. В манифесте нового Pod'а заменяет точку входа (command/args) проблемного контейнера на sh (или sleep 3600, если в образе нет sh).
  4. Удаляет привязки к меткам (Labels), чтобы новый Pod не попал под балансировку трафика Service'а.

Теперь у вас есть точная копия окружения (те же ConfigMap, Secret, ServiceAccount, переменные окружения), но приложение не запускается автоматически. Вы находитесь в оболочке и можете вручную запустить бинарный файл, чтобы увидеть причину падения (например, нехватку прав или ошибку синтаксиса в конфиге).

Режим 3: Отладка узла (Node Troubleshooting)

Иногда проблема кроется не в Pod'е, а в самом Worker Node. Например, нужно проверить конфигурацию kube-proxy, прочитать логи kubelet через journalctl или проверить правила iptables, а прямого SSH-доступа к узлу у вас нет (стандартная ситуация в управляемых облачных кластерах вроде EKS или GKE).

kubectl debug node/worker-node-1 -it --image=ubuntu

Эта команда создаёт обычный (не эфемерный) Pod, но с максимальными привилегиями:

  • hostNetwork: true — Pod использует сеть самого узла, а не CNI.
  • hostPID: true — Pod видит все процессы узла.
  • hostIPC: true — Pod имеет доступ к межпроцессному взаимодействию узла.

Главная особенность: корневая файловая система самого узла автоматически монтируется внутрь этого отладочного Pod'а в директорию /host.

Зайдя в такой контейнер, вы можете выполнить chroot /host, и ваш терминал фактически превратится в root-сессию на самом узле. Вы сможете использовать systemctl, читать /var/log/syslog и менять конфигурацию сети узла, не имея SSH-ключей, опираясь исключительно на ваши права RBAC в Kubernetes.

Резюме

Эфемерные контейнеры элегантно решают парадокс безопасности контейнеризации: мы хотим собирать максимально урезанные образы для production, чтобы уменьшить поверхность атаки, но при этом нам нужны мощные инструменты диагностики при авариях. kubectl debug позволяет приносить инструменты с собой ровно в тот момент, когда они нужны, не загрязняя ими рабочие образы.

В следующей главе мы объединим все изученные инструменты интроспекции — logs, describe, exec, cp и debug — в единый фреймворк и применим его на практике в большом чекпоинте по траблшутингу.

Практикум: Чекпоинт по дебагу Pod'ов в реальном времени

Практикум: Чекпоинт по дебагу Pod'ов в реальном времени

Пятница, 23:00. Срабатывает алерт PagerDuty: критический микросервис обработки платежей перестал отвечать. В дашбордах метрики обрываются, а клиенты получают 502 Bad Gateway. Вы открываете терминал. Что вы введёте первым делом?

Хаотичный перебор команд — главный враг инженера в стрессовой ситуации. Профессиональный траблшутинг в Kubernetes опирается на строгую методологию. В этом чекпоинте мы свяжем все изученные ранее инструменты интроспекции (describe, logs, exec, debug) в единый алгоритм и применим его к реальным production-сценариям.

Пирамида расследования SRE

Отладка Pod'а всегда идет снаружи внутрь. Мы начинаем с того, как систему видит Control Plane, и погружаемся на уровень ядра Linux только в том случае, если верхние слои не дали ответа.

  1. Слой API (Control Plane): kubectl get, kubectl describe, kubectl get events. Запрашиваем состояние объекта в etcd и решения планировщика.
  2. Слой Приложения (CRI Logs): kubectl logs. Читаем стандартный вывод процесса.
  3. Слой Окружения (Namespaces): kubectl exec, kubectl cp. Выполняем команды внутри существующих пространств имен контейнера.
  4. Слой Хирургии (CRI/Node): kubectl debug. Внедряемся в песочницу (PodSandbox) снаружи, когда штатные средства недоступны.

Рассмотрим применение этого фреймворка на трех классических инцидентах.

Сценарий 1: Призрак в системе (CrashLoopBackOff)

Симптомы: Микросервис payment-worker постоянно перезапускается.

Выполняем первый шаг фреймворка — опрашиваем API:

kubectl get pods -l app=payment-worker

Вывод показывает статус CrashLoopBackOff и 12 рестартов. Команда kubectl describe pod <имя> в блоке Containers State показывает: Last State: Terminated, Reason: Error, Exit Code: 1.

Приложение падает из-за внутренней ошибки. Переходим на второй слой (Логи):

kubectl logs <имя-пода>

Вывод абсолютно пуст.

Здесь новички часто впадают в ступор. Логи пусты, потому что текущий процесс контейнера мог только что запуститься и еще не успел ничего вывести до очередного падения, либо kubelet прямо сейчас применяет задержку перед следующим запуском.

В Kubernetes реализован алгоритм Exponential Backoff для защиты узла от исчерпания ресурсов при циклических падениях. Время ожидания перед каждым новым рестартом вычисляется по формуле Twait=102nT_{wait} = 10 \cdot 2^n секунд, где nn — количество падений (но не более 5 минут). На 12-м рестарте Pod просто «висит» в ожидании таймера, и его текущий лог пуст.

Чтобы увидеть причину, нам нужно заглянуть в прошлое — прочитать логи предыдущего, уже мертвого контейнера, которые Container Runtime всё ещё хранит на диске узла.

kubectl logs <имя-пода> --previous

Инсайт: Флаг --previous — ваш главный инструмент при CrashLoopBackOff. Он указывает kubelet'у запросить у CRI лог-файл контейнера, который завершился с ошибкой, а не того, который пытается стартовать сейчас.

Выполнив команду с флагом --previous, вы видите FATAL: Database connection timeout. Проблема локализована — приложение не может достучаться до базы данных.

Сценарий 2: Сетевая ловушка (Distroless)

Продолжаем расследование. Почему нет связи с БД? Нам нужно проверить сетевую доступность порта базы данных прямо изнутри Pod'а payment-worker.

Переходим на третий слой фреймворка (Окружение). Вы пытаетесь запустить ping или nc (netcat) внутри контейнера:

kubectl exec -it <имя-пода> -- sh

Ответ API: OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "sh": executable file not found in $PATH: unknown.

Приложение упаковано в distroless-образ. Внутри нет ни оболочки sh, ни сетевых утилит. Мы находимся в глухом ящике.

Слой 3 не сработал. Опускаемся на 4-й слой (Хирургия). Нам нужно прикрепить к этому PodSandbox временный эфемерный контейнер со всеми сетевыми утилитами, который разделит с проблемным приложением общее сетевое пространство (Network Namespace).

Используем образ netshoot, созданный специально для сетевого траблшутинга:

kubectl debug -it <имя-пода> \
  --image=nicolaka/netshoot \
  --target=payment-container

Как только открывается shell эфемерного контейнера, мы находимся в той же «сетевой комнате», что и наше падающее приложение. Выполняем nc -zv db-service 5432 и получаем Connection refused. Проблема найдена: база данных отклоняет соединения (например, из-за неверных NetworkPolicies или упавшего сервиса БД), а сам payment-worker работает корректно.

Сценарий 3: Застывшее время (Deadlock)

Симптомы: Java-приложение inventory-api имеет статус Running, Ready: True. Ошибок в логах нет. Однако клиенты жалуются на бесконечную загрузку при обращении к сервису.

Первые два слоя фреймворка (describe, logs) показывают идеальную картину. Kubernetes считает Pod абсолютно здоровым. Это классический признак взаимной блокировки потоков (Deadlock) внутри самого приложения.

Нам нужен дамп потоков (thread dump) или дамп памяти (heap dump) из JVM, чтобы отдать его разработчикам.

Переходим на 3-й слой (Окружение). Контейнер содержит необходимые утилиты (не distroless), поэтому мы можем использовать exec для генерации дампа и cp для его извлечения.

  1. Генерируем дамп прямо внутри работающего контейнера:
kubectl exec <имя-пода> -- jcmd 1 Thread.print > /tmp/threaddump.txt
  1. Извлекаем файл на локальную машину инженера:
kubectl cp <namespace>/<имя-пода>:/tmp/threaddump.txt ./threaddump-local.txt

Важно: kubectl cp не работает магическим образом. Под капотом он выполняет kubectl exec и использует утилиту tar для архивации файла в контейнере, передачи его потоком через API-сервер и распаковки на вашей машине. Если в контейнере нет tar (как в distroless), команда cp завершится ошибкой.

Итоги интроспекции

Вы научились смотреть на Pod не как на черный ящик, а как на набор пространств имен и процессов, к которым можно подобрать правильный ключ:

  • Если процесс умирает — читаем прошлое (--previous).
  • Если процесс жив, но завис — заходим внутрь (exec, cp).
  • Если процесс заперт в distroless — пробиваем стену снаружи (debug).

До сих пор мы работали с Pod'ами как с одиночными сущностями. Но в реальном production никто не запускает Pod'ы напрямую. Если узел с нашим отлаженным микросервисом физически сгорит, Pod исчезнет навсегда. Чтобы система была отказоустойчивой, нам нужны механизмы, которые будут автоматически создавать, масштабировать и воскрешать Pod'ы. Этим занимаются контроллеры, к изучению которых мы готовы перейти.