Files
resume/stack/kubernetes/LEARNING.md
2026-07-18 23:33:26 +03:00

841 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Учебный курс: Kubernetes с нуля
Это учебный документ, а не легенда — здесь нет утверждений «делал на работе». Цель: пройти путь от нуля до уверенного pet-уровня, объясняя себе на каждом шаге «что это и зачем», а не просто копируя команды. Итоговая позиция на собеседовании не меняется: K8s — честная домашняя практика поверх рабочего опыта с Docker/Ansible/мониторингом (см. [../../legend/LEGEND.md](../../legend/LEGEND.md) → «Домашняя лаборатория / pet-проект»).
**Как этим пользоваться:** проходить модули по порядку, не пропускать практику — каждый следующий модуль опирается на состояние кластера, оставленное предыдущим. Когда весь курс пройден руками один раз, для повторения перед конкретным собеседованием используй более компактный [CASE.md](CASE.md) — он про то же самое, но без объяснений, как шпаргалка. Вопросы с готовыми ответами — в [QUESTIONS.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](SERVER.md). Текущий прогресс прохождения (какой модуль пройден, на чём остановился, состояние кластера) — в [PROGRESS.md](PROGRESS.md), построчный разбор каждой выполненной команды с объяснением аргументов — в [COMMANDS.md](COMMANDS.md), а как просто запустить/остановить/проверить стенд между сессиями без учебных объяснений — в [USAGE.md](USAGE.md).
---
## Модуль 0. Установка инструментов
### Зачем это
Три разных инструмента решают три разные задачи, и важно не путать их роли: **Docker** — где физически крутятся контейнеры, **kind** — как за секунды поднять тестовый K8s-кластер поверх Docker без отдельных виртуалок, **kubectl** — как разговаривать с любым K8s-кластером (хоть pet, хоть прод), **Helm** — как ставить готовые наборы манифестов одной командой вместо ручного `kubectl apply` на десяток файлов.
### Теория-минимум
`kind` = Kubernetes IN Docker: каждая «нода» кластера — это на самом деле Docker-контейнер, внутри которого запущен полноценный K8s. Это не то, как выглядит прод (там ноды — реальные VM/железо), но API и поведение объектов идентичны — то, что выучено на kind, переносится на любой кластер.
### Практика руками
```bash
# 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**. Ключевая идея: не «выполни команду один раз», а «поддерживай вот такое состояние постоянно».
### Практика руками
```bash
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](../linux-bash/CASE.md).
---
## Модуль 2. Архитектура кластера
### Зачем это
Вопрос «из чего состоит Kubernetes» — один из самых частых на собеседованиях (см. [QUESTIONS.md](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, но легче).
### Практика руками
```bash
# пересоздаём кластер с явной топологией — 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` — собственно желаемое состояние: что именно нужно запустить.
### Практика руками
```yaml
# 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
```
```bash
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), это и есть обновление без даунтайма.
### Практика руками
```yaml
# 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
```
```bash
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, с балансировкой между всеми подходящими подами.
### Практика руками
```yaml
# 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
```
```bash
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 даёт стабильную точку входа).
- Чем `endpoints` Service отличаются от самого Service? (endpoints — динамический список реальных IP подходящих подов, обновляется автоматически при масштабировании/пересоздании).
- В чём разница ClusterIP / NodePort / LoadBalancer одним предложением каждая.
### Как это в реальных проектах
Это ровно то, что подразумевается под вопросом «как работает трафик в K8s» (см. [QUESTIONS.md](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` → под отвечает.
### Практика руками
```bash
# 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
```
```yaml
# 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
```
```bash
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](QUESTIONS.md)): внешний L4/L7-балансировщик перед ЦОДами → внутри каждого ЦОД Ingress Controller/Service → поды. Личная параллель с рабочим опытом — Nginx как балансировщик перед сервисами (см. [../nginx/CASE.md](../nginx/CASE.md), [../../legend/STORY.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](../vault/CASE.md)) или Sealed Secrets.
### Практика руками
```yaml
# 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`:
```yaml
# добавить в spec.template.spec.containers[0] demo-app из deployment.yaml
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secret
```
```bash
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](../ansible/CASE.md)) примерно с той же целью, ради которой в проде K8s-секреты часто заводят не как голый `Secret`, а через внешний Vault-интеграцию (модуль pet — [../vault/CASE.md](../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`) — от него зависит, что убьют первым при нехватке ресурсов на ноде.
### Практика руками
```yaml
# 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"
```
```bash
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](QUESTIONS.md)). Здесь специально ломаем всё, что можно сломать, чтобы увидеть диагностику вживую, а не в теории.
### Теория-минимум
Три частых состояния поломанного пода:
- **ImagePullBackOff** — K8s не может скачать указанный образ (опечатка в имени, несуществующий тег, нет доступа к registry).
- **CrashLoopBackOff** — контейнер стартует и тут же падает, K8s пытается перезапустить снова и снова с растущей паузой между попытками.
- **Pending** — под не может быть запланирован ни на одну ноду (не хватает ресурсов под `requests`, или не подходит ни одна нода по ограничениям).
### Практика руками
```bash
# 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
```
### Самопроверка
Чек-лист «под не работает — что смотреть по порядку» (проговорить вслух на реальном примере из практики выше):
1. `kubectl get pods` — какой статус? (ImagePullBackOff / CrashLoopBackOff / Pending / Running-но-не-Ready).
2. `kubectl describe pod` — секция Events внизу почти всегда прямо называет причину.
3. Если контейнер стартовал и упал — `kubectl logs <pod> --previous`, посмотреть, что вывело само приложение перед падением.
4. Если Pending — смотреть Events на предмет нехватки ресурсов или несовпадения ограничений с нодами.
5. Если ничего не понятно — `kubectl get events --sort-by=.lastTimestamp` по всему namespace, искать связанные события.
### Как это в реальных проектах
Это прямая параллель с рабочим инцидент-флоу из [../../legend/STORY.md](../../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).
### Практика руками
```yaml
# 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
```
```bash
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](QUESTIONS.md)): StatefulSet + PVC на каждую реплику + обычно готовый оператор (CloudNativePG, Zalando Postgres Operator), который поверх StatefulSet настраивает master-replica топологию и Service, разделяющий трафик на запись/чтение. Принцип репликации сам по себе прогнан отдельно в Docker Compose — см. [../databases/CASE.md](../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` (удалить релиз).
### Практика руками
```bash
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](../../legend/CAPACITY.md)): не вручную, а Helm-чартом с `values.yaml`, где параметризуются количество реплик, ресурсы, remote_write в Mimir — тот же принцип «Prometheus + Grafana», что и в рабочем стеке (см. [../monitoring/CASE.md](../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` (подсказка, как получить доступ к приложению).
### Практика руками
```bash
helm create demo-chart
```
Разобрать построчно, что `helm create` сгенерировал по умолчанию — открыть `demo-chart/Chart.yaml`, `demo-chart/values.yaml`, `demo-chart/templates/deployment.yaml`, `demo-chart/templates/_helpers.tpl` и сопоставить с манифестами, которые писались руками в модулях 37: `templates/deployment.yaml` — это тот же Deployment из модуля 4, но `replicas`, `image.repository`, `image.tag` заменены на `{{ .Values.replicaCount }}`, `{{ .Values.image.repository }}` и т.д.
```bash
# проверка шаблона без реальной установки — что синтаксис корректен
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` под приложение из модулей 37 (образ `nginxdemos/hello`, свой ConfigMap/Secret) и поставить своим чартом вместо ручных `kubectl apply`:
```bash
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](../ansible/CASE.md)): тот же принцип «шаблон + параметры», другой уровень (конфигурация ОС/сервисов vs манифесты K8s).
---
## Модуль 13. Как это устроено в реальных проектах
### Зачем это
Финальный модуль — не новая техническая тема, а сборка всех модулей в целостную картину «как это выглядит в реальной команде», плюс честная граница pet-опыта.
### Теория-минимум
Типичный процесс в компании, использующей K8s+Helm:
- Чарты (свои и сторонние) хранятся в git-репозитории вместе с манифестами инфраструктуры — это прямая параллель с рабочим опытом хранения Ansible-ролей в Gitea (см. [../../legend/LEGEND.md](../../legend/LEGEND.md)).
- Раскатка идёт не руками с ноутбука разработчика, а через CI/CD-пайплайн (`helm upgrade` как шаг джобы) — параллель с TeamCity-раскаткой ролей/дашбордов из рабочего опыта (см. [../../legend/STORY.md](../../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` в репозитории чарта до работающего пода», используя реальные названия компонентов, которые прошли в модулях 012 (Deployment, Service, Helm release и т.д.) вместо абстрактных слов.
### Самопроверка
- Объяснить своими словами идею GitOps и чем она отличается от «CI-пайплайн просто выполняет helm upgrade».
- Честно сформулировать для себя одним предложением, что из модуля 13 — реальный pet-опыт (структура чартов, values, helm upgrade/rollback), а что — только понимание на уровне «знаю, что это и зачем» (ArgoCD/Flux, продовые GitOps-пайплайны).
### Как это в реальных проектах
Это тот самый честный формат ответа на собеседовании, уже принятый в этом репозитории для других инструментов «на границе опыта» (см. [../../legend/DEPT_STACK.md](../../legend/DEPT_STACK.md) → «Как этим пользоваться на собеседовании»): по GitOps — «сам не разворачивал, но понимаю идею и зачем она нужна», без попытки выдать это за практический опыт.
---
## Выходной контроль
Курс пройден, когда пройдены оба пункта:
1. **Устный прогон [QUESTIONS.md](QUESTIONS.md)** — вслух, без подглядывания в готовые ответы; если запинаешься — вернуться к соответствующему модулю выше (не к готовому ответу), разобраться и сформулировать заново своими словами.
2. **Две схемы на бумаге без подсказок**: архитектура кластера (control-plane/worker, модуль 2) и путь трафика (клиент → под, модуль 56).
### Таблица соответствия: вопрос из 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](CASE.md) как к компактному конспекту для повторения перед конкретным собеседованием.