From 53032f1fd7e05253410fe14cc8e48da8ef305215 Mon Sep 17 00:00:00 2001
From: Tot Maxim
Date: Sat, 18 Jul 2026 23:33:26 +0300
Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?=
=?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?=
=?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.vscode/settings.json | 7 +
notes.txt | 4 +
stack/kubernetes/COMMANDS.md | 297 +++++++++++++++++++++++++++++++++++
stack/kubernetes/LEARNING.md | 2 +-
stack/kubernetes/PROGRESS.md | 44 +++++-
stack/kubernetes/USAGE.md | 133 ++++++++++++++++
6 files changed, 479 insertions(+), 8 deletions(-)
create mode 100644 .vscode/settings.json
create mode 100644 notes.txt
create mode 100644 stack/kubernetes/USAGE.md
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..cf88154
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "cSpell.ignoreWords": [
+ "armhf",
+ "gitea",
+ "раннеров"
+ ]
+}
\ No newline at end of file
diff --git a/notes.txt b/notes.txt
new file mode 100644
index 0000000..e0b292a
--- /dev/null
+++ b/notes.txt
@@ -0,0 +1,4 @@
+«Почему в серьёзном предприятии Gitea, а не GitLab?» Честный и защищаемый ответ:
+GitLab CE тяжёлый по ресурсам (Postgres + Redis + Sidekiq + Gitaly, легко 4+ ГБ RAM).
+Для скромного парка в закрытом контуре Gitea — один лёгкий Go-бинарь, разворачивается и обслуживается минимальными силами.
+Под задачу «внутреннее хранение конфигов/ролей + CI кросс-сборки» этого достаточно.
\ No newline at end of file
diff --git a/stack/kubernetes/COMMANDS.md b/stack/kubernetes/COMMANDS.md
index bba3b56..7f3c5e7 100644
--- a/stack/kubernetes/COMMANDS.md
+++ b/stack/kubernetes/COMMANDS.md
@@ -638,6 +638,303 @@ gitea http: 302
---
+### 24. Модуль 2 — проверка CI-стека + `kind-config.yaml` новой топологии
+
+Начиная с этой записи, команды выполнял **ты сам** напрямую в интерактивной SSH-сессии на сервере (login shell, `~/.local/bin` уже в `PATH`), а мне присылал готовый лог — поэтому команды ниже идут без SSH-обвязки из пункта 0.
+
+**Команда:**
+```bash
+docker ps --filter name=gitea --format '{{.Names}}\t{{.Status}}'
+
+cat > ~/k8s/kind-config.yaml << 'EOF'
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+nodes:
+ - role: control-plane
+ extraPortMappings:
+ - containerPort: 80
+ hostPort: 8080
+ - containerPort: 443
+ hostPort: 8443
+ - role: worker
+ - role: worker
+EOF
+cat ~/k8s/kind-config.yaml
+```
+
+**Разбор аргументов:**
+- `docker ps --filter name=gitea --format '{{.Names}}\t{{.Status}}'` — сужает список контейнеров до тех, чьё имя содержит `gitea`, и печатает только имя и статус — быстрая проверка перед началом работы, что боевой CI-стек жив (чек-лист из SERVER.md).
+- `cat > файл << 'EOF' ... EOF` — heredoc: всё, что между `<< 'EOF'` и завершающим `EOF`, построчно записывается в файл через `cat`; кавычки вокруг `'EOF'` отключают подстановку переменных/спецсимволов внутри блока (не нужна здесь, но привычка на будущее для блоков с `$`).
+- `role: control-plane` / `role: worker` — три записи в списке `nodes` = kind создаст три Docker-контейнера, один control-plane и два worker (топология модуля 2 из LEARNING.md).
+- `extraPortMappings` внутри control-plane-ноды — пробрасывает порты хоста на порты внутри этого контейнера-ноды: `hostPort: 8080` → `containerPort: 80` и `hostPort: 8443` → `containerPort: 443`. Сделано сразу в модуле 2 (адаптация из SERVER.md), а не только в модуле 6 (когда понадобится Ingress) — иначе пришлось бы пересоздавать кластер повторно. Именно 8080/8443, а не 80/443, потому что порты 80/443 хоста уже заняты Gitea/nginx (см. `ss -tulpn` в сессии — 80/443/3000/3030/222/4400/9090 заняты, 8080/8443 свободны).
+
+**Получен ответ:**
+```
+gitea_runner Up 7 hours
+gitea Up 7 hours (healthy)
+gitea_db Up 7 hours (healthy)
+gitea_runner_armhf Up 7 hours
+
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+nodes:
+ - role: control-plane
+ extraPortMappings:
+ - containerPort: 80
+ hostPort: 8080
+ - containerPort: 443
+ hostPort: 8443
+ - role: worker
+ - role: worker
+```
+
+**Значит:** CI-стек в порядке, можно продолжать. Файл `kind-config.yaml` записан корректно — готов к использованию в `kind create cluster --config`.
+
+---
+
+### 25. Модуль 2 — пересоздание кластера с топологией control-plane + 2 worker
+
+**Команда:**
+```bash
+kind delete cluster --name lab
+kind create cluster --name lab --config kind-config.yaml
+kubectl cluster-info --context kind-lab
+```
+
+**Разбор аргументов:**
+- `kind delete cluster --name lab` — удаляет старый однонодовый кластер (созданный в модуле 0-1 без конфига) вместе с его Docker-контейнером, чтобы освободить имя `lab` под новую топологию.
+- `kind create cluster --name lab --config kind-config.yaml` — `--config` вместо голого `--name` заставляет kind читать топологию и extraPortMappings из файла, а не создавать кластер дефолтной однонодовой конфигурацией.
+- `kubectl cluster-info --context kind-lab` — печатает адрес API-сервера и CoreDNS для явно указанного контекста `kind-lab` (kind сам создаёт и переключает kubectl-контекст с таким именем при каждом `create cluster`).
+
+**Получен ответ:**
+```
+Deleting cluster "lab" ...
+Deleted nodes: ["lab-control-plane"]
+
+Creating cluster "lab" ...
+ ✓ Ensuring node image (kindest/node:v1.36.1) 🖼
+ ✓ Preparing nodes 📦 📦 📦
+ ✓ Writing configuration 📜
+ ✓ Starting control-plane 🕹️
+ ✓ Installing CNI 🔌
+ ✓ Installing StorageClass 💾
+ ✓ Joining worker nodes 🚜
+Set kubectl context to "kind-lab"
+
+Kubernetes control plane is running at https://127.0.0.1:36761
+CoreDNS is running at https://127.0.0.1:36761/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
+```
+
+**Значит:** пересоздание прошло без ошибок и быстро (образ ноды `kindest/node:v1.36.1` уже был в кэше Docker с прошлого раза — не пришлось скачивать заново). Обрати внимание на новый шаг в выводе, которого не было при однонодовом создании: `✓ Joining worker nodes` — именно он добавляет `lab-worker`/`lab-worker2` к кластеру после того, как control-plane уже поднят. kubectl-контекст переключён на `kind-lab` автоматически.
+
+---
+
+### 26. Модуль 2 — проверка нод
+
+**Команда:**
+```bash
+kubectl get nodes -o wide
+```
+
+**Получен ответ:**
+```
+NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
+lab-control-plane Ready control-plane 57s v1.36.1 172.22.0.4 Debian GNU/Linux 13 (trixie) 6.8.0-136-generic (amd64) containerd://2.3.1
+lab-worker Ready 46s v1.36.1 172.22.0.2 Debian GNU/Linux 13 (trixie) 6.8.0-136-generic (amd64) containerd://2.3.1
+lab-worker2 Ready 47s v1.36.1 172.22.0.3 Debian GNU/Linux 13 (trixie) 6.8.0-136-generic (amd64) containerd://2.3.1
+```
+
+**Значит:** все 3 ноды поднялись и перешли в `Ready` за 46-57 секунд — быстрее, чем однонодовый кластер с нуля (модуль 0, ~53с только на control-plane), потому что образ ноды уже был в кэше. Колонка `ROLES` подтверждает топологию: одна `control-plane`, две `` (worker — kind не проставляет им явную роль-метку по умолчанию, в отличие от control-plane). `KERNEL-VERSION` (6.8.0-136) новее, чем было зафиксировано в SERVER.md на момент установки (6.8.0-124) — сервер обновлялся ядром между сессиями, это не связано с kind и не требует действий.
+
+---
+
+### 27. Модуль 2 — компоненты control-plane и DaemonSet'ы вживую
+
+**Команда:**
+```bash
+kubectl get pods -n kube-system -o wide
+```
+
+**Получен ответ:**
+```
+NAME READY STATUS RESTARTS AGE NODE
+coredns-589f44dc88-s8m6z 1/1 Running 0 86s lab-control-plane
+coredns-589f44dc88-xlkk7 1/1 Running 0 86s lab-control-plane
+etcd-lab-control-plane 1/1 Running 0 93s lab-control-plane
+kindnet-62npg 1/1 Running 0 86s lab-control-plane
+kindnet-9lsrg 1/1 Running 0 86s lab-worker2
+kindnet-xwjqx 1/1 Running 0 85s lab-worker
+kube-apiserver-lab-control-plane 1/1 Running 0 93s lab-control-plane
+kube-controller-manager-lab-control-plane 1/1 Running 0 93s lab-control-plane
+kube-proxy-6ltl9 1/1 Running 0 85s lab-worker
+kube-proxy-9z5x9 1/1 Running 0 86s lab-control-plane
+kube-proxy-h7xtp 1/1 Running 0 86s lab-worker2
+kube-scheduler-lab-control-plane 1/1 Running 0 93s lab-control-plane
+```
+
+**Значит:** ровно та картина, которую описывает теория модуля 2. Четыре компонента control-plane (`kube-apiserver`, `etcd`, `kube-controller-manager`, `kube-scheduler`) — все с суффиксом `-lab-control-plane` в имени и все в колонке `NODE` только на `lab-control-plane`, других экземпляров нет. Два DaemonSet'а — `kindnet-*` (CNI, сетевая связность между подами на разных нодах) и `kube-proxy-*` (сетевые правила для Service, разбирается в модуле 5) — у каждого ровно по одному поду на каждую из 3 нод (control-plane и оба worker), что и есть определение DaemonSet: «один под на каждую подходящую ноду». `coredns-*` — не DaemonSet, а обычный Deployment на 2 реплики; обе оказались на `lab-control-plane`, потому что worker-ноды пока полностью пустые, и scheduler разместил их там, где выгоднее (это никак не привязано к ролям нод — просто текущее решение scheduler'а по ресурсам).
+
+Устная самопроверка модуля 2 (3 вопроса из LEARNING.md — компоненты control-plane, компоненты worker-ноды, почему etcd в проде выносят отдельно с нечётным числом нод) пройдена развёрнуто и верно. Дополнительно самостоятельно разобрана разница iptables/nftables применительно к двум режимам работы `kube-proxy` — не входит в буквальную программу модуля 2, но напрямую относится к роли `kube-proxy` и пригодится в модуле 5 (Service и сеть).
+
+---
+
+### 28. Модуль 3 — `pod.yaml` (Namespace + Pod) и создание
+
+**Команда:**
+```bash
+cat > ~/k8s/pod.yaml << 'EOF'
+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
+EOF
+
+kubectl apply -f ~/k8s/pod.yaml
+```
+
+**Разбор аргументов:**
+- Один файл, два объекта через разделитель `---` — YAML позволяет описать несколько документов в одном файле, `kubectl apply -f` применяет их по порядку сверху вниз. Namespace идёт первым не случайно: Pod ссылается на `namespace: demo` во втором документе, а этот namespace должен уже существовать (или быть создан в той же команде apply) к моменту создания Pod.
+- Четыре ключа верхнего уровня из теории модуля 3: `apiVersion` (версия API — `v1` для обоих базовых типов), `kind` (тип объекта), `metadata` (имя/namespace/labels — «паспорт»), `spec` (только у Pod — желаемое состояние: какой контейнер запускать).
+- `labels: app: demo` — метка на Pod, пока не используется явно, но именно так Service/Deployment в следующих модулях будут находить нужные поды через selector.
+
+**Получен ответ:**
+```
+namespace/demo created
+pod/demo-pod created
+```
+
+**Значит:** оба объекта созданы с первого раза, ошибок в манифесте нет.
+
+---
+
+### 29. Модуль 3 — базовый набор команд: get, describe, logs, exec
+
+**Команда:**
+```bash
+kubectl -n demo get pods
+kubectl -n demo get pods -o wide
+kubectl -n demo describe pod demo-pod
+kubectl -n demo logs demo-pod
+kubectl -n demo exec -it demo-pod -- sh
+```
+
+**Разбор аргументов:**
+- `-n demo` — на каждой команде явно указывает namespace (без него kubectl смотрит в `default`, где ничего нет).
+- `get pods -o wide` — `-o wide` добавляет колонки `IP` и `NODE` к базовому выводу.
+- `describe pod` — в отличие от `get`, разворачивает объект полностью: условия (`Conditions`), примонтированные volume, и главное — секцию `Events` (хронологический лог того, что control-plane и kubelet делали с этим подом: `Scheduled` → `Pulling`/`Pulled` → `Created` → `Started`).
+- `logs demo-pod` — без указания контейнера (работает, потому что в поде он один); печатает stdout/stderr процесса внутри контейнера.
+- `exec -it ... -- sh` — `-i` (interactive) + `-t` (tty) дают полноценный интерактивный терминал внутри контейнера; `sh`, а не `bash`, потому что образ `nginxdemos/hello` собран на Alpine (slim-образ без bash).
+
+**Получен ответ:**
+```
+NAME READY STATUS RESTARTS AGE
+demo-pod 1/1 Running 0 25s
+
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+demo-pod 1/1 Running 0 31s 10.244.2.2 lab-worker
+
+Node: lab-worker/172.22.0.2
+Status: Running
+...
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal Scheduled 89s default-scheduler Successfully assigned demo/demo-pod to lab-worker
+ Normal Pulling 88s kubelet Pulling image "nginxdemos/hello:latest"
+ Normal Pulled 82s kubelet Successfully pulled image ... in 6.311s
+ Normal Created 82s kubelet Container created
+ Normal Started 82s kubelet Container started
+
+2026/07/18 19:54:20 [notice] 1#1: nginx/1.29.1
+2026/07/18 19:54:20 [notice] 1#1: start worker process 27..30
+
+/ # exit
+```
+
+**Значит:** под запланирован планировщиком на `lab-worker` (control-plane исключён taint'ом), образ скачался за 6.3с, контейнер стартовал и поднял 4 worker-процесса nginx. `logs` показал именно вывод приложения (nginx), `describe`/`Events` — действия самого Kubernetes вокруг пода как объекта; это и есть разница между ними. `exec` подтвердил, что контейнер полноценно интерактивен.
+
+---
+
+### 30. Модуль 3 — полный манифест объекта (`-o yaml`)
+
+**Команда:**
+```bash
+kubectl -n demo get pod demo-pod -o yaml
+```
+
+**Разбор аргументов:**
+- `-o yaml` — просит API-сервер отдать объект целиком в том виде, в каком он реально хранится в etcd, а не только колонки таблицы `get pods`.
+
+**Получен ответ (сокращённо, полный лог — в сессии):**
+```
+metadata:
+ annotations:
+ kubectl.kubernetes.io/last-applied-configuration: |
+ {...исходный JSON манифеста...}
+ resourceVersion: "2565"
+ uid: ea8061a9-eca7-4cc0-91d0-86b6e87af510
+spec:
+ containers:
+ - imagePullPolicy: Always
+ ...
+ nodeName: lab-worker
+ restartPolicy: Always
+ schedulerName: default-scheduler
+ serviceAccountName: default
+ terminationGracePeriodSeconds: 30
+ tolerations: [...]
+ volumes:
+ - name: kube-api-access-p5g68
+ projected: {...}
+status:
+ conditions: [...]
+ containerStatuses: [...]
+ phase: Running
+ podIP: 10.244.2.2
+ hostIP: 172.22.0.2
+```
+
+**Значит:** написанные 12 строк манифеста развернулись примерно в 120. Сверх исходного `pod.yaml` появилось: служебные поля `metadata` (`uid`, `resourceVersion`, аннотация с сохранённым исходным манифестом — на ней основан three-way merge при следующих `apply`); дефолты в `spec`, которые Kubernetes подставил сам (`imagePullPolicy`, `restartPolicy`, `schedulerName`, `serviceAccountName`, `tolerations`, автомонтированный volume с токеном сервис-аккаунта, `nodeName` — результат работы планировщика); и целиком раздел `status`, которого в манифесте нет вообще — это runtime-состояние, которое пишет kubelet, наблюдая за реальным контейнером. Итог: `spec` — желаемое состояние (то, что попросили, плюс дефолты), `status` — фактическое состояние (что control loop реально наблюдает) — основа declarative-модели Kubernetes.
+
+---
+
+### 31. Модуль 3 — удаление голого Pod (без self-healing)
+
+**Команда:**
+```bash
+kubectl -n demo delete pod demo-pod
+kubectl -n demo get pods
+```
+
+**Разбор аргументов:**
+- В отличие от модуля 1 (Deployment), здесь Pod создан напрямую, без контроллера сверху.
+
+**Получен ответ:**
+```
+pod "demo-pod" deleted from demo namespace
+No resources found in demo namespace.
+```
+
+**Значит:** под удалился и не пересоздался — namespace `demo` пуст. Это и есть отличие голого Pod от Pod'а под управлением Deployment/ReplicaSet: `restartPolicy: Always` перезапускает контейнер *внутри* пода при его падении, но если исчезает сам Pod (удалён вручную или нода, на которой он жил, выходит из строя), пересоздать его некому — за это отвечает контроллер (ReplicaSet), которого здесь нет. Отсюда правило курса: голые Pod'ы в проде почти не используют, кроме короткоживущих задач.
+
+Устная самопроверка модуля 3 (роли ключей манифеста, `logs` vs `describe`, что добавляет `-o yaml`, плюс доп. вопрос про поведение при падении ноды) пройдена развёрнуто и верно.
+
+---
+
## Соглашение на будущее
Начиная с этой сессии, каждая команда, которую я выполняю в рамках курса (не только сегодня), добавляется в этот файл новой датированной секцией в том же формате: задача → команда → разбор аргументов → полученный ответ → что он значит. `PROGRESS.md` при этом остаётся коротким журналом-указателем — со ссылкой на соответствующий раздел здесь.
diff --git a/stack/kubernetes/LEARNING.md b/stack/kubernetes/LEARNING.md
index b5c7dcb..9619197 100644
--- a/stack/kubernetes/LEARNING.md
+++ b/stack/kubernetes/LEARNING.md
@@ -8,7 +8,7 @@
**Окружение курса** (одно на все модули, то же, что в 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).
+Альтернативное окружение — прохождение курса на удалённом Ubuntu-сервере по SSH вместо Windows + Docker Desktop: спеки железа, бенчмарки и точечные адаптации команд по модулям — в [SERVER.md](SERVER.md). Текущий прогресс прохождения (какой модуль пройден, на чём остановился, состояние кластера) — в [PROGRESS.md](PROGRESS.md), построчный разбор каждой выполненной команды с объяснением аргументов — в [COMMANDS.md](COMMANDS.md), а как просто запустить/остановить/проверить стенд между сессиями без учебных объяснений — в [USAGE.md](USAGE.md).
---
diff --git a/stack/kubernetes/PROGRESS.md b/stack/kubernetes/PROGRESS.md
index dc66daa..53878c4 100644
--- a/stack/kubernetes/PROGRESS.md
+++ b/stack/kubernetes/PROGRESS.md
@@ -1,6 +1,6 @@
# Прогресс прохождения курса Kubernetes
-Живой журнал, а не справочник — обновляется в конце каждой сессии по курсу. Цель: за 30 секунд понять, где остановился, и продолжить без перечитывания всего [LEARNING.md](LEARNING.md). Окружение и бенчмарки — в [SERVER.md](SERVER.md), построчный разбор каждой выполненной команды (что делает каждый аргумент, что значит ответ) — в [COMMANDS.md](COMMANDS.md); сюда их не дублировать.
+Живой журнал, а не справочник — обновляется в конце каждой сессии по курсу. Цель: за 30 секунд понять, где остановился, и продолжить без перечитывания всего [LEARNING.md](LEARNING.md). Окружение и бенчмарки — в [SERVER.md](SERVER.md), построчный разбор каждой выполненной команды (что делает каждый аргумент, что значит ответ) — в [COMMANDS.md](COMMANDS.md), как запустить/остановить/проверить стенд между сессиями — в [USAGE.md](USAGE.md); сюда их не дублировать.
## Статус модулей
@@ -8,8 +8,8 @@
|---|---|---|---|
| 0 | Установка инструментов | ✅ Пройден | 2026-07-18 |
| 1 | Зачем вообще Kubernetes | ✅ Пройден (как смоук-тест наладки) | 2026-07-18 |
-| 2 | Архитектура кластера | ⬜ Не начат | |
-| 3 | Pod и kubectl-база | ⬜ Не начат | |
+| 2 | Архитектура кластера | ✅ Пройден | 2026-07-18 |
+| 3 | Pod и kubectl-база | ✅ Пройден | 2026-07-18 |
| 4 | Deployment и ReplicaSet | ⬜ Не начат | |
| 5 | Service и сеть | ⬜ Не начат | |
| 6 | Ingress | ⬜ Не начат | |
@@ -41,12 +41,42 @@
Затыки: нет (модуль 0 и смоук-тест прошли гладко).
+### 2026-07-18 — модуль 2: архитектура кластера
+
+Формат сессии новый: команды на сервере выполнял сам, мне присылались логи шаг за шагом — я вёл по шагам и разбирал вывод.
+
+Сделано:
+- Кластер `lab` пересоздан с топологией 1 control-plane + 2 worker через `~/k8s/kind-config.yaml`, сразу с `extraPortMappings` 8080→80/8443→443 (адаптация из SERVER.md) — все 3 ноды `Ready` за 46-57с. Разбор команд — [COMMANDS.md](COMMANDS.md) #24-26.
+- Вживую найдены и разобраны все 4 компонента control-plane (`kube-apiserver`, `etcd`, `kube-controller-manager`, `kube-scheduler` — все на `lab-control-plane`) и оба DaemonSet'а (`kindnet`, `kube-proxy` — по одному поду на каждую из 3 нод); `coredns` опознан как обычный Deployment (2 реплики), а не DaemonSet. Разбор — [COMMANDS.md](COMMANDS.md) #27.
+- Устная самопроверка модуля 2 (компоненты control-plane/worker, кворум etcd) пройдена развёрнуто и верно. Дополнительно самостоятельно разобрана разница iptables/nftables применительно к режимам `kube-proxy` (сверх программы модуля 2, пригодится в модуле 5).
+- CI-стек Gitea проверен перед началом — все 4 контейнера Up/healthy, не задет.
+
+Осталось на следующую сессию:
+- **Модуль 3** — Pod и kubectl-база: создать `pod.yaml` (Namespace `demo` + Pod `demo-pod`), пройти базовый набор команд (`get`, `describe`, `logs`, `exec`, `-o yaml`, `delete`).
+
+Затыки: нет, модуль 2 прошёл гладко.
+
+### 2026-07-18 — модуль 3: Pod и kubectl-база
+
+Формат сессии тот же: команды на сервере выполнял сам, логи присылались шаг за шагом.
+
+Сделано:
+- Создан `~/k8s/pod.yaml` (Namespace `demo` + Pod `demo-pod`, образ `nginxdemos/hello:latest`), применён — под ушёл в `Running` на `lab-worker` за ~7с (время скачивания образа). Разбор — [COMMANDS.md](COMMANDS.md) #28.
+- Пройден базовый набор команд: `get pods [-o wide]`, `describe pod` (разобрана секция `Events` как таймлайн действий control-plane/kubelet), `logs` (вывод самого nginx), `exec -it ... -- sh` (интерактивный вход внутрь контейнера). Разбор — [COMMANDS.md](COMMANDS.md) #29.
+- Разобран `kubectl get pod -o yaml` построчно: чем ~120 строк реального объекта отличаются от 12 строк исходного манифеста (служебные поля metadata, дефолты в spec, целиком раздел status как runtime-состояние от kubelet) — основа declarative-модели K8s (`spec` = желаемое, `status` = фактическое). Разбор — [COMMANDS.md](COMMANDS.md) #30.
+- Под удалён (`kubectl delete pod`) — наглядно показано отсутствие self-healing у голого Pod без контроллера сверху (в отличие от Deployment в модуле 1). Разбор — [COMMANDS.md](COMMANDS.md) #31.
+- Устная самопроверка модуля 3 (4 ключа манифеста, `logs` vs `describe`, что добавляет `-o yaml`, плюс доп. вопрос про поведение при падении ноды) пройдена развёрнуто и верно.
+
+Осталось на следующую сессию:
+- **Модуль 4** — Deployment и ReplicaSet: `deployment.yaml` (3 реплики), посмотреть, как Deployment управляет ReplicaSet, а ReplicaSet — подами.
+
+Затыки: нет, модуль 3 прошёл гладко.
+
## Состояние кластера прямо сейчас
-- Кластер `lab` **существует**, 1-нодовый (control-plane, без workers), создан дефолтной командой `kind create cluster --name lab` — это ещё не топология из модуля 2.
-- Namespace `demo` и `monitoring` — **не созданы**.
-- В кластере ничего не развёрнуто (deployment `demo` из смоук-теста удалён).
-- Перед модулем 2 кластер будет пересоздан командой из курса (`kind delete cluster --name lab` + `kind create cluster --name lab --config kind-config.yaml`) — это ожидаемо и совпадает с шагами модуля 2, ничего вручную чистить не нужно.
+- Кластер `lab` **существует**, топология модуля 2: 1 control-plane (`lab-control-plane`) + 2 worker (`lab-worker`, `lab-worker2`), создан через `kind-config.yaml` с `extraPortMappings` 8080→80/8443→443.
+- Namespace `demo` **существует** (создан в модуле 3), но пуст — `demo-pod` был удалён по завершении практики модуля 3. Namespace `monitoring` — не создан.
+- В кластере, кроме namespace `demo`, ничего не развёрнуто.
## Открытые вопросы / затыки
diff --git a/stack/kubernetes/USAGE.md b/stack/kubernetes/USAGE.md
new file mode 100644
index 0000000..bb87262
--- /dev/null
+++ b/stack/kubernetes/USAGE.md
@@ -0,0 +1,133 @@
+# USAGE: как пользоваться стендом (запуск / остановка / проверка)
+
+Операционная шпаргалка, а не учебный материал — сюда смотришь, когда нужно просто поднять/погасить стенд между сессиями курса, без объяснений «зачем». Учебный курс — [LEARNING.md](LEARNING.md), окружение и характеристики сервера — [SERVER.md](SERVER.md), на чём остановился по модулям — [PROGRESS.md](PROGRESS.md), разбор каждой команды — [COMMANDS.md](COMMANDS.md).
+
+## 1. Подключение
+
+```bash
+ssh totserver@192.168.31.163
+```
+
+Ключ уже настроен, пароль не спросит. Дальше два варианта:
+
+- **Обычная интерактивная SSH-сессия** (просто зашёл и работаешь в терминале) — это login shell, `kind`/`helm`/`kubectl`/`docker` уже все видны в `PATH` без танцев.
+- **Одна неинтерактивная команда с хоста** (`ssh totserver@192.168.31.163 'команда'`) — это **non-login shell**, `~/.local/bin` (где лежат `kind` и `helm`) в `PATH` не попадает. Либо вызывать по полному пути (`~/.local/bin/kind ...`), либо оборачивать в `bash -lc "..."`:
+
+```bash
+ssh totserver@192.168.31.163 'bash -lc "kubectl get nodes"'
+```
+
+`kubectl` (snap) и `docker` видны в обоих случаях без исключений.
+
+## 2. Быстрая проверка состояния
+
+Одним блоком — что вообще сейчас поднято:
+
+```bash
+bash -lc '
+echo "--- kind-контейнеры ---"
+docker ps -a --filter "name=^lab-" --format "{{.Names}}\t{{.Status}}"
+echo "--- kubectl context ---"
+kubectl config current-context 2>&1
+echo "--- ноды ---"
+kubectl get nodes -o wide 2>&1
+echo "--- поды по всем namespace ---"
+kubectl get pods -A 2>&1
+echo "--- helm-релизы ---"
+helm list -A 2>&1
+echo "--- CI-стек Gitea (не трогать, но убедиться, что жив) ---"
+docker ps --filter name=gitea --format "{{.Names}}\t{{.Status}}"
+'
+```
+
+Если первый блок (`docker ps -a --filter "name=^lab-"`) пустой — кластера нет вообще, нужно создавать (раздел 3). Если контейнеры есть, но статус `Exited` — кластер существует, но остановлен, поднимать без пересоздания (раздел 3). Если `Up` — кластер уже работает, `kubectl get nodes` покажет `Ready`.
+
+## 3. Запуск кластера
+
+**Кластера нет вообще** (`docker ps -a` по `lab-*` пусто) — создать заново:
+
+```bash
+# одна нода, как сейчас (модули 0-1 пройдены на таком)
+bash -lc "kind create cluster --name lab"
+
+# либо с топологией модуля 2 (1 control-plane + 2 worker), если уже дошёл до неё —
+# см. kind-config.yaml с extraPortMappings 8080/8443 в SERVER.md
+bash -lc "kind create cluster --name lab --config kind-config.yaml"
+```
+
+Занимает ~50-60 секунд с нуля (в основном скачивание образа ноды при первом разе).
+
+**Контейнеры есть, но `Exited`** (типичная ситуация после перезагрузки сервера — у kind-нод, в отличие от Gitea, нет `restart: unless-stopped`, сами не поднимаются) — поднять существующие без потери состояния:
+
+```bash
+ssh totserver@192.168.31.163 'docker start $(docker ps -aq --filter "name=^lab-")'
+ssh totserver@192.168.31.163 'bash -lc "kubectl wait --for=condition=Ready nodes --all --timeout=120s"'
+```
+
+После любого из двух вариантов — проверить, что kubectl смотрит именно в этот кластер:
+
+```bash
+ssh totserver@192.168.31.163 'bash -lc "kubectl config current-context"'
+# ожидается: kind-lab
+```
+
+## 4. Остановка кластера (пауза, не удаление)
+
+Когда заканчиваешь сессию курса и не хочешь держать кластер прогретым (он ест ~500 МБ RAM и часть ядра в простое — не критично, но незачем без нужды):
+
+```bash
+ssh totserver@192.168.31.163 'docker stop $(docker ps -aq --filter "name=^lab-")'
+```
+
+Поды, PVC, всё состояние кластера остаётся на диске и возвращается как было после `docker start` (раздел 3).
+
+## 5. Полный снос кластера
+
+Когда нужно чистое состояние — например, начиная модуль 2 с новой топологией (см. [PROGRESS.md](PROGRESS.md) → «Осталось на следующую сессию»):
+
+```bash
+ssh totserver@192.168.31.163 'bash -lc "kind delete cluster --name lab"'
+```
+
+Безопасно для остального на сервере — kind-образы отдельные от всего, что использует Gitea.
+
+## 6. Чего не трогать
+
+Рядом на том же Docker крутится боевой CI-стек Gitea — не часть курса, но легко случайно задеть. Коротко (полный чек-лист — [SERVER.md → «CI-инфраструктура на сервере — что нельзя ломать»](SERVER.md#ci-инфраструктура-на-сервере--что-нельзя-ломать)):
+
+- Не занимать порты **80, 443, 3000, 3030, 222, 4400, 9090** в `kind-config.yaml` (`extraPortMappings` — только 8080/8443).
+- Не выполнять `docker system prune -a` / `docker image prune -a` — унесёт локально собранный образ `gitea-runner-armhf`, которого больше нигде нет.
+- Не удалять `.runner`-файлы раннеров (`/opt/gitea/runner*/`) и `.env` рядом.
+- Не запускать `docker-compose ... --remove-orphans` внутри `/opt/gitea`.
+- `kind delete cluster` (раздел 5) — безопасен, гасит только кластер, Gitea не задевает.
+
+## 7. Куда дальше
+
+- На чём остановился по модулям курса — [PROGRESS.md](PROGRESS.md).
+- Что проходить дальше и как — [LEARNING.md](LEARNING.md).
+
+### Шпаргалка
+```
+# === СОСТОЯНИЕ КЛАСТЕРА ===
+kubectl get nodes -o wide
+kubectl get pods -A
+kubectl get pods,svc,deploy # pods, services, deployments разом
+
+# === ДЕТАЛИ ===
+kubectl describe pod <имя>
+kubectl logs <имя-пода> --tail=20
+kubectl logs -f <имя-пода> # follow
+
+# === ВНУТРЬ ПОДА ===
+kubectl exec -it <имя-пода> -- /bin/sh
+kubectl exec <имя-пода> -- ls /app
+
+# === СОЗДАТЬ/УДАЛИТЬ ===
+kubectl apply -f файл.yaml
+kubectl delete -f файл.yaml
+kubectl delete pod <имя>
+
+# === ПОМОЩЬ ===
+kubectl api-resources | grep -v "^NAME" | less # что бывает
+kubectl explain deployment --recursive | less # все поля deployment
+```
\ No newline at end of file