# Учебный курс: 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-имени вида `..svc.cluster.local` (или просто ``, если в том же 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 -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 --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` и сопоставить с манифестами, которые писались руками в модулях 3–7: `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` под приложение из модулей 3–7 (образ `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` в репозитории чарта до работающего пода», используя реальные названия компонентов, которые прошли в модулях 0–12 (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) и путь трафика (клиент → под, модуль 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](CASE.md) как к компактному конспекту для повторения перед конкретным собеседованием.