Files
resume/stack/kubernetes/LEARNING.md
Tot Maxim 40ff73f647 Оформить опыт с Gitea CI-раннерами в легенду и docker-кейс
Реальная настройка self-hosted CI Gitea Actions (починка раннера из
restart-loop, кастомный cross-builder под ARM) вписана в опыт АО ТНИИС:
LEGEND.md/STORY.md — расширены строки Gitea/Docker и раздел про два
CI-инструмента отдела, docker/CASE.md — новый раздел 9 с
воспроизводимыми фрагментами, ci-cd/CASE.md — ссылка на реальный кейс.

SERVER.md/PROGRESS.md актуализированы: исправлена ошибочная запись
про порт 3000 (это Grafana, не Gitea) и добавлен чек-лист, что нельзя
ломать в CI-инфраструктуре сервера при прохождении курса Kubernetes.
2026-07-18 15:06:57 +03:00

63 KiB
Raw Blame History

Учебный курс: 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.


Модуль 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 даёт стабильную точку входа).
  • Чем endpoints Service отличаются от самого 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

Самопроверка

Чек-лист «под не работает — что смотреть по порядку» (проговорить вслух на реальном примере из практики выше):

  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 → «Как решается инцидент»: там формула «дашборд → логи (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 и сопоставить с манифестами, которые писались руками в модулях 37: 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 под приложение из модулей 37 (образ 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 в репозитории чарта до работающего пода», используя реальные названия компонентов, которые прошли в модулях 012 (Deployment, Service, Helm release и т.д.) вместо абстрактных слов.

Самопроверка

  • Объяснить своими словами идею GitOps и чем она отличается от «CI-пайплайн просто выполняет helm upgrade».
  • Честно сформулировать для себя одним предложением, что из модуля 13 — реальный pet-опыт (структура чартов, values, helm upgrade/rollback), а что — только понимание на уровне «знаю, что это и зачем» (ArgoCD/Flux, продовые GitOps-пайплайны).

Как это в реальных проектах

Это тот самый честный формат ответа на собеседовании, уже принятый в этом репозитории для других инструментов «на границе опыта» (см. ../../legend/DEPT_STACK.md → «Как этим пользоваться на собеседовании»): по GitOps — «сам не разворачивал, но понимаю идею и зачем она нужна», без попытки выдать это за практический опыт.


Выходной контроль

Курс пройден, когда пройдены оба пункта:

  1. Устный прогон 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 как к компактному конспекту для повторения перед конкретным собеседованием.