Оформить опыт с 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.
This commit is contained in:
Tot Maxim
2026-07-18 15:06:57 +03:00
parent 8f431e4798
commit 40ff73f647
8 changed files with 907 additions and 4 deletions

View File

@@ -0,0 +1,643 @@
# Журнал команд: что запускалось и зачем
Построчный разбор каждой команды, которую я выполнял по SSH при наладке курса на сервере — с разбором каждого аргумента и объяснением, что значит полученный ответ. Цель файла — чтобы ты мог повторить любой шаг сам, точно понимая, что делает каждый флаг, а не копируя команду вслепую.
Формат каждой записи:
- **Задача** — зачем вообще эта команда.
- **Команда** — целиком, можно копировать.
- **Разбор аргументов** — что делает каждая часть.
- **Получен ответ** — реальный фрагмент вывода из этой сессии.
- **Значит** — какой вывод/решение делаем на основе этого ответа.
Общее для всех команд ниже, кроме отмеченных отдельно: они выполняются в моей SSH-сессии как `ssh -o BatchMode=yes [-o ConnectTimeout=N] totserver@192.168.31.163 'команда'`. Разбор этой обвязки — в самом первом пункте, дальше не повторяю.
---
## 2026-07-18
### 0. SSH-обвязка (используется почти во всех командах ниже)
**Команда:**
```bash
ssh -o BatchMode=yes -o ConnectTimeout=5 totserver@192.168.31.163 'команда'
```
**Разбор аргументов:**
- `-o BatchMode=yes` — запрещает SSH задавать любые интерактивные вопросы (в первую очередь пароль). Если аутентификация по ключу вдруг не сработает, соединение сразу упадёт с ошибкой вместо того, чтобы зависнуть в ожидании ввода, которого никто не даст — критично для автоматических/неинтерактивных вызовов.
- `-o ConnectTimeout=5` — сколько секунд ждать установления TCP-соединения, прежде чем сдаться. Защита от зависания, если сервер недоступен по сети.
- `'команда'` — то, что выполняется на сервере одной неинтерактивной **non-login shell**-сессией (важно, см. пункт 18 — от этого зависит, виден ли `~/.local/bin` в `PATH`).
---
### 1. Проверка, что уже стоит на сервере
**Команда:**
```bash
ssh -o BatchMode=yes -o ConnectTimeout=5 totserver@192.168.31.163 'echo SSH_OK; docker --version 2>&1; kind --version 2>&1; kubectl version --client 2>&1 | head -2; helm version 2>&1; echo ---; docker ps --format "{{.Names}}\t{{.Image}}\t{{.Status}}" 2>&1; echo ---; sysctl fs.inotify.max_user_watches fs.inotify.max_user_instances; stat -fc %T /sys/fs/cgroup; echo ---; snap list 2>/dev/null | grep -Ei "kubectl|go|helm|microk8s"'
```
**Разбор аргументов:**
- `echo SSH_OK` — простой маркер в начале вывода, чтобы сразу видеть, что соединение вообще установилось (до того, как читать остальное).
- `docker --version`, `kind --version`, `kubectl version --client`, `helm version`у каждого инструмента своя команда проверки версии; `2>&1` перенаправляет stderr в stdout, чтобы в одном потоке видеть и версию (если стоит), и ошибку `command not found` (если не стоит) — иначе ошибка могла бы потеряться.
- `kubectl version --client | head -2``--client` просит показать только версию локального `kubectl`, не пытаясь достучаться до кластера (которого ещё нет); `head -2` обрезает вывод до первых двух строк, там уже есть номер версии.
- `docker ps --format "{{.Names}}\t{{.Image}}\t{{.Status}}"``--format` с Go-шаблоном вместо стандартной широкой таблицы Docker печатает только три нужные колонки (имя контейнера, образ, статус) через табуляцию — компактно видно, что уже крутится на сервере.
- `sysctl fs.inotify.max_user_watches fs.inotify.max_user_instances` — без `-w` и без `=значение` `sysctl` работает в режиме чтения: показывает текущие значения этих двух лимитов ядра на число файлов/директорий, за которыми можно следить через inotify (механизм Linux для отслеживания изменений файлов) — важно для K8s, где kubelet и служебные процессы следят за множеством файлов конфигурации.
- `stat -fc %T /sys/fs/cgroup``-f` заставляет `stat` показывать информацию не о самом файле, а о файловой системе, на которой он лежит; `-c %T` — вывести только тип этой файловой системы. Так проверяется версия cgroup (`cgroup2fs` = cgroup v2, нужен для нормальной работы современного `kind`/`containerd`).
- `snap list | grep -Ei "kubectl|go|helm|microk8s"``snap list` без фильтров печатает все установленные snap-пакеты; `grep -E` (расширенные регулярки, `|` как «или») `-i` (без учёта регистра) сужает список до тех, что могут относиться к K8s-инструментам.
**Получен ответ:**
```
Docker version 28.5.1, build e180ab8
bash: line 1: kind: command not found
Client Version: v1.35.6
bash: line 1: helm: command not found
---
gitea_runner docker.gitea.com/act_runner:0.2.10 Up 4 weeks
gitea docker.gitea.com/gitea:1.24.7 Up 4 weeks (healthy)
---
fs.inotify.max_user_watches = 122407
fs.inotify.max_user_instances = 128
cgroup2fs
```
**Значит:** Docker и kubectl уже есть (kubectl — через snap), kind и helm нужно ставить с нуля. На сервере уже крутится рабочий стек Gitea — его нельзя случайно задеть. cgroup v2 — `kind` будет работать без дополнительных танцев с конфигурацией. inotify-лимиты низкие (122407/128) — стандартные для Ubuntu, для многонодового кластера может не хватить, стоит поднять (см. шаг с sudo-блоком).
---
### 2. Проверка sudo, занятых портов, памяти и ядра
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'sudo -n true 2>/dev/null && echo SUDO_NOPASSWD_OK || echo SUDO_NEEDS_PASSWORD; echo ---; ss -tlnp 2>/dev/null | awk "{print \$4}" | grep -E ":(80|443|8080|8443|3000|9090|6443)$" ; echo ---; free -m | head -2; nproc; uname -r'
```
**Разбор аргументов:**
- `sudo -n true 2>/dev/null && echo ... || echo ...``-n` («non-interactive») запрещает sudo самому спрашивать пароль: если пароль нужен, команда сразу проваливается вместо зависания в ожидании ввода, которого некому дать по неинтерактивному SSH. `true` — команда-пустышка, которая просто успешно завершается; нужна только как повод проверить, сработает ли sudo без пароля. `&&`/`||` — печатают разный текст в зависимости от того, удался ли `sudo -n true`.
- `ss -tlnp``-t` только TCP-сокеты, `-l` только слушающие (LISTEN), `-n` показывать порты числом, а не именами сервисов, `-p` пытаться показать владеющий процесс (без root может быть пусто, но порты всё равно видны).
- `awk '{print $4}'` — берёт 4-е поле по пробелам из вывода `ss` — это колонка «локальный адрес:порт».
- `grep -E ":(80|443|8080|8443|3000|9090|6443)$"` — оставляет только строки, заканчивающиеся на один из этих портов — это как раз порты, важные для курса (HTTP/HTTPS, стандартные альтернативы, порт API-сервера K8s).
- `free -m | head -2` — память в мегабайтах, `head -2` берёт только заголовок и строку `Mem:` (без `Swap:`) для краткости.
- `nproc` — сколько логических процессоров видно текущему пользователю.
- `uname -r` — версия ядра.
**Получен ответ:**
```
SUDO_NEEDS_PASSWORD
---
0.0.0.0:443
0.0.0.0:80
*:3000
*:9090
---
4
6.8.0-124-generic
```
**Значит:** sudo требует пароль → все мои дальнейшие команды должны обходиться без sudo (root-права я предоставить неинтерактивно не могу). Порты 80, 443, 3000, 9090 заняты чем-то другим (Gitea/файл-сервер) — при настройке Ingress в модуле 6 нельзя занимать их напрямую, нужны другие порты (8080/8443 через `extraPortMappings`, см. SERVER.md). 4 ядра — используется дальше при выборе `--threads=4` в бенчмарках.
---
### 3. Блок для пользователя — обновление системы, sysctl, reboot
Эту команду выполнял **ты сам** в своей интерактивной SSH-сессии (пароль sudo знаешь только ты) — но она часть той же последовательности и нужна, чтобы повторить сетап с нуля.
**Команда:**
```bash
sudo apt update && sudo apt upgrade -y
echo -e "fs.inotify.max_user_watches=524288\nfs.inotify.max_user_instances=512" | sudo tee /etc/sysctl.d/99-kind.conf && sudo sysctl --system
sudo reboot
```
**Разбор аргументов:**
- `apt update` — обновляет локальный список доступных версий пакетов из репозиториев (не устанавливает ничего сам по себе).
- `apt upgrade -y` — устанавливает все доступные обновления пакетов; `-y` отвечает «да» на все подтверждения автоматически.
- `echo -e "...\n..."``-e` включает интерпретацию `\n` как перевода строки (без `-e` строка выведется буквально с `\n` внутри) — формирует две строки с нужными sysctl-параметрами.
- `| sudo tee /etc/sysctl.d/99-kind.conf``tee` одновременно пишет полученный со входа текст в файл и печатает его на экран; используется вместо `sudo echo ... > file`, потому что перенаправление `>` само по себе выполняется от имени твоего обычного пользователя (а не sudo) и не имело бы прав писать в `/etc`, а `tee`, запущенный через `sudo`, эти права имеет.
- `sudo sysctl --system` — перечитывает все конфигурационные файлы sysctl (включая только что созданный) и применяет их немедленно, без перезагрузки.
- `sudo reboot` — полная перезагрузка сервера (нужна, чтобы применились обновления ядра/пакетов, помеченные `*** System restart required ***`).
**Получен ответ (после перезагрузки, проверено отдельной командой ниже):**
```
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 512
```
**Значит:** лимиты применились и сохранятся после будущих перезагрузок (файл в `/etc/sysctl.d/` — постоянный, не временный). Пакеты обновлены, дальнейшая работа идёт на актуальной системе.
---
### 4. Проверка сервера после перезагрузки
**Команда:**
```bash
ssh -o BatchMode=yes -o ConnectTimeout=8 totserver@192.168.31.163 'echo UPTIME:; uptime; echo ---DOCKER---; docker ps --format "{{.Names}}\t{{.Status}}"; echo ---SYSCTL---; sysctl fs.inotify.max_user_watches fs.inotify.max_user_instances; echo ---FREE---; free -h; echo ---APT---; cat /var/run/reboot-required 2>/dev/null || echo "no reboot required"'
```
**Разбор аргументов:**
- `uptime` — печатает, сколько времени сервер работает без перезагрузки, плюс load average; маленькое значение здесь — прямое доказательство, что reboot реально произошёл.
- `sysctl ...` (без `-w`) — снова чтение значений, теперь чтобы убедиться, что лимиты из sysctl.d применились и после ребута.
- `free -h``-h` («human-readable») показывает память в удобных единицах (Gi/Mi) вместо голых байт/килобайт.
- `cat /var/run/reboot-required 2>/dev/null || echo "no reboot required"` — этот файл существует на диске только когда система считает, что нужна ещё одна перезагрузка (обычно после обновления ядра). `cat` на несуществующий файл вернёт ошибку (подавленную через `2>/dev/null`) и ненулевой код — тогда сработает `||` и выведется «no reboot required».
**Получен ответ:**
```
UPTIME:
13:23:58 up 2 min, 2 users, load average: 0.25, 0.22, 0.09
---DOCKER---
gitea_runner Restarting (1) 44 seconds ago
gitea Up About a minute (healthy)
gitea_db Up About a minute (healthy)
---SYSCTL---
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 512
---APT---
no reboot required
```
**Значит:** сервер поднялся 2 минуты назад — ребут прошёл успешно, повторной перезагрузки система не требует, лимиты применились. `gitea` и `gitea_db` — здоровы. `gitea_runner` в `Restarting` — насторожило, проверено следующей командой.
---
### 5. Диагностика gitea_runner
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'docker logs --tail 20 gitea_runner 2>&1'
```
**Разбор аргументов:**
- `docker logs <container>` — показывает stdout/stderr процесса внутри контейнера.
- `--tail 20` — только последние 20 строк вместо всей истории логов с момента старта контейнера (которая может быть огромной при постоянном restart-loop).
**Получен ответ:**
```
level=error msg="fail to invoke Declare" error="unknown: rpc error: code = Unauthenticated desc = unregistered runner"
```
**Значит:** это ошибка регистрации act_runner в Gitea (runner потерял/не имеет токена), не связана с перезагрузкой или настройкой K8s. Отдельная проблема вне периметра курса, зафиксирована в SERVER.md/PROGRESS.md, не трогал.
---
### 6. Подготовка папки для бенчмарков + базовая температура
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'mkdir -p ~/bench && cd ~/bench && sensors 2>/dev/null | grep -E "Package|Composite"'
```
**Разбор аргументов:**
- `mkdir -p ~/bench` — создаёт папку для временных файлов бенчмарков; `-p` не выдаёт ошибку, если папка уже есть (и заодно создала бы промежуточные каталоги, если бы путь был вложенным).
- `sensors` — печатает показания всех датчиков, которые видит `lm-sensors`.
- `grep -E "Package|Composite"` — оставляет только строки с температурой пакета CPU (`Package id 0`) и NVMe (`Composite`) — самые показательные точки для проверки перегрева.
**Получен ответ:**
```
Composite: +55.9°C
Package id 0: +49.0°C
```
**Значит:** стартовая температура в норме (крит. отметки — 89.8°C у NVMe, 105°C у CPU), есть с чем сравнить температуру после нагрузки бенчмарками.
---
### 7. Скачивание образа для sysbench (фоновая задача)
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'docker pull severalnines/sysbench'
```
**Разбор аргументов:**
- `docker pull <образ>` — без тега по умолчанию тянет тег `latest`; просто скачивает образ в локальный кэш Docker, ничего не запускает. Вынесено отдельной командой (а не как часть `docker run`), чтобы сетевая задержка скачивания не искажала время самого CPU-бенчмарка.
**Получен ответ:**
```
Status: Downloaded newer image for severalnines/sysbench:latest
```
**Значит:** образ в кэше, следующий `docker run` с этим образом стартует мгновенно, без сетевых задержек — можно измерять чистую производительность CPU.
---
### 8. Бенчмарк CPU, 4 потока
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'docker run --rm severalnines/sysbench sysbench cpu --threads=4 --time=15 run 2>&1'
```
**Разбор аргументов:**
- `docker run --rm <образ> <команда...>` — запускает контейнер и сразу после завершения процесса удаляет его (`--rm`) — не оставляет мусора от одноразовых тестовых прогонов.
- `sysbench cpu ... run` — подкоманда `cpu` запускает тест производительности CPU (вычисление простых чисел), `run` — команда «выполнить тест» (в отличие от `prepare`/`cleanup`, которые нужны только для тестов БД/файловой системы).
- `--threads=4` — количество параллельных рабочих потоков; выставлено равным числу ядер сервера (см. шаг 2, `nproc` = 4), чтобы измерить производительность при полной загрузке всех ядер.
- `--time=15` — тест длится фиксированные 15 секунд вместо фиксированного числа операций, дальше замеряется, сколько «событий» (вычислений) успело пройти за это время.
**Получен ответ:**
```
CPU speed:
events per second: 11283.72
total time: 15.0003s
```
**Значит:** ~11.3k событий/сек при полной загрузке всех 4 ядер — точка отсчёта для сравнения с однопоточным результатом ниже и ориентир, что CPU не является узким местом для стандартных подов курса.
---
### 9. Бенчмарк CPU (1 поток) и RAM
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'docker run --rm severalnines/sysbench sysbench cpu --threads=1 --time=10 run 2>&1 | grep -E "events per second|total time"; echo "---MEMORY---"; docker run --rm severalnines/sysbench sysbench memory --threads=4 --time=10 run 2>&1 | grep -E "MiB/sec|MB/sec|transferred|Total operations"'
```
**Разбор аргументов:**
- `--threads=1` — та же CPU-нагрузка, но только на одном ядре. Важно отдельно от 4-поточного теста: многие ключевые компоненты K8s (например, fsync у etcd, обработка одного запроса kube-apiserver) упираются в производительность одного ядра, а не в суммарную многопоточную мощность.
- `sysbench memory ... run` — тест пропускной способности памяти (сколько данных можно прочитать/записать в RAM в единицу времени); без явного `--memory-oper` использует режим по умолчанию (запись).
- `grep -E "events per second|total time"` / `grep -E "MiB/sec|...|Total operations"` — сжимают многословный вывод sysbench до одной-двух строк с самими цифрами.
**Получен ответ:**
```
events per second: 3301.05
---MEMORY---
Total operations: 101389750 (10137580.18 per second)
99013.43 MiB transferred (9899.98 MiB/sec)
```
**Значит:** однопоточная производительность (3.3k events/sec) заметно ниже многопоточной (11.3k) — ожидаемо для N100 (это энергоэффективный, а не высокочастотный процессор), но всё ещё достаточно для K8s control-plane в pet-масштабе. Память гоняется на ~9.9 ГБ/с — не узкое место.
---
### 10. Бенчмарк диска — неудачная попытка (обучающий момент)
**Команда (не сработала):**
```bash
docker run --rm -v ~/bench:/bench ljishen/fio fio --name=seqwrite --directory=/bench --rw=write --bs=1M --size=1G --direct=1 --numjobs=1 --group_reporting
```
**Разбор аргументов:**
- `-v ~/bench:/bench` — монтирует папку `~/bench` с сервера (на реальном NVMe) внутрь контейнера по пути `/bench`. Принципиально важно: без volume fio писал бы во внутренний слой контейнера, а не на настоящий диск, и результат был бы не про физический NVMe.
- `ljishen/fio fio --name=...` — так я по ошибке продублировал `fio` как первый аргумент.
**Получен ответ:**
```
fio: unable to open 'fio' job file
```
**Значит:** ошибка. Проверил причину отдельной командой ниже — образ `ljishen/fio` уже сам является обёрткой над `fio` (`ENTRYPOINT=["fio"]`), поэтому переданное мной слово `fio` было воспринято не как команда, а как имя job-файла, который fio пытался (и не смог) открыть.
---
### 11. Проверка entrypoint образа
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'docker run --rm ljishen/fio --version 2>&1; echo "---"; docker inspect ljishen/fio --format "{{.Config.Entrypoint}} {{.Config.Cmd}}"'
```
**Разбор аргументов:**
- `docker run --rm ljishen/fio --version` — без слова `fio` в начале: если `--version` действительно долетает до самой программы fio, значит entrypoint уже подставляет `fio` сам.
- `docker inspect <образ> --format "{{.Config.Entrypoint}} {{.Config.Cmd}}"``inspect` печатает полные метаданные образа в JSON; `--format` с Go-шаблоном вытаскивает из этого JSON только два конкретных поля — `Entrypoint` (неизменяемая часть команды запуска) и `Cmd` (аргументы по умолчанию, которые можно переопределить).
**Получен ответ:**
```
fio-3.6
---
[fio] []
```
**Значит:** подтверждено — `Entrypoint` уже `["fio"]`, а `Cmd` пустой. Значит все аргументы, которые я передаю после имени образа в `docker run`, должны быть аргументами **для fio напрямую**, без повторения слова `fio`.
---
### 12. Бенчмарк диска — последовательная запись (исправлено)
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'docker run --rm -v ~/bench:/bench ljishen/fio --name=seqwrite --directory=/bench --rw=write --bs=1M --size=1G --direct=1 --numjobs=1 --group_reporting 2>&1 | tail -25'
```
**Разбор аргументов:**
- `--name=seqwrite` — произвольная метка этого job'а fio, используется только в заголовках вывода.
- `--directory=/bench` — куда писать тестовый файл (внутрь смонтированного volume — то есть реально на NVMe хоста).
- `--rw=write` — паттерн доступа: чисто последовательная запись (эмулирует, например, запись большого лога/бэкапа целиком).
- `--bs=1M` — размер одного блока ввода-вывода — 1 мегабайт; крупные блоки типичны для последовательных операций и дают максимальную пропускную способность.
- `--size=1G` — сколько всего данных записать за тест.
- `--direct=1` — включает `O_DIRECT`: запись идёт мимо кэша страниц ОС напрямую на диск. Без этого флага тест мог бы измерить скорость RAM-кэша, а не реального диска.
- `--numjobs=1` — один параллельный процесс fio (для последовательного теста больше не нужно — параллельность как раз мешает последовательности).
- `--group_reporting` — объединяет статистику всех `numjobs` в одну сводку вместо вывода по каждому job'у отдельно (при `numjobs=1` эффекта почти нет, но привычка нужна для следующего теста).
**Получен ответ:**
```
write: IOPS=1024, BW=1024MiB/s (1074MB/s)(1024MiB/1000msec)
```
**Значит:** ~1 ГБ/с на последовательной записи — быстрый NVMe, загрузка образов контейнеров и запись больших файлов не будет узким местом.
---
### 13. Бенчмарк диска — случайный доступ 4k (IOPS)
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'docker run --rm -v ~/bench:/bench ljishen/fio --name=randrw --directory=/bench --rw=randrw --rwmixread=70 --bs=4k --size=512M --direct=1 --numjobs=4 --iodepth=16 --group_reporting --runtime=15 --time_based 2>&1 | grep -E "read:|write:|IOPS|clat"'
```
**Разбор аргументов (отличия от предыдущего теста):**
- `--rw=randrw --rwmixread=70` — случайный доступ, смешанный на чтение/запись, где 70% операций — чтение, 30% — запись; это ближе к реальной нагрузке БД/etcd, чем чистая последовательная запись.
- `--bs=4k` — маленький блок 4 КБ — именно мелкие случайные операции сильнее всего нагружают диск и определяют IOPS (операций в секунду), а не мегабайты в секунду. Это ключевая метрика для etcd, который делает частые маленькие fsync.
- `--size=512M` — меньше, чем в первом тесте, потому что при `--time_based` (см. ниже) размер — это просто верхняя граница, реальная длительность определяется временем.
- `--numjobs=4` — 4 параллельных процесса fio, по числу ядер CPU — эмулирует одновременную нагрузку от нескольких подов/процессов.
- `--iodepth=16` — сколько запросов ввода-вывода держится «в полёте» одновременно на каждый job, до получения ответа от диска — имитирует не единичный запрос-ответ, а очередь, как под реальной многопользовательской нагрузкой.
- `--runtime=15 --time_based` — тест идёт ровно 15 секунд (`--time_based` заставляет fio перезапускать проход по данным заново, если `--size` кончился раньше времени), а не «пока не запишется 512 МБ» — нужно для честного измерения именно IOPS/сек, а не общего времени на фиксированный объём.
**Получен ответ:**
```
read: IOPS=12.6k, BW=49.3MiB/s
write: IOPS=5435, BW=21.2MiB/s
clat avg=224.59us (read) / 208.86us (write)
```
**Значит:** ~12.6k IOPS на чтение и ~5.4k на запись при латентности <0.25мс с большим запасом достаточно для etcd (которому обычно достаточно однозначных десятков IOPS с задержкой fsync <10мс) и для любых баз данных в подах курса.
---
### 14. Уборка временных файлов бенчмарка
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'rm -rf ~/bench/*.0.0 ~/bench/seqwrite* ~/bench/randrw*; ls -la ~/bench/'
```
**Разбор аргументов:**
- `rm -rf <шаблоны>` удаляет файлы, оставшиеся от fio (тестовые файлы данных и *.0.0 вспомогательные файлы состояния fio); `-r` рекурсивно (на случай, если что-то создалось как директория), `-f` без запроса подтверждения и без ошибки, если файла с таким именем нет.
- `ls -la ~/bench/` проверка, что папка действительно пуста после уборки; `-l` подробный формат (размеры, права), `-a` показывает и скрытые файлы (начинающиеся с точки), если fio такие оставил.
**Получен ответ:**
```
total 8
(только . и .. — папка пустая)
```
**Значит:** временные файлы бенчмарков убраны, диск не засорён тестовыми артефактами.
---
### 15. Установка kind без sudo
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 '
set -e
mkdir -p ~/.local/bin
ARCH=$(dpkg --print-architecture)
KIND_VER=$(curl -s https://api.github.com/repos/kubernetes-sigs/kind/releases/latest | grep -oP "\"tag_name\": \"\K[^\"]+")
curl -Lo ~/.local/bin/kind "https://kind.sigs.k8s.io/dl/${KIND_VER}/kind-linux-${ARCH}"
chmod +x ~/.local/bin/kind
~/.local/bin/kind --version
'
```
**Разбор аргументов:**
- `set -e` заставляет весь скрипт немедленно остановиться при первой же команде, завершившейся с ошибкой, вместо того чтобы продолжать выполнение с уже сломанным состоянием (например, пытаться `chmod +x` файл, который не скачался).
- `mkdir -p ~/.local/bin` целевая папка для бинарников без прав root; на Ubuntu она по умолчанию входит в `PATH` для интерактивных сессий (см. пункт 18).
- `dpkg --print-architecture` узнаёт архитектуру процессора этой системы (`amd64`), чтобы не хардкодить её в URL скрипт остаётся рабочим и на ARM-сервере.
- `curl -s https://api.github.com/.../releases/latest` `-s` (silent) запрашивает у GitHub API JSON с данными о последнем релизе kind без индикатора прогресса, засоряющего вывод.
- `grep -oP '"tag_name": "\K[^"]+'` `-o` печатает только совпавший фрагмент текста (не всю строку); `-P` включает Perl-регулярки, нужные для `\K` этот спецсимвол «сбрасывает» начало совпадения, то есть в захват попадает только то, что идёт **после** `tag_name": "`, так извлекается чистый номер версии (`v0.32.0`) без лишних кавычек и ключа.
- `curl -Lo ~/.local/bin/kind "URL"` `-L` заставляет curl следовать HTTP-редиректам (GitHub Releases часто отдаёт 302 на CDN); `-o <путь>` сохраняет скачанное в файл вместо печати в терминал.
- `chmod +x` добавляет право на исполнение без этого бинарник нельзя будет запустить напрямую.
**Получен ответ:**
```
kind version 0.32.0
```
**Значит:** kind установлен и работает, версия 0.32.0 (актуальная на момент установки). Это прямая замена шага `choco install kind` из Модуля 0 курса только без прав администратора и без Windows-пакетного менеджера.
---
### 16. Установка helm без sudo
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 '
cd /tmp
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod +x get_helm.sh
HELM_INSTALL_DIR=$HOME/.local/bin USE_SUDO=false ./get_helm.sh
rm -f get_helm.sh
~/.local/bin/helm version
'
```
**Разбор аргументов:**
- `curl -fsSL -o get_helm.sh <url>` `-f` fail») заставляет curl вернуть ошибку и не сохранять файл, если сервер ответил HTTP-ошибкой (например, 404) без этого флага в файл сохранилась бы страница с текстом ошибки, и скрипт попытался бы её выполнить как bash; `-s` тихий режим (без прогресс-бара); `-S` но при этом всё равно показывать текст ошибки, если она возникла (иначе `-s` подавил бы и её); `-L` следовать редиректам.
- `HELM_INSTALL_DIR=$HOME/.local/bin USE_SUDO=false ./get_helm.sh` это официальный установочный скрипт Helm, который сам умеет определять ОС/архитектуру, скачивать нужный архив и распаковывать бинарник. Две переменные окружения меняют его поведение: `HELM_INSTALL_DIR` куда класть бинарник (по умолчанию `/usr/local/bin`, для чего нужен root); `USE_SUDO=false` явно запрещает скрипту самому подставлять `sudo` перед своими внутренними командами создания папок/копирования файлов.
**Получен ответ:**
```
Downloading https://get.helm.sh/helm-v3.21.3-linux-amd64.tar.gz
Verifying checksum... Done.
helm installed into /home/totserver/.local/bin/helm
helm not found. Is /home/totserver/.local/bin on your $PATH?
Failed to install helm
```
(команда завершилась с exit code 1, несмотря на текст "helm installed into...")
**Значит:** это ложная тревога, не реальная ошибка установки. Сам скрипт в конце **тоже** пытается вызвать `helm` напрямую (полагаясь на `PATH`) для самопроверки но моя SSH-команда выполняется как неинтерактивная non-login shell, в которой `~/.local/bin` ещё не добавлен в `PATH` (эта строчка подключается только в `~/.profile`, который читается лишь при **login shell** см. пункт 18). Бинарник при этом реально записан на диск корректно проверено следующим шагом прямым обращением по полному пути.
---
### 17. Проверка, что helm реально установился
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'ls -la ~/.local/bin/; ~/.local/bin/helm version; echo "---PATH---"; echo $PATH'
```
**Разбор аргументов:**
- `~/.local/bin/helm version` вызов бинарника по **полному пути**, минуя `PATH` целиком если он и так работает, ошибка из шага 16 точно была только про видимость в `PATH`, а не про сам бинарник.
- `echo $PATH` печатает список директорий, в которых шелл ищет команды, чтобы наглядно убедиться в отсутствии `~/.local/bin`.
**Получен ответ:**
```
-rwxr-xr-x 1 totserver totserver 58654882 ... helm
-rwxrwxr-x 1 totserver totserver 10522750 ... kind
version.BuildInfo{Version:"v3.21.3", ...}
---PATH---
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
```
**Значит:** оба бинарника на месте и рабочие; в `PATH` этой non-login-сессии `~/.local/bin` действительно нет подтверждена гипотеза из пункта 16, дальше нужно проверить именно login shell.
---
### 18. Проверка PATH в login shell (как в реальной SSH-сессии)
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'bash -lc "which kind helm kubectl docker; kind --version; helm version --short; kubectl version --client -o json | grep gitVersion; docker --version"'
```
**Разбор аргументов:**
- `bash -lc "..."` `-l` login») запускает bash так, будто ты только что залогинился интерактивно: он читает `/etc/profile` и `~/.profile`, где как раз и лежит стандартный для Ubuntu кусок, добавляющий `~/.local/bin` в `PATH`, если папка существует; `-c "..."` выполняет переданную строку как единственную команду вместо запуска интерактивного приглашения. Обычный `ssh host 'команда'` (как во всех предыдущих шагах) login shell не создаёт поэтому там `.profile` не читался.
- `which kind helm kubectl docker` для каждого имени печатает полный путь к найденному в `PATH` исполняемому файлу (или ничего, если не найден) быстрый способ проверить видимость сразу всех инструментов.
- `helm version --short` `--short` печатает только номер версии одной строкой вместо полной структуры `BuildInfo{...}`.
- `kubectl version --client -o json | grep gitVersion` `-o json` выводит версию в формате JSON вместо человекочитаемого текста, `grep gitVersion` вытаскивает из этого JSON только строку с номером версии.
**Получен ответ:**
```
/home/totserver/.local/bin/kind
/home/totserver/.local/bin/helm
/snap/bin/kubectl
/usr/bin/docker
kind version 0.32.0
v3.21.3+g1ad6e68
"gitVersion": "v1.35.6",
Docker version 29.6.2, build dfc4efb
```
**Значит:** в login shell (то есть в твоей обычной интерактивной SSH-сессии) все четыре инструмента находятся сами, без ручных полных путей установка полностью рабочая, дополнительных действий с `PATH` не требуется. (Заодно видно, что Docker подтянулся до 29.6.2 during `apt upgrade` в шаге 3.)
---
### 19. Создание кластера kind (замер времени)
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'bash -lc "time kind create cluster --name lab"'
```
**Разбор аргументов:**
- `time <команда>` встроенная в bash обёртка: выполняет команду как обычно, а после завершения печатает, сколько заняло `real` (реальное время по часам), `user` и `sys` (CPU-время самого процесса и ядра). Используется только для замера, на результат самой команды не влияет.
- `kind create cluster --name lab` `--name lab` задаёт имя кластера оно используется как префикс для Docker-контейнеров нод (`lab-control-plane`) и как способ отличить этот кластер от других, если их будет несколько. Имя `lab` то же самое, что использует курс во всех модулях, что важно для совместимости всех последующих команд курса без изменений.
**Получен ответ:**
```
✓ Ensuring node image
✓ Preparing nodes
✓ Starting control-plane
✓ Installing CNI
✓ Installing StorageClass
real 0m52.836s
```
**Значит:** кластер с нуля поднимается за ~53 секунды практически всё это время уходит на скачивание образа ноды `kindest/node` при первом запуске (при повторных `kind create cluster` будет заметно быстрее, образ уже в кэше).
---
### 20. Проверка нод и системных подов
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'bash -lc "kubectl get nodes -o wide; echo ---; kubectl get pods -n kube-system"'
```
**Разбор аргументов:**
- `kubectl get nodes -o wide` `-o wide` добавляет к стандартному выводу (имя, статус) дополнительные колонки: внутренний IP, версия ОС, версия ядра, container runtime.
- `kubectl get pods -n kube-system` `-n kube-system` указывает конкретный namespace для запроса вместо намерения по умолчанию (`default`) именно в `kube-system` живут поды control-plane-компонентов (это ровно то, о чём Модуль 2 курса).
**Получен ответ (сразу после создания, ещё стартует):**
```
lab-control-plane NotReady ...
coredns-... 0/1 Pending
kube-apiserver-lab-control-plane 0/1 Running
```
**Значит:** сразу после `kind create cluster` нода и часть подов ещё не готовы (control-plane только инициализируется) это ожидаемо, не ошибка. Нужно дождаться готовности отдельной командой (следующий шаг).
---
### 21. Замер времени до полной готовности
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'bash -lc "time kubectl wait --for=condition=Ready nodes --all --timeout=120s; time kubectl wait --for=condition=Ready pods --all -n kube-system --timeout=120s"'
```
**Разбор аргументов:**
- `kubectl wait --for=condition=Ready <объекты> --timeout=Ns` вместо разового снимка состояния (`get`) эта команда **блокируется** и ждёт, пока указанное условие не станет истинным для всех выбранных объектов, либо не истечёт таймаут.
- `--for=condition=Ready` конкретное условие ожидания: статус `Ready=True` в списке conditionов объекта.
- `nodes --all` в первом вызове применить ко всем объектам типа Node (а не к одной по имени).
- `pods --all -n kube-system` во втором ко всем подам именно в этом namespace.
- `--timeout=120s` если условие не выполнится за 2 минуты, команда вернёт ошибку вместо вечного ожидания.
**Получен ответ:**
```
node/lab-control-plane condition met
real 0m4.100s
pod/coredns-... condition met
(и так все поды)
real 0m2.389s
```
**Значит:** после создания кластера потребовалось ещё ~6.5 секунды (4.1 + 2.4), чтобы нода и все системные поды перешли в `Ready` итоговое время от нуля до полностью рабочего кластера 53 + 6.5 59-60 секунд.
---
### 22. Смоук-тест self-healing (практика Модуля 1)
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'bash -lc "
kubectl create deployment demo --image=nginxdemos/hello
kubectl wait --for=condition=Ready pod -l app=demo --timeout=60s
kubectl get pods -o wide
POD=\$(kubectl get pods -l app=demo -o jsonpath=\"{.items[0].metadata.name}\")
echo \"Killing pod: \$POD\"
kubectl delete pod \$POD
sleep 2
kubectl get pods -o wide
kubectl delete deployment demo
"'
```
**Разбор аргументов:**
- `kubectl create deployment demo --image=nginxdemos/hello` создаёт Deployment по имени `demo`; `--image=` задаёт образ, который будет запущен в подах тот же образ, что использует курс в Модуле 1.
- `kubectl wait --for=condition=Ready pod -l app=demo --timeout=60s` `-l app=demo` это **label selector**: отбирает поды с меткой `app=demo`, которую `kubectl create deployment` проставляет автоматически (по умолчанию равна имени деплоймента) так не нужно заранее знать случайно сгенерированное имя пода.
- `POD=$(kubectl get pods -l app=demo -o jsonpath="{.items[0].metadata.name}")` `-o jsonpath="..."` вытаскивает из JSON-ответа API конкретное поле по JSONPath-выражению: `.items[0]` первый объект в списке подов, `.metadata.name` его имя. Результат сохраняется в shell-переменную `POD`, чтобы не копировать имя пода вручную в следующую команду.
- `kubectl delete pod $POD` удаляет конкретный под по имени это и есть намеренная «поломка», которую предлагает сделать Модуль 1, чтобы увидеть self-healing.
- `sleep 2` пауза в 2 секунды, чтобы дать контроллеру ReplicaSet время заметить пропажу пода и создать замену, прежде чем смотреть результат.
- `kubectl delete deployment demo` уборка за собой в конце (курс явно требует убрать Deployment после демонстрации, чтобы дальше начинать модули с чистого состояния).
**Получен ответ:**
```
pod/demo-6484fc4fb6-m5fmv condition met
Killing pod: demo-6484fc4fb6-m5fmv
pod "demo-6484fc4fb6-m5fmv" deleted
demo-6484fc4fb6-vtkrd 1/1 Running 0 4s
```
**Значит:** после удаления пода `demo-6484fc4fb6-m5fmv` контроллер ReplicaSet тут же создал новый под `demo-6484fc4fb6-vtkrd` другое имя (случайный суффикс), тот же Deployment. Это и есть self-healing: желаемое состояние 1 реплика демо-приложения») поддерживается автоматически, без вмешательства человека.
---
### 23. Финальная проверка — ресурсы и здоровье Gitea
**Команда:**
```bash
ssh -o BatchMode=yes totserver@192.168.31.163 'echo ---DOCKER-STATS---; docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"; echo ---FREE---; free -h; echo ---GITEA---; docker ps --format "{{.Names}}\t{{.Status}}" | grep -E "gitea|file-server"; echo ---TEMP---; sensors 2>/dev/null | grep -E "Package|Composite"; echo ---CURL-GITEA---; curl -s -o /dev/null -w "gitea http: %{http_code}\n" http://localhost:3000'
```
**Разбор аргументов:**
- `docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"` `docker stats` по умолчанию это живой, постоянно обновляющийся вывод (как `top`), что зависло бы неинтерактивную SSH-команду навсегда; `--no-stream` берёт один снимок текущих значений и сразу завершается. `--format "table ..."` задаёт три нужные колонки (имя контейнера, % CPU, использование памяти) вместо полного набора столбцов Docker по умолчанию.
- `docker ps ... | grep -E "gitea|file-server"` сужает список контейнеров до тех, что относятся к прод-стеку, который нельзя было задевать.
- `curl -s -o /dev/null -w "gitea http: %{http_code}\n" http://localhost:3000` `-s` без индикатора прогресса; `-o /dev/null` выбрасывает тело ответа (страницу логина Gitea) интересен не HTML, а сам факт и код ответа; `-w "...%{http_code}..."` печатает после запроса кастомную строку, подставляя туда код HTTP-ответа минимальная проверка «жив ли сервис», без вывода целой страницы.
**Получен ответ:**
```
lab-control-plane 20.83% 516.1MiB / 15.4GiB
gitea 0.05% 99.2MiB / 15.4GiB
gitea Up 8 minutes (healthy)
gitea_db Up 9 minutes (healthy)
Composite: +59.9°C
Package id 0: +50.0°C
gitea http: 302
```
**Значит:** kind-кластер в простое ест ~516 МБ RAM и часть одного ядра на сервере с 16 ГБ это несущественно. Gitea и Postgres по-прежнему `healthy`, отвечают на запросы (302 это редирект на страницу логина, ожидаемый и корректный ответ живого сервиса, а не ошибка). Температура поднялась всего на ~5-10°C от базовой до критических отметок (89.8°C/105°C) очень далеко, троттлинга нет. Вывод: кластер можно держать поднятым параллельно с рабочим стеком без риска для него.
---
## Соглашение на будущее
Начиная с этой сессии, каждая команда, которую я выполняю в рамках курса (не только сегодня), добавляется в этот файл новой датированной секцией в том же формате: задача команда разбор аргументов полученный ответ что он значит. `PROGRESS.md` при этом остаётся коротким журналом-указателем со ссылкой на соответствующий раздел здесь.

View File

@@ -8,6 +8,8 @@
**Окружение курса** (одно на все модули, то же, что в 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).
---
## Модуль 0. Установка инструментов

View File

@@ -0,0 +1,58 @@
# Прогресс прохождения курса Kubernetes
Живой журнал, а не справочник — обновляется в конце каждой сессии по курсу. Цель: за 30 секунд понять, где остановился, и продолжить без перечитывания всего [LEARNING.md](LEARNING.md). Окружение и бенчмарки — в [SERVER.md](SERVER.md), построчный разбор каждой выполненной команды (что делает каждый аргумент, что значит ответ) — в [COMMANDS.md](COMMANDS.md); сюда их не дублировать.
## Статус модулей
| Модуль | Тема | Статус | Дата |
|---|---|---|---|
| 0 | Установка инструментов | ✅ Пройден | 2026-07-18 |
| 1 | Зачем вообще Kubernetes | ✅ Пройден (как смоук-тест наладки) | 2026-07-18 |
| 2 | Архитектура кластера | ⬜ Не начат | |
| 3 | Pod и kubectl-база | ⬜ Не начат | |
| 4 | Deployment и ReplicaSet | ⬜ Не начат | |
| 5 | Service и сеть | ⬜ Не начат | |
| 6 | Ingress | ⬜ Не начат | |
| 7 | ConfigMap и Secret | ⬜ Не начат | |
| 8 | Probes и ресурсы | ⬜ Не начат | |
| 9 | Диагностика поломок | ⬜ Не начат | |
| 10 | Хранилище: PV, PVC, StatefulSet | ⬜ Не начат | |
| 11 | Helm, часть 1 — пользователь чартов | ⬜ Не начат | |
| 12 | Helm, часть 2 — автор чарта | ⬜ Не начат | |
| 13 | Как это устроено в реальных проектах | ⬜ Не начат | |
| — | Выходной контроль (QUESTIONS.md + 2 схемы) | ⬜ Не начат | |
## Лог по датам
### 2026-07-18 — наладка окружения на сервере + модуль 0 + модуль 1
Сделано:
- Сервер `totserver@192.168.31.163` обновлён (`apt upgrade`), перезагружен, добавлены sysctl-лимиты inotify (524288/512) для kind.
- Установлены `kind` v0.32.0 и `helm` v3.21.3 в `~/.local/bin` (без sudo). `kubectl` уже был через snap (1.35.6), `docker` уже стоял (обновился до 29.6.2 при апгрейде).
- Сняты бенчмарки CPU/RAM/диска и практический замер подъёма kind-кластера — см. [SERVER.md](SERVER.md). Вердикт: сервер тянет весь курс с запасом.
- Поднят кластер `kind create cluster --name lab` (1-нодовый, дефолтный) — пройдена практика модуля 1: self-healing пода через Deployment продемонстрирован (под пересоздался с новым именем после `kubectl delete pod`).
- Deployment `demo` удалён после демонстрации (как требует практика модуля 1).
- Проверено: Gitea/Postgres/file-server не пострадали от ребута и от работы kind-кластера рядом (все healthy, отвечают на запросы).
- Создан [SERVER.md](SERVER.md) — окружение, бенчмарки, адаптации курса под сервер по модулям.
- Создан [COMMANDS.md](COMMANDS.md) — построчный разбор каждой выполненной команды этой сессии (задача → аргументы → ответ → значение), чтобы можно было повторить любой шаг самостоятельно.
Осталось на следующую сессию:
- Начать с **модуля 2** — пересоздать кластер `lab` с топологией 1 control-plane + 2 worker через `kind-config.yaml`. **Важно**: сразу добавить `extraPortMappings` (8080→80, 8443→443) из [SERVER.md](SERVER.md#модуль-2--топология-кластера), чтобы не пересоздавать кластер повторно в модуле 6.
Затыки: нет (модуль 0 и смоук-тест прошли гладко).
## Состояние кластера прямо сейчас
- Кластер `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, ничего вручную чистить не нужно.
## Открытые вопросы / затыки
Пока пусто — появятся, если самопроверка какого-то модуля не пройдёт с первого раза.
## Вне периметра курса (не забыть)
- `gitea_runner` был в restart-loop (`unregistered runner`, обнаружено 2026-07-18 при проверке после ребута) — **починено в тот же день** (чистая перерегистрация). Дополнительно поднят второй CI-раннер `gitea_runner_armhf` (cross-builder для ARM) под реальный проект `maxim/Pluto-SDR`; все 4 CI-workflow зелёные. Разбор — [../docker/CASE.md](../docker/CASE.md) → раздел 9.
- **Перед каждой сессией курса** сверяться с чек-листом [«CI-инфраструктура на сервере — что нельзя ломать»](SERVER.md#ci-инфраструктура-на-сервере--что-нельзя-ломать) в SERVER.md — там же актуальный список занятых портов (3000/3030/222/4400/9090).

123
stack/kubernetes/SERVER.md Normal file
View File

@@ -0,0 +1,123 @@
# Окружение курса: удалённый сервер вместо Windows + Docker Desktop
Курс [LEARNING.md](LEARNING.md) написан под Windows + Docker Desktop + Git Bash. Этот документ — альтернативное окружение: тот же курс, тот же kind, но на удалённом Ubuntu-сервере по SSH. Здесь — характеристики железа, результаты бенчмарков и точечные адаптации команд там, где они отличаются от курса. Сам курс не переписан — по нему проходишь как есть, отличия смотришь здесь по мере необходимости.
## Сервер
| | |
|---|---|
| Хост | `totserver@192.168.31.163`, Ubuntu 24.04.3 LTS, kernel 6.8.0-124-generic |
| CPU | Intel N100, 4 ядра / 4 потока, до 3.4 ГГц, пассивное охлаждение |
| RAM | 16 ГБ DDR4-2667 (1 планка, второй слот пустой — апгрейд до 32 ГБ возможен) |
| Диск | Kingston NVMe 1 ТБ, ~852 ГБ свободно |
| Сеть | 1 Гбит/с (enp2s0) |
| Уже занято | Gitea + Postgres + два CI-раннера + file-server (docker) + Grafana + nginx (systemd), ~1.3 ГБ RAM в простое. Порты 80, 443, **3000 (Grafana)**, **3030 (Gitea)**, 222 (Gitea SSH), 4400 (file-server), 9090 (Prometheus) на хосте заняты этим стеком — **не трогать**. Подробный чек-лист — [«CI-инфраструктура на сервере — что нельзя ломать»](#ci-инфраструктура-на-сервере--что-нельзя-ломать) ниже |
| sudo | Требует пароль — все команды курса на этом сервере выполняются без sudo (kind/helm стоят в `~/.local/bin`) |
## Результаты бенчмарков (2026-07-18)
| Тест | Результат |
|---|---|
| CPU, sysbench, 4 потока, 15с | 11 283 events/sec |
| CPU, sysbench, 1 поток, 10с | 3 301 events/sec |
| RAM, sysbench memory, 4 потока | 9 900 МиБ/с |
| Диск, fio seq write 1M blocks | ~1024 МиБ/с (1.07 ГБ/с) |
| Диск, fio random 4k, mix 70/30, 4 jobs, iodepth=16 | read 12.6k IOPS / write 5.4k IOPS, avg latency ~0.2 мс |
| `kind create cluster` (1 нода, с нуля) | 52.8 с (в основном — скачивание образа ноды `kindest/node`) |
| Нода Ready + все поды kube-system Ready | ещё +6.5 с сверху |
| Idle-потребление 1-нодового kind-кластера | ~516 МБ RAM, ~20% CPU одного ядра |
| Температура CPU под нагрузкой | 60°C (crit 89.8°C у NVMe, 105°C у CPU) — троттлинга нет |
## Вердикт
Сервер уверенно тянет весь курс, модули 013, включая самый тяжёлый — модуль 11 (`kube-prometheus-stack`, обычно 23 ГБ RAM под Prometheus+Grafana+Alertmanager) — с большим запасом в 16 ГБ. Диск с латентностью ~0.2 мс на случайных 4k операциях с запасом покрывает требования etcd к fsync. Ограничение курса «Docker Desktop не меньше 8 ГБ RAM» здесь неактуально — нативный Docker на Linux видит всю память хоста, никакого выделенного лимита нет.
Единственное реальное ограничение — 4 ядра CPU: под тяжёлой нагрузкой (например, одновременно 3-нодовый kind-кластер + Helm-чарт с Prometheus) возможны просадки по отзывчивости, но не критично для pet-лабы.
## Точечные адаптации курса по модулям
### Модуль 0 — установка инструментов
Вместо `choco install kind kubernetes-cli kubernetes-helm` (Windows):
```bash
# kubectl уже стоит через snap (kubectl 1.35.6) — можно оставить, ставить заново не нужно
snap list kubectl
# kind — бинарь в ~/.local/bin (без sudo)
mkdir -p ~/.local/bin
KIND_VER=$(curl -s https://api.github.com/repos/kubernetes-sigs/kind/releases/latest | grep -oP '"tag_name": "\K[^"]+')
curl -Lo ~/.local/bin/kind "https://kind.sigs.k8s.io/dl/${KIND_VER}/kind-linux-amd64"
chmod +x ~/.local/bin/kind
# helm — официальный установочный скрипт, без sudo
curl -fsSL -o /tmp/get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod +x /tmp/get_helm.sh
HELM_INSTALL_DIR=$HOME/.local/bin USE_SUDO=false /tmp/get_helm.sh
rm -f /tmp/get_helm.sh
# проверка (в интерактивной SSH-сессии ~/.local/bin уже в PATH через ~/.profile)
docker --version; kind --version; kubectl version --client; helm version
```
Bash-синтаксис курса (heredoc, `base64 -d`, `curl`) работает нативно в SSH-сессии — отдельный Git Bash не нужен, это уже полноценный Linux.
### Модуль 2 — топология кластера
Порты 80/443 хоста заняты Gitea, поэтому в `kind-config.yaml` из модуля 2 сразу добавляй `extraPortMappings`, чтобы не пересоздавать кластер повторно в модуле 6:
```yaml
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
```
### Модуль 6 — Ingress
Порты 80/443 заняты — доступ к `demo.local` идёт через 8080:
```bash
# с самого сервера
curl -H "Host: demo.local" http://localhost:8080
# с Windows-машины: добавить в C:\Windows\System32\drivers\etc\hosts
# 192.168.31.163 demo.local
# и открывать http://demo.local:8080 в браузере
```
### Модуль 7 — Secret/base64
`base64 -d` и heredoc работают нативно, отдельного Git Bash не требуется (актуально только для Windows-окружения курса).
### Модуль 11 — Helm, kube-prometheus-stack
Оговорку курса про «Docker Desktop не меньше 8 ГБ RAM» — игнорировать, неприменимо. Ресурсов в `my-values.yaml` из модуля 11 достаточно оставить как есть или ослабить (сервер потянет и дефолтные значения чарта).
## CI-инфраструктура на сервере — что нельзя ломать
Вне периметра K8s-курса, но на этом же сервере: Gitea + два CI-раннера Gitea Actions обслуживают реальный проект (`maxim/Pluto-SDR`, кросс-сборка C-кода под ARM). 2026-07-18 раннер `gitea_runner` был починен из restart-loop (устаревшая регистрация act_runner — вылечено чистой перерегистрацией), и поднят второй раннер `gitea_runner_armhf` — кастомный образ cross-builder'а (Debian + `arm-linux-gnueabihf-gcc` + статические armhf-либы в `/root/xarm`, собираются в образе через `build_deps.sh`). Подробный разбор этой настройки — в [../docker/CASE.md](../docker/CASE.md) → раздел 9 (это реальный кейс легенды, не учебный).
**Перед каждой сессией K8s-курса и в конце неё — быстрая проверка, что CI-стек жив:**
```bash
docker ps --filter name=gitea --format '{{.Names}}\t{{.Status}}'
# ожидаем Up у всех четырёх: gitea, gitea_db, gitea_runner, gitea_runner_armhf
```
Чек-лист, что можно ломать курсом, а что нет:
- **Не выполнять** `docker system prune -a` / `docker image prune -a`, пока запущен курс или контейнеры Gitea остановлены: образ `gitea-runner-armhf:latest` собран локально и нигде больше не существует (пересборка — кросс-компиляция 5 ARM-библиотек, ~20 мин и нужен интернет). `kind delete cluster` — безопасен, kind-образы отдельные.
- **Не удалять** файлы регистрации раннеров: `/opt/gitea/runner/.runner` и `/opt/gitea/runner-armhf/data/.runner` (личность раннера на сервере) — их удаление форсирует restart-loop, аналогичный уже починенному. `.env`-файлы в тех же каталогах — тоже не трогать.
- **Не запускать** `docker-compose ... --remove-orphans` внутри `/opt/gitea` — несколько compose-файлов делят один каталог/проект, `--remove-orphans` может снести соседний стек.
- **Порты в `kind-config.yaml`** (`extraPortMappings` из модуля 2) — использовать только 8080/8443. Не занимать 3000 (Grafana), 3030 (Gitea), 222 (Gitea SSH), 4400 (file-server), 9090 (Prometheus).
- **Конфиг Gitea**: `REQUIRE_SIGNIN_VIEW = false` в `app.ini` обязателен для CI-клонов (бэкап рядом: `app.ini.bak-2026-07-18`) — не возвращать в `true`. Воркфлоу Pluto-SDR клонируют `localhost:3000` через socat-проброс **внутри** `gitea_runner_armhf` на порт Gitea (:3030) — хостовый порт 3000 (Grafana) тут ни при чём, трогать его конфигурацию не требуется.
- **После ребута сервера** всё поднимается само (`restart: unless-stopped` у обоих раннеров + `gitea.service` для Gitea/Postgres) — вмешательство не требуется.
- **Признак, что CI жив**: в админке https://totmaxim.ru/-/admin/actions/runners оба раннера (`my-runner`, `my-runner-ci`) online.