✻ Урок 5.13 · Тема 5: Kubernetes и Helm
Диагностика Kubernetes: разбор поломок
Содержание урока
Зачем это нужно
В два часа ночи алерт (автоматическое сообщение мониторинга «что-то сломалось», обычно звонок или сообщение в мессенджер) говорит «под не работает», а не «у тебя опечатка в теге образа». Под (pod) это запущенный контейнер приложения, минимальная единица Kubernetes (урок 5.2). Кто ищет причину наугад, тратит час; кто идёт по одному и тому же алгоритму, находит её за пять минут. Почти любая поломка в Kubernetes видна в четырёх местах: статус пода (короткое слово в kubectl get pods: Running, Pending и так далее), события (events: записи кластера «что с этим объектом произошло», как журнал вахтёра), описание (describe: подробная карточка объекта) и логи (то, что сама программа пишет о своей работе). Подробно каждое разберём ниже. Умение пройти их по порядку проверяют на собеседованиях фразой «под не стартует, твои действия».
Шаг проекта: в «Заметках» появляется docs/runbooks/k8s-triage.md, короткий алгоритм разбора пода для дежурного. Код и манифесты проекта не меняются.
Что нужно знать
- Урок 1.4: процессы и сигналы: что такое процесс, код завершения (число, с которым программа сообщает системе, как закончила: 0 «всё хорошо», остальное «что-то не так») и сигналы SIGTERM («заверши работу аккуратно», как вежливая просьба освободить стол) и SIGKILL («прекрати немедленно», как выключение из розетки). Из них вырастает вся таблица «код выхода» ниже.
- Урок 5.2: поды и Deployment: жизненный цикл пода,
describe,logs. - Урок 5.3: Service и DNS кластера: Service даёт подам постоянный адрес; selector (условие отбора «мои поды те, у которых такая метка») решает, какие поды входят в Service; endpoints (endpoints, «конечные адреса») это список найденных подов. Пустой список значит, что запросу некуда идти.
- Урок 5.5: хранилище и StatefulSet: PVC (PersistentVolumeClaim, заявка на диск: «мне нужен диск такого размера») и
postgres-0(первый под базы данных). - Урок 5.6: ConfigMap и Secret:
envFrom, ключи Secretnotes-db. - Урок 5.7: пробы, ресурсы, выкатка: readiness и liveness (две проверки «под готов принимать запросы» и «под жив»), requests и limits (сколько ресурсов под просит и сколько ему максимум можно), OOMKilled (контейнер убит за то, что занял больше памяти, чем разрешено).
- Урок 5.9: Helm: релиз,
helm history,helm rollback. - Урок 5.12: безопасность кластера: PSS, NetworkPolicy, RBAC: они тоже дают поломки.
- Урок 2.8: путь запроса и диагностика: идея «идти по пути слой за слоем».
Картина целиком
Диагностика это работа врача скорой. Он не начинает с рентгена, а идёт по короткому списку: пульс, давление, жалобы, и только потом выбирает обследование. Так же в Kubernetes: сначала быстрые признаки (статус, события), потом подробности (describe, логи), потом сеть.
Главная идея: под проходит несколько стадий, и каждая стадия ломается по-своему. Узнай, на какой стадии остановился под, и ты знаешь, какие причины возможны, а какие нет. Аналогия с доставкой посылки: заказ оформлен, посылка собрана, доставлена, распакована, работает. Если посылка «не доехала», глупо разбираться, почему она не включается.
flowchart TD
S1["создан, нет узла<br>Pending<br>смотреть: события (планировщик)"] --> S2["узел выбран<br>ContainerCreating<br>смотреть: события (kubelet, тома)"]
S2 --> S3["образ скачивается<br>ErrImagePull / ImagePullBackOff<br>смотреть: событие Failed"]
S3 --> S4["конфиг собирается<br>CreateContainerConfigError<br>смотреть: событие, Secret/ConfigMap"]
S4 --> S5["процесс запущен<br>Running<br>смотреть: логи"]
S5 --> S6["процесс упал<br>CrashLoopBackOff<br>смотреть: логи --previous, код выхода"]
S5 --> S7["проба не прошла<br>Running, но READY 0/1<br>смотреть: события Unhealthy"]
S5 --> S8["под здоров<br>Running 1/1 Ready<br>идти по сети: Service, endpoints, Gateway"]
Три вопроса на любой поломке: где остановился (статус), что говорит система (события и describe), что говорит приложение (логи). Дальше по порядку разберём каждую стадию, потом сеть, потом откат и отладочные инструменты.
Теория
Стадии жизни пода и почему их надо знать
Под (pod) это минимальная единица запуска в Kubernetes: один или несколько контейнеров с общей сетью (урок 5.2). Между командой kubectl apply и работающим приложением под проходит цепочку шагов, и в ней участвуют разные компоненты:
- Планировщик (scheduler): выбирает узел (node, машину кластера), на котором хватает места. Пока узел не выбран, под в статусе
Pending. - Kubelet: агент на узле. Он готовит тома (диски), собирает конфигурацию из Secret и ConfigMap, просит скачать образ.
- Контейнерный рантайм: скачивает образ и запускает процесс.
- Kubelet снова: запускает пробы, следит за процессом, перезапускает при падении.
Зачем это знать? Потому что каждый компонент пишет свои сообщения. Если под висит в Pending, виноват планировщик, и логов приложения не будет: приложение ещё не запускалось. Если в ImagePullBackOff, виноват образ или реестр, и снова логов нет. Смотреть логи для этих статусов то же, что искать в чате переписку с человеком, которого ещё не пригласили.
Разобранный пример. Ты обновил образ, а через минуту kubectl get pods показывает ImagePullBackOff. Рассуждение: сеть и Service ни при чём, под ещё даже не стартовал. Значит, ищем на стадии «скачивание образа»: имя, тег, доступ к реестру. Одним статусом мы отбросили три четверти возможных причин.
Осторожно, путаница: «под красный, значит приложение сломано». Нет: приложение часто вообще не запускалось. Ошибка в конфигурации, образе или ресурсах, а не в коде.
Прикинь сам: под в
Pending, аkubectl logsвыдаёт пустоту. Это ошибка команды?
Нет: контейнера ещё не было, логам взяться неоткуда. Причину ищи в describe pod, в Events: FailedScheduling объяснит, что планировщику не подошло.
Проверь понимание: под в
Pending, аkubectl logsвыдаёт пустоту. Это ошибка команды?
Ответ
Нет. Контейнера ещё не было, логам взяться неоткуда. Причину смотрят в kubectl describe pod, секция Events: сообщение FailedScheduling объяснит, что планировщику не подошло.
Главное: под проходит стадии планирование, подготовка узла, скачивание образа, запуск; статус говорит, на какой он остановился, и это отсекает лишние причины.
Теперь соберём из этого порядок команд.
Алгоритм: четыре команды и порядок
Правило одно: сначала узнай, где остановился под, и только потом ищи почему. Порядок такой, от дешёвого к дорогому.
- Статус.
kubectl get pods -n notes -o wide. Флаг-n notesвыбирает namespace,-o wideдобавляет колонки с IP пода и узлом. СмотриSTATUS,READY(сколько контейнеров готово из скольких),RESTARTS. Статус сразу сужает область поиска. - События.
kubectl get events -n notes --sort-by=.lastTimestamp. Событие (event) это короткая запись «что случилось», которую пишут планировщик, kubelet и контроллеры.--sort-by=.lastTimestampставит свежие в конец. - Описание.
kubectl describe pod <имя> -n notes. Читай блокState/Last State(причина и код выхода) и в самом концеEventsименно этого пода. - Логи.
kubectl logs <под> -n notes, а для уже упавшего запуска добавь--previous. Логи есть только у контейнера, который хоть раз стартовал.
Если под здоров, а сервис не отвечает, идём дальше по сети: Service, endpoints, Gateway (уроки 5.3 и 5.4).
Почему такой порядок? Каждый следующий шаг дороже предыдущего по времени и объёму вывода: статус это одна строка, события десяток строк, describe экран, логи сколько угодно. Идёшь от малого к большому, останавливаешься, как только причина найдена. И второй довод: логи есть не всегда, а статус и события всегда.
Чего не делать: сразу kubectl delete pod. Контроллер (Deployment) пересоздаст под, ты потеряешь улики (логи и события старого пода), а причина останется на месте.
Прикинь сам: почему логи смотрят четвёртым, а не первым?
У многих поломок логов нет: Pending, ImagePullBackOff и CreateContainerConfigError контейнера не создали. Статус и события показывают, до какой стадии дошёл под.
Осторожно: не делай сразу kubectl delete pod: Deployment пересоздаст под, а улики (логи и события старого пода) пропадут.
Проверь понимание: почему логи смотрят четвёртым, а не первым?
Ответ
У многих поломок логов нет: Pending не запущен, ImagePullBackOff не скачал образ, CreateContainerConfigError не создал контейнер. Статус и события показывают, до какой стадии дошёл под, а логи имеют смысл, только если контейнер стартовал.
Главное: порядок такой: статус, события,
describe, логи; идёшь от дешёвого к дорогому и останавливаешься, когда причина найдена.
Первый шаг этого порядка опирается на статусы, и их надо читать как карту.
Статус пода как карта
Каждый статус указывает на свою стадию и свой набор причин. Разберём по одному, что он значит и почему возникает.
Pending. Под создан в базе кластера (etcd), но ни на какой узел не поставлен. Планировщик проверяет для каждого узла: хватает ли свободных ресурсов под requests пода (гарантированный минимум CPU и памяти, урок 5.7), нет ли taint (пометка узла «сюда не ставить, если под не имеет разрешения»), подходит ли nodeSelector (требование «только на узлы с такой меткой»), смонтируется ли PVC (запрос на диск, урок 5.5). Если ни один узел не подошёл, под ждёт, а в событиях пишется FailedScheduling.
ContainerCreating. Узел выбран, kubelet собирает контейнер. Зависает, если не монтируется том или нет Secret или ConfigMap, на который ссылается под.
ErrImagePull / ImagePullBackOff. Рантайм не смог скачать образ: опечатка в имени или теге, тега нет в реестре, реестр приватный, а пароля нет (нужен imagePullSecrets), сети до реестра нет. ErrImagePull это сама ошибка, ImagePullBackOff состояние «жду и пробую снова с растущей паузой».
CreateContainerConfigError. Образ есть, но контейнер не собрался из-за конфигурации: в переменной окружения secretKeyRef указан ключ, которого нет в Secret, или ConfigMap не существует.
CrashLoopBackOff. Контейнер стартует, процесс завершается (ошибка, нехватка памяти, неверная команда), kubelet перезапускает, снова падение. Между перезапусками kubelet выдерживает растущую паузу (backoff): по умолчанию 10 с, 20 с, 40 с и так до 5 минут (зависит от версии и настроек kubelet). Поэтому «под просто ещё не перезапустился» после пяти минут не оправдание.
Running, но 0/1 READY. Процесс жив, но readiness-проба (проверка «готов ли под принимать трафик») не проходит. Kubernetes не перезапускает такой под, а исключает его из списка адресов Service. Пользователь видит 502 или 503, хотя под «зелёный».
OOMKilled (виден в Last State). Процесс превысил limits.memory и убит ядром (OOM: out of memory, «кончилась память»). Код выхода 137.
Error / Completed. Контейнер завершился, а перезапускать его не должны: это нормально для Job (урок 5.8), причина смотрится по коду выхода.
| Статус | Стадия | Что искать |
|---|---|---|
Pending |
под не размещён на узле | события FailedScheduling: requests, taint, nodeSelector, PVC |
ContainerCreating |
контейнер готовится | том не смонтировался, нет Secret или ConfigMap |
ErrImagePull / ImagePullBackOff |
образ не скачивается | имя и тег, реестр, imagePullSecrets, сеть |
CreateContainerConfigError |
контейнер не создан из-за конфига | ключ в Secret или ConfigMap |
CrashLoopBackOff |
стартует и падает, пауза растёт | logs --previous, код выхода |
Running, 0/1 READY |
процесс жив, проба не проходит | путь пробы, зависимость (БД), события Unhealthy |
OOMKilled (Last State) |
код 137, память выше limit | лимит memory, утечка |
Error / Completed |
завершился Job или контейнер | код выхода, логи |
Осторожно, путаница: Error и CrashLoopBackOff. Первое одно падение, второе повторяющиеся падения с паузами. И Running не значит «работает»: смотри колонку READY.
Прикинь сам: под
Running,READY 0/1, рестартов 0. Перезапустит ли его Kubernetes?
Нет: readiness-проба не перезапускает контейнер, а лишь убирает под из Service. Перезапускает liveness. Причину ищи в событиях Unhealthy.
Проверь понимание: под
Running,READY 0/1, рестартов 0. Перезапустит ли его Kubernetes?
Ответ
Нет. Readiness-проба не перезапускает контейнер, она лишь убирает под из Service. Перезапускает livenessProbe. Причину ищи в событиях Unhealthy и вручную дёрни путь пробы.
Главное: каждый статус указывает на свою стадию:
Pendingпланировщик,ImagePullBackOffобраз,CrashLoopBackOffпадение процесса,0/1 READYпроба.
За статусом идут события и describe.
События и describe: где система объясняет себя
События (events) это записи вида «объект, что произошло, кто написал, почему». Например планировщик пишет FailedScheduling, kubelet пишет Failed (не скачался образ), BackOff (перезапуск с паузой), Unhealthy (проба не прошла). События живут в кластере около часа и потом стираются, поэтому в инциденте их смотрят сразу, а важное копируют в заметки.
Полезная команда: kubectl get events -n notes --sort-by=.lastTimestamp. А --field-selector reason=FailedScheduling оставит события только с нужной причиной. Type: Warning это проблема, Normal информация.
kubectl describe pod собирает всё про один под. Читай его так, сверху вниз:
Status,Node,IP: где живёт под;Containers->State(что сейчас) иLast State(как завершился прошлый запуск): здесьReason(Error,OOMKilled) иExit Code;Conditions:PodScheduled,Initialized,ContainersReady,Ready: какая стадия не пройдена (значениеFalseпоказывает, где остановились);Eventsв самом низу: последняя строка чаще всего и есть причина.
Прикинь сам: зачем
describeчитать с конца?
Блок Events внизу содержит записи именно этого пода по порядку, и последние обычно называют причину. Начало описания справочное.
Осторожно: события стираются примерно через час, поэтому читать их нужно сразу, а не «потом разберусь».
Проверь понимание: зачем
describeчитать с конца?
Ответ
Блок Events внизу содержит записи именно этого пода в хронологии, а последние сообщения обычно называют причину («Failed to pull image», «Insufficient cpu», «Back-off restarting failed container»). Начало описания это справочные поля.
Главное: события живут около часа,
describeчитай по порядку:State,Last State,Conditions,Eventsв самом низу.
Одна из самых важных строк в Last State это код выхода.
Код выхода: как процесс объясняет свою смерть
Код выхода (exit code) это число, с которым завершился процесс (урок 1.4). Ноль значит «всё хорошо», любое другое значит «что-то пошло не так». В describe он виден в Last State.
0: завершился штатно;1: общая ошибка приложения, смотри логи;2: часто неверные аргументы или файл не найден (интерпретатор Python выдаёт 2, если не может открыть скрипт);126/127: команда не исполняемая или не найдена (ошибка вcommand);137: убит сигналом 9 (SIGKILL): чащеOOMKilled(память) или kill поlivenessProbeпосле таймаута;143: остановлен сигналом 15 (SIGTERM): при выкатке это нормально.
Формула для сигналов: 128 + номер сигнала. Сигнал 9 даёт 137, сигнал 15 даёт 143. SIGKILL нельзя перехватить, процесс не успевает ничего записать в лог: поэтому у OOMKilled в логах приложения тишина.
Разобранный пример. Last State: Terminated, Reason: Error, Exit Code: 2 и в логах python: can't open file '/no/such/app.py'. Код 2 и текст совпадают: не найден файл запуска. А Reason: OOMKilled, Exit Code: 137 и пустые логи говорит другое: память. Разные коды, разные исправления.
Осторожно, путаница: 137 не всегда OOMKilled. Если Reason не OOMKilled, а в событиях есть Liveness probe failed, процесс убил kubelet.
Прикинь сам:
Last State: Terminated,Reason: Error,Exit Code: 1. Куда смотреть дальше?
В kubectl logs <под> --previous: приложение само завершилось с ошибкой, причина в последних строках лога. Если бы это была память, код был бы 137.
Проверь понимание:
Last State:Terminated,Reason: Error,Exit Code: 1. Куда смотреть дальше?
Ответ
В kubectl logs <под> --previous: приложение само завершилось с ошибкой, причина написана в последних строках лога. Если бы это была память, код был бы 137.
Главное: код
128 + номер сигнала: 137 это SIGKILL (чащеOOMKilled), 143 это SIGTERM;126и127ошибка вcommand.
Причина самого падения лежит в логах прошлого запуска.
Логи упавшего запуска
kubectl logs <под> показывает лог текущего контейнера. Если контейнер только что перезапустился после падения, там пусто или несколько первых строк нового запуска. Причина же лежит в логе предыдущего запуска, и его показывает --previous (короткая форма -p). Ещё полезны --tail=20 (последние 20 строк), -l app.kubernetes.io/name=notes (все поды по метке, -l означает label selector, отбор по меткам), -f (следить за новыми строками).
Логи отсутствуют, если контейнер ни разу не стартовал (Pending, ImagePullBackOff, CreateContainerConfigError). Тогда ошибка previous terminated container ... not found не баг, а подсказка: падений ещё не было.
Осторожно, путаница: logs --previous ждут для OOMKilled, но текст об OOM в нём не появится, потому что процесс убит без предупреждения: причину покажет describe.
Прикинь сам: контейнер падает сразу после старта. Как увидеть ошибку, из-за которой он упал в прошлый раз?
kubectl logs <под> --previous: текущий запуск мог ещё ничего не написать, а предыдущий уже записал причину.
Проверь понимание: контейнер падает сразу после старта. Как увидеть ошибку, из-за которой он упал в прошлый раз?
Ответ
kubectl logs <под> --previous. Текущий запуск может ещё ничего не написать, а предыдущий уже записал причину.
Главное:
logsпоказывает текущий контейнер, а причина падения лежит в--previous; если контейнер не стартовал, логов нет.
Что делать, если под здоров, а сайт не отвечает, посмотрим дальше.
Под здоров, а сайт не отвечает: путь запроса
Если под Running и 1/1 Ready, а клиент получает ошибку, идём по пути запроса изнутри наружу (как в уроке 2.8):
flowchart LR
B["браузер<br>curl"] --> G["Gateway (Envoy)<br>Accepted / Programmed"] --> H["HTTPRoute<br>правила маршрута"] --> S["Service notes<br>selector = метки?"] --> E["endpoints<br>Ready-адреса"] --> P["под notes<br>логи, пробы"] --> D["db (Service)"] --> PG["postgres-0"]
Service даёт группе подов одно постоянное имя и адрес (урок 5.3). Какие поды входят в группу, решает selector (отбор по меткам): Service берёт все Ready-поды, у которых метки совпадают. Список найденных адресов называется endpoints. Если он пуст, трафику некуда идти.
Проверки по порядку:
kubectl get endpoints notes -n notes: пусто значит selector не совпал с метками подов или нет Ready-подов.kubectl get svc notes -n notes -o wideиkubectl get pods -n notes --show-labels: сравни selector и метки глазами.kubectl port-forward svc/notes 8080:8080 -n notesиcurl http://127.0.0.1:8080/readyz(port-forwardпробрасывает порт с твоей машины прямо в кластер в обход Gateway). Если так работает, проблема выше по пути: Gateway или HTTPRoute.kubectl get gateway,httproute -n notes: условияAcceptedиProgrammed.- Если приложение обращается к БД:
kubectl get endpoints db -n notes, статусpostgres-0, событие про PVC. - Если после урока 5.12 всё внезапно оборвалось:
kubectl get networkpolicy -n notes. NetworkPolicy молча отбрасывает пакеты: клиент видит таймаут, а не отказ. Ещё одно свойство:port-forwardидёт через kubelet и NetworkPolicy не проходит, поэтому работающийport-forwardпри мёртвом Gateway часто указывает именно на сетевую политику.
Как различать коды. Они говорят, кто в цепочке не смог. Envoy отвечает 503, когда у маршрута нет живых адресов (пустые endpoints, все поды не Ready), и 502, когда апстрим ответил некорректным HTTP. Если соединение с подом сброшено или оборвано до ответа, Envoy тоже обычно отвечает 503 (upstream connect error or disconnect/reset before headers). 504 значит, что апстрим не ответил вовремя.
Прикинь сам: что покажет
kubectl get endpoints notes, если в Service поменять selector наapp.kubernetes.io/name: notez?
Endpoints станут <none>, а поды останутся Running и Ready: Service просто их не находит. Клиент через Gateway получит 503.
Осторожно: 503 Envoy выдаёт, когда у маршрута нет живых адресов, 502 когда апстрим ответил некорректным HTTP, а при сбросе или обрыве соединения до ответа тоже обычно 503, 504 когда не ответил вовремя.
Проверь понимание: что покажет
kubectl get endpoints notes, если в Service ошибочно поменять selector наapp.kubernetes.io/name: notez? Что будет с подами?
Ответ
Endpoints станут <none>, а поды останутся Running и Ready: с ними всё в порядке, Service просто их не находит. Клиент через Gateway получит 503. Классика: здоровые поды и мёртвый сервис.
Главное: иди по пути запроса:
endpoints, метки подов,port-forward, Gateway и HTTPRoute, NetworkPolicy; пустой endpoints значит selector не совпал или нет Ready-подов.
Когда причину искать долго, сначала возвращай сервис откатом.
Сначала откат, потом разбор
Первая цель инцидента: вернуть сервис, а не понять причину. Если поломка началась сразу после выкатки, откатывай:
helm history notes -n notes: список ревизий релиза (каждыйhelm upgradeсоздаёт новую), в колонкеSTATUSотмечена текущая (deployed);helm rollback notes <ревизия> -n notes: вернуть релиз к указанной ревизии;- или
kubectl rollout undo deployment/notes -n notes: вернуть Deployment к предыдущему набору подов. Учти, что для релиза Helm дрейф между Helm и кластером потом надо убрать черезhelm upgrade(урок 5.9).
Откат занимает минуту и безопасен: причина остаётся в истории ревизий и логах, разберёшься спокойно после. Важно, что при выкатке RollingUpdate (постепенная замена подов) с maxUnavailable: 0 (урок 5.7) старые поды не гасятся, пока новые не стали Ready: плохая версия «зависает», а сервис продолжает обслуживать пользователей. Поэтому зависшая выкатка не всегда авария: сначала проверь, отвечает ли сайт.
Осторожно, путаница: откат это не лечение. Он возвращает старую версию, а причина, например ошибочный тег или опечатка в values, остаётся в репозитории и сломает следующую выкатку.
Прикинь сам: зачем сначала откат, а потом разбор?
Цель инцидента: восстановить сервис. Откат быстрый и безопасный, а причина остаётся в истории ревизий, событиях и логах для спокойного анализа.
Проверь понимание: зачем сначала откат, а потом разбор?
Ответ
Цель инцидента: восстановить сервис. Откат быстрый и безопасный, а причина остаётся в истории ревизий, событиях и логах для спокойного анализа.
Главное: первая цель это вернуть сервис:
helm rollbackилиkubectl rollout undo; откат не лечит причину, она остаётся в репозитории.
Перед любыми действиями надо убедиться, что работаешь в нужном кластере.
Сначала проверь, где ты: кластер, контекст, namespace
Самая обидная ошибка диагностики: полчаса смотреть не туда. kubectl get pods вернул «No resources found», хотя приложение работает, потому что ты смотришь в другой namespace. Или чинишь «прод», а команды идут в dev.
Навигатор с невыбранным городом: маршрут построится, но не в тот город. Оговорка: навигатор хотя бы показывает город на экране, а kubectl молчит, пока не спросишь.
Две настройки решают, куда попадёт команда:
- Контекст (context) это выбранная запись в файле
~/.kube/config: «какой кластер и под каким пользователем». Посмотреть:kubectl config current-context. Переключить:kubectl config use-context ИМЯ. - Namespace (пространство имён, «комната» в кластере) можно указать в каждой команде флагом
-n notes. Если флага нет, используется namespace из контекста, а он чаще всегоdefault.
kubectl get pods в пустом default отвечает No resources found in default namespace. Это не значит, что подов нет: они в notes. Правильно: kubectl get pods -n notes. Для поиска по всем комнатам сразу есть kubectl get pods -A (-A значит «все namespace»): видно, где живёт нужное.
Осторожно, путаница: «Нет ресурсов» считают ответом «приложение не задеплоено». Сначала проверь namespace и контекст, потом делай выводы. В runbook для дежурного первая строка именно kubectl config current-context.
Прикинь сам:
kubectl get deploy notesотвечаетNotFound, хотя ты уверен, что он есть. Что проверишь первым?
Namespace и контекст: добавь -n notes и проверь kubectl config current-context. Чаще всего объект ищут не там.
Проверь понимание:
kubectl get deploy notesотвечаетError from server (NotFound): deployments.apps "notes" not found, хотя ты уверен, что он есть. Что проверишь первым?
Ответ
Namespace и контекст: добавь -n notes и проверь kubectl config current-context. Чаще всего объект найден не там, где ищешь.
Главное: куда попадёт команда, решают контекст и namespace; «No resources found» не значит, что приложения нет.
Отдельная тема: выкатка через Helm, которая может оборваться на полпути.
Выкатка через Helm не удалась: как не оставить прод на полпути
helm upgrade может оборваться посередине: часть объектов уже обновлена, новые поды не стали Ready, а команда завершилась ошибкой или зависла. Без страховки релиз остаётся в полусломанном состоянии, и дежурный разбирается в нём ночью.
Операция с обязательным планом «если пошло не так, вернуть как было». Без такого плана хирург, обнаружив проблему, уже не может зашить всё обратно. Оговорка: откат возвращает только манифесты, данные в базе он не откатывает (урок 5.9).
У Helm два флага, которые страхуют выкатку:
--waitговорит: не считай выкатку успешной, пока все поды не станут Ready (или не выйдет время--timeout). Без него команда вернётся сразу после отправки манифестов, даже если поды сломаны.--rollback-on-failureговорит: если выкатка не удалась, вернись на прошлую рабочую ревизию сам. Он включает ожидание--wait. В Helm 3 и старых статьях этот флаг называется--atomic; в Helm 4 (на нём построен курс) старое имя устарело.
Если флагов не было, смотри статус релиза: helm status notes -n notes и helm history notes -n notes. Статус failed значит выкатка не удалась, pending-upgrade значит операция идёт или оборвалась, и параллельно запускать вторую нельзя.
Выкатка с битым тегом образа. Без флагов: helm upgrade сразу отвечает deployed, а kubectl get pods через минуту показывает ImagePullBackOff. С --rollback-on-failure --timeout 3m: Helm ждёт три минуты, не дожидается Ready, откатывает релиз, печатает ошибку. В helm history появится ревизия со статусом failed и следующая с описанием отката. Старые поды при этом никто не гасил, и сервис работал.
Прикинь сам:
helm upgradeбез--waitзавершился без ошибок, а поды вImagePullBackOff. Почему Helm не сообщил о проблеме?
Без --wait Helm только отправляет манифесты в API и не ждёт Ready. Для ответа «выкатка удалась» нужны --wait или --rollback-on-failure.
Осторожно: не думай, что helm upgrade с кодом выхода 0 означает «всё работает». Без --wait он означает только «манифесты отправлены». Работает ли приложение, показывает статус подов.
Проверь понимание:
helm upgradeбез--waitзавершился без ошибок, а поды вImagePullBackOff. Почему Helm не сообщил о проблеме?
Ответ
Без --wait Helm только отправляет манифесты в API-сервер и не ждёт, пока поды станут Ready. Для ответа «выкатка удалась» нужны --wait или --rollback-on-failure.
Главное:
--waitждёт Ready,--rollback-on-failureоткатывает сам; в Helm 3 он назывался--atomic; статусfailedиpending-upgradeсмотри вhelm status.
Чтобы дежурному не изобретать порядок ночью, его записывают в runbook.
Runbook: что это и из чего состоит
В два часа ночи у дежурного нет сил вспоминать порядок действий. Runbook (инструкция-памятка для дежурного) это короткий документ «при такой поломке делай то-то». Без него каждый дежурный изобретает порядок заново и пропускает шаги.
Инструкция по эвакуации на стене офиса: сработала сирена, не размышляй, а иди по списку. Оговорка: инструкция не заменяет голову. Если ситуация не похожа на написанное, нужно подумать и дополнить инструкцию.
Хороший runbook для подов короткий и состоит из: симптома («под в CrashLoopBackOff»), первых команд (то, что выполнить прямо сейчас), развилок («если код выхода 137, смотри память; если 1, смотри логи»), условий эскалации (когда звать старшего) и способа отката. Каждая строка это команда или вопрос с понятным ответом «да» или «нет».
Шаг из runbook: «Статус Pending? Выполни kubectl describe pod ПОД -n notes и найди FailedScheduling в Events. Если написано Insufficient memory (не хватает памяти на узлах), уменьши requests или добавь узел». Человек, который ни разу не видел такую ошибку, выполнит шаг за минуту.
Осторожно, путаница: Runbook пишут один раз и забывают. На деле его надо править после каждого инцидента, где выяснилось, что шага не хватает. Устаревшая инструкция опаснее её отсутствия.
Прикинь сам: почему в runbook нужны условия эскалации?
Дежурный должен знать, когда перестать искать в одиночку и позвать того, кто лучше знает систему. Иначе он тратит время, которое стоит денег.
Проверь понимание: почему в runbook нужны условия эскалации?
Ответ
Потому что дежурный должен знать, когда перестать искать в одиночку и позвать того, кто лучше знает систему. Без этого он тратит на поиск время, которое стоит сервису денег.
Главное: runbook состоит из симптома, первых команд, развилок, условий эскалации и способа отката; после каждого инцидента его правят.
Иногда сломано сразу два места, и важно не запутаться.
Две поломки сразу: как не запутаться
В реальном инциденте причин бывает две. Ты починил одну, под всё равно не работает, и кажется, что починка не помогла. На самом деле ты продвинулся на шаг.
Пробка на дороге, в которой сначала стоит авария, а за ней ремонт. Убрали аварию, а машины всё ещё стоят: очередь упёрлась во вторую причину. Оговорка: в отличие от дороги, вторую причину в Kubernetes видно сразу, если снова запустить тот же алгоритм.
После каждого исправления не гадай, а повторяй тот же порядок: статус, события, describe, логи. Статус подскажет, на какой стадии под остановился теперь. Если стадия другая (был ImagePullBackOff, стал CrashLoopBackOff), первая причина устранена, а под дошёл до следующей.
Под в ImagePullBackOff: в теге образа опечатка. Ты исправил тег, статус стал CreateContainerConfigError с событием про отсутствующий ключ в Secret. Первая поломка устранена (образ скачался), под дошёл до сборки конфигурации и упёрся во вторую. Ты поправляешь Secret, и статус становится Running, но READY 0/1: теперь виновата проба готовности. Три поломки подряд, и каждая видна по смене статуса.
Прикинь сам: после правки тега статус сменился с
ImagePullBackOffнаCrashLoopBackOff. Правка помогла или нет?
Помогла: образ скачался и контейнер запустился. Теперь он падает по другой причине, её смотри в logs --previous и в коде выхода.
Осторожно: не думай, что смена статуса означает «стало хуже». Обычно наоборот: под пошёл дальше по стадиям, а значит, прошлый шаг пройден. Возвращаться к исправленной причине не нужно.
Проверь понимание: после правки тега образа статус сменился с
ImagePullBackOffнаCrashLoopBackOff. Правка помогла или нет?
Ответ
Помогла: образ скачался и контейнер запустился. Теперь он падает уже по другой причине, её смотри в kubectl logs --previous и в коде выхода.
Главное: после каждого исправления повторяй порядок: статус, события,
describe, логи; смена статуса значит, что под прошёл дальше.
И последний инструмент: отладка, когда в образе нет утилит.
kubectl debug: когда в образе нет инструментов
Образ «Заметок» построен на python:3.13-slim: в нём нет curl, nslookup и ps, и kubectl exec мало помогает. Утилиты в прод-образ не кладут (лишняя поверхность атаки, урок 4.8). Вместо этого поверх работающего пода подключают ephemeral-контейнер (временный, «одноразовый» контейнер с инструментами внутри уже запущенного пода):
kubectl debug -it -n notes <под> --image=busybox:1.37 --target=notes --profile=restricted -- sh
Разбор: -it даёт интерактивный терминал; --image=busybox:1.37 образ с набором утилит (wget, nslookup); --target=notes делить процессы с контейнером notes, чтобы видеть его процессы; --profile=restricted набор настроек безопасности, нужный в namespace с PSS restricted (урок 5.12), иначе admission отклонит контейнер (профиль требует runAsNonRoot, и busybox стартует, потому что runAsUser: 10001 задан на уровне пода и ephemeral-контейнер его наследует; в поде без runAsUser добавь --custom с runAsUser); -- sh команда для запуска.
Контейнер делит с целевым сетевое пространство, поэтому wget -qO- http://127.0.0.1:8080/healthz и nslookup db видят то же, что видит приложение (в том числе действие NetworkPolicy, ведь IP пода один). После выхода ephemeral-контейнер остаётся записанным в описании пода, но не работает; удалить его можно только пересозданием пода.
Осторожно, путаница: kubectl debug не «чинит» под и не заменяет exec, он лишь даёт инструменты. Для проблем узла есть kubectl debug node/<имя>.
Прикинь сам: зачем
--targetвkubectl debug?
Чтобы временный контейнер видел процессы целевого. Без него он получит свои процессы и не увидит приложение, а сеть у них общая в любом случае.
Проверь понимание: зачем
--target?
Ответ
Чтобы временный контейнер видел процессы целевого контейнера. Без --target он получит свои процессы и не увидит приложение (сеть у них общая в любом случае).
Главное: ephemeral-контейнер даёт
curl,nslookupиpsповерх работающего пода; в namespace с PSSrestrictedнужен--profile=restricted.
Теперь можно потренироваться на настоящих поломках.
Практика
Проверь, что кластер запущен и контекст правильный. kubectl config current-context печатает имя выбранного кластера, kubectl get pods -n notes список подов:
kubectl config current-context
kubectl get pods -n notes
kind-notes
NAME READY STATUS RESTARTS AGE
notes-6d9c7b8f5d-4kx2p 1/1 Running 0 12m
notes-6d9c7b8f5d-9zq7w 1/1 Running 0 12m
notes-6d9c7b8f5d-tm8hn 1/1 Running 0 12m
postgres-0 1/1 Running 0 40m
Как читать вывод: kind-notes значит контекст верный (иначе ты сломаешь чужой кластер). У всех подов READY 1/1 и STATUS Running: это норма, с которой будем сравнивать. Имена подов у тебя будут другие. Дальше <под> означает «подставь своё имя».
Задание 1. Три статуса за пять минут
Цель: вызвать три типовые поломки и по каждой пройти алгоритм.
Предскажи: если в Deployment написать несуществующий тег образа, что увидит пользователь сайта: ошибки или работающий сайт (у Deployment maxUnavailable: 0)?
Ответ
Сайт продолжит работать. Новый под застрянет в ImagePullBackOff, но RollingUpdate не гасит старые поды, пока новый не стал Ready.
Шаги:
-
Поломка A: несуществующий тег образа.
kubectl set image deployment/notes notes=<образ>меняет образ контейнераnotes(имя контейнера до знака равенства);grep -A2 'Failed'печатает строки сFailedи две следующие;rollout undoоткатывает:kubectl set image deployment/notes notes=ghcr.io/<github-user>/notes:9.9.9 -n notes kubectl get pods -n notes kubectl describe pod -n notes -l app.kubernetes.io/name=notes | grep -A2 'Failed' kubectl rollout undo deployment/notes -n notes -
Поломка B: слишком большие requests.
kubectl set resourcesменяет ресурсы контейнеров; limits задаём тоже, иначе API отклонит requests больше limits;--field-selector reason=FailedSchedulingоставляет события только нужного вида:kubectl set resources deployment/notes -n notes \ --requests=cpu=64,memory=512Gi --limits=cpu=64,memory=512Gi kubectl get pods -n notes kubectl get events -n notes --field-selector reason=FailedScheduling | tail -2 kubectl rollout undo deployment/notes -n notes -
Поломка C: ключ, которого нет в Secret.
patchвносит правку из JSON; списокenvсливается по имени, поэтому существующие переменные остаются.grep -B1 -A3печатает строки вокруг совпадения,headобрезает вывод:kubectl patch deployment notes -n notes -p '{"spec":{"template":{"spec":{"containers":[{"name":"notes","env":[{"name":"X","valueFrom":{"secretKeyRef":{"name":"notes-db","key":"NO_SUCH_KEY"}}}]}]}}}}' kubectl get pods -n notes kubectl describe pod -n notes -l app.kubernetes.io/name=notes | grep -B1 -A3 'NO_SUCH_KEY' | head kubectl rollout undo deployment/notes -n notes
Между поломками дожидайся kubectl rollout status deployment/notes -n notes, чтобы состояние вернулось к норме.
Что должно получиться (числа и хеши у тебя другие):
NAME READY STATUS RESTARTS AGE
notes-5f7d8b6c94-x2v8m 0/1 ImagePullBackOff 0 20s
Warning Failed kubelet Failed to pull image "ghcr.io/<github-user>/notes:9.9.9": ... not found
notes-7c6b9d4f8-q5r2s 0/1 Pending 0 15s
0s Warning FailedScheduling pod/notes-7c6b9d4f8-q5r2s 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 Insufficient cpu, 2 Insufficient memory.
notes-8b5c7d9f6-m4n7p 0/1 CreateContainerConfigError 0 10s
Error: couldn't find key NO_SUCH_KEY in Secret notes/notes-db
Как читать вывод: в каждом случае новый под 0/1 и в «плохом» статусе, а старые три остались 1/1. Первый блок: not found в событии Failed значит, что такого тега нет в реестре (для доступа было бы unauthorized). Второй: Pending и FailedScheduling со списком «нет узлов»: число узлов в кластере kind у тебя может быть другим. Третий: статус сам называет причину, а describe показывает, какой ключ и в каком Secret.
Объясни себе:
- Какой статус у какой стадии: скачивание, размещение, создание контейнера?
- Почему в случаях A, B, C не помогла бы команда
kubectl logs? - Почему старые поды продолжали обслуживать трафик все три раза?
Типичные ошибки:
error: unable to find container named "notes": имя контейнера вset imageневерное; проверьkubectl get deploy notes -n notes -o jsonpath='{.spec.template.spec.containers[*].name}'.- Пустой вывод
grep: событие уже стёрлось или ты смотришь не тот под; повториkubectl get podsи возьми новый. The Deployment "notes" is invalid: ... must be less than or equal to cpu limit: приset resourcesзабыт--limits.
Задание 2. CrashLoopBackOff и код выхода
Цель: отличить падение приложения от убийства по памяти и научиться пользоваться --previous.
Предскажи: контейнер падает сразу при старте. Какой ключ нужен, чтобы увидеть ошибку, из-за которой контейнер упал в прошлый раз?
Ответ
--previous (или -p): текущий запуск может ещё не успеть ничего написать, а предыдущий уже записал причину смерти.
Шаги:
-
Подмени команду запуска на несуществующий файл.
--type=jsonвключает формат правки «операция, путь, значение»;sleep 45даёт Kubernetes время перезапустить контейнер:kubectl patch deployment notes -n notes --type=json \ -p='[{"op":"add","path":"/spec/template/spec/containers/0/command","value":["python","/no/such/app.py"]}]' sleep 45 kubectl get pods -n notes -
Найди причину.
logs --previous --tail=3последние три строки прошлого запуска;grep -A6 'Last State'блокLast Stateи шесть строк после:kubectl logs -n notes -l app.kubernetes.io/name=notes --previous --tail=3 kubectl describe pod -n notes -l app.kubernetes.io/name=notes | grep -A6 'Last State' -
Верни как было:
kubectl rollout undo deployment/notes -n notes.
Что должно получиться:
NAME READY STATUS RESTARTS AGE
notes-6d59f5c7b8-hd9wz 0/1 CrashLoopBackOff 3 (28s ago) 62s
python: can't open file '/no/such/app.py': [Errno 2] No such file or directory
Last State: Terminated
Reason: Error
Exit Code: 2
Как читать вывод: CrashLoopBackOff и RESTARTS 3 (28s ago) показывают: контейнер уже три раза перезапускался, последний раз 28 секунд назад. Причина в первой строке лога: файла нет. Exit Code: 2 и Reason: Error значит ошибка приложения, не память. Старые поды при этом продолжают работать.
Объясни себе:
- Чем
Exit Code: 2принципиально отличается от137? - Почему у
OOMKilledв логах приложения может не быть ни строчки об ошибке?
Типичные ошибки:
Error from server (BadRequest): previous terminated container "notes" in pod "..." not found: контейнер ещё не падал; подожди или убери--previous.error: unable to upgrade connection: container not found: контейнер как раз перезапустился; повтори команду.
Задание 3. Под здоров, а сайт не отвечает
Цель: пройти путь запроса и найти разрыв на уровне Service.
Предскажи: что покажет kubectl get endpoints notes -n notes после смены selector на notez и что при этом будет с подами?
Ответ
Endpoints станут <none>, поды останутся Running и Ready, а клиент через Gateway получит ошибку 503.
Шаги:
- Убедись в норме:
kubectl get endpoints notes -n notes. -
Сломай selector.
--type=mergeозначает «слей мой JSON с существующим объектом»; вcurl-sбез прогресса,-o /dev/nullбез тела,-w '%{http_code}\n'только код ответа:kubectl patch svc notes -n notes --type=merge -p '{"spec":{"selector":{"app.kubernetes.io/name":"notez"}}}' kubectl get endpoints notes -n notes kubectl get pods -n notes -l app.kubernetes.io/name=notes curl -s -o /dev/null -w '%{http_code}\n' http://notes.lab/ -
Найди причину сравнением и верни исправление.
-o wideдобавляет вget svcколонкуSELECTOR,--show-labelsвget podsколонкуLABELS:kubectl get svc notes -n notes -o wide kubectl get pods -n notes --show-labels | head -3 kubectl patch svc notes -n notes --type=merge -p '{"spec":{"selector":{"app.kubernetes.io/name":"notes"}}}' kubectl get endpoints notes -n notes
Что должно получиться:
NAME ENDPOINTS AGE
notes <none> 2h
503
Как читать вывод: <none> в колонке ENDPOINTS значит, что Service не нашёл ни одного подходящего пода. Код 503 говорит, что Gateway жив, но пересылать некуда. После исправления в ENDPOINTS появятся адреса вида 10.244.x.x:8080 (по числу подов).
Если ты выкатывал Service через Helm, правка через kubectl patch временная: следующий helm upgrade её перезапишет.
Объясни себе:
- Как связаны
selectorService и метки подов, и где это видно в выводе? - Что отличает 503 от 502 в цепочке Gateway, Service, под?
Типичные ошибки:
curl: (6) Could not resolve host: notes.lab: нет записи в/etc/hosts; добавь127.0.0.1 notes.lab.Error from server (NotFound): services "notes" not found: не тот namespace; добавь-n notes.
Задание 4. Шаг проекта: runbook docs/runbooks/k8s-triage.md
Цель: закрепить алгоритм документом, который откроет дежурный в ночь инцидента.
Предскажи: какая строка в таком документе должна стоять первой: «как искать причину» или «как вернуть сервис»?
Ответ
«Как вернуть сервис»: если сбой начался после выкатки, сначала откат, потом разбор.
Шаги:
-
Создай файл.
mkdir -pсоздаёт каталог вместе с родителями;cat > файл <<'EOF' ... EOFзаписывает всё до строкиEOFв файл, а кавычки вокругEOFне дают оболочке трогать$и обратные кавычки внутри:mkdir -p ~/notes/docs/runbooks cat > ~/notes/docs/runbooks/k8s-triage.md <<'EOF' # Runbook: под или сервис «Заметок» не работает Контекст `kind-notes`, namespace `notes`. Сначала откат, потом причина. ## 0. Началось после выкатки? - `helm history notes -n notes`, затем `helm rollback notes <ревизия> -n notes`. - Проверка: `kubectl rollout status deployment/notes -n notes`. ## 1. Статус - `kubectl get pods -n notes -o wide` - Pending: события `FailedScheduling` (requests, taint, PVC). - ImagePullBackOff: имя и тег образа, доступ к реестру. - CreateContainerConfigError: ключ в Secret или ConfigMap. - CrashLoopBackOff: шаг 3, код выхода. - Running, но 0/1 Ready: путь readiness-пробы и зависимость (БД). ## 2. События - `kubectl get events -n notes --sort-by=.lastTimestamp` - `kubectl describe pod <под> -n notes` (блок Events в конце). ## 3. Логи и код выхода - `kubectl logs <под> -n notes --previous` - 1 или 2: ошибка приложения. 137: OOMKilled или kill по liveness. 143: штатная остановка. ## 4. Сеть - `kubectl get endpoints notes db -n notes`: пусто значит selector или нет Ready-подов. - `kubectl port-forward svc/notes 8080:8080 -n notes`, затем `curl -i http://127.0.0.1:8080/readyz`. - `kubectl get gateway,httproute -n notes`: условия Accepted и Programmed. - `kubectl get networkpolicy -n notes`: не режет ли политика DNS или порт 5432. - В поде без инструментов: `kubectl debug -it <под> -n notes --image=busybox:1.37 --target=notes --profile=restricted -- sh`. EOF -
Проверь файл и добавь в git.
wc -lсчитает строки,git addиgit commitсохраняют файл в истории:wc -l ~/notes/docs/runbooks/k8s-triage.md cd ~/notes && git add docs/runbooks/k8s-triage.md && git commit -m "docs: runbook k8s-triage"
Что должно получиться (путь, хеш коммита и число строк у тебя будут свои):
35 /home/user/notes/docs/runbooks/k8s-triage.md
[main 3f2a1c7] docs: runbook k8s-triage
1 file changed, 35 insertions(+)
Как читать вывод: первая строка это число строк файла и его путь, вторая имя ветки и короткий хеш коммита, третья сколько строк добавлено. Если в файле пусто, ищи проблему в EOF.
Чарт helm/notes (версия 0.2.0) и k8s/base/ не меняются. Эталон: project/notes/docs/runbooks/k8s-triage.md в репозитории курса.
Объясни себе:
- Чем runbook помогает уставшему человеку в три часа ночи, чего не даёт память?
- Что в нём нужно поправить после первого настоящего инцидента?
Типичные ошибки:
fatal: not a git repository (or any of the parent directories): .git: командаgitзапущена вне~/notes; выполниcd ~/notes.- Пустой файл или
EOF: command not found: закрывающийEOFдолжен стоять в начале строки без пробелов. В уроке он показан с отступом из-за списка: в терминале убери отступ.
Нейросеть не видит твой кластер: каждую её команду из runbook выполни сам на тестовом поде и сверь с выводом.
Сломай и почини
Теперь без подсказок. Скрипт применит одну из поломок, знакомых по урокам 5.x. Читать его нельзя: цель найти причину алгоритмом, а не подсмотреть. Запускай без sudo, нужны релиз из урока 5.9 и Service из урока 5.3.
curl -fsSL -o /tmp/break-5.13.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/5.13/break.sh
bash /tmp/break-5.13.sh random
Скрипт печатает только «поломка применена». Засеки время. Задача решена, когда все поды Ready, а curl http://notes.lab/readyz отвечает 200. Номера 1-5 запускают конкретную поломку. Вернуть как было: bash /tmp/break-5.13.sh fix.
Симптом
Сайт notes.lab отвечает ошибкой или не отвечает, а состояние подов ты не знаешь. Запиши симптом одной строкой: код ответа, статус подов, время начала.
Гипотезы
Составь список из 3-4 вариантов, прежде чем что-то менять:
- Образ или тег не тот (
ImagePullBackOff). - Под не размещается или не создаётся (
Pending,CreateContainerConfigError). - Приложение падает (
CrashLoopBackOff: конфиг, команда, память). - Под жив, но Service или пробы его не видят (пустые endpoints,
0/1 Ready).
Проверки
Иди по алгоритму, каждый шаг вычёркивает часть гипотез:
kubectl get pods -n notes -o wide
kubectl get events -n notes --sort-by=.lastTimestamp | tail -15
kubectl describe pod -n notes '<проблемный-под>' | tail -25
kubectl logs -n notes '<проблемный-под>' --previous --tail=20
kubectl get endpoints notes db -n notes
kubectl get svc notes -n notes -o wide
Исправление
Сначала попробуй найти и исправить сам. Если завис более 10 минут, откат: helm rollback notes <ревизия> -n notes или kubectl rollout undo deployment/notes -n notes. Сломанный Service откатом Deployment не лечится, там правка selector. Разбор поломок скрипта:
Разбор сценариев
- Неверный образ.
ImagePullBackOff, в событияхFailed to pull image ... not found. Исправление: вернуть верный тег (helm upgradeилиkubectl rollout undo). Старые поды работают, сайт отвечает. - Нет ключа в Secret.
CreateContainerConfigError,couldn't find key NO_SUCH_KEY. Исправление: убрать лишнюю переменнуюBREAK_X(kubectl -n notes set env deploy/notes BREAK_X-) или добавить ключ в Secret. - Пустые endpoints. Поды здоровы,
ENDPOINTS <none>, сайт отвечает 503. Исправление: selector Service равен меткам подов (app.kubernetes.io/name: notes). - Падает приложение.
CrashLoopBackOff,Exit Code: 2, причина вlogs --previous: не найден/no/such/app.py. Исправление: убрать переопределённуюcommand. - Проба.
Running,0/1 Ready, в событияхReadiness probe failed ... 404. Исправление: путь пробы/readyz, порт 8080.
Контроль: все поды Ready, kubectl get endpoints notes -n notes показывает адреса, curl -s -o /dev/null -w '%{http_code}\n' http://notes.lab/readyz печатает 200.
ИИ в помощь
Нейросеть хорошо расшифровывает describe pod и коды выхода, но не видит твой кластер и может пойти по неверной стадии. Общие правила: ИИ-помощник.
Задача: определить стадию и причину.
Я учу Kubernetes. Под не работает. Вот вывод kubectl get pods и kubectl describe pod (блоки State, Last State, Events):
<вставь вывод>
Скажи, на какой стадии остановился под, какая причина вероятнее всего и какую одну команду выполнить дальше.
Проверь ответ: сверь с таблицей статусов из урока: стадия должна совпадать с STATUS, а код выхода с причиной. Типичная ошибка: советует смотреть логи у пода, который не стартовал.
Задача: найти, где обрывается путь запроса.
Поды Running и Ready, но сайт отвечает 503. Вот вывод kubectl get endpoints, get svc -o wide, get pods --show-labels:
<вставь вывод>
Пройди по пути запроса от Gateway до пода и скажи, на каком звене обрыв.
Проверь ответ: сравни selector Service и метки подов сам и проверь endpoints. Типичная ошибка: объяснение про нехватку реплик вместо проверки меток.
Задача: составить runbook.
Составь короткий runbook для дежурного по статусам Pending, ImagePullBackOff и CrashLoopBackOff:
симптом, первые команды, развилки по коду выхода, когда эскалировать, как откатить.
Проверь ответ: убедись, что есть условия эскалации и откат, а команды выполняются у тебя без правок. Типичная ошибка: начинать с kubectl delete pod.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Под (pod) | минимальная единица запуска: один или несколько контейнеров с общей сетью |
| Планировщик (scheduler) | компонент, который выбирает узел для нового пода |
| Kubelet | агент на узле: готовит тома и конфиг, запускает и перезапускает контейнеры |
| Узел (node) | машина кластера, на которой работают поды |
| Событие (event) | короткая запись «что случилось с объектом», живёт около часа |
| Requests | гарантированный минимум CPU и памяти, по нему планировщик выбирает узел |
| Taint | пометка узла «сюда не ставить поды без разрешения» |
nodeSelector |
требование «поставить под только на узлы с такой меткой» |
| PVC | запрос пода на постоянный диск |
Pending |
под создан, но не размещён на узле |
ImagePullBackOff |
образ не скачивается, kubelet повторяет с растущей паузой |
CreateContainerConfigError |
контейнер не создан из-за ошибки в конфигурации (ключ, ConfigMap) |
CrashLoopBackOff |
контейнер повторно падает, паузы между запусками растут до 5 минут |
| Код выхода (exit code) | число, с которым завершился процесс: 0 штатно, иначе ошибка |
| SIGKILL, SIGTERM | сигналы завершения: жёсткий (нельзя перехватить, код 137) и мягкий (код 143) |
OOMKilled |
процесс убит за превышение лимита памяти |
| Readiness-проба | проверка «готов ли под принимать трафик», при провале под исключается из Service |
| Liveness-проба | проверка «жив ли процесс», при провале контейнер перезапускают |
| Selector | отбор объектов по меткам: так Service находит свои поды |
| Endpoints | список адресов Ready-подов, которые нашёл Service |
port-forward |
проброс порта с твоей машины в под или Service в обход Gateway |
| Ревизия Helm | номер версии релиза, каждый helm upgrade создаёт новую |
| Ephemeral-контейнер | временный контейнер с инструментами, подключаемый к работающему поду |
| Runbook | короткая инструкция для дежурного: что делать, когда случилось X |
| Контекст (context) | Запись в ~/.kube/config: в какой кластер и под каким пользователем идут команды kubectl |
-n и -A |
Флаги kubectl: указать namespace / искать во всех namespace |
--rollback-on-failure |
Флаг Helm 4: при неудачной выкатке вернуть прошлую рабочую ревизию (раньше --atomic) |
--wait |
Флаг Helm: ждать, пока поды станут Ready, прежде чем считать выкатку успешной |
| Алерт (alert) | Автоматическое сообщение мониторинга о том, что что-то сломалось |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Под в статусе CrashLoopBackOff. Что делаешь?
Ответ
Смотрю kubectl describe pod, блок Last State: причину и код выхода. Затем kubectl logs --previous, потому что нужен лог упавшего запуска. Дальше по коду: 1 или 2 значит ошибка приложения или конфига, 137 значит память или kill по liveness.
Что хотят услышать: --previous, код выхода, различие ошибки приложения и OOMKilled, проверку конфигурации и зависимостей.
Красный флаг: «удалю под, и он пересоздастся» без выяснения причины.
2. [junior] [часто] Под висит в Pending. Что проверишь?
Ответ
describe pod и события: планировщик пишет FailedScheduling и причину (не хватает CPU или памяти под requests, taint без toleration, nodeSelector, PVC не привязан). Логов нет, потому что контейнер не запускался.
Что хотят услышать: Pending значит «не размещён», а не «приложение сломано»; requests, taints, PVC, квоты.
Красный флаг: искать причину в логах приложения.
3. [junior] [часто] [на скорость] Какие четыре команды ты выполнишь первыми, когда под не работает?
Ответ
get pods (статус и READY), get events --sort-by=.lastTimestamp (что говорит система), describe pod (Last State, Events), logs с --previous для упавшего. Порядок от дешёвого к дорогому, логи последними, потому что у многих статусов их нет.
Что хотят услышать: порядок, --previous, понимание, где остановился под, до вопроса «почему».
Красный флаг: «сразу зайду внутрь контейнера» или «удалю под».
4. [junior] Образ не скачивается: ImagePullBackOff. Причины?
Ответ
Опечатка в имени или теге, тега нет в реестре, приватный реестр без imagePullSecrets, нет сети до реестра, лимиты реестра. Смотрю точный текст события Failed: он скажет, not found это или unauthorized.
Что хотят услышать: чтение события, различие not found и unauthorized, imagePullSecrets, запрет latest.
Красный флаг: «перезапущу под».
5. [middle] [на скорость] После выкатки сайт отвечает 502/503, поды Running. Твои действия?
Ответ
Сначала rollout status и get pods: 0/1 Ready значит readiness не проходит. Если началось сразу после релиза, откатываю (helm rollback или rollout undo), сервис поднимается, потом разбираю причину: путь пробы, зависимость от БД, endpoints, Gateway.
Что хотят услышать: откат в приоритете, endpoints, readiness против liveness, проверка по слоям (под, Service, Gateway).
Красный флаг: правка манифестов прямо на проде без отката и без понимания причины.
6. [middle] Контейнер убит с кодом 137, но в логах приложения тишина. Почему?
Ответ
SIGKILL не даёт приложению ничего записать. Причины: OOMKilled (превысил limits.memory, видно в Last State: Reason) или kill по livenessProbe, если приложение не отвечало. Смотрю describe, метрики памяти и историю рестартов.
Что хотят услышать: 137 равно 128 плюс 9, различие причин, kubectl top, requests и limits, утечка против заниженного лимита.
Красный флаг: «просто увеличу лимит вдвое» без проверки, течёт ли память.
7. [middle] Service создан, поды здоровы, а curl по имени сервиса не проходит. Как ищешь?
Ответ
kubectl get endpoints: пусто значит selector не совпал с метками (или под не Ready). Сравниваю svc -o wide и pods --show-labels, проверяю порт и targetPort, затем DNS изнутри пода и NetworkPolicy.
Что хотят услышать: endpoints как первый шаг, selector и метки, targetPort, DNS и NetworkPolicy.
Красный флаг: пересоздание пода и Service «на удачу».
8. [middle] kubectl exec не помогает, в образе нет ни shell, ни curl. Как отлаживаешь?
Ответ
kubectl debug с ephemeral-контейнером, --target на нужный контейнер, чтобы видеть его процессы; сеть общая с подом. В namespace с PSS restricted добавляю --profile=restricted. Для проблем узла есть kubectl debug node/....
Что хотят услышать: ephemeral-контейнеры, --target, отказ от «поставлю curl в прод-образ».
Красный флаг: «пересоберу образ с отладочными утилитами и выкачу в прод».
9. [middle] Приложение после включения NetworkPolicy не резолвит db. Причина?
Ответ
Политика default-deny закрыла и исходящий трафик, включая DNS. Нужно разрешить egress на kube-dns (порт 53 UDP и TCP) и на сам db:5432. Проверяю nslookup из ephemeral-контейнера и kubectl get networkpolicy.
Что хотят услышать: DNS как частая забытая зависимость, разница ingress и egress, проверка изнутри пода.
Красный флаг: «отключу NetworkPolicy совсем».
10. [middle] [на скорость] Под Running, но 0/1 Ready, рестартов нет. Почему это не баг Kubernetes?
Ответ
Readiness-проба не проходит: неверный путь или порт, зависимость (БД) недоступна, приложение долго стартует. Kubernetes намеренно не перезапускает такой под, а убирает его из endpoints. Смотрю события Unhealthy и вручную дёргаю /readyz.
Что хотят услышать: различие readiness и liveness, startupProbe, связь с endpoints.
Красный флаг: «readiness перезапускает контейнер».
11. [middle] helm upgrade завис или завершился ошибкой, прод деградирует. Порядок?
Ответ
helm status и helm history, откатываю helm rollback на последнюю рабочую ревизию. Потом причина: helm get values, kubectl get events, describe. Если релиз в pending-upgrade, проверяю, не идёт ли параллельное обновление.
Что хотят услышать: откат до анализа, ревизии Helm, --rollback-on-failure (в Helm 3 и старых статьях он назывался --atomic, в Helm 4 устарел) и --wait как профилактика.
Красный флаг: kubectl delete ресурсов чарта руками.
12. [middle] Узел в статусе NotReady. Что проверишь?
Ответ
Начинаю с kubectl describe node: секция Conditions (Ready, MemoryPressure, DiskPressure, PIDPressure) и события. Потом захожу на узел и смотрю systemctl status kubelet и journalctl -u kubelet: kubelet мог упасть или потерять связь с API-сервером. Проверяю среду выполнения контейнеров, место на диске, память, сеть до control plane и время на узле. Поды на узле через некоторое время будут вытеснены и пересозданы на других, если есть контроллеры.
Что хотят услышать: describe node и conditions, kubelet и его логи, runtime, диск, память, сеть, поведение подов.
Красный флаг: Сразу пересоздать узел без диагностики.
13. [middle] Под завис в Terminating и не удаляется. Что делаешь?
Ответ
Смотрю kubectl describe pod и kubectl get pod -o yaml: есть ли finalizers, и на каком узле он живёт. Причины: узел недоступен, kubelet не подтверждает остановку, процесс не завершается по SIGTERM (до terminationGracePeriodSeconds), висит финалайзер. Сначала устраняю причину: восстанавливаю узел или убираю финалайзер, если владелец его уже не обработает. Принудительное удаление --grace-period=0 --force оставляю на крайний случай: для StatefulSet это риск двух копий.
Что хотят услышать: finalizers, состояние узла, grace period, force как крайняя мера и её риск для StatefulSet.
Красный флаг: Всегда удалять с –force.
14. [middle] Под в статусе Evicted. Почему так бывает и что делать?
Ответ
Kubelet вытесняет поды, когда на узле заканчиваются ресурсы: память, диск, inode, PID. Причину вижу в kubectl describe pod (Reason и Message) и в conditions узла. Чиню корень: ставлю requests и limits, включая ephemeral-storage, чищу логи и образы, добавляю диск или узлы. Сами Evicted-поды остаются в статусе Failed и их можно удалить kubectl delete pod. Если Deployment жив, новые поды создаются автоматически.
Что хотят услышать: node-pressure eviction, причина в describe, ephemeral-storage, requests и limits, уборка Failed-подов.
Красный флаг: Считать Evicted ошибкой приложения, не глядя на узел.
Проверено на версиях
- Проверено командами:
shellcheckдляbreak.shбез замечаний (скрипт на кластере не запускался). - Не прогонялось: кластер kind не запускался. Вывод
kubectl,describe, события и коды ответов написаны по документации и предыдущей редакции урока; хеши, возраст и число узлов у тебя будут другими. - kind: версия не закреплена, проверь актуальную версию на странице проекта
- kubectl: версия не закреплена, проверь актуальную версию на странице проекта
- Envoy Gateway: v1.9.2
- PostgreSQL: 18
- Python (образ приложения): 3.13
- Образ приложения «Заметки»: 0.4.1, chart 0.2.0
- busybox (для отладки): 1.37
Итог урока: ты умеешь
- умею по статусу пода определить стадию, на которой он остановился
- умею читать события и блок
Eventsвkubectl describe - умею отличать ошибку приложения (код 1 или 2) от
OOMKilled(код 137) и получать логи упавшего запуска через--previous - умею найти пустые endpoints и сравнить
selectorс метками подов - умею пройти путь запроса Gateway, Service, endpoints, под и найти разрыв
- умею подключить ephemeral-контейнер через
kubectl debugв namespace с PSSrestricted - умею откатить релиз (
helm rollback,rollout undo) раньше, чем найду причину - умею оформить алгоритм разбора в runbook
docs/runbooks/k8s-triage.md
Дальше: Тема 6: Облако
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.