Files
resume/stack/kubernetes/COMMANDS.md
2026-07-18 23:44:30 +03:00

75 KiB
Raw Blame History

Журнал команд: что запускалось и зачем

Построчный разбор каждой команды, которую я выполнял по SSH при наладке курса на сервере — с разбором каждого аргумента и объяснением, что значит полученный ответ. Цель файла — чтобы ты мог повторить любой шаг сам, точно понимая, что делает каждый флаг, а не копируя команду вслепую.

Формат каждой записи:

  • Задача — зачем вообще эта команда.
  • Команда — целиком, можно копировать.
  • Разбор аргументов — что делает каждая часть.
  • Получен ответ — реальный фрагмент вывода из этой сессии.
  • Значит — какой вывод/решение делаем на основе этого ответа.

Общее для всех команд ниже, кроме отмеченных отдельно: они выполняются в моей SSH-сессии как ssh -o BatchMode=yes [-o ConnectTimeout=N] totserver@192.168.31.163 'команда'. Разбор этой обвязки — в самом первом пункте, дальше не повторяю.


18-07-2026

0. SSH-обвязка (используется почти во всех командах ниже)

Команда:

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. Проверка, что уже стоит на сервере

Команда:

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, занятых портов, памяти и ядра

Команда:

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 знаешь только ты) — но она часть той же последовательности и нужна, чтобы повторить сетап с нуля.

Команда:

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.conftee одновременно пишет полученный со входа текст в файл и печатает его на экран; используется вместо 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. Проверка сервера после перезагрузки

Команда:

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

Команда:

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. Подготовка папки для бенчмарков + базовая температура

Команда:

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 (фоновая задача)

Команда:

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 потока

Команда:

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

Команда:

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. Бенчмарк диска — неудачная попытка (обучающий момент)

Команда (не сработала):

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 образа

Команда:

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. Бенчмарк диска — последовательная запись (исправлено)

Команда:

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)

Команда:

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. Уборка временных файлов бенчмарка

Команда:

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

Команда:

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

Команда:

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 реально установился

Команда:

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-сессии)

Команда:

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 (замер времени)

Команда:

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. Проверка нод и системных подов

Команда:

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. Замер времени до полной готовности

Команда:

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)

Команда:

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

Команда:

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) очень далеко, троттлинга нет. Вывод: кластер можно держать поднятым параллельно с рабочим стеком без риска для него.


24. Модуль 2 — проверка CI-стека + kind-config.yaml новой топологии

Начиная с этой записи, команды выполнял ты сам напрямую в интерактивной SSH-сессии на сервере (login shell, ~/.local/bin уже в PATH), а мне присылал готовый лог — поэтому команды ниже идут без SSH-обвязки из пункта 0.

Команда:

docker ps --filter name=gitea --format '{{.Names}}\t{{.Status}}'

cat > ~/k8s/kind-config.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 80
        hostPort: 8080
      - containerPort: 443
        hostPort: 8443
  - role: worker
  - role: worker
EOF
cat ~/k8s/kind-config.yaml

Разбор аргументов:

  • docker ps --filter name=gitea --format '{{.Names}}\t{{.Status}}' — сужает список контейнеров до тех, чьё имя содержит gitea, и печатает только имя и статус — быстрая проверка перед началом работы, что боевой CI-стек жив (чек-лист из SERVER.md).
  • cat > файл << 'EOF' ... EOF — heredoc: всё, что между << 'EOF' и завершающим EOF, построчно записывается в файл через cat; кавычки вокруг 'EOF' отключают подстановку переменных/спецсимволов внутри блока (не нужна здесь, но привычка на будущее для блоков с $).
  • role: control-plane / role: worker — три записи в списке nodes = kind создаст три Docker-контейнера, один control-plane и два worker (топология модуля 2 из LEARNING.md).
  • extraPortMappings внутри control-plane-ноды — пробрасывает порты хоста на порты внутри этого контейнера-ноды: hostPort: 8080containerPort: 80 и hostPort: 8443containerPort: 443. Сделано сразу в модуле 2 (адаптация из SERVER.md), а не только в модуле 6 (когда понадобится Ingress) — иначе пришлось бы пересоздавать кластер повторно. Именно 8080/8443, а не 80/443, потому что порты 80/443 хоста уже заняты Gitea/nginx (см. ss -tulpn в сессии — 80/443/3000/3030/222/4400/9090 заняты, 8080/8443 свободны).

