Представь дежурство. Прод лежит, в Slack паника, у тебя терминал и боль. Ты не зайдёшь по ssh на ноду чинить руками - в Kubernetes так не делают. У тебя есть ровно один инструмент, через который проходит ВСЁ общение с кластером - kubectl. Это HTTP-клиент к kube-apiserver, не больше и не меньше. Каждая команда kubectl - это REST-запрос к API: GET, POST, PATCH, DELETE по объектам в etcd. Понимаешь это - перестаёшь бояться и начинаешь видеть, что происходит под капотом.
Разница между джуном и сеньором на инциденте часто не в знании архитектуры, а в скорости рук. Джун открывает дашборд, кликает, теряется. Сеньор за 15 секунд набирает три команды kubectl и уже знает, какой под падает, почему и с какого коммита. Этот урок - про то, как из медленного тыканья сделать мгновенную навигацию по кластеру. Разберём kubectl get во всех режимах, kubectl describe с событиями, explain как встроенную документацию, и jsonpath - язык, которым ты выжимаешь из API ровно нужное поле.
Маленькая, но важная вещь сразу. Заведи алиас, иначе сотрёшь клавиатуру:
Код: Выделить всё
alias k=kubectl
# и автодополнение, без него ты слепой:
source <(kubectl completion bash) # для zsh: kubectl completion zsh
complete -o default -F __start_kubectl k

