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