Получен ответ:

gitea_runner    Up 7 hours
gitea   Up 7 hours (healthy)
gitea_db        Up 7 hours (healthy)
gitea_runner_armhf      Up 7 hours

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 80
        hostPort: 8080
      - containerPort: 443
        hostPort: 8443
  - role: worker
  - role: worker

Значит: CI-стек в порядке, можно продолжать. Файл kind-config.yaml записан корректно — готов к использованию в kind create cluster --config.


25. Модуль 2 — пересоздание кластера с топологией control-plane + 2 worker

Команда:

kind delete cluster --name lab
kind create cluster --name lab --config kind-config.yaml
kubectl cluster-info --context kind-lab

Разбор аргументов:

  • kind delete cluster --name lab — удаляет старый однонодовый кластер (созданный в модуле 0-1 без конфига) вместе с его Docker-контейнером, чтобы освободить имя lab под новую топологию.
  • kind create cluster --name lab --config kind-config.yaml--config вместо голого --name заставляет kind читать топологию и extraPortMappings из файла, а не создавать кластер дефолтной однонодовой конфигурацией.
  • kubectl cluster-info --context kind-lab — печатает адрес API-сервера и CoreDNS для явно указанного контекста kind-lab (kind сам создаёт и переключает kubectl-контекст с таким именем при каждом create cluster).

Получен ответ:

Deleting cluster "lab" ...
Deleted nodes: ["lab-control-plane"]

Creating cluster "lab" ...
 ✓ Ensuring node image (kindest/node:v1.36.1) 🖼
 ✓ Preparing nodes 📦 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-lab"

Kubernetes control plane is running at https://127.0.0.1:36761
CoreDNS is running at https://127.0.0.1:36761/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

Значит: пересоздание прошло без ошибок и быстро (образ ноды kindest/node:v1.36.1 уже был в кэше Docker с прошлого раза — не пришлось скачивать заново). Обрати внимание на новый шаг в выводе, которого не было при однонодовом создании: ✓ Joining worker nodes — именно он добавляет lab-worker/lab-worker2 к кластеру после того, как control-plane уже поднят. kubectl-контекст переключён на kind-lab автоматически.


26. Модуль 2 — проверка нод

Команда:

kubectl get nodes -o wide

Получен ответ:

NAME                STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                       KERNEL-VERSION              CONTAINER-RUNTIME
lab-control-plane   Ready    control-plane   57s   v1.36.1   172.22.0.4    <none>        Debian GNU/Linux 13 (trixie)   6.8.0-136-generic (amd64)   containerd://2.3.1
lab-worker          Ready    <none>          46s   v1.36.1   172.22.0.2    <none>        Debian GNU/Linux 13 (trixie)   6.8.0-136-generic (amd64)   containerd://2.3.1
lab-worker2         Ready    <none>          47s   v1.36.1   172.22.0.3    <none>        Debian GNU/Linux 13 (trixie)   6.8.0-136-generic (amd64)   containerd://2.3.1

Значит: все 3 ноды поднялись и перешли в Ready за 46-57 секунд — быстрее, чем однонодовый кластер с нуля (модуль 0, ~53с только на control-plane), потому что образ ноды уже был в кэше. Колонка ROLES подтверждает топологию: одна control-plane, две <none> (worker — kind не проставляет им явную роль-метку по умолчанию, в отличие от control-plane). KERNEL-VERSION (6.8.0-136) новее, чем было зафиксировано в SERVER.md на момент установки (6.8.0-124) — сервер обновлялся ядром между сессиями, это не связано с kind и не требует действий.


27. Модуль 2 — компоненты control-plane и DaemonSet'ы вживую

Команда:

kubectl get pods -n kube-system -o wide

Получен ответ:

NAME                                        READY   STATUS    RESTARTS   AGE   NODE
coredns-589f44dc88-s8m6z                    1/1     Running   0          86s   lab-control-plane
coredns-589f44dc88-xlkk7                    1/1     Running   0          86s   lab-control-plane
etcd-lab-control-plane                      1/1     Running   0          93s   lab-control-plane
kindnet-62npg                               1/1     Running   0          86s   lab-control-plane
kindnet-9lsrg                               1/1     Running   0          86s   lab-worker2
kindnet-xwjqx                               1/1     Running   0          85s   lab-worker
kube-apiserver-lab-control-plane            1/1     Running   0          93s   lab-control-plane
kube-controller-manager-lab-control-plane   1/1     Running   0          93s   lab-control-plane
kube-proxy-6ltl9                            1/1     Running   0          85s   lab-worker
kube-proxy-9z5x9                            1/1     Running   0          86s   lab-control-plane
kube-proxy-h7xtp                            1/1     Running   0          86s   lab-worker2
kube-scheduler-lab-control-plane            1/1     Running   0          93s   lab-control-plane