kubectl get: от списка до точного поля
kubectl get - самая частая команда дня. По умолчанию она показывает урезанную таблицу, которую формирует не клиент, а сам сервер (server-side printing): apiserver отдаёт уже готовые колонки. Поэтому ширины и набор колонок зависят от версии. Базовый вызов:
Код: Выделить всё
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
api-7d9f8c5b6-2xk9p 1/1 Running 0 4d2h
api-7d9f8c5b6-jq4lm 0/1 CrashLoopBackOff 7 (2m ago) 18m
worker-5c4d8b9f7-pp7wq 2/2 Running 1 (3h ago) 4d2h
Дальше включаем рентген. Флаг -o wide добавляет ноду, IP пода и образ - бесценно, когда подозреваешь конкретную ноду:
Код: Выделить всё
$ kubectl get pods -o wide
NAME READY STATUS NODE IP NOMINATED NODE
api-7d9f8c5b6-2xk9p 1/1 Running cl-node-fra-03 10.112.4.27 <none>
- -A (или --all-namespaces) - смотреть по всему кластеру, а не только в текущем namespace. На инциденте почти всегда нужен именно он.
- -w - watch, поток событий в реальном времени. Открой в соседнем окне kubectl get pods -w и смотри, как под проходит Pending -> ContainerCreating -> Running.
- --show-labels - показать все метки, чтобы понять, какому Deployment/релизу принадлежит под.
- -l app=api,tier!=cache - селектор по меткам, фильтрует выборку на стороне сервера.
- --sort-by=.status.startTime - сортировка по любому полю объекта.
Код: Выделить всё
# все непущенные поды по всему кластеру, отсортированные по числу рестартов:
$ kubectl get pods -A --field-selector=status.phase!=Running \
--sort-by='.status.containerStatuses[0].restartCount'
Теперь главное оружие kubectl get - форматы вывода. -o yaml и -o json отдают объект целиком, как он лежит в etcd, со status и managedFields. Отсюда родился jsonpath - способ вытащить одно конкретное поле без grep и без глаз на весь YAML:
Код: Выделить всё
# IP всех подов в столбик:
$ kubectl get pods -o jsonpath='{.items[*].status.podIP}'
10.112.4.27 10.112.5.13 10.112.4.41
# образ в каждом поде, по строке на под (range - это цикл):
$ kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'
api-7d9f8c5b6-2xk9p registry.cyberlake.ru/api:1.8.2
worker-5c4d8b9f7-pp7wq registry.cyberlake.ru/worker:0.4.1
Код: Выделить всё
$ kubectl get pods -o custom-columns=\
'NAME:.metadata.name,NODE:.spec.nodeName,RESTARTS:.status.containerStatuses[0].restartCount,IMG:.spec.containers[0].image'
NAME NODE RESTARTS IMG
api-7d9f8c5b6-2xk9p cl-node-fra-03 0 registry.cyberlake.ru/api:1.8.2
kubectl describe: читаем события, а не только спеку
Если kubectl get отвечает на вопрос "что есть", то kubectl describe отвечает "что с ним происходит". Под капотом describe делает не один запрос: тянет сам объект плюс связанные Events из namespace и склеивает в человекочитаемый текст. Самое ценное внизу - секция Events:
Код: Выделить всё
$ kubectl describe pod api-7d9f8c5b6-jq4lm
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 3m scheduler Successfully assigned default/api-... to cl-node-fra-03
Normal Pulled 3m kubelet Container image "api:1.8.2" already present on machine
Warning BackOff 30s (x6 over 2m) kubelet Back-off restarting failed container api
Warning Unhealthy 10s (x4 over 1m) kubelet Liveness probe failed: HTTP probe returned 500
Важная ловушка: события в Kubernetes по умолчанию живут около часа, потом etcd их вычищает. Если инцидент был ночью, а ты смотришь утром - событий уже нет. Поэтому на проде Events экспортируют в централизованное хранилище. И ещё: x6 over 2m рядом с Age значит, что событие схлопнуто - оно повторялось 6 раз, kubectl не дублирует строки.
Отдельная команда, которую недооценивают, - kubectl get events. Она даёт хронологию по всему namespace, а не по одному объекту:
Код: Выделить всё
$ kubectl get events --sort-by=.lastTimestamp -A
Никто не помнит наизусть структуру манифеста. И не надо. kubectl explain - это встроенная документация по API прямо из кластера, она всегда соответствует твоей версии Kubernetes, в отличие от статьи из интернета:
Код: Выделить всё
$ kubectl explain pod.spec.containers.resources
KIND: Pod
FIELD: resources <ResourceRequirements>
DESCRIPTION:
Compute Resources required by this container...
FIELDS:
limits <map[string]Quantity>
requests <map[string]Quantity>
Поскольку 2026 год, помни про актуальные ресурсы: для трафика на входе индустрия мигрирует с Ingress на Gateway API (Gateway, HTTPRoute), для безопасности подов вместо мёртвого PodSecurityPolicy работает Pod Security Admission. explain знает обо всех CRD, установленных в кластере, - проверь explain httproute.spec, если в кластере стоит Gateway API.
Изменяем объекты: apply, diff, patch, edit
Менять кластер можно по-разному, и выбор способа отличает аккуратного инженера. Главное правило 2026 - декларативно через kubectl apply -f, а перед применением всегда смотреть diff:
Код: Выделить всё
$ kubectl diff -f deployment.yaml
Связанный флаг - --dry-run, и тут важно различать два значения. --dry-run=client просто печатает объект, который УШЁЛ БЫ на сервер, локально, без отправки. А --dry-run=server реально гоняет запрос через apiserver: проходит валидацию, admission-контроллеры, applies defaults - но не пишет в etcd. Для генерации манифестов используют client, для проверки "пройдёт ли это вообще" - server:
Код: Выделить всё
# сгенерить заготовку Deployment, не создавая его:
$ kubectl create deployment api --image=api:1.8.2 \
--dry-run=client -o yaml > deployment.yaml
Код: Выделить всё
# strategic (по умолчанию) - меняем число реплик, остальное цело:
$ kubectl patch deployment api -p '{"spec":{"replicas":5}}'
# json patch - точечная операция по индексу:
$ kubectl patch deployment api --type='json' \
-p '[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"api:1.8.3"}]'
Боль инцидента: logs, exec, debug, port-forward, top
kubectl logs - первое, что ты смотришь, когда под жив, но врёт. Базовое - logs <pod>, но реальная мощь в флагах:
- -f - follow, поток в реальном времени, как tail -f.
- --previous (-p) - логи ПРЕДЫДУЩЕГО упавшего контейнера. При CrashLoopBackOff текущий контейнер только что родился и пуст - правда в логах прошлого воплощения, бери -p.
- -l app=api - логи сразу со всех подов по метке, не нужно перечислять имена.
- --all-containers и -c <name> - в поде с sidecar укажи, чей лог хочешь.
- --since=10m, --tail=100 - ограничить хвост, иначе утонешь.
Код: Выделить всё
$ kubectl logs -l app=api --all-containers --since=5m --prefix
Код: Выделить всё
$ kubectl exec -it api-7d9f8c5b6-2xk9p -- sh
Код: Выделить всё
$ kubectl debug -it api-7d9f8c5b6-2xk9p \
--image=nicolaka/netshoot --target=api -- bash
Дополни арсенал: kubectl port-forward svc/api 8080:80 пробрасывает порт сервиса/пода на localhost - незаменимо, чтобы потыкать внутренний сервис без Ingress. kubectl cp копирует файлы в под и обратно (внутри контейнера должен быть tar). kubectl top pods и kubectl top nodes показывают реальное потребление CPU/памяти - но только если в кластере стоит metrics-server, иначе команда молча вернёт ошибку.
Контексты, namespace и навигация без боли
Один ~/.kube/config держит несколько кластеров. Триада понятий: cluster (адрес apiserver), user (твои креды), context (связка cluster+user+namespace). Команда смотрит на текущий контекст:
Код: Выделить всё
$ kubectl config get-contexts
CURRENT NAME CLUSTER NAMESPACE
* cl-prod cl-prod payments
cl-staging cl-staging default
Расширяется kubectl плагинами через менеджер krew. kubectl krew install ставит плагины, дальше они вызываются как kubectl <plugin>. Маст-хэв на 2026: ns/ctx (если без kubectx), neat (чистит вывод -o yaml от мусора managedFields и status), tree (показывает дерево владения объектов через ownerReferences), netshoot. И финальный аккорд скорости - метки. kubectl label и kubectl annotate массово навешивают метки/аннотации, что превращает -l в точный фильтр по сотням объектов.
Грабли и антипаттерны из реальной эксплуатации
- Смотришь logs упавшего пода без --previous и видишь пустоту - ты читаешь свежий, пустой контейнер. Правда в -p.
- kubectl edit на проде с GitOps - правка живёт минуты, потом Argo откатит. Меняй в git.
- Спутал --field-selector и -l: первый по серверным полям, второй по меткам. По метке через field-selector не отфильтруешь.
- JSON merge patch там, где нужен strategic - затёр список контейнеров/портов целиком. Для нативных объектов оставляй стратегию по умолчанию.
- kubectl top без metrics-server - команда падает не из-за кластера, а из-за отсутствия аддона.
- Забыл -A на инциденте и полчаса смотрел не тот namespace. На проде по умолчанию ставь -A.
- Доверяешь статье из интернета про поля манифеста вместо kubectl explain своей версии - и ловишь unknown field при apply.
Нужен любой кластер - minikube, kind, k3s или Yandex Managed Kubernetes. Выполни по шагам:
- Подними деплой: kubectl create deployment lab --image=nginx --replicas=3.
- Смотри рождение подов в реальном времени: kubectl get pods -l app=lab -w (во втором окне).
- Достань ноды и IP всех подов одним jsonpath с {range}...{end}.
- Сломай образ: kubectl set image deployment/lab nginx=nginx:doesnotexist - и найди причину через kubectl describe pod (ищи ErrImagePull/ImagePullBackOff в Events).
- Сделай kubectl diff -f с исправленным манифестом и убедись, что видишь разницу до применения.
- Зайди в под через kubectl exec -it ... -- sh, потом то же через kubectl debug с образом netshoot.
- Прокинь порт: kubectl port-forward deploy/lab 8080:80 и открой localhost:8080.
- Чем отличается --dry-run=client от --dry-run=server и в какой задаче нужен каждый?
- Почему при CrashLoopBackOff надо смотреть kubectl logs с флагом --previous?
- В чём разница strategic merge patch и JSON merge patch при правке списка контейнеров?
- Где kubectl describe берёт секцию Events и почему утром их может уже не быть?
kubectl - это REST-клиент к apiserver, и вся его магия раскладывается на простые кубики: get для обзора и точечной выборки через jsonpath/custom-columns, describe для событий и причин, explain как живую документацию по схеме, diff/patch/apply для безопасных изменений, logs/exec/debug/port-forward для разбора инцидента. Добавь алиас, автодополнение, kubectx/kubens и пару плагинов krew - и навигация по кластеру из мучения превратится в три уверенных нажатия. Эти команды kubectl ты будешь набирать тысячи раз, так что вложенные сегодня полчаса в мышечную память окупятся на первом же ночном дежурстве.