Restart policy и CrashLoopBackOff
Когда контейнер в Pod завершается, kubelet смотрит на spec.restartPolicy и решает: запустить заново, оставить как есть, или повторить только если выход был с ошибкой. Эта механика лежит в основе двух классов задач: stateless приложений (всегда перезапускаем) и batch-задач (запустить один раз и забыть). И именно она порождает наш любимый статус — CrashLoopBackOff, который на CKAD-экзамене встречается чаще, чем все остальные ошибки вместе взятые.
--restart: жизненный цикл контейнера в Docker
Три значения restartPolicy
spec.restartPolicy — поле на уровне Pod, не контейнера. Применяется ко всем контейнерам Pod одинаково:
Главное ограничение: контроллеры верхнего уровня требуют конкретный restartPolicy.
| Workload | restartPolicy |
|---|---|
| Deployment, ReplicaSet, StatefulSet, DaemonSet | только Always (валидируется API server) |
| Job, CronJob | только OnFailure или Never |
| Голый Pod (kubectl run) | любое из трёх |
Это потому что Deployment предполагает «должно быть N replicas, всегда». Если контейнер вышел — Deployment не пересоздаёт Pod (это сделал бы только если Pod исчез). Он рассчитывает, что kubelet перезапустит контейнер внутри того же Pod-а.
apiVersion: v1
kind: Pod
metadata:
name: one-shot
spec:
restartPolicy: OnFailure
containers:
- name: migrate
image: myapp:1.0
command: ["./migrate.sh"]
Exponential backoff
Когда контейнер падает быстро (упал, kubelet запустил, снова упал), kubelet не хочет тратить CPU на бесконечный restart loop. Поэтому каждый следующий restart происходит с возрастающей паузой.
В это время state.waiting.reason = CrashLoopBackOff и message: back-off Xs restarting failed container. Pod в фазе Running, но restartCount растёт каждый цикл.
С v1.33 настройки backoff для отдельных рабочих нагрузок (KEP-4603 / per-workload backoff) ещё не общедоступны — это поведение жёстко зашито в kubelet. Не пытайтесь искать restartBackoffMaxSeconds в Pod spec для GA-кластеров.
CrashLoopBackOff — это симптом, не причина
CrashLoopBackOff — это состояние kubelet, не контейнера. Оно означает: «контейнер падает быстрее, чем я могу его перезапускать, поэтому я жду». Сама причина падения — где-то в логах или конфиге.
Что НЕ помогает:
kubectl delete pod backend-7d8
Если Pod управляется Deployment-ом, Deployment пересоздаст его с тем же образом и той же конфигурацией. Получится точно такой же crash. Удаление Pod лечит только редкие случаи (засевший corrupt state), но не системные причины — bug в коде, неправильный env, отсутствующий Secret.
Что помогает:
# 1. Логи последнего падения
kubectl logs backend-7d8
# 2. Логи ПРЕДЫДУЩЕГО запуска — критично
kubectl logs backend-7d8 --previous
# 3. События K8s вокруг этого Pod
kubectl describe pod backend-7d8
# 4. Статус контейнеров: lastState.terminated.reason и exitCode
kubectl get pod backend-7d8 -o yaml | yq '.status.containerStatuses'
--previous — самый важный флаг. Когда kubelet ждёт backoff, контейнер не запущен, и обычный kubectl logs вернёт пусто. --previous достаёт логи последнего упавшего инстанса (kubelet хранит их некоторое время на узле).
Алгоритм CKAD при CrashLoopBackOff: (1) kubectl describe pod <name> — смотрим Events внизу; (2) kubectl logs <name> --previous — смотрим, что писало приложение перед смертью; (3) проверяем exitCode и reason в containerStatuses. Большинство кейсов закрываются на шаге 1 или 2.
Типичные причины crash-loop
| Причина | Что увидите | Как лечить |
|---|---|---|
| exit 1 + log с stack trace | kubectl logs --previous покажет ошибку приложения | Чинить код или окружение |
| exit 137, reason OOMKilled | lastState.terminated.reason: OOMKilled | Увеличить memory limit или починить утечку |
| exit 1, нет логов | Приложение падает до записи логов | Проверить ENTRYPOINT/CMD, env variables, ConfigMap |
CreateContainerConfigError | Reason в waiting state | Не существует Secret/ConfigMap из envFrom или volumeMount |
| Падает на startup probe | Events: Liveness probe failed: ... | Увеличить initialDelaySeconds или починить health endpoint |
ContainerCannotRun + exit 127 | exitCode 127 — «command not found» | command/args ссылается на бинарник, которого нет в образе |
ContainerCannotRun + exit 126 | exitCode 126 — «permission denied» | runAsUser не имеет прав на запуск entrypoint |
CKAD типовая задача
Из реальных задач сертификации:
Pod
app-brokenв namespacedevнаходится в CrashLoopBackOff. Определите причину и почините, не пересоздавая Pod вручную.
Решение шаг за шагом:
# 1. Что говорит K8s
kubectl describe pod app-broken -n dev | tail -20
# Events:
# Warning BackOff 10s (x12 over 3m) kubelet Back-off restarting failed container
# 2. Логи предыдущего запуска
kubectl logs app-broken -n dev --previous
# Error: cannot find environment variable DATABASE_URL
# 3. Проверим, откуда должен браться DATABASE_URL
kubectl get pod app-broken -n dev -o yaml | yq '.spec.containers[0].envFrom'
# - configMapRef:
# name: app-config
# 4. Существует ли этот ConfigMap?
kubectl get configmap app-config -n dev
# Error from server (NotFound): configmaps "app-config" not found
# 5. Чиним:
kubectl create configmap app-config -n dev \
--from-literal=DATABASE_URL=postgres://...
# 6. kubelet сам подхватит изменение при следующем restart-попытке (в пределах backoff)
kubectl get pod app-broken -n dev -w
Заметьте: ничего не пересоздавали. Pod в Running после следующего restart-tick — потому что envFrom ConfigMap резолвится при старте контейнера, и теперь ConfigMap есть.
Если бы это было CreateContainerConfigError — kubelet вообще не запускает контейнер, пока конфиг не починят. В этом случае restart backoff не применяется (нечего перезапускать), Pod просто висит в waiting. После создания ConfigMap kubelet увидит изменение и попробует снова — но иногда нужно kubectl delete pod (Deployment пересоздаст), чтобы ускорить. Это исключение из правила «не удаляйте Pod».
Зачем нужен Never
restartPolicy: Never кажется бесполезным — зачем не перезапускать? Реальные кейсы:
- Миграция БД: должна выполниться один раз. Если упала — нужно вручную разобраться, не повторять автоматически. Job с
restartPolicy: NeverиbackoffLimit: 0гарантирует «одна попытка». - Debug-pod:
kubectl run debug --rm -it --restart=Never --image=busybox -- /bin/sh. Послеexitshell Pod удаляется. - Один билд в CI-под: запустил, отработал, забыл.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: myapp:1.0
command: ["./migrate.sh"]