Значит: ровно та картина, которую описывает теория модуля 2. Четыре компонента control-plane (kube-apiserver, etcd, kube-controller-manager, kube-scheduler) — все с суффиксом -lab-control-plane в имени и все в колонке NODE только на lab-control-plane, других экземпляров нет. Два DaemonSet'аkindnet-* (CNI, сетевая связность между подами на разных нодах) и kube-proxy-* (сетевые правила для Service, разбирается в модуле 5) — у каждого ровно по одному поду на каждую из 3 нод (control-plane и оба worker), что и есть определение DaemonSet: «один под на каждую подходящую ноду». coredns-* — не DaemonSet, а обычный Deployment на 2 реплики; обе оказались на lab-control-plane, потому что worker-ноды пока полностью пустые, и scheduler разместил их там, где выгоднее (это никак не привязано к ролям нод — просто текущее решение scheduler'а по ресурсам).

Устная самопроверка модуля 2 (3 вопроса из LEARNING.md — компоненты control-plane, компоненты worker-ноды, почему etcd в проде выносят отдельно с нечётным числом нод) пройдена развёрнуто и верно. Дополнительно самостоятельно разобрана разница iptables/nftables применительно к двум режимам работы kube-proxy — не входит в буквальную программу модуля 2, но напрямую относится к роли kube-proxy и пригодится в модуле 5 (Service и сеть).


28. Модуль 3 — pod.yaml (Namespace + Pod) и создание

Команда:

cat > ~/k8s/pod.yaml << 'EOF'
apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo
  labels:
    app: demo
spec:
  containers:
    - name: hello
      image: nginxdemos/hello:latest
      ports:
        - containerPort: 80
EOF

kubectl apply -f ~/k8s/pod.yaml

Разбор аргументов:

  • Один файл, два объекта через разделитель --- — YAML позволяет описать несколько документов в одном файле, kubectl apply -f применяет их по порядку сверху вниз. Namespace идёт первым не случайно: Pod ссылается на namespace: demo во втором документе, а этот namespace должен уже существовать (или быть создан в той же команде apply) к моменту создания Pod.
  • Четыре ключа верхнего уровня из теории модуля 3: apiVersion (версия API — v1 для обоих базовых типов), kind (тип объекта), metadata (имя/namespace/labels — «паспорт»), spec (только у Pod — желаемое состояние: какой контейнер запускать).
  • labels: app: demo — метка на Pod, пока не используется явно, но именно так Service/Deployment в следующих модулях будут находить нужные поды через selector.

Получен ответ:

namespace/demo created
pod/demo-pod created

Значит: оба объекта созданы с первого раза, ошибок в манифесте нет.


29. Модуль 3 — базовый набор команд: get, describe, logs, exec

Команда:

kubectl -n demo get pods
kubectl -n demo get pods -o wide
kubectl -n demo describe pod demo-pod
kubectl -n demo logs demo-pod
kubectl -n demo exec -it demo-pod -- sh

Разбор аргументов:

  • -n demo — на каждой команде явно указывает namespace (без него kubectl смотрит в default, где ничего нет).
  • get pods -o wide-o wide добавляет колонки IP и NODE к базовому выводу.
  • describe pod — в отличие от get, разворачивает объект полностью: условия (Conditions), примонтированные volume, и главное — секцию Events (хронологический лог того, что control-plane и kubelet делали с этим подом: ScheduledPulling/PulledCreatedStarted).
  • logs demo-pod — без указания контейнера (работает, потому что в поде он один); печатает stdout/stderr процесса внутри контейнера.
  • exec -it ... -- sh-i (interactive) + -t (tty) дают полноценный интерактивный терминал внутри контейнера; sh, а не bash, потому что образ nginxdemos/hello собран на Alpine (slim-образ без bash).

Получен ответ:

NAME       READY   STATUS    RESTARTS   AGE
demo-pod   1/1     Running   0          25s

NAME       READY   STATUS    RESTARTS   AGE   IP           NODE         NOMINATED NODE   READINESS GATES
demo-pod   1/1     Running   0          31s   10.244.2.2   lab-worker   <none>           <none>

Node:             lab-worker/172.22.0.2
Status:           Running
...
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  89s   default-scheduler  Successfully assigned demo/demo-pod to lab-worker
  Normal  Pulling    88s   kubelet            Pulling image "nginxdemos/hello:latest"
  Normal  Pulled     82s   kubelet            Successfully pulled image ... in 6.311s
  Normal  Created    82s   kubelet            Container created
  Normal  Started    82s   kubelet            Container started

2026/07/18 19:54:20 [notice] 1#1: nginx/1.29.1
2026/07/18 19:54:20 [notice] 1#1: start worker process 27..30

/ # exit

Значит: под запланирован планировщиком на lab-worker (control-plane исключён taint'ом), образ скачался за 6.3с, контейнер стартовал и поднял 4 worker-процесса nginx. logs показал именно вывод приложения (nginx), describe/Events — действия самого Kubernetes вокруг пода как объекта; это и есть разница между ними. exec подтвердил, что контейнер полноценно интерактивен.


30. Модуль 3 — полный манифест объекта (-o yaml)

Команда:

kubectl -n demo get pod demo-pod -o yaml

Разбор аргументов:

  • -o yaml — просит API-сервер отдать объект целиком в том виде, в каком он реально хранится в etcd, а не только колонки таблицы get pods.

Получен ответ (сокращённо, полный лог — в сессии):

metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {...исходный JSON манифеста...}
  resourceVersion: "2565"
  uid: ea8061a9-eca7-4cc0-91d0-86b6e87af510
spec:
  containers:
  - imagePullPolicy: Always
    ...
  nodeName: lab-worker
  restartPolicy: Always
  schedulerName: default-scheduler
  serviceAccountName: default
  terminationGracePeriodSeconds: 30
  tolerations: [...]
  volumes:
  - name: kube-api-access-p5g68
    projected: {...}
status:
  conditions: [...]
  containerStatuses: [...]
  phase: Running
  podIP: 10.244.2.2
  hostIP: 172.22.0.2

Значит: написанные 12 строк манифеста развернулись примерно в 120. Сверх исходного pod.yaml появилось: служебные поля metadata (uid, resourceVersion, аннотация с сохранённым исходным манифестом — на ней основан three-way merge при следующих apply); дефолты в spec, которые Kubernetes подставил сам (imagePullPolicy, restartPolicy, schedulerName, serviceAccountName, tolerations, автомонтированный volume с токеном сервис-аккаунта, nodeName — результат работы планировщика); и целиком раздел status, которого в манифесте нет вообще — это runtime-состояние, которое пишет kubelet, наблюдая за реальным контейнером. Итог: spec — желаемое состояние (то, что попросили, плюс дефолты), status — фактическое состояние (что control loop реально наблюдает) — основа declarative-модели Kubernetes.


31. Модуль 3 — удаление голого Pod (без self-healing)

Команда:

kubectl -n demo delete pod demo-pod
kubectl -n demo get pods

Разбор аргументов:

  • В отличие от модуля 1 (Deployment), здесь Pod создан напрямую, без контроллера сверху.

Получен ответ:

pod "demo-pod" deleted from demo namespace
No resources found in demo namespace.

Значит: под удалился и не пересоздался — namespace demo пуст. Это и есть отличие голого Pod от Pod'а под управлением Deployment/ReplicaSet: restartPolicy: Always перезапускает контейнер внутри пода при его падении, но если исчезает сам Pod (удалён вручную или нода, на которой он жил, выходит из строя), пересоздать его некому — за это отвечает контроллер (ReplicaSet), которого здесь нет. Отсюда правило курса: голые Pod'ы в проде почти не используют, кроме короткоживущих задач.

Устная самопроверка модуля 3 (роли ключей манифеста, logs vs describe, что добавляет -o yaml, плюс доп. вопрос про поведение при падении ноды) пройдена развёрнуто и верно.


Соглашение на будущее

Начиная с этой сессии, каждая команда, которую я выполняю в рамках курса (не только сегодня), добавляется в этот файл новой датированной секцией в том же формате: задача → команда → разбор аргументов → полученный ответ → что он значит. PROGRESS.md при этом остаётся коротким журналом-указателем — со ссылкой на соответствующий раздел здесь.