Files
resume/stack/kubernetes/COMMANDS.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

644 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Журнал команд: что запускалось и зачем
Построчный разбор каждой команды, которую я выполнял по 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` при этом остаётся коротким журналом-указателем со ссылкой на соответствующий раздел здесь.