63 KiB
Учебный курс: Kubernetes с нуля
Это учебный документ, а не легенда — здесь нет утверждений «делал на работе». Цель: пройти путь от нуля до уверенного pet-уровня, объясняя себе на каждом шаге «что это и зачем», а не просто копируя команды. Итоговая позиция на собеседовании не меняется: K8s — честная домашняя практика поверх рабочего опыта с Docker/Ansible/мониторингом (см. ../../legend/LEGEND.md → «Домашняя лаборатория / pet-проект»).
Как этим пользоваться: проходить модули по порядку, не пропускать практику — каждый следующий модуль опирается на состояние кластера, оставленное предыдущим. Когда весь курс пройден руками один раз, для повторения перед конкретным собеседованием используй более компактный CASE.md — он про то же самое, но без объяснений, как шпаргалка. Вопросы с готовыми ответами — в QUESTIONS.md.
Формат каждого модуля: зачем это → теория-минимум → практика руками → самопроверка → как это в реальных проектах.
Окружение курса (одно на все модули, то же, что в CASE.md): Windows + Docker Desktop, кластер kind с именем lab, 1 control-plane + 2 worker. Все команды курса — в bash-синтаксисе (heredoc, base64 -d, настоящий curl), поэтому выполнять их нужно в Git Bash (ставится вместе с Git for Windows), а не в PowerShell/cmd — там половина команд не сработает или сработает иначе (curl в PowerShell — алиас на Invoke-WebRequest). Docker Desktop стоит выделить не меньше 8 ГБ RAM — иначе стек мониторинга из модуля 12 повиснет в Pending. Не пересоздавай кластер между модулями без необходимости — большинство модулей продолжают работать в одном и том же кластере и в одних и тех же namespace (demo, monitoring), это ближе к тому, как выглядит реальная работа с уже существующим кластером.
Альтернативное окружение — прохождение курса на удалённом Ubuntu-сервере по SSH вместо Windows + Docker Desktop: спеки железа, бенчмарки и точечные адаптации команд по модулям — в SERVER.md. Текущий прогресс прохождения (какой модуль пройден, на чём остановился, состояние кластера) — в PROGRESS.md, построчный разбор каждой выполненной команды с объяснением аргументов — в COMMANDS.md, а как просто запустить/остановить/проверить стенд между сессиями без учебных объяснений — в USAGE.md.
Модуль 0. Установка инструментов
Зачем это
Три разных инструмента решают три разные задачи, и важно не путать их роли: Docker — где физически крутятся контейнеры, kind — как за секунды поднять тестовый K8s-кластер поверх Docker без отдельных виртуалок, kubectl — как разговаривать с любым K8s-кластером (хоть pet, хоть прод), Helm — как ставить готовые наборы манифестов одной командой вместо ручного kubectl apply на десяток файлов.
Теория-минимум
kind = Kubernetes IN Docker: каждая «нода» кластера — это на самом деле Docker-контейнер, внутри которого запущен полноценный K8s. Это не то, как выглядит прод (там ноды — реальные VM/железо), но API и поведение объектов идентичны — то, что выучено на kind, переносится на любой кластер.
Практика руками
# Docker Desktop должен быть уже установлен и запущен — это фундамент, без него ничего не работает
# Windows, через choco (одной командой ставит все три инструмента)
choco install kind kubernetes-cli kubernetes-helm
# проверить, что каждый инструмент реально встал
docker --version
kind --version
kubectl version --client
helm version
Самопроверка
- Все четыре команды
--versionотвечают без ошибок. - Понимаешь своими словами разницу между Docker, kind, kubectl и Helm — если нет, перечитай «Зачем это» выше.
Как это в реальных проектах
В проде kind не используют (это чисто локальный/CI инструмент для тестов) — реальные кластеры разворачивают managed-сервисами облаков (EKS/GKE/managed K8s) или kubeadm/аналогами on-premise. kubectl и Helm — те же самые, что и в pet-кластере, это и есть смысл разработки на kind: инструменты один в один.
Модуль 1. Зачем вообще Kubernetes
Зачем это
Прежде чем учить объекты K8s, важно понимать проблему, которую он решает — иначе все дальнейшие абстракции выглядят как искусственная сложность ради сложности.
Теория-минимум
Без оркестратора эксплуатация выглядит так: docker run руками на конкретной VM, и если контейнер упал — никто, кроме человека или самопального скрипта, его не перезапустит; если VM легла — сервис просто недоступен, пока кто-то не отреагирует. K8s переворачивает модель: администратор описывает desired state («хочу 3 реплики этого образа, слушающие порт 8080») декларативно в YAML, а control-plane кластера постоянно сверяет фактическое состояние с желаемым и сам исправляет расхождение — это и называется self-healing. Ключевая идея: не «выполни команду один раз», а «поддерживай вот такое состояние постоянно».
Практика руками
kind create cluster --name lab
# создаём под через Deployment — контроллер, который будет поддерживать desired state
# (голый `kubectl run demo --image=...` создал бы под без контроллера — его никто бы не пересоздал)
kubectl create deployment demo --image=nginxdemos/hello
kubectl get pods
# демо self-healing: убиваем под руками (имя вида demo-<хэш> — взять из вывода выше)
kubectl delete pod <имя-пода>
kubectl get pods
# под пересоздан автоматически с новым именем, потому что desired state требует, чтобы существовала 1 реплика
# уборка — дальше в курсе работаем в отдельном namespace
kubectl delete deployment demo
Самопроверка
- Своими словами: что такое desired state и чем декларативный подход отличается от «выполнить команду»?
- Почему после
kubectl delete pod <имя>под появился снова — и почему с другим именем?
(Забегая вперёд: пересоздал под не «Kubernetes вообще», а конкретный контроллер — Deployment. Голый под, созданный через kubectl run, не пересоздаст никто — за него никто не отвечает. Иерархия Deployment → ReplicaSet → Pod подробно разобрана в модуле 4; здесь цель — просто увидеть идею self-healing на практике.)
Как это в реальных проектах
Эта идея — причина, почему K8s вообще существует: чем больше сервисов и нод, тем дороже человеку вручную следить за их состоянием. В резюме и легенде это прямая параллель с системами вроде systemd (Restart=always — тот же self-healing, но на уровне одной VM, не кластера) — see ../linux-bash/CASE.md.
Модуль 2. Архитектура кластера
Зачем это
Вопрос «из чего состоит Kubernetes» — один из самых частых на собеседованиях (см. QUESTIONS.md). Нельзя понимать объекты (Pod, Service и т.д.), не понимая, кто внутри кластера отвечает за их существование.
Теория-минимум
Кластер делится на control-plane (мозг) и worker-ноды (мышцы, где реально крутятся приложения):
kube-apiserver— единственная точка входа для всех запросов (в том числе отkubectl); всё остальное общается только через него, напрямую в обход API никто ничего не меняет.etcd— key-value хранилище, единственный источник правды о состоянии кластера (все объекты, их спеки и статусы).kube-scheduler— решает, на какую worker-ноду поставить новый под (по ресурсам, тегам, ограничениям).kube-controller-manager— набор контроллеров, каждый из которых следит за своим типом объектов и подгоняет реальность к желаемому состоянию (это и есть механизм self-healing из модуля 1).kubelet(на каждой worker-ноде) — агент, который реально запускает/останавливает контейнеры на своей ноде через container runtime.kube-proxy(на каждой worker-ноде) — настраивает сетевые правила, чтобы трафик до Service доходил до нужных подов.containerd— сам container runtime, который непосредственно тянет образы и стартует контейнеры (аналог Docker engine, но легче).
Практика руками
# пересоздаём кластер с явной топологией — 1 control-plane + 2 worker, чтобы было что показать на схеме
cat > kind-config.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
kind delete cluster --name lab
kind create cluster --name lab --config kind-config.yaml
kubectl cluster-info
kubectl get nodes -o wide
# все компоненты control-plane живут как поды в системном namespace kube-system
kubectl get pods -n kube-system -o wide
Найди в выводе kube-apiserver-lab-control-plane, etcd-lab-control-plane, kube-scheduler-lab-control-plane, kube-controller-manager-lab-control-plane — все живьём, не абстракция. kube-proxy увидишь как DaemonSet (по одному поду на каждую ноду — забегая вперёд к модулю 9 про DaemonSet).
Самопроверка
- Назвать вслух 4 компонента control-plane и 2 компонента worker-ноды, объяснить роль каждого без подсматривания.
- Почему
etcdв pet-кластере находится на той же ноде, что и control-plane, а в проде его часто выносят отдельно? (Кворум при отказоустойчивости — нечётное число нод: 3 или 5, при потере части живой кворум продолжает работать, если жива больше половины.)
Как это в реальных проектах
В managed-кластерах (EKS/GKE) весь control-plane скрыт от пользователя облаком — вы видите только worker-ноды. В on-premise-кластерах (ближе к тому, что было бы в закрытом контуре без облаков) control-plane разворачивается и обслуживается вручную или через kubeadm, и вопрос про размещение etcd становится реальным архитектурным решением, а не абстракцией.
Модуль 3. Pod и kubectl-база
Зачем это
Pod — минимальная единица деплоя в K8s. Нельзя запустить «просто контейнер» напрямую через API K8s — всегда через Pod. Здесь же нарабатывается базовый набор команд kubectl, которым пользуешься постоянно.
Теория-минимум
Pod — это один или несколько контейнеров, которые всегда живут и умирают вместе, на одной ноде, с общей сетью (localhost между контейнерами внутри пода) и опционально общими volume. В большинстве случаев в поде один контейнер — многоконтейнерные поды (sidecar-паттерн) нужны реже, когда вспомогательный процесс должен жить бок о бок с основным (например, агент логирования).
Анатомия любого манифеста K8s — четыре ключа верхнего уровня:
apiVersion— версия API, к которой относится объект (у Pod этоv1, у более сложных объектов частоapps/v1и т.д.).kind— тип объекта (Pod,Deployment,Service...).metadata— имя, namespace, labels, аннотации — «паспорт» объекта.spec— собственно желаемое состояние: что именно нужно запустить.
Практика руками
# pod.yaml
apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: v1
kind: Pod
metadata:
name: demo-pod
namespace: demo
labels:
app: demo
spec:
containers:
- name: hello
image: nginxdemos/hello:latest
ports:
- containerPort: 80
kubectl apply -f pod.yaml
kubectl -n demo get pods # список подов
kubectl -n demo get pods -o wide # + на какой ноде, IP пода
kubectl -n demo describe pod demo-pod # полная инфа: события, статус, причина проблем
kubectl -n demo logs demo-pod # логи контейнера
kubectl -n demo exec -it demo-pod -- sh # зайти внутрь контейнера интерактивно
kubectl -n demo get pod demo-pod -o yaml # реальный полный манифест объекта (K8s дополнил своими полями)
kubectl -n demo delete pod demo-pod # удалить
Самопроверка
- Из четырёх ключей манифеста назвать, какой отвечает за «что мы хотим получить», а какой — за «как объект называется и где живёт».
- Чем
logsотличается отdescribe? (logs — вывод приложения; describe — метаданные/события/статус от самого K8s, годится для диагностики, почему под не стартует). - Что покажет
kubectl -n demo get pod demo-pod -o yamlтакого, чего нет в исходномpod.yaml? (K8s подставляет статус, IP, дополнительные системные поля — разница между тем, что ты запросил, и тем, что реально существует).
Как это в реальных проектах
Голые Pod'ы в проде почти никогда не создают напрямую (кроме короткоживущих задач) — если под упадёт, его никто не пересоздаст, потому что за него никто не отвечает (в отличие от модуля 1, где под был создан через Deployment и контроллер его пересоздал). Правильный способ — через Deployment, это модуль 4.
Модуль 4. Deployment и ReplicaSet
Зачем это
Это самый частый способ запускать stateless-приложения в K8s — и то, что даёт настоящий self-healing и rolling-обновления без даунтайма.
Теория-минимум
Deployment управляет ReplicaSet, а ReplicaSet управляет подами — трёхуровневая иерархия. Deployment описывает: сколько реплик (replicas), как найти «свои» поды (selector), и шаблон, из которого их создавать (template, внутри — точно такой же spec, как у голого Pod). Если поменять образ в Deployment, он не убивает все поды разом — создаёт новый ReplicaSet и постепенно переключает трафик со старого на новый (rolling update), это и есть обновление без даунтайма.
Практика руками
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
namespace: demo
spec:
replicas: 3
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: demo-app
image: nginxdemos/hello:latest
ports:
- containerPort: 80
kubectl apply -f deployment.yaml
kubectl -n demo get deployment demo-app
kubectl -n demo get replicaset # Deployment создал ReplicaSet
kubectl -n demo get pods -o wide # ReplicaSet создал 3 пода
# масштабирование
kubectl -n demo scale deployment demo-app --replicas=5
kubectl -n demo get pods
# self-healing по-настоящему: убиваем один под
kubectl -n demo delete pod <имя-любого-пода>
kubectl -n demo get pods # ReplicaSet тут же пересоздал под — реплик снова 5
# rolling update
kubectl -n demo set image deployment/demo-app demo-app=nginxdemos/hello:plain-text
kubectl -n demo rollout status deployment/demo-app
kubectl -n demo rollout history deployment/demo-app
# откат, если что-то пошло не так
kubectl -n demo rollout undo deployment/demo-app
Самопроверка
- Нарисовать иерархию Deployment → ReplicaSet → Pod своими словами.
- Что произойдёт с количеством ReplicaSet после
set image? (появится новый ReplicaSet под новую версию, старый останется с 0 репликами — это и даётrollout undo). - Чем rolling update отличается от простого «убить все старые поды и поднять новые»? (нет даунтайма — старые поды продолжают обслуживать трафик, пока новые не станут готовы).
Как это в реальных проектах
Deployment — рабочая лошадка для 90% приложений в проде (stateless-сервисы). Rolling update с постепенным переключением — прямая параллель с тем, зачем в принципе нужен readiness probe (модуль 8): K8s не переключает трафик на новый под, пока тот не сообщит о готовности.
Модуль 5. Service и сеть
Зачем это
У подов IP-адреса нестабильны (под пересоздали — IP другой). Нужен стабильный способ достучаться до набора подов, не завязываясь на их конкретные IP — это и есть Service.
Теория-минимум
Service — это стабильный виртуальный IP + DNS-имя, стоящее перед набором подов, отобранных по selector (по тем же labels, что использует Deployment). Три основных типа:
ClusterIP(по умолчанию) — доступен только внутри кластера, для внутреннего взаимодействия сервисов.NodePort— открывает порт на каждой worker-ноде наружу кластера, для простого внешнего доступа без облачного балансировщика.LoadBalancer— просит облако выдать внешний балансировщик (в pet-кластере kind не работает без эмуляции, поэтому для локального внешнего доступа используемport-forwardили Ingress из модуля 6).
Внутри кластера любой под может обратиться к Service по DNS-имени вида <service>.<namespace>.svc.cluster.local (или просто <service>, если в том же namespace) — kube-proxy на каждой ноде транслирует это имя в конкретный IP пода по правилам iptables/IPVS, с балансировкой между всеми подходящими подами.
Практика руками
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: demo-app-svc
namespace: demo
spec:
selector:
app: demo-app
ports:
- port: 80
targetPort: 80
type: ClusterIP
kubectl apply -f service.yaml
kubectl -n demo get svc demo-app-svc
kubectl -n demo get endpoints demo-app-svc # реальные IP подов, на которые сейчас указывает Service
# проверка снаружи через port-forward
kubectl -n demo port-forward svc/demo-app-svc 8080:80
curl http://localhost:8080 # в отдельном терминале
# проверка балансировки изнутри кластера — заходим в один под и стучимся в Service несколько раз
kubectl -n demo run curl-test --image=curlimages/curl -it --rm -- sh
# внутри: curl demo-app-svc.demo.svc.cluster.local, повторить несколько раз
Самопроверка
- Почему нельзя просто обращаться напрямую к IP пода в проде? (IP меняется при каждом пересоздании пода — Service даёт стабильную точку входа).
- Чем
endpointsService отличаются от самого Service? (endpoints — динамический список реальных IP подходящих подов, обновляется автоматически при масштабировании/пересоздании). - В чём разница ClusterIP / NodePort / LoadBalancer одним предложением каждая.
Как это в реальных проектах
Это ровно то, что подразумевается под вопросом «как работает трафик в K8s» (см. QUESTIONS.md): Service + kube-proxy — обязательный слой между внешним балансировщиком/Ingress и конкретными подами, в любой архитектуре, от pet-кластера до мультидатацентрового прода.
Модуль 6. Ingress
Зачем это
NodePort/port-forward годятся для лабы, но не масштабируются на реальный внешний трафик с доменными именами и путями (/api, /app на одном IP). Ingress — стандартный способ завести весь HTTP(S)-трафик в кластер через один вход с роутингом по host/path.
Теория-минимум
Ingress — это правило роутинга (host/path → Service), но само по себе оно ничего не делает — нужен Ingress Controller (отдельное приложение внутри кластера, например NGINX Ingress Controller), которое читает Ingress-ресурсы и реально проксирует трафик. Полная цепочка запроса: клиент → Ingress Controller (по внешнему IP/порту) → на основе host/path выбирает нужный Service → Service выбирает под по selector → под отвечает.
Практика руками
# kind поставляется с готовым манифестом NGINX Ingress Controller под себя
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl -n ingress-nginx wait --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=120s
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-ingress
namespace: demo
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: demo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-app-svc
port:
number: 80
kubectl apply -f ingress.yaml
kubectl -n demo get ingress
# добавить demo.local в hosts-файл (Windows: C:\Windows\System32\drivers\etc\hosts) → 127.0.0.1
curl http://demo.local
Самопроверка
- Своими словами: чем Ingress отличается от Ingress Controller? (Ingress — декларативное правило-манифест; Controller — программа, которая это правило исполняет).
- Нарисовать полную цепочку «клиент → под» через Ingress, включая все промежуточные звенья.
Как это в реальных проектах
Это прямой ответ на реальный вопрос с собеседования «нарисуй два ЦОД под K8s с балансировщиком» (Реалист банк, см. QUESTIONS.md): внешний L4/L7-балансировщик перед ЦОДами → внутри каждого ЦОД Ingress Controller/Service → поды. Личная параллель с рабочим опытом — Nginx как балансировщик перед сервисами (см. ../nginx/CASE.md, ../../legend/STORY.md → nginx.conf для x3 Mimir): Ingress Controller здесь и есть, по сути, Nginx, только управляемый декларативно через K8s-объекты, а не ручным редактированием nginx.conf.
Модуль 7. ConfigMap и Secret
Зачем это
Образ контейнера должен быть одинаковым во всех окружениях (dev/stage/prod) — конфигурация, которая между окружениями отличается, не должна быть зашита в образ. ConfigMap и Secret — стандартный способ передать конфиг снаружи образа.
Теория-минимум
ConfigMap — для несекретных данных (URL, флаги, конфиг-файлы), Secret — для чувствительных значений (пароли, токены, ключи). Оба можно подключить к поду двумя способами: как переменные окружения (envFrom/env) или как смонтированный файл (volumeMounts). Важный технический нюанс: Secret по умолчанию хранит значения в base64 — это кодирование, а не шифрование. Любой, у кого есть доступ к API/etcd, может декодировать значение одной командой — реальная защита требует либо шифрования etcd at rest, либо внешних систем вроде Vault (см. ../vault/CASE.md) или Sealed Secrets.
Практика руками
# config-secret.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: demo
data:
GREETING: "Привет из ConfigMap"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: demo
type: Opaque
stringData:
API_KEY: "lab-secret-value"
Подключаем к Deployment из модуля 4 через envFrom:
# добавить в spec.template.spec.containers[0] demo-app из deployment.yaml
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret
kubectl apply -f config-secret.yaml
kubectl apply -f deployment.yaml # обновлённый с envFrom
# проверка, что переменные реально попали в под
kubectl -n demo exec -it <имя-пода> -- env | grep -E "GREETING|API_KEY"
# ключевая демонстрация: base64 — не шифрование
kubectl -n demo get secret app-secret -o yaml
# скопировать значение data.API_KEY и декодировать:
echo "<base64-значение>" | base64 -d
# получится исходный "lab-secret-value" — открытым текстом
Самопроверка
- Почему нельзя просто зашить конфиг в Dockerfile/образ? (тогда под каждое окружение нужен свой образ — теряется смысл «один образ везде»).
- Объяснить вслух, почему
kubectl get secret -o yaml+base64 -d— не взлом, а штатная возможность любого, у кого есть доступ к API.
Как это в реальных проектах
Прямая параллель с рабочим опытом: ansible-vault шифрует секреты в Ansible-ролях (см. ../ansible/CASE.md) примерно с той же целью, ради которой в проде K8s-секреты часто заводят не как голый Secret, а через внешний Vault-интеграцию (модуль pet — ../vault/CASE.md) или Sealed Secrets, чтобы решить проблему base64 ≠ шифрование.
Модуль 8. Probes и ресурсы
Зачем это
K8s не знает, «готово» ли приложение внутри контейнера, просто по факту, что процесс запущен — процесс может быть жив, но зависшим, или ещё прогревающимся. Probes дают K8s способ реально спросить приложение о его состоянии.
Теория-минимум
Три вида проверок:
- livenessProbe — «жив ли процесс вообще?» Если проверка не проходит несколько раз подряд — K8s убивает и пересоздаёт контейнер.
- readinessProbe — «готов ли принимать трафик прямо сейчас?» Если не проходит — под не убивают, но Service временно перестаёт слать в него трафик (именно это делает rolling update из модуля 4 безопасным).
- startupProbe — для медленно стартующих приложений: пока не пройдёт, liveness/readiness не проверяются (чтобы долгий старт не приняли за зависание).
Ресурсы (requests/limits): requests — сколько CPU/RAM гарантированно резервируется под контейнер (scheduler использует это число, чтобы решить, на какую ноду его поставить), limits — потолок, выше которого контейнер не может подняться (превышение по памяти → OOMKilled, превышение по CPU → троттлинг, не убийство). Комбинация requests/limits определяет QoS-класс пода (Guaranteed/Burstable/BestEffort) — от него зависит, что убьют первым при нехватке ресурсов на ноде.
Практика руками
# probes.yaml — добавить в spec.template.spec.containers[0] demo-app
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "250m"
memory: "128Mi"
kubectl apply -f deployment.yaml
kubectl -n demo describe pod <имя-пода> # секция Liveness/Readiness видна прямо в describe
# демо OOMKilled: временно поставить memory limit заведомо ниже, чем нужно приложению
# (например memory: "8Mi"), применить, посмотреть:
kubectl -n demo get pods
kubectl -n demo describe pod <имя-пода> # Last State: Terminated, Reason: OOMKilled
Самопроверка
- В чём разница между liveness и readiness одним предложением каждая — и что K8s делает в каждом случае при провале проверки.
- Что произойдёт, если задать
limits.memoryменьше, чем реально требуется приложению? (контейнер убьют по OOM, и он уйдёт в CrashLoopBackOff при повторных попытках — мост к модулю 9).
Как это в реальных проектах
Правильно настроенные probes — то, что отличает «работающий на бумаге» деплой от реально безопасного rolling update: без readinessProbe K8s может начать слать трафик на под, который ещё не прогрелся, и получить ошибки у пользователей в момент релиза.
Модуль 9. Диагностика поломок
Зачем это
Это самый практический навык для собеседования: «покажи, как будешь диагностировать» — частый формат реальных вопросов (см. QUESTIONS.md). Здесь специально ломаем всё, что можно сломать, чтобы увидеть диагностику вживую, а не в теории.
Теория-минимум
Три частых состояния поломанного пода:
- ImagePullBackOff — K8s не может скачать указанный образ (опечатка в имени, несуществующий тег, нет доступа к registry).
- CrashLoopBackOff — контейнер стартует и тут же падает, K8s пытается перезапустить снова и снова с растущей паузой между попытками.
- Pending — под не может быть запланирован ни на одну ноду (не хватает ресурсов под
requests, или не подходит ни одна нода по ограничениям).
Практика руками
# 1. ImagePullBackOff — несуществующий образ
kubectl -n demo run broken1 --image=nginxdemos/this-image-does-not-exist:latest
kubectl -n demo get pods
kubectl -n demo describe pod broken1 # Events внизу прямо назовут причину
# 2. CrashLoopBackOff — команда, которая тут же завершается с ошибкой
kubectl -n demo run broken2 --image=busybox --restart=Always -- sh -c "exit 1"
kubectl -n demo get pods -w # смотрим, как RESTARTS растёт
kubectl -n demo logs broken2 --previous # логи упавшего перед текущим рестартом контейнера
kubectl -n demo describe pod broken2
# 3. Pending — запросить заведомо нереальные ресурсы
kubectl -n demo run broken3 --image=nginx --requests='cpu=100,memory=100Gi' --restart=Always
kubectl -n demo get pods # STATUS: Pending
kubectl -n demo describe pod broken3 # Events: "0/3 nodes are available: insufficient cpu/memory"
# общая хронология по namespace — полезно, когда непонятно, с чего начать
kubectl -n demo get events --sort-by=.lastTimestamp
# уборка после экспериментов
kubectl -n demo delete pod broken1 broken2 broken3
Самопроверка
Чек-лист «под не работает — что смотреть по порядку» (проговорить вслух на реальном примере из практики выше):
kubectl get pods— какой статус? (ImagePullBackOff / CrashLoopBackOff / Pending / Running-но-не-Ready).kubectl describe pod— секция Events внизу почти всегда прямо называет причину.- Если контейнер стартовал и упал —
kubectl logs <pod> --previous, посмотреть, что вывело само приложение перед падением. - Если Pending — смотреть Events на предмет нехватки ресурсов или несовпадения ограничений с нодами.
- Если ничего не понятно —
kubectl get events --sort-by=.lastTimestampпо всему namespace, искать связанные события.
Как это в реальных проектах
Это прямая параллель с рабочим инцидент-флоу из ../../legend/STORY.md → «Как решается инцидент»: там формула «дашборд → логи (Kibana) → диагностика (systemctl/journalctl) → рестарт/эскалация», здесь та же логика, но инструмент — kubectl describe/logs/events вместо systemctl/journalctl. Смысл вопроса на собеседовании «как будешь диагностировать под» — не проверить знание конкретной команды, а проверить, есть ли системный порядок действий.
Модуль 10. Хранилище: PV, PVC и StatefulSet
Зачем это
Поды по умолчанию эфемерны — при пересоздании пода локальные данные внутри контейнера теряются. Для приложений с состоянием (базы данных, очереди) это неприемлемо, и K8s даёт отдельный слой абстракций для постоянного хранилища.
Теория-минимум
emptyDir— временный volume, живущий столько же, сколько под (переживает рестарт контейнера внутри пода, но не пересоздание самого пода) — полезен для промежуточных данных между контейнерами одного пода.PersistentVolume(PV) — реальный кусок хранилища в кластере (диск/NFS/облачный volume),PersistentVolumeClaim(PVC) — запрос пода «дай мне столько-то места», который K8s сопоставляет с подходящим PV.StorageClass— шаблон, по которому PV создаются автоматически по запросу PVC (динамическое провижининг вместо ручного создания PV заранее).- StatefulSet vs Deployment: Deployment создаёт взаимозаменяемые поды с случайными именами и общим PVC (или без него) — годится для stateless. StatefulSet даёт каждому поду стабильное имя (
app-0,app-1, ...) и свой собственный PVC, который переживает пересоздание конкретного пода — обязательно для баз данных и любых систем, где реплики не взаимозаменяемы (например, знают свою роль master/replica).
Практика руками
# statefulset-demo.yaml — учебный пример на минимальном приложении, не на реальной БД
apiVersion: v1
kind: Service
metadata:
name: demo-headless
namespace: demo
spec:
clusterIP: None # headless-сервис — нужен StatefulSet для стабильных DNS-имён подов
selector:
app: demo-stateful
ports:
- port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: demo-stateful
namespace: demo
spec:
serviceName: demo-headless
replicas: 2
selector:
matchLabels:
app: demo-stateful
template:
metadata:
labels:
app: demo-stateful
spec:
containers:
- name: demo
image: nginxdemos/hello:latest
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Mi
kubectl apply -f statefulset-demo.yaml
kubectl -n demo get pods -l app=demo-stateful # demo-stateful-0, demo-stateful-1 — стабильные имена, не случайные
kubectl -n demo get pvc # у каждого пода свой PVC
kubectl -n demo get pv
# показать, что PVC переживает пересоздание своего пода
kubectl -n demo delete pod demo-stateful-0
kubectl -n demo get pods -l app=demo-stateful # пересоздался с тем же именем demo-stateful-0 и тем же PVC
Самопроверка
- Своими словами: почему для базы данных в K8s используют StatefulSet, а не Deployment?
- Что произойдёт с данными в
emptyDir, если под пересоздан целиком (не просто перезапущен контейнер внутри него)? (данные потеряются — emptyDir живёт не дольше самого пода).
Как это в реальных проектах
Это прямой ответ на реальный вопрос с собеседования «как будешь разворачивать PostgreSQL на две ноды K8s» (см. QUESTIONS.md): StatefulSet + PVC на каждую реплику + обычно готовый оператор (CloudNativePG, Zalando Postgres Operator), который поверх StatefulSet настраивает master-replica топологию и Service, разделяющий трафик на запись/чтение. Принцип репликации сам по себе прогнан отдельно в Docker Compose — см. ../databases/CASE.md; K8s здесь — просто уровень оркестрации поверх той же логики.
Модуль 11. Helm, часть 1 — пользователь чартов
Зачем это
Реальные приложения (особенно готовые open-source системы вроде Prometheus/Grafana) состоят из десятков манифестов. Helm — золотой стандарт для их установки и обновления одной командой вместо ручного kubectl apply -f на каждый файл.
Теория-минимум
- Chart — упакованный набор шаблонизированных манифестов K8s (аналог пакета в apt/npm).
- Release — конкретный установленный в кластер экземпляр чарта (можно поставить один и тот же чарт несколько раз под разными именами релиза — например, две независимые инсталляции Prometheus).
- Repository — источник, откуда качаются чарты (аналог apt-репозитория или npm registry).
values.yaml— файл с параметрами, которые чарт подставляет в свои шаблоны при установке — то, чем конкретная инсталляция отличается от дефолтной.
Ключевые команды: helm repo add/update (подключить и обновить репозиторий), helm search repo (найти чарт), helm install (поставить новый релиз), helm upgrade (обновить существующий релиз новыми values/версией), helm rollback (откатить релиз к прошлой ревизии — аналог kubectl rollout undo, но на уровне всего чарта), helm uninstall (удалить релиз).
Практика руками
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm search repo prometheus-community/kube-prometheus-stack
# посмотреть дефолтные values перед установкой — не ставить вслепую
helm show values prometheus-community/kube-prometheus-stack > default-values.yaml
# ставим с переопределением — уменьшаем ресурсы под pet-кластер
cat > my-values.yaml << 'EOF'
grafana:
adminPassword: "lab-admin"
prometheus:
prometheusSpec:
resources:
requests:
cpu: 200m
memory: 512Mi
EOF
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
-f my-values.yaml
kubectl -n monitoring get pods
helm list -n monitoring
helm status monitoring -n monitoring
# обновление релиза — меняем values и применяем поверх существующей инсталляции
helm upgrade monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--set grafana.adminPassword=new-lab-admin \
-f my-values.yaml
helm history monitoring -n monitoring
# при необходимости: helm rollback monitoring 1 -n monitoring
kubectl -n monitoring port-forward svc/monitoring-grafana 3000:80
Самопроверка
- Своими словами объяснить разницу между Chart и Release (пакет vs конкретная установленная копия пакета).
- Зачем перед
helm installсмотретьhelm show values, а не сразу ставить с дефолтами вслепую? - Чем
helm upgradeотличается от повторногоhelm install? (upgrade применяет изменения к существующему релизу с сохранением истории ревизий, install создаст конфликт с уже существующим релизом).
Как это в реальных проектах
Это прямой ответ на реальный вопрос с собеседования Bi.Zone «как задеплоишь мониторинг на 300 000 серверов» (см. ../../legend/CAPACITY.md): не вручную, а Helm-чартом с values.yaml, где параметризуются количество реплик, ресурсы, remote_write в Mimir — тот же принцип «Prometheus + Grafana», что и в рабочем стеке (см. ../monitoring/CASE.md), но развёрнутый декларативно и параметризуемо, а не через docker-compose с ручными правками.
Модуль 12. Helm, часть 2 — автор чарта
Зачем это
Пользоваться готовыми чартами (модуль 11) — половина навыка. Уметь упаковать своё приложение в чарт — то, что отличает «ставил через Helm» от «понимаю, как Helm устроен изнутри», и именно это спрашивают на более глубоких технических собеседованиях.
Теория-минимум
Структура чарта, созданного helm create:
Chart.yaml— метаданные чарта (имя, версия чарта, версия приложения).values.yaml— дефолтные параметры, доступные в шаблонах как.Values.*.templates/— сами манифесты K8s, но с шаблонизацией Go-template:{{ .Values.replicaCount }}подставит значение из values.yaml,{{ include "chart.fullname" . }}— переиспользуемый фрагмент из_helpers.tpl,{{- if .Values.ingress.enabled }}...{{- end }}— условное включение блока манифеста._helpers.tpl— вспомогательные именованные шаблоны (чтобы не дублировать логику вроде генерации имени релиза в каждом файле).NOTES.txt— текст, который Helm печатает пользователю сразу послеhelm install(подсказка, как получить доступ к приложению).
Практика руками
helm create demo-chart
Разобрать построчно, что helm create сгенерировал по умолчанию — открыть demo-chart/Chart.yaml, demo-chart/values.yaml, demo-chart/templates/deployment.yaml, demo-chart/templates/_helpers.tpl и сопоставить с манифестами, которые писались руками в модулях 3–7: templates/deployment.yaml — это тот же Deployment из модуля 4, но replicas, image.repository, image.tag заменены на {{ .Values.replicaCount }}, {{ .Values.image.repository }} и т.д.
# проверка шаблона без реальной установки — что синтаксис корректен
helm lint demo-chart
# рендер шаблонов локально — увидеть итоговый YAML, который получится после подстановки values
helm template demo-chart
# рендер с переопределёнными values — увидеть разницу
helm template demo-chart --set replicaCount=3 --set image.repository=nginxdemos/hello
Адаптировать values.yaml и templates/deployment.yaml под приложение из модулей 3–7 (образ nginxdemos/hello, свой ConfigMap/Secret) и поставить своим чартом вместо ручных kubectl apply:
helm install demo-release ./demo-chart --namespace demo --create-namespace
kubectl -n demo get all -l app.kubernetes.io/instance=demo-release
helm uninstall demo-release -n demo
Самопроверка
- Открыть
templates/deployment.yamlсгенерированного чарта и найти в нём 3 места, где вместо жёстко заданного значения подставляется.Values.*— объяснить, зачем каждое из них параметризовано. - Чем
helm templateотличается отhelm install? (template — просто рендерит YAML локально, ничего не применяет к кластеру; install — рендерит и сразу применяет). - Зачем нужен
_helpers.tpl, если можно было бы просто продублировать{{ .Release.Name }}-demo-chartв каждом файле шаблонов?
Как это в реальных проектах
Собственные чарты пишут, когда приложение — своё (не готовый open-source продукт вроде Prometheus), и его нужно раскатывать в несколько окружений с разными параметрами (dev/stage/prod) без копипаста манифестов — это прямая параллель с тем, как в рабочем стеке Ansible-роли параметризуются через переменные вместо копирования плейбуков под каждое окружение (см. ../ansible/CASE.md): тот же принцип «шаблон + параметры», другой уровень (конфигурация ОС/сервисов vs манифесты K8s).
Модуль 13. Как это устроено в реальных проектах
Зачем это
Финальный модуль — не новая техническая тема, а сборка всех модулей в целостную картину «как это выглядит в реальной команде», плюс честная граница pet-опыта.
Теория-минимум
Типичный процесс в компании, использующей K8s+Helm:
- Чарты (свои и сторонние) хранятся в git-репозитории вместе с манифестами инфраструктуры — это прямая параллель с рабочим опытом хранения Ansible-ролей в Gitea (см. ../../legend/LEGEND.md).
- Раскатка идёт не руками с ноутбука разработчика, а через CI/CD-пайплайн (
helm upgradeкак шаг джобы) — параллель с TeamCity-раскаткой ролей/дашбордов из рабочего опыта (см. ../../legend/STORY.md → «Версионирование дашбордов»). - Разные
values-dev.yaml/values-staging.yaml/values-prod.yamlпод одно и то же приложение — тот же чарт, разные параметры на окружение. - GitOps (инструменты ArgoCD/Flux) — следующий шаг развития идеи: вместо того чтобы CI сам выполнял
helm upgrade, в git-репозитории просто описывается желаемое состояние кластера, а отдельный контроллер внутри кластера постоянно сверяет его с реальностью и сам подтягивает изменения (тот же принцип desired state из модуля 1, но применённый к самому процессу деплоя). Здесь достаточно понимания идеи, не практики — pet-кластер для этого не поднимался. - Umbrella-чарт — чарт, который сам ничего не разворачивает напрямую, а просто объявляет зависимости от нескольких других чартов (например, «наше приложение» + «его конфигурация Redis» одним релизом) — используется, когда несколько связанных сервисов нужно ставить и версионировать вместе.
Практика руками
Практики в этом модуле нет — это сборочный/рефлексивный модуль. Вместо неё: выписать на бумаге для своего pet-кластера полный путь «от git commit в репозитории чарта до работающего пода», используя реальные названия компонентов, которые прошли в модулях 0–12 (Deployment, Service, Helm release и т.д.) вместо абстрактных слов.
Самопроверка
- Объяснить своими словами идею GitOps и чем она отличается от «CI-пайплайн просто выполняет helm upgrade».
- Честно сформулировать для себя одним предложением, что из модуля 13 — реальный pet-опыт (структура чартов, values, helm upgrade/rollback), а что — только понимание на уровне «знаю, что это и зачем» (ArgoCD/Flux, продовые GitOps-пайплайны).
Как это в реальных проектах
Это тот самый честный формат ответа на собеседовании, уже принятый в этом репозитории для других инструментов «на границе опыта» (см. ../../legend/DEPT_STACK.md → «Как этим пользоваться на собеседовании»): по GitOps — «сам не разворачивал, но понимаю идею и зачем она нужна», без попытки выдать это за практический опыт.
Выходной контроль
Курс пройден, когда пройдены оба пункта:
- Устный прогон QUESTIONS.md — вслух, без подглядывания в готовые ответы; если запинаешься — вернуться к соответствующему модулю выше (не к готовому ответу), разобраться и сформулировать заново своими словами.
- Две схемы на бумаге без подсказок: архитектура кластера (control-plane/worker, модуль 2) и путь трафика (клиент → под, модуль 5–6).
Таблица соответствия: вопрос из QUESTIONS.md → модуль курса
| Вопрос из QUESTIONS.md | Модуль |
|---|---|
| Из чего состоит Kubernetes? | 2 |
| etcd будет на одной виртуалке с K8s или отдельно? | 2 |
| Что такое DaemonSet? | 2 (kube-proxy как DaemonSet) |
| Нарисуй два ЦОД под K8s с балансировщиком | 5, 6 |
| Как будешь разворачивать PostgreSQL на две ноды K8s? | 10 |
| Что такое Namespace и зачем он нужен? | 3 |
| Чем отличается ConfigMap от Secret? | 7 |
| Какой у тебя опыт с Helm и разворотом кластеров K8s? | 11, 12 |
| Как работает трафик в K8s? | 5, 6 |
| Как проверить логи и поды в кластере? | 3, 9 |
После прохождения курса — переходи к CASE.md как к компактному конспекту для повторения перед конкретным собеседованием.