75 KiB
Журнал команд: что запускалось и зачем
Построчный разбор каждой команды, которую я выполнял по 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.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. Проверка сервера после перезагрузки
Команда:
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: 8080→containerPort: 80иhostPort: 8443→containerPort: 443. Сделано сразу в модуле 2 (адаптация из SERVER.md), а не только в модуле 6 (когда понадобится Ingress) — иначе пришлось бы пересоздавать кластер повторно. Именно 8080/8443, а не 80/443, потому что порты 80/443 хоста уже заняты Gitea/nginx (см.ss -tulpnв сессии — 80/443/3000/3030/222/4400/9090 заняты, 8080/8443 свободны).
Получен ответ:
gitea_runner Up 7 hours
gitea Up 7 hours (healthy)
gitea_db Up 7 hours (healthy)
gitea_runner_armhf Up 7 hours
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 80
hostPort: 8080
- containerPort: 443
hostPort: 8443
- role: worker
- role: worker
Значит: CI-стек в порядке, можно продолжать. Файл kind-config.yaml записан корректно — готов к использованию в kind create cluster --config.
25. Модуль 2 — пересоздание кластера с топологией control-plane + 2 worker
Команда:
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 делали с этим подом:Scheduled→Pulling/Pulled→Created→Started).logs demo-pod— без указания контейнера (работает, потому что в поде он один); печатает stdout/stderr процесса внутри контейнера.exec -it ... -- sh—-i(interactive) +-t(tty) дают полноценный интерактивный терминал внутри контейнера;sh, а неbash, потому что образnginxdemos/helloсобран на Alpine (slim-образ без bash).
Получен ответ:
NAME READY STATUS RESTARTS AGE
demo-pod 1/1 Running 0 25s
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
demo-pod 1/1 Running 0 31s 10.244.2.2 lab-worker <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 при этом остаётся коротким журналом-указателем — со ссылкой на соответствующий раздел здесь.