✻ Урок 5.2 · Тема 5: Kubernetes и Helm
Поды и Deployment: запускаем «Заметки»
Содержание урока
Зачем это нужно
В Compose ты говорил: «запусти этот контейнер». В Kubernetes ты говоришь: «хочу три копии приложения, всегда», и кластер сам следит, чтобы так и было. Копия (реплика, replica) - это один из одинаковых запущенных экземпляров приложения: три копии значит три одинаковых контейнера, и если один упадёт, остальные продолжат отвечать. Под (Pod) - обёртка, внутри которой в кластере запускается контейнер (подробно разберём в теории). Упал контейнер, умер узел (машина кластера, урок 5.1), кто-то случайно удалил под: копия вернётся без твоего участия. На работе с этого начинается любой деплой (выкатку новой версии приложения): манифест (manifest, описание объекта в YAML), kubectl apply и разбор статусов вроде ImagePullBackOff (кластер не может скачать образ, например, из-за опечатки в имени) и CrashLoopBackOff (контейнер запускается и сразу падает, кластер пробует снова и снова), которые видит каждый, кто хоть раз выкатывал сервис. kubectl apply - команда, которая отдаёт кластеру файл с описанием: «вот как должно быть». Подробно разберём в теории.
Шаг проекта: в кластере kind появляется Deployment (объект, который держит нужное число копий и умеет обновлять версию без простоя) notes в namespace notes: 3 реплики образа notes:0.4.0, данные в emptyDir (временная пустая папка, которая живёт ровно столько, сколько жив под; после удаления пода заметки пропадают, это осознанный долг до урока 5.5), файл k8s/base/10-deployment.yaml.
Что нужно знать
- Урок 5.1: зачем Kubernetes и кластер kind: у тебя есть кластер
notes, контекстkind-notesи namespacenotes; ты знаешь, что такое желаемое и фактическое состояние, контроллер (программа, которая сравнивает желаемое с реальным и исправляет расхождение), манифест (YAML-файл с описанием объекта) и метка (пара «ключ = значение» на объекте; в этом уроке разберём подробнее). - Урок 4.2: Dockerfile и образ: образ
notes, порт 8080, пользователь uid 10001 (номер непривилегированного пользователя, под которым работает приложение), переменные окружения (пары «имя = значение», которые программа читает при старте; так ей передают настройки). - Урок 4.7: образы и реестр: образ
ghcr.io/<github-user>/notes:0.4.0и почему тегlatestне используем. - Урок 4.9: диагностика Docker:
logs,inspect,exec, код выхода 137. - Урок 1.4: процессы и сигналы: что такое SIGTERM (вежливая просьба завершиться) и SIGKILL (немедленное убийство процесса), пригодится при удалении подов.
Картина целиком
Представь бригаду на стройке.
- Рабочий это твой контейнер: делает работу, но сам по себе за собой не следит.
- Вагончик это под (Pod): в нём работает один рабочий, иногда с помощником. У вагончика свой номер телефона (IP-адрес), и всё, что внутри, общается друг с другом по внутренней связи. Вагончик одноразовый: сломался, привезли новый с новым номером.
- Бригадир это ReplicaSet. Ему сказали: «На объекте всегда три вагончика». Он пересчитывает их и, если не хватает, заказывает ещё.
- Прораб это Deployment. Он руководит бригадирами и умеет менять версию работы: нанимает нового бригадира с новыми инструкциями, постепенно переводит вагончики к нему, а старого бригадира держит про запас на случай отката.
flowchart TD
D["Deployment notes<br>хочу 3 копии образа notes:0.4.0"] -->|владеет| R["ReplicaSet notes-6d8c9b7f5<br>держи ровно 3 пода с меткой<br>app.kubernetes.io/name=notes"]
R -->|владеет| P1["Pod ...x2k7p<br>контейнер, свой IP"]
R -->|владеет| P2["Pod ...a1b2c<br>контейнер, свой IP"]
R -->|владеет| P3["Pod ...z3y4x<br>контейнер, свой IP"]
На схеме есть метка (label): пара «ключ = значение», которую ты приклеиваешь к объекту, как цветной стикер на коробку. Здесь стикер app.kubernetes.io/name=notes означает «этот под относится к приложению notes». Бригадир ReplicaSet по такому стикеру узнаёт «своих» вагончиков: он считает все поды с этой меткой. Без меток кластеру пришлось бы искать поды по именам, а имена у подов случайные. Подробно про метки и про то, почему у ключа такое длинное имя, разберём в теории ниже.
Ты пишешь один файл, 10-deployment.yaml, и отдаёшь его кластеру. Всё, что ниже Deployment на схеме, кластер создаёт сам. Где аналогия перестаёт работать: рабочие в вагончике одинаковые копии одной программы, а не разные люди.
Теория
Под: минимальная единица запуска
Docker запускает контейнеры по одному. Но иногда программе нужен помощник, который всегда работает рядом с ней: тот, кто собирает и отправляет её логи, или прокси (посредник, через который идёт трафик). Им нужно быть на одной машине, видеть друг друга по localhost и делить папку. Отдельная сущность для «контейнеры, которые живут вместе» и есть под (Pod). Kubernetes управляет не контейнерами, а подами.
Комната в общежитии. У комнаты один адрес (IP), соседи по комнате делят и стол (общие тома), и телефон. Аналогия перестаёт работать в том, что комнату не «ремонтируют»: если под сломался, его выбрасывают и заводят новый.
Под это обёртка вокруг одного или нескольких контейнеров, которые:
- всегда работают на одном узле (никогда не разъедутся);
- делят один IP-адрес и один набор портов: внутри пода контейнеры достают друг до друга по
localhost; - могут делить тома (volumes, папки, подключаемые в контейнеры, как
-vв Docker, урок 4.3).
Обычно в поде один контейнер: твоё приложение. Второй добавляют, когда он нужен только рядом с первым (сборщик логов, прокси). Такой контейнер называют sidecar («коляска мотоцикла»). Контейнер, который отработал перед стартом основного и завершился (миграции базы, ожидание зависимости), это init-контейнер (init container).
Так выглядит под с двумя контейнерами. Не применяй его, это только для чтения:
apiVersion: v1 # основная группа API
kind: Pod # тип объекта: под
metadata:
name: demo # имя пода
spec: # желаемое состояние
containers: # список контейнеров пода (дефис = один элемент списка)
- name: app # первый контейнер: приложение
image: busybox:1.37
command: ["sh", "-c", "while true; do date > /shared/now; sleep 5; done"]
volumeMounts: # куда подключить том внутри контейнера
- name: shared
mountPath: /shared
- name: reader # второй контейнер, «коляска»: читает то, что пишет первый
image: busybox:1.37
command: ["sh", "-c", "while true; do cat /shared/now; sleep 5; done"]
volumeMounts:
- name: shared
mountPath: /shared
volumes: # тома пода: общие для всех его контейнеров
- name: shared
emptyDir: {} # пустая папка, которая живёт, пока жив под
Контейнер app каждые 5 секунд записывает время в файл, reader читает тот же файл. Они делят том shared, потому что оба подключили его. Ключ command заменяет команду запуска из образа. Оба контейнера при этом стартуют, останавливаются и удаляются вместе.
Прикинь сам: в поде два контейнера, один слушает порт 8080. Как второй контейнер того же пода достучится до него?
По localhost:8080: у контейнеров пода один IP и один набор портов. Ни имя сервиса, ни отдельный адрес не нужны. Зато два контейнера одного пода не могут слушать один и тот же порт.
Осторожно: не думай, что под и контейнер это одно и то же. Контейнер это программа в образе, а под это «жилплощадь» для одного или нескольких контейнеров. Второе: что «голый» под (созданный прямо манифестом kind: Pod или командой kubectl run) можно держать в работе. Если он умрёт, его никто не вернёт: нет контроллера, который следил бы за количеством копий. Поэтому в работе поды создают не руками, а через контроллеры (controller): объекты, которые следят за состоянием, как ReplicaSet и Deployment ниже.
Проверь понимание: ты запустил под командой
kubectl runбез Deployment и удалил его. Вернётся ли он?
Ответ
Нет. У такого пода нет контроллера, который следил бы за количеством копий. Желаемое состояние «под должен существовать» нигде не записано, поэтому и возвращать нечего.
Главное: под это общая «жилплощадь» контейнеров: один узел, один IP, общие тома; сам по себе он не восстанавливается, для этого нужен контроллер.
Что именно происходит с подом от создания до удаления, видно по его статусам.
Жизненный цикл пода и статусы, которые ты будешь видеть
Колонка STATUS в kubectl get pods это первое, что ты увидишь при любой проблеме. Если не знать, что стоит за каждым словом, поиск причины превращается в гадание.
Путь курьера: получил заказ, поехал за посылкой, едет к клиенту, доставил. Если что-то сломалось, статус говорит, на каком этапе.
У пода есть фаза (phase) в поле status: крупный этап.
| Фаза | Значение |
|---|---|
Pending |
Под принят кластером, но ещё не запущен: ждёт выбора узла или скачивания образа |
Running |
Под назначен на узел, хотя бы один контейнер работает |
Succeeded, Failed |
Все контейнеры завершились (успешно или нет), для разовых задач |
Unknown |
Кластер потерял связь с узлом и не знает состояние |
Колонка STATUS у kubectl get pods богаче, в ней уже причины, а не фазы:
| STATUS | Что происходит |
|---|---|
ContainerCreating |
Узел назначен, kubelet готовит контейнер: скачивает образ, подключает тома |
ErrImagePull, ImagePullBackOff |
Образ скачать не удаётся; BackOff значит «повторяю с нарастающей паузой» |
CrashLoopBackOff |
Контейнер стартует, падает, кластер перезапускает его с растущей паузой |
Terminating |
Под удаляется |
Когда под удаляют, происходит вот что (вспомни SIGTERM из урока 1.4):
flowchart TD
A["t=0: kubectl delete pod<br>статус Terminating, под убирают из списка получателей трафика"] --> B["t=0: kubelet шлёт SIGTERM<br>заканчивай"]
B --> C{"Процесс вышел<br>за 30 секунд?"}
C -->|да| D["Под удалён"]
C -->|нет, t=30| E["SIGKILL: принудительно"]
E --> D
Эти 30 секунд называются terminationGracePeriodSeconds (пауза до принудительного убийства), 30 секунд это значение по умолчанию. Наше приложение при SIGTERM пишет в лог shutting down и выходит: это заложено в нём с темы 1.
Ты видишь CrashLoopBackOff и RESTARTS 5. Расшифровка: контейнер уже запускался, упал, был запущен ещё раз, и так пять раз. Пауза между попытками растёт: по умолчанию 10, 20, 40 секунд и так до потолка в 5 минут (числа зависят от версии и настроек kubelet; кластер не хочет забивать узел бесконечными перезапусками). Значит, искать надо ошибку внутри приложения: смотреть логи упавшего запуска и код выхода, а не проблемы с образом (образ-то скачан, иначе был бы ImagePullBackOff).
Осторожно, путаница: Pending и «образ скачивается». Скачивание образа это уже ContainerCreating, а Pending чаще означает, что планировщик пока не нашёл узел. И вторая путаница: Running не значит «работает правильно», а только «процесс запущен». Проверка, что приложение реально готово принимать запросы, это пробы (урок 5.7).
Прикинь сам: под показывает
STATUS: Pending, а вdescribeесть строкаPulling image. Это ошибка планировщика?
Нет. Скачивание образа идёт после выбора узла, а в колонке STATUS его обычно показывает ContainerCreating. Если в describe уже виден Pulling, узел назначен, и проблему планировщика искать не надо.
Проверь понимание: статус
ImagePullBackOffиCrashLoopBackOff. Что общего и какая главная разница в причинах?
Ответ
Общее: оба слова BackOff значат «кластер повторяет попытку с растущей паузой». Разница: при ImagePullBackOff контейнер вообще не запускался, потому что образ не скачался (опечатка, нет прав). При CrashLoopBackOff образ скачан и контейнер стартует, но процесс внутри падает.
Главное: статус в
get podsназывает причину:ImagePullBackOffзначит «образ не скачать»,CrashLoopBackOffзначит «процесс падает»,Runningне гарантирует, что приложение готово.
Следить за числом подов и их перезапуском должен не человек, а специальный объект: ReplicaSet.
ReplicaSet: держит число копий
Голый под никто не вернёт. Нужен объект, чья единственная работа: следить, что подов ровно столько, сколько надо.
Бригадир, который каждое утро пересчитывает вагончики на объекте по бумажке «нужно три». Лишний увезёт, недостающий закажет.
ReplicaSet хранит три вещи: сколько нужно копий (replicas), по какому признаку узнавать «свои» поды (селектор, selector: условие по меткам) и шаблон пода (template): как выглядит новая копия. Работает как цикл согласования из урока 5.1: посчитал поды с подходящими метками, сравнил с replicas, добавил или убрал разницу. Каждому созданному поду он записывает себя в поле ownerReferences («мой владелец»). Оно нужно для порядка: при удалении владельца кластер убирает и то, чем он владел.
replicas: 3, селектор app.kubernetes.io/name=notes.
| Событие | Подходящих подов | Действие ReplicaSet |
|---|---|---|
| Старт | 0 | создать 3 |
| Удалили один под руками | 2 | создать 1 |
Вручную запустили ещё под с меткой app.kubernetes.io/name=notes |
4 | удалить 1 (он подходит под селектор, значит «свой») |
Убрали метку app.kubernetes.io/name у одного пода |
2 (тот больше не подходит) | создать 1; «освобождённый» под остаётся жить сам по себе |
Последняя строка неочевидна и полезна на практике: если поду нужно «сделать карантин» для разбора, у него снимают метку, и ReplicaSet тут же запускает замену, а подозрительный под остаётся для изучения.
Прикинь сам:
replicas: 3, у тебя три пода. Ты убрал метку у одного из них. Сколько подов станет в кластере и сколько из них ReplicaSet считает своими?
Всего четыре: ReplicaSet создаст замену, потому что видит только два подходящих пода. «Освобождённый» под без метки остаётся жить сам по себе, а считаются «своими» только три: два старых и новый.
Осторожно: не думай, что ReplicaSet создают руками. Почти никогда: его создаёт Deployment. Ещё путают: ReplicaSet следит за числом подов, но не умеет обновлять их образ. Для обновления нужен Deployment.
Проверь понимание:
replicas: 3, а у тебя пять подов с меткойapp.kubernetes.io/name=notes: три от Deployment и два запущены руками. Что сделает ReplicaSet?
Ответ
Удалит два лишних. Он считает все поды, подходящие под селектор, и не различает «свои» и «чужие» по происхождению. Поэтому метки нельзя раздавать случайным подам.
Главное: ReplicaSet сравнивает число подов с подходящими метками и
replicasи добирает разницу; обновлять образ он не умеет.
Чтобы новая версия приложения доезжала до подов без простоя, над ReplicaSet стоит Deployment.
Deployment: обновления над ReplicaSet
ReplicaSet держит число копий, но при смене образа не умеет ничего сделать: шаблон у него неизменяем. Нужен объект, который умеет выкатывать новые версии плавно, чтобы сайт не падал, и откатывать назад. Это Deployment.
Прораб. Бригадиров (ReplicaSet) он не подменяет, а нанимает нового на каждую новую «инструкцию» (шаблон пода), постепенно переводит вагончики к нему и оставляет старого бригадира про запас: если что, вернёт всё обратно.
Цепочка владения такая: Deployment notes владеет ReplicaSet notes-<хэш>, тот владеет подами notes-<хэш>-<случайные символы>. Хэш (hash) в имени это короткий «отпечаток» шаблона пода: из содержимого шаблона считается набор символов, и при любом изменении шаблона он меняется. Поменял образ, значит, поменялся хэш, значит, Deployment создаёт новый ReplicaSet с новым хэшем и постепенно переводит на него поды (шаг за шагом: чуть поднял новых, чуть погасил старых), старый ReplicaSet остаётся с нулём подов для отката. Подробно об обновлении и откате в уроке 5.7.
Сейчас notes-6d8c9b7f5 держит три пода. Ты меняешь образ с 0.4.0 на 0.4.1 и применяешь манифест:
до: RS notes-6d8c9b7f5 (0.4.0): 3 пода
RS ... (нет)
в ходе: RS notes-6d8c9b7f5 (0.4.0): 2 пода <- гасим по одному
RS notes-7c54d8b9d (0.4.1): 2 пода <- поднимаем по одному
после: RS notes-6d8c9b7f5 (0.4.0): 0 подов <- остался для отката
RS notes-7c54d8b9d (0.4.1): 3 пода
Имена хэшей здесь придуманы для иллюстрации: у тебя будут другие. Смысл: в любой момент поды-работники есть, сервис не останавливается.
Прикинь сам: ты поменял в манифесте
replicas: 3на5и применил. Появится ли новый ReplicaSet?
Нет. Число копий не входит в шаблон пода, хэш не меняется, поэтому тот же ReplicaSet просто довозит ещё два пода. Новый ReplicaSet рождается, когда меняется шаблон: образ, переменные, тома, метки пода.
Осторожно: не думай, что при kubectl scale или смене replicas создаётся новый ReplicaSet. Нет: число копий не входит в шаблон пода, хэш не меняется, ReplicaSet тот же. Новый ReplicaSet появляется, только когда меняется шаблон (образ, переменные, тома, метки пода).
Проверь понимание: ты вручную удалил ReplicaSet, которым владеет Deployment. Что произойдёт?
Ответ
Deployment заметит, что ReplicaSet для текущего шаблона нет, и создаст его заново. Поды пересоздадутся. Управлять нужно верхним объектом (Deployment), а не тем, что он создаёт.
Главное: Deployment владеет ReplicaSet, а тот подами; новая версия образа создаёт новый ReplicaSet, а старый остаётся с нулём подов для отката.
Всё, что связывает эти объекты между собой, держится на метках и селекторах.
Метки и селекторы: как объекты находят друг друга
В кластере тысячи объектов. Кто из подов принадлежит ReplicaSet? Каким подам отправить трафик? Имена для этого не годятся (они случайные), нужна связь по свойствам.
Цветные стикеры на коробках. Ты клеишь на коробки красные и синие наклейки, а грузчику говоришь: «Возьми всё с красной наклейкой». Ему не важно, что в коробке.
Метки (labels) это пары ключ: значение на объекте. Придумать можно любые, но в курсе (и в большинстве компаний) принята метка с длинным именем app.kubernetes.io/name: notes. Разберём, почему оно такое.
Ключ метки состоит из двух частей через /: префикс (app.kubernetes.io, похож на адрес сайта) и имя (name). Префикс нужен, чтобы ключи разных авторов не столкнулись. Представь, что в общем холодильнике офиса каждый пишет на контейнере просто «Обед»: чей он? Если подписать «Обед (Аня, бухгалтерия)», путаницы нет. Так и здесь: коротким app пользуется кто угодно, и один инструмент может понять его не так, как другой, а префикс app.kubernetes.io закреплён за самим Kubernetes как общий договор.
Этот договор называется рекомендуемые метки (recommended labels): Kubernetes советует всем подписывать приложения одинаково, и потому их понимают Helm (урок 5.9), панели мониторинга, Backstage и прочие инструменты. Набор такой:
| Ключ метки | Что означает | Пример |
|---|---|---|
app.kubernetes.io/name |
имя приложения | notes |
app.kubernetes.io/instance |
конкретная установка приложения | notes-prod |
app.kubernetes.io/version |
версия | 0.4.0 |
app.kubernetes.io/component |
роль части приложения | api, database |
app.kubernetes.io/part-of |
к какой большой системе относится | notes-platform |
app.kubernetes.io/managed-by |
чем управляется | Helm |
Нам в курсе нужна только name, остальные ты увидишь, когда дойдёшь до Helm. Пример из жизни: на работе у одной системы два сервиса, api и worker. Обоим ставят app.kubernetes.io/part-of: notes, а разным ещё и component. Тогда одной командой можно найти все части системы, а другой только API. Ключ без префикса (version, tier) тоже допустим для своих нужд, как ниже в примере.
Селектор (selector) это условие «объекты, у которых метка такая-то». В нашем Deployment два места, которые должны совпадать:
spec:
selector:
matchLabels:
app.kubernetes.io/name: notes # условие: искать поды с меткой app.kubernetes.io/name=notes
template:
metadata:
labels:
app.kubernetes.io/name: notes # метка, которую получит каждый новый под
Если они не совпадают, ReplicaSet создавал бы поды, которых сам же не видит, и бесконечно плодил бы новые. Поэтому API отклоняет такой манифест сразу. Ещё одно ограничение: селектор существующего Deployment менять нельзя (поле неизменяемо), только удалить и создать заново. Позже по меткам Service найдёт поды (урок 5.3), а ты будешь искать их командой kubectl get pods -l app.kubernetes.io/name=notes (-l от label).
Три пода, у каждого метки:
под A: app.kubernetes.io/name=notes, version=0.4.0
под B: app.kubernetes.io/name=notes, version=0.4.0
под C: app.kubernetes.io/name=other
селектор app.kubernetes.io/name=notes -> A, B
селектор app.kubernetes.io/name=notes,version=0.4.1 -> никого
Запятая в -l означает «и»: должны выполняться оба условия.
Осторожно, путаница: метки и имена. Имя уникально и идентифицирует один объект, метка общая для многих и служит для группировки. И второе: метки в metadata Deployment (labels: app.kubernetes.io/name: notes на самом верху) не то же самое, что метки в шаблоне пода. Первые вешаются на сам Deployment, вторые на поды, и селектор смотрит на вторые.
Прикинь сам: у пода A метки
name=notes, version=0.4.0, у пода Cname=other. Какие поды выберет селекторname=notes,version=0.4.1?
Никакие: запятая означает «и», а ни у кого нет версии 0.4.1. Это не ошибка, просто пустой результат.
Проверь понимание: ты поменял в шаблоне пода
app.kubernetes.io/name: notesнаapp.kubernetes.io/name: notes-v2, а вselector.matchLabelsоставилapp.kubernetes.io/name: notes. Что скажетkubectl apply?
Ответ
Ошибку валидации: селектор не совпадает с метками шаблона (selector does not match template labels). API не принимает такой Deployment, потому что созданные им поды не подходили бы под его же селектор.
Главное: метки это общие стикеры для группировки, имя это уникальный идентификатор; селектор Deployment обязан совпасть с метками в шаблоне пода.
Метки невидимы, пока о них не спросишь, поэтому дальше научимся их показывать.
Как увидеть метки и отобрать по ним: show-labels и -l
Метки невидимы, пока о них не спросишь. Когда что-то не находится (Service не видит под, ReplicaSet создаёт лишние), первым делом смотрят именно метки: опечатка в одном символе ломает связь молча, без ошибки.
Стикеры на коробках видны, только если повернуть коробку этикеткой к себе. Флаг --show-labels и есть такой поворот.
Флаг --show-labels добавляет к выводу kubectl get последнюю колонку LABELS: все метки объекта через запятую. Флаг -l (от label, длинная форма --selector) работает наоборот: показывает только объекты, подходящие под условие.
Так выглядит вывод, когда в namespace notes три пода Deployment (имена и время у тебя будут другими):
$ kubectl -n notes get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
notes-6d8c9b7f5-2k7pl 1/1 Running 0 5m app.kubernetes.io/name=notes,pod-template-hash=6d8c9b7f5
notes-6d8c9b7f5-a1b2c 1/1 Running 0 5m app.kubernetes.io/name=notes,pod-template-hash=6d8c9b7f5
notes-6d8c9b7f5-z3y4x 1/1 Running 0 5m app.kubernetes.io/name=notes,pod-template-hash=6d8c9b7f5
$ kubectl -n notes get pods -l app.kubernetes.io/name=notes
Колонка LABELS: две метки. Первая, app.kubernetes.io/name=notes, твоя, из шаблона пода. Вторую, pod-template-hash, Deployment дописывает сам: это тот же хэш, что в имени ReplicaSet, и по нему ReplicaSet отличает «свои» поды от подов старых версий. Вторая команда покажет те же три строки, потому что все три пода подходят под условие.
Осторожно, путаница: в -l пишут знак = (app.kubernetes.io/name=notes), а в манифесте двоеточие с пробелом (app.kubernetes.io/name: notes). Это одна и та же метка в двух записях: YAML-файл и командная строка просто устроены по-разному. Вторая путаница: если в -l допустить опечатку, kubectl не выдаст ошибку, а напишет No resources found in notes namespace (найдено ноль объектов).
Прикинь сам: в
-lты написалapp.kubernetes.io/name=notezс опечаткой. Что выдастkubectl get pods -l ...?
Не ошибку, а No resources found in notes namespace: условию просто никто не соответствует. Поэтому при пустом выводе сначала смотри метки через --show-labels.
Проверь понимание:
kubectl get pods -l app.kubernetes.io/name=notesпишетNo resources found, хотяkubectl get podsпоказывает три пода. Какие две причины проверить первыми?
Ответ
Во-первых, опечатка в ключе или значении: сравни с колонкой LABELS из --show-labels. Во-вторых, неверный namespace: без -n notes команда ищет в default, а там подов с такой меткой нет. Начинай с --show-labels: он показывает реальные метки, а не те, что ты ожидаешь.
Главное:
--show-labelsпоказывает реальные метки,-lотбирает по условию, а опечатка ломает связь молча.
Теперь можно прочитать целиком манифест Deployment, зная, что значат метки в нём.
Манифест Deployment построчно
Ты уже знаешь четыре верхних ключа манифеста из урока 5.1: apiVersion, kind, metadata, spec. Теперь посмотрим, как выглядит spec у Deployment. Это тот самый файл, который ты создашь в задании 1.
Разберём его по блокам (полный текст в практике):
apiVersion: apps/v1 # у Deployment группа apps, версия v1
kind: Deployment
metadata:
name: notes # имя Deployment
namespace: notes # в каком namespace создать (из урока 5.1)
labels:
app.kubernetes.io/name: notes # метка на самом Deployment
spec:
replicas: 1 # сколько подов держать
selector: # как ReplicaSet находит «свои» поды
matchLabels:
app.kubernetes.io/name: notes
template: # шаблон пода: как выглядит каждая копия
metadata:
labels:
app.kubernetes.io/name: notes # метка подов (должна совпадать с селектором)
spec: # spec самого ПОДА (не Deployment)
containers:
- name: notes
image: ghcr.io/<github-user>/notes:0.4.0 # образ с фиксированным тегом
ports:
- containerPort: 8080 # на каком порту слушает приложение
env: # переменные окружения контейнера
- name: STORE
value: "file"
template это «под внутри Deployment»: внутри него снова metadata и spec, как у обычного пода. Ключ containerPort служит для документации: он не открывает порт наружу, а лишь сообщает читателю (и инструментам), что приложение слушает 8080. Переменная окружения (environment variable) это пара имя-значение, которую программа читает при старте; наше приложение берёт из них настройки (STORE, NOTES_DATA, HOST, PORT из урока 1.8).
kubectl apply -f файл объявляет желаемое: объекта нет, создаст, есть, приведёт к манифесту, и команду можно повторять сколько угодно раз (идемпотентность, idempotency). kubectl create -f только создаёт и падает с AlreadyExists, если объект уже есть. Для файлов в git используют apply. Перед применением полезны две проверки: kubectl diff -f файл показывает разницу между файлом и живым объектом (строки - убираются, + добавляются), а kubectl apply --dry-run=server -f файл прогоняет манифест через проверку на сервере, ничего не сохраняя. Режим client проверяет только формат на твоей стороне и не знает, что скажет сервер (например, что namespace не существует).
Прикинь сам: сколько раз в манифесте Deployment встречается ключ
specи чем они различаются?
Два раза. Верхний spec описывает сам Deployment (реплики, селектор), вложенный в template описывает под (контейнеры, тома).
Осторожно: не думай, что spec встречается один раз. У Deployment два spec: верхний (про Deployment: реплики, селектор) и вложенный в template (про под: контейнеры, тома). Ошибка в отступе легко переносит containers не на тот уровень.
Проверь понимание: чем
kubectl applyотличается отkubectl create?
Ответ
create императивно создаёт объект и падает, если он уже есть. apply декларативно приводит объект к манифесту и безопасно запускается повторно, поэтому его используют в CI и при работе с файлами из git.
Главное: у Deployment два
spec: верхний про Deployment, вложенный вtemplateпро под;applyповторяем,createпадает на существующем.
Манифест описывает контейнер, но куда ему писать файлы, решают тома.
Тома: почему данные в поде временные
Файловая система контейнера исчезает вместе с ним (как в уроке 4.3). Чтобы приложению было куда писать файл, к поду подключают том.
Ящик в вагончике: рабочие могут класть туда бумаги, но когда вагончик увозят, ящик уезжает вместе с ним.
Том задаётся в двух местах: в volumes пода (что за том) и в volumeMounts контейнера (куда его подключить). У emptyDir («пустой каталог») жизнь такая: создаётся пустым при запуске пода на узле, исчезает при удалении пода. Если контейнер внутри пода просто перезапустился, файлы остаются, потому что пода не удаляли. Ключ sizeLimit ограничивает размер (при превышении под будет вытеснен).
У «Заметок» файл /data/notes.txt, а в образе (урок 4.7) каталог /data принадлежит пользователю 10001, от которого работает приложение. Мы подключаем emptyDir в /data. У Deployment три реплики, значит, три пода, значит, три отдельных emptyDir, три независимых файла. Заметка, записанная в одну реплику, не видна в двух других. Это ловушка, из-за которой пользователи скажут «заметки то пропадают, то появляются». Решение это общее хранилище, а не вынесенное в отдельную базу: PostgreSQL в уроке 5.5.
Прикинь сам: у «Заметок» три реплики с
emptyDirв/data. Ты записал заметку через одну реплику. Увидишь ли ты её при следующем запросе?
Только если запрос попадёт в ту же реплику, то есть примерно в одном случае из трёх. У каждого пода свой emptyDir, поэтому заметки «то пропадают, то появляются».
Осторожно: не думай, что три реплики дают отказоустойчивость сами по себе. Реплики дают её только приложению, у которого нет собственного состояния (stateless). «Заметки» на файлах в каждом поде состояние хранят, поэтому три копии это три разные базы.
Проверь понимание: под удалили, а Deployment создал новый. Что с файлом в
emptyDirстарого пода?
Ответ
Файл пропал вместе со старым подом: emptyDir живёт, пока жив под. Новый под получает свой пустой каталог. А вот при перезапуске только контейнера внутри того же пода файл сохранился бы.
Главное:
emptyDirживёт столько, сколько жив под, и у каждой реплики он свой; общее состояние лежит в отдельном хранилище.
Остался вопрос, откуда узел возьмёт образ, чтобы под вообще стартовал.
Образы в kind и политика загрузки
Самая частая причина, по которой первый Deployment не запускается: узел не может получить образ. Надо понимать, откуда он его берёт.
Узел kind это отдельный маленький компьютер со своим складом образов. То, что лежит у тебя на хосте в Docker, для него как на соседнем этаже: он туда не заходит.
Узлы kind это контейнеры Docker со своим containerd внутри (урок 5.1), поэтому образы с твоего хоста они сами не видят. Есть два пути. Первый: образ лежит в публичном реестре ghcr.io (он там с урока 4.7), и узлы скачивают его сами. Второй: kind load docker-image notes:0.4.0 --name notes копирует локальный образ внутрь каждого узла.
Поведение при старте пода задаёт imagePullPolicy:
| Значение | Что делает |
|---|---|
IfNotPresent |
Тянуть, только если образа нет на узле |
Always |
Проверять реестр при каждом запуске пода |
Never |
Брать только локальный, а если нет, ошибка |
Если поле не задано, Kubernetes выбирает сам: для тега latest или без тега это Always, для остальных IfNotPresent. Отсюда правило курса: тег latest не используем, иначе поведение зависит от неявных правил и невоспроизводимо. Всегда фиксированная версия.
Образ приватный (ghcr.io/<user>/notes:0.4.0 в закрытом пакете). Узел приходит в реестр без пароля и получает unauthorized. Пароль передают через Secret типа docker-registry (объект для хранения секретных данных, подробно в уроке 5.6), а в шаблоне пода указывают его в imagePullSecrets. Мы этого делать не будем, потому что пакет notes публичный, но команду увидишь в разборе поломок.
Прикинь сам: образ
notes:0.4.0есть в твоём локальном Docker, но ты не делалkind loadи он не опубликован. Что покажет под?
Статус ErrImagePull, потом ImagePullBackOff: узел kind не видит образы хоста и ищет образ в реестре, где его нет.
Осторожно: не думай, что ImagePullBackOff значит «образ сломан». Чаще всего образ просто не найден (опечатка в теге) или закрыт паролем.
Проверь понимание: зачем
kind load, если образ уже есть в локальном Docker?
Ответ
Кластер kind работает внутри своих контейнеров-узлов со своим хранилищем образов. Docker хоста для него не источник. Без kind load (или реестра) узел не найдёт образ и поды зависнут в ErrImagePull.
Главное: узлы kind берут образ из реестра или из
kind load; тегlatestне используем, чтобы не зависеть от неявногоimagePullPolicy.
Теперь соберём всё вместе и посмотрим, как цепочка от apply до Running разворачивается шаг за шагом.
Что происходит между kubectl apply и работающим подом
Когда под не появился, надо понимать, на каком шаге цепочка порвалась. Иначе ты будешь смотреть не туда: например, искать ошибку в приложении, когда планировщик ещё даже не выбрал узел.
Заказ в интернет-магазине: оформил, склад собрал, курьер повёз, курьер вручил. На каждом этапе свой исполнитель и свой статус.
Цепочка из урока 5.1 на нашем примере выглядит так:
flowchart TD
A["1. kubectl apply<br>читает файл, отправляет в API"] --> B["2. API-сервер<br>проверяет формат и права, сохраняет Deployment в etcd"]
B --> C["3. контроллер Deployment<br>создаёт ReplicaSet"]
C --> D["4. контроллер ReplicaSet<br>видит 0 подов из 3, создаёт 3 объекта Pod<br>без узла, статус Pending"]
D --> E["5. планировщик<br>выбирает узел для каждого пода"]
E --> F["6. kubelet на узле<br>просит containerd скачать образ<br>ContainerCreating, потом запуск"]
F --> G["7. kubelet<br>докладывает в API: Running"]
Важно, что исполнители не разговаривают друг с другом напрямую. Каждый смотрит в API-сервер, находит «свою» работу и записывает результат обратно. Поэтому если один компонент на время остановился, остальные не ломаются: работа ждёт в API-сервере, пока исполнитель вернётся.
Хочешь увидеть эту цепочку своими глазами: после apply выполни kubectl -n notes get events --sort-by=.lastTimestamp. Ты увидишь события примерно такого вида (возраст и хэши у тебя будут другие):
Normal ScalingReplicaSet deployment-controller Scaled up replica set notes-6d8c9b7f5 from 0 to 3
Normal SuccessfulCreate replicaset-controller Created pod: notes-6d8c9b7f5-x2k7p
Normal Scheduled default-scheduler Successfully assigned notes/notes-6d8c9b7f5-x2k7p to notes-worker
Normal Pulled kubelet Container image "..." already present on machine
Normal Created kubelet Created container notes
Normal Started kubelet Started container notes
Колонка From называет исполнителя. Каждая строка соответствует шагу цепочки: deployment-controller это шаг 3, replicaset-controller шаг 4, default-scheduler шаг 5, kubelet шаги 6 и 7. Если последней строки нет, а под в ContainerCreating, беда на узле (образ, том). Если нет строки Scheduled, под висит в Pending, и виноват планировщик (мало ресурсов на узлах).
Прикинь сам:
applyответилcreatedза долю секунды, а под запускается минуту. Почему так?
Команда лишь кладёт запись в API-сервер и сразу возвращается. Остальное делают контроллеры, планировщик и kubelet уже после ответа. Дождаться результата помогает kubectl rollout status.
Осторожно: не думай, что kubectl apply сам запускает контейнеры. Он только кладёт запись в API-сервер и сразу возвращается. Поэтому сообщение created приходит мгновенно, а под может запускаться ещё минуту. Чтобы дождаться результата, нужен rollout status.
Проверь понимание: под висит в
Pending, а в событиях нет строкиScheduled. Какой исполнитель ещё не сделал свою работу?
Ответ
Планировщик: он не назначил поду узел. Причина почти всегда в ресурсах или ограничениях на узлах (describe pod в разделе Events назовёт её). Kubelet и образ тут ни при чём: до назначения узла они под даже не видят.
Главное: цепочка идёт через API-сервер: каждый исполнитель сам видит свою работу, а по колонке
Fromв событиях видно, на каком шаге остановка.
Манифест, который мы применяем, это YAML, а в нём смысл задают пробелы.
YAML: отступы это структура
Манифест это YAML, а в YAML смысл задают пробелы. Ошибка в два пробела превращает рабочий файл в другой объект или в ошибку.
Оглавление книги с вложенными пунктами: подпункт с большим отступом принадлежит ближайшему пункту выше. Сдвинул строку, значит, переселил её в другую главу.
Три конструкции покрывают весь наш манифест. Словарь (пары ключ: значение) записывается строками с одним отступом. Список записывается строками с дефисом, каждый дефис начинает новый элемент. Вложенность это отступ на два пробела глубже. Табуляции запрещены, только пробелы. Значение можно взять в кавычки, и это важно для строк, которые YAML может принять за число или логическое значение: "0.4.0" и "file" в кавычках, а 8080 без кавычек, потому что это число.
Разобранный пример.
containers: # ключ, значение которого список
- name: notes # первый элемент списка (дефис) и в нём ключ name
image: notes:0.4.0 # тот же элемент: отступ выровнен с name, не с дефисом
ports:
- containerPort: 8080
Строка image выровнена с name (а не с дефисом), поэтому это второй ключ того же контейнера. Сдвинь её на два пробела влево, и получится, что дефис и image оказались на одном уровне, и разбор YAML остановится: apply вернёт ошибку разбора (did not find expected '-' indicator). Проверять отступы дёшево: kubectl apply --dry-run=server -f файл или kubeconform найдут такую ошибку до выката.
Прикинь сам: в списке
containersты сдвинул строкуimageна два пробела влево, к дефису. Что получится?
Разбор YAML остановится: apply вернёт ошибку разбора (did not find expected '-' indicator), до проверки схемы дело не дойдёт. Ловится заранее через --dry-run=server или kubeconform.
Осторожно: не думай, что дефис и ключ это разные элементы. Дефис только отмечает начало элемента списка, а первый ключ элемента пишется на той же строке после дефиса. Отсюда и выравнивание следующих ключей.
Проверь понимание: сколько контейнеров описано, если в списке
containersдва дефиса на одном уровне?
Ответ
Два: каждый дефис на этом уровне начинает новый элемент списка, то есть новый контейнер.
Главное: в YAML вложенность задают пробелы, дефис начинает элемент списка, первый ключ элемента пишется на той же строке.
Остался инструмент, без которого не диагностировать ни одну поломку: заглянуть внутрь пода.
kubectl logs, exec и port-forward: как заглянуть в под
Под работает где-то на узле, к которому ты не подключаешься по SSH. Нужны способы посмотреть его логи, зайти внутрь и обратиться к приложению.
Все три команды идут через API-сервер (напомню из урока 5.1: он единственная дверь в кластер), а тот передаёт их kubelet нужного узла:
kubectl logs <под>печатает то, что приложение пишет в вывод (тот же смысл, что уdocker logs). Флаг--previousпоказывает логи предыдущего запуска упавшего контейнера, это главный инструмент приCrashLoopBackOff: текущий запуск ещё пуст.kubectl exec <под> -- командавыполняет команду внутри контейнера (аналогdocker exec). Двойной дефис--отделяет флагиkubectlот команды.kubectl port-forward <под> 18080:8080открывает на твоей машине порт 18080 и туннелем ведёт его в порт 8080 пода. Балансировки нет: ты говоришь с одним конкретным подом. Это способ для отладки, а не для пользователей.
Обращаться можно не к имени пода, а к Deployment: deploy/notes. Тогда kubectl сам выберет один из его подов. Логи при этом берутся тоже из одного выбранного пода, а не со всех сразу.
Прикинь сам: под упал и перезапустился. Текущий
kubectl logsпуст. Как увидеть причину падения?
С флагом --previous: он покажет логи прошлого запуска контейнера, где и записана причина.
Осторожно: не думай, что логи хранит под. На самом деле логи лежат на узле рядом с контейнером. Когда под удалён, логи пропали; чтобы они жили дольше, их собирают в централизованное хранилище (тема 8).
Главное:
logs,execиport-forwardидут через API-сервер к kubelet; логи лежат на узле и пропадают вместе с подом.
Теперь можно запускать «Заметки» руками, и мы начнём с первого Deployment.
Практика
Все команды выполняются из ~/notes. Предполагается, что кластер из 5.1 запущен: kubectl config current-context должен вернуть kind-notes. Подставь свой логин GitHub вместо <github-user> (строчными буквами). Версии: kind v0.33.0, kubectl 1.37.1.
Задание 1. Первый Deployment с одной репликой
Цель: описать приложение манифестом и получить работающий под.
Предскажи: сколько объектов появится после apply одного Deployment (не считая самого Deployment) и как будут называться поды?
Ответ
Появятся ReplicaSet (один) и под (один по числу реплик). Имя ReplicaSet: notes-<хэш>, имя пода: notes-<хэш>-<5 символов>.
Шаги:
- Создай временный манифест (в проект он попадёт в задании 4 в окончательном виде). Здесь
cat > файл <<'YAML'записывает всё до строкиYAMLв файл; кавычки вокругYAMLзапрещают оболочке подставлять переменные внутри текста. Поэтому логин мы подставляем отдельной командойsed:export GH_USER=...задаёт переменную (как в уроке 4.7), аsed -i "s|<github-user>|$GH_USER|"заменяет в файле слово<github-user>на её значение (-iправит файл на месте,|служит разделителем вместо/).
export GH_USER="<твой-github-логин-строчными>"
mkdir -p ~/notes/k8s/base
cat > ~/notes/k8s/base/10-deployment.yaml <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
name: notes
namespace: notes
labels:
app.kubernetes.io/name: notes
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: notes
template:
metadata:
labels:
app.kubernetes.io/name: notes
spec:
containers:
- name: notes
image: ghcr.io/<github-user>/notes:0.4.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: STORE
value: "file"
- name: NOTES_DATA
value: "/data/notes.txt"
- name: APP_VERSION
value: "0.4.0"
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
YAML
sed -i "s|<github-user>|$GH_USER|" ~/notes/k8s/base/10-deployment.yaml
Новое здесь: imagePullPolicy: IfNotPresent (см. теорию), APP_VERSION (версия, которую приложение показывает на /), STORE=file и NOTES_DATA (хранить заметки в файле, как в уроке 4.4), emptyDir: {} (пустые фигурные скобки значат «настроек нет»). Образ уже задаёт HOST=0.0.0.0 и PORT=8080, поэтому их не указываем.
- Проверь манифест сервером и примени.
rollout statusждёт, пока выкатка закончится (--timeoutограничивает ожидание);deploy/notesэто сокращённая запись «Deployment с именем notes»;get deploy,rs,podsвыводит три типа объектов сразу;-o wideдобавляет колонки.
cd ~/notes
kubectl apply --dry-run=server -f k8s/base/10-deployment.yaml
kubectl apply -f k8s/base/10-deployment.yaml
kubectl -n notes rollout status deploy/notes --timeout=120s
kubectl -n notes get deploy,rs,pods -o wide
Что должно получиться:
deployment.apps/notes created (server dry run)
deployment.apps/notes created
Waiting for deployment "notes" rollout to finish: 0 of 1 updated replicas are available...
deployment "notes" successfully rolled out
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES
deployment.apps/notes 1/1 1 1 12s notes ghcr.io/<github-user>/notes:0.4.0
NAME DESIRED CURRENT READY AGE
replicaset.apps/notes-6d8c9b7f5 1 1 1 12s
NAME READY STATUS RESTARTS AGE IP NODE
pod/notes-6d8c9b7f5-x2k7p 1/1 Running 0 12s 10.244.1.3 notes-worker
Вывод сокращён по колонкам. Хэш, имена и IP у тебя будут другие, а вместо <github-user> твой логин.
Как читать вывод: для Deployment READY 1/1 это «готово 1 из 1 нужных копий», UP-TO-DATE сколько копий уже на актуальном шаблоне, AVAILABLE сколько готово принимать запросы. Для ReplicaSet DESIRED желаемое, CURRENT существующее, READY готовое: три числа, которые должны сойтись. У пода RESTARTS 0 показывает, сколько раз перезапускался контейнер (если растёт, это сигнал), IP это адрес пода в сети кластера (диапазон 10.244.0.0/16 в kind), NODE узел, куда его назначил планировщик. Строка Waiting for deployment... может не появиться, если под успел стартовать быстро.
Объясни себе:
- Откуда в имени пода два «хвоста» и что каждый означает?
- Почему в манифесте
emptyDir, а не том Docker, как в Compose? - Что такое
--dry-run=serverи чем он лучшеclient?
Типичные ошибки:
error: the namespace from the provided object does not match the namespace "default": неверный флаг-nили контекст. Убери-n(namespace уже в манифесте) или поправь значение.Error from server (NotFound): namespaces "notes" not found: не создан namespace из урока 5.1. Выполниkubectl apply -f k8s/base/00-namespace.yaml.error validating data: ValidationError(Deployment.spec): missing required field "selector": забыт селектор или сломаны отступы в YAML. Проверь, чтоselectorиtemplateна одном уровне подspec.- Под в
ErrImagePullиImagePullBackOffсразу после apply: в манифесте остался<github-user>или пакет на ghcr.io приватный. Проверьgrep image k8s/base/10-deployment.yamlи видимость пакета в настройках GitHub.
Задание 2. Заглянуть в под: логи, exec, port-forward
Цель: убедиться, что приложение отвечает, и научиться смотреть внутрь пода.
Разбор команд: logs deploy/notes --tail=5 печатает последние 5 строк лога. port-forward deploy/notes 18080:8080 & открывает туннель порт-на-твоей-машине:порт-в-поде; & уводит команду в фон, чтобы вернулся приглашение; sleep 2 даёт туннелю секунду-две подняться; kill %1 останавливает первую фоновую задачу оболочки. curl -sS тихо ходит по адресу, но показывает ошибки; -X POST -d '...' отправляет POST с телом. Приложение принимает заметки только в JSON вида {"text":"..."}, иначе ответит 400. В exec ... -- sh -c 'id -u; ls -l /data' двойной дефис отделяет команду от флагов kubectl, sh -c '...' выполняет две команды подряд, id -u печатает номер пользователя, а ls -l /data показывает файлы каталога данных.
Предскажи: port-forward прокидывает порт с твоей машины в под. Что вернёт curl http://127.0.0.1:18080/healthz и пойдёт ли этот запрос через какой-либо балансировщик?
Ответ
Ответ ok с кодом 200. Балансировщика нет: kubectl открывает туннель к API-серверу, а тот к конкретному поду. Service мы ещё не создавали (урок 5.3).
Шаги:
kubectl -n notes logs deploy/notes --tail=5
# туннель в фоне на порт 18080, чтобы не мешать другим сервисам на 8080
kubectl -n notes port-forward deploy/notes 18080:8080 &
sleep 2
curl -sS http://127.0.0.1:18080/healthz
curl -sS -X POST -d '{"text":"первая заметка"}' http://127.0.0.1:18080/notes
curl -sS http://127.0.0.1:18080/notes
kill %1
# зайти в контейнер и посмотреть пользователя и файл данных
kubectl -n notes exec deploy/notes -- sh -c 'id -u; ls -l /data'
Что должно получиться:
2026-09-30 12:00:01,204 INFO started host=0.0.0.0 port=8080
Forwarding from 127.0.0.1:18080 -> 8080
Forwarding from [::1]:18080 -> 8080
ok
Handling connection for 18080
{"id": 1}
Handling connection for 18080
[{"id": 1, "text": "первая заметка", "created_at": "2026-09-30T12:00:20+00:00"}]
10001
total 4
-rw-r--r-- 1 notes notes 79 Sep 30 12:00 notes.txt
Как читать вывод: первая строка это лог приложения: время (в UTC, часовом поясе контейнера), уровень INFO и сообщение о старте на 0.0.0.0:8080. Строки Forwarding from печатает port-forward: туннель поднят для IPv4 и IPv6, а Handling connection for появляется на каждый запрос. Ответ ok это /healthz, {"id": 1} ответ на запись заметки (код 201), а следующий вывод это список заметок в JSON: created_at время создания в UTC. 10001 это номер пользователя, под которым работает процесс. В ls -l владелец файла notes (имя пользователя uid 10001 из образа), а размер и время у тебя будут другими. Строки лога о самих запросах приложение пишет тоже, но /healthz и /readyz из лога скрыты намеренно.
Объясни себе:
- Почему
id -uвозвращает 10001, а не 0, и где это задано? - Что будет с файлом
/data/notes.txt, если удалить под? - Почему
logs deploy/notesработает, хотя логи хранит под?
Типичные ошибки:
error: unable to forward port because pod is not running. Current status=Pending: под ещё не запущен. Дождисьrollout status.Unable to listen on port 18080: Listeners failed to create with the following errors: [unable to create listener: Error listen tcp4 127.0.0.1:18080: bind: address already in use]: порт занят прошлым туннелем. Найди и останови:pkill -f 'port-forward'.{"error": "нужен JSON {\"text\": \"...\"}"}в ответ на POST: тело отправлено не в JSON. Пиши-d '{"text":"..."}'.error: Internal error occurred: error executing command in container: exec: "bash": executable file not found in $PATH: в минимальных образахbashможет не быть. Используйsh.
Нейросеть хорошо переписывает вывод
kubectl describe podчеловеческим языком. Но вставляй в неё только свой вывод без токенов и паролей, а совет «удалить и создать заново» проверяй: так теряется след поломки.
Задание 3. Самовосстановление и масштабирование
Цель: увидеть, как контроллеры возвращают желаемое состояние.
Разбор команд: scale --replicas=3 императивно меняет число копий. POD=$(kubectl ... | head -1) кладёт в переменную имя первого пода из списка (-o name печатает имена в виде pod/notes-..., head -1 берёт первую строку). delete "$POD" --wait=false удаляет под и не ждёт завершения, чтобы ты успел увидеть Terminating. get pods -w (от watch) выводит изменения по мере появления, --request-timeout=10s сам оборвёт наблюдение через 10 секунд. -o custom-columns=ИМЯ:путь выводит свои колонки: .metadata.ownerReferences[0].name это имя первого владельца пода. sed -n '/Events/,$p' печатает часть вывода от строки со словом Events до конца.
Предскажи: ты удалишь под, у которого replicas: 3. Сколько подов будет через 2 секунды и будет ли у нового то же имя?
Ответ
Снова 3 (один будет в статусе ContainerCreating или Running), плюс старый в Terminating некоторое время. Имя другое: суффикс случаен, IP тоже новый. Под одноразовый, ReplicaSet создаёт замену, а не воскрешает старый.
Шаги:
# масштабирование императивно (для опыта; в манифесте потом поправим)
kubectl -n notes scale deploy/notes --replicas=3
kubectl -n notes get pods -o wide
# удаляем один под и смотрим за событиями
POD=$(kubectl -n notes get pods -o name | head -1)
kubectl -n notes delete "$POD" --wait=false
kubectl -n notes get pods -w --request-timeout=10s
Останови -w по Ctrl+C, если он не завершился сам.
Затем посмотри, кто чем владеет:
kubectl -n notes get pod -o custom-columns=NAME:.metadata.name,OWNER:.metadata.ownerReferences[0].name
kubectl -n notes describe deploy/notes | sed -n '/Events/,$p'
Что должно получиться:
NAME READY STATUS RESTARTS AGE
notes-6d8c9b7f5-x2k7p 1/1 Terminating 0 3m
notes-6d8c9b7f5-q9m4d 0/1 ContainerCreating 0 1s
notes-6d8c9b7f5-a1b2c 1/1 Running 0 40s
notes-6d8c9b7f5-z3y4x 1/1 Running 0 40s
NAME OWNER
notes-6d8c9b7f5-a1b2c notes-6d8c9b7f5
notes-6d8c9b7f5-q9m4d notes-6d8c9b7f5
notes-6d8c9b7f5-z3y4x notes-6d8c9b7f5
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 1m deployment-controller Scaled up replica set notes-6d8c9b7f5 from 1 to 3
Имена и возраст у тебя будут другие; порядок строк в get pods тоже может отличаться.
Как читать вывод: в первой таблице виден весь путь замены: удаляемый под в Terminating, новый в ContainerCreating (образ уже на узле, поэтому создание быстрое), два нетронутых в Running. Второй вывод показывает, что у каждого пода владелец один и тот же ReplicaSet: по этому полю кластер понимает, чей под. Событие ScalingReplicaSet от deployment-controller подтверждает, что число копий менял именно контроллер Deployment, а не ты напрямую с подом.
Объясни себе:
- Почему на короткое время подов «больше» трёх (один
Terminating)? - Что вернёт ownerReferences у пода и как это связано со сборкой мусора при удалении Deployment?
- Что произойдёт с
kubectl scale, если ты потом применишь манифест сreplicas: 1?
Типичные ошибки:
Error from server (NotFound): pods "notes-..." not found: под уже заменён, имя устарело. Возьми свежий изget pods.error: unknown flag: --replica: опечатка, флаг называется--replicas.
Задание 4. Шаг проекта: Deployment notes на 3 реплики
Цель: зафиксировать окончательный манифест в проекте и выкатить его.
Предскажи: что покажет kubectl diff -f k8s/base/10-deployment.yaml, если ты уже вручную сделал scale --replicas=3, а в файле стоит replicas: 3?
Ответ
Про replicas ничего: желаемое состояние совпало с живым. Но в файле поменялся том (sizeLimit), поэтому дифф покажет эту добавленную строку, и шаблон пода изменится: пройдёт выкатка. Если в файле остаётся replicas: 1, дифф покажет - replicas: 3 и + replicas: 1. Служебные поля вроде generation тоже могут попасть в дифф.
Шаги:
- Обнови
k8s/base/10-deployment.yamlцеликом (три реплики, ограничение на данныеsizeLimit, значение64Miэто 64 мебибайта):
apiVersion: apps/v1
kind: Deployment
metadata:
name: notes
namespace: notes
labels:
app.kubernetes.io/name: notes
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: notes
template:
metadata:
labels:
app.kubernetes.io/name: notes
spec:
containers:
- name: notes
image: ghcr.io/<github-user>/notes:0.4.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: STORE
value: "file"
- name: NOTES_DATA
value: "/data/notes.txt"
- name: APP_VERSION
value: "0.4.0"
volumeMounts:
- name: data
mountPath: /data
volumes:
# данные эфемерны: живут, пока жив под (долг, закроется в 5.5)
- name: data
emptyDir:
sizeLimit: 64Mi
Не забудь снова заменить <github-user> на логин (sed из задания 1). Изменился шаблон пода (добавился sizeLimit), поэтому Deployment создаст новый ReplicaSet и заменит поды: старые заметки пропадут.
- Посмотри дифф, примени и проверь.
kubectl diffзавершается с кодом 1, если различия есть: это не ошибка.get rsпокажет оба ReplicaSet: старый с нулём подов (запас для отката) и новый.
kubectl diff -f k8s/base/10-deployment.yaml
kubectl apply -f k8s/base/10-deployment.yaml
kubectl -n notes rollout status deploy/notes
kubectl -n notes get deploy notes
kubectl -n notes get rs,pods -o wide
- Проверь, что у каждой реплики свои данные. Сначала запиши одну заметку (
port-forwardпопадёт в какой-то один под), потом посчитай строки в файле каждого пода. В циклеfor p in $(...)переменнаяpпо очереди принимает имя пода;wc -l < файлсчитает строки;{ ...; } 2>/dev/null || echo 0печатает 0, если файла ещё нет (в поде, куда заметок не попало):
kubectl -n notes port-forward deploy/notes 18080:8080 &
sleep 2
curl -sS -X POST -d '{"text":"заметка для одного пода"}' http://127.0.0.1:18080/notes
kill %1
for p in $(kubectl -n notes get pods -o name); do
echo -n "$p: "
kubectl -n notes exec "$p" -- sh -c '{ wc -l < /data/notes.txt; } 2>/dev/null || echo 0'
done
- Зафиксируй в git:
git add k8s/base/10-deployment.yaml
git commit -m "k8s: Deployment notes, 3 реплики, emptyDir"
Что должно получиться:
deployment.apps/notes configured
deployment "notes" successfully rolled out
NAME READY UP-TO-DATE AVAILABLE AGE
notes 3/3 3 3 9m
{"id": 1}
pod/notes-7c54d8b9d-4hq2v: 0
pod/notes-7c54d8b9d-m8x5r: 1
pod/notes-7c54d8b9d-t6w9z: 0
Три пода Running на разных workers. Хэши и то, какой из подов получил заметку, у тебя будут другими: важно, что строка больше нуля только в одном.
Как читать вывод: configured значит, что объект существовал и apply изменил его. 3/3 и UP-TO-DATE 3 говорят, что все три копии на новом шаблоне. Счётчики строк 0, 1, 0 показывают главный урок задания: каждая реплика хранит заметку в своём emptyDir, поэтому три копии это три разные базы. Решается это общим хранилищем в 5.5.
Объясни себе:
- Почему при трёх репликах и файловом хранилище «заметки то есть, то нет» и какой объект в 5.3-5.5 это исправит?
- Что изменится в данных, если удалить один под?
- Почему нельзя просто поставить
replicas: 3и считать приложение отказоустойчивым?
Типичные ошибки:
The Deployment "notes" is invalid: spec.selector: Invalid value: ... field is immutable: ты изменилmatchLabelsу существующего Deployment. Селектор менять нельзя: удали Deployment (kubectl delete -f ...) и создай заново.Error from server (BadRequest): error when creating "k8s/base/10-deployment.yaml": Deployment in version "v1" cannot be handled as a Deployment: ... unknown field: опечатка в имени поля. Проверь его черезkubectl explain deploy.spec.template.spec.containers.
Сломай и почини
Скачай скрипт поломки и запусти один сценарий (1, 2 или 3). Скрипт не читай: ломай, диагностируй, чини. Он работает без sudo, берёт твой k8s/base/10-deployment.yaml, применяет испорченную копию к Deployment notes (сам файл в проекте не меняется) и требует, чтобы задание 4 было выполнено.
curl -fsSL -o /tmp/break-5.2.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/5.2/break.sh
bash /tmp/break-5.2.sh 1 # или 2, или 3
kubectl -n notes get pods
Вернуть рабочее состояние: bash /tmp/break-5.2.sh fix (безопасно запускать повторно).
Симптом
Ты выкатил новую версию, а kubectl -n notes get pods показывает часть подов не в Running. Возможные картины (по одной за запуск): ErrImagePull и ImagePullBackOff, CrashLoopBackOff (перезапуски растут), либо Pending без узла. Старые поды при этом продолжают работать (выкатка идёт постепенно и останавливается на новом поде), но обновление не завершается: rollout status ждёт и не дожидается.
Гипотезы
- Образ не найден или недоступен: опечатка в теге, нет прав на приватный образ.
- Контейнер стартует и сразу падает: неверная команда, ошибка конфигурации.
- Под не может быть размещён: запрошено больше ресурсов, чем есть на узлах.
Проверки
Всегда начинай с событий и с точной причины, а не с догадок. <имя> замени именем проблемного пода из get pods. logs --previous показывает логи упавшего запуска, get events --sort-by=.lastTimestamp | tail -10 десять самых свежих событий по времени, describe nodes | grep -A6 'Allocated resources' сколько ресурсов узлов уже занято запросами.
kubectl -n notes get pods
kubectl -n notes describe pod "<имя>" | sed -n '/Events/,$p'
kubectl -n notes logs <имя> --previous # логи упавшего запуска
kubectl -n notes get events --sort-by=.lastTimestamp | tail -10
kubectl describe nodes | grep -A6 'Allocated resources'
Исправление
Разбор сценариев
1. ImagePullBackOff (опечатка в теге). В describe в событиях: Failed to pull image "ghcr.io/<user>/notes:0.4.O": ... manifest unknown или not found. Кластер пытается снова с нарастающей паузой, отсюда BackOff. Это не «упало приложение», образа просто нет. Причины бывают такие: опечатка в теге или имени, образ не опубликован, приватный реестр без imagePullSecrets (текст unauthorized или denied), лимит запросов реестра. Исправление: kubectl -n notes set image deploy/notes notes=ghcr.io/<github-user>/notes:0.4.0 (и поправить файл, чтобы git совпадал с кластером). Для приватного образа:
kubectl -n notes create secret docker-registry ghcr-pull \
--docker-server=ghcr.io --docker-username=<github-user> \
--docker-password=CHANGE_ME
и в шаблоне пода imagePullSecrets: [{name: ghcr-pull}].
2. CrashLoopBackOff. Статус означает: контейнер запускается, завершается с ошибкой, кластер перезапускает его с растущей паузой (10, 20, 40 секунд, потолок 5 минут). kubectl logs покажет уже новый пустой запуск, поэтому нужен --previous: там будет ошибка: PORT должен быть числом. В describe смотри Last State: Terminated, Exit Code: 1 или 2 (ошибка приложения, например неверная переменная, у нас 2), 127 (команда не найдена), 137 (убит, SIGKILL, часто из-за памяти). Исправление: вернуть корректные command и env в манифесте и применить его.
3. Pending (слишком большие requests). В describe: 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 Insufficient cpu (или Insufficient memory; узел control-plane отсекает taint, поэтому «не хватает» у двух рабочих). Планировщик (scheduler) не нашёл узел, где хватит запрошенных ресурсов; requests это гарантия при размещении, а не фактическое потребление. Исправление: уменьшить resources.requests до разумных (для «Заметок» 50m и 64Mi, подробнее в 5.7) и применить.
Общий вывод: не гадай по STATUS, читай Events и --previous.
После починки проверь kubectl -n notes rollout status deploy/notes. Проще всего вернуть исходное состояние скриптом (fix) или заново применить файл: kubectl apply -f k8s/base/10-deployment.yaml.
ИИ в помощь
Нейросеть хорошо объясняет статусы подов и ошибки в YAML, но не видит твой кластер и может предложить устаревший формат манифеста. Общие правила: ИИ-помощник.
Задача: разобрать статус пода и события.
Я учу Kubernetes. Под в статусе <вставь STATUS и RESTARTS из kubectl get pods>.
Вот хвост kubectl describe pod (блок Events): <вставь строки>.
Объясни, какой компонент (планировщик, kubelet, containerd) написал каждую строку, на каком шаге
цепочки apply остановка и что проверить. Дай список из трёх команд от самой дешёвой.
Проверь ответ: сверь с таблицей статусов из теории и колонкой From в событиях. Типичная ошибка: нейросеть советует «пересоздать кластер» или путает ImagePullBackOff (образ не скачан) с CrashLoopBackOff (процесс падает).
Задача: найти ошибку в манифесте Deployment.
Вот манифест Deployment, kubectl apply отвечает: <вставь текст ошибки>.
<вставь манифест>
Найди ошибку, объясни, что в YAML не так (отступ, селектор, метки шаблона), и покажи исправленный
фрагмент. Не меняй имена и образ.
Проверь ответ: примени исправление с kubectl apply --dry-run=server -f и проверь, что селектор совпадает с метками в template. Типичная ошибка: нейросеть переписывает манифест целиком и подставляет тег latest или другое имя образа.
Задача: составить манифест с нуля.
Напиши манифест Deployment: имя notes, namespace notes, 3 реплики, образ ghcr.io/<github-user>/notes:0.4.0,
порт 8080, том emptyDir в /data. Добавь рекомендуемые метки app.kubernetes.io и комментарии на русском.
Проверь ответ: сравни с k8s/base/10-deployment.yaml из задания 4 и убедись, что selector.matchLabels совпадает с метками в template, а тег не latest.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Под (Pod) | Обёртка вокруг одного или нескольких контейнеров: один узел, один IP, общие тома |
| Sidecar | Второй контейнер в поде, который работает рядом с основным (логи, прокси) |
| Init-контейнер | Контейнер, который отрабатывает и завершается до старта основных |
| Контроллер (controller) | Объект, который следит за состоянием и поддерживает желаемое |
| ReplicaSet | Контроллер, который держит заданное число одинаковых подов |
| Deployment | Контроллер над ReplicaSet: обновления и откаты |
| Реплика (replica) | Одна из одинаковых копий приложения (один под) |
| Шаблон пода (template) | Описание пода внутри Deployment: как выглядит каждая копия |
| Селектор (selector) | Условие по меткам: какие объекты «свои» |
| Метка (label) | Пара ключ: значение на объекте |
| Префикс ключа метки | Часть до / (app.kubernetes.io): не даёт ключам разных авторов столкнуться |
| Рекомендуемые метки | Общий договор об именах меток (app.kubernetes.io/name, instance, version, component, part-of, managed-by): их понимают Helm и другие инструменты |
app.kubernetes.io/name |
Метка с именем приложения; в курсе везде notes |
| Хэш в имени | Отпечаток шаблона пода; меняется, когда шаблон меняется |
ownerReferences |
Поле «мой владелец» у объекта |
Фаза (phase) |
Крупный этап жизни пода: Pending, Running и т.д. |
ImagePullBackOff |
Образ не скачивается, кластер повторяет попытки с паузой |
CrashLoopBackOff |
Контейнер стартует и падает, перезапуски с растущей паузой |
Terminating |
Под удаляется |
terminationGracePeriodSeconds |
Пауза (по умолчанию 30 с) между SIGTERM и SIGKILL |
emptyDir |
Пустая папка, которая живёт, пока жив под |
imagePullPolicy |
Правило: когда скачивать образ (IfNotPresent, Always, Never) |
| Переменная окружения (env) | Пара «имя = значение», которую программа читает при старте |
kubectl apply |
Привести объект к описанию в файле (повторяем сколько угодно) |
kubectl port-forward |
Туннель с порта твоей машины в порт пода |
| Идемпотентность | Повторный запуск не меняет результат |
| Stateless | Приложение без собственного состояния: любую копию можно заменить |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Чем отличаются Pod, ReplicaSet и Deployment?
Ответ
Под это один или несколько контейнеров с общим IP на одном узле, он одноразовый. ReplicaSet держит нужное число одинаковых подов по селектору. Deployment управляет ReplicaSet и умеет обновлять и откатывать. Руками я почти всегда создаю только Deployment.
Что хотят услышать: цепочка Deployment, ReplicaSet, Pod; поды заменяются, а не чинятся; обновления и откат через Deployment.
Красный флаг: «под и контейнер это одно и то же» или «ReplicaSet я создаю вручную».
2. [middle] [часто] Под в CrashLoopBackOff. Как разбираешься?
Ответ
Смотрю kubectl logs --previous, потому что текущий запуск ещё пуст. В describe читаю Exit Code и Reason: 1 или 2 это ошибка приложения, 127 нет команды, 137 SIGKILL (часто OOMKilled, нехватка памяти). Проверяю переменные окружения, монтирования, зависимости (БД недоступна). Пробы (проверки здоровья, урок 5.7) могут убивать здоровое, но медленное приложение.
Что хотят услышать: --previous, коды выхода, растущая пауза, отличие падения приложения от убийства пробой.
Красный флаг: «увеличу restartPolicy» или «удалю под».
3. [middle] [часто] Под висит в Pending. Что проверишь?
Ответ
describe pod, раздел Events: сообщение планировщика вроде 0/3 nodes are available: Insufficient memory, node(s) had untolerated taint (на узле стоит «запрет» для подов без специального разрешения), didn't match Pod's node affinity (под просил узел с определённым признаком), либо pod has unbound immediate PersistentVolumeClaims (не выдан диск). Далее describe nodes, раздел Allocated resources. Лечу requests, taints и tolerations, или добавляю узлы.
Что хотят услышать: планировщик решает по requests, а не по фактической нагрузке; taints, PVC.
Красный флаг: «Pending значит образ скачивается».
4. [middle] [на скорость] Я удалил под из Deployment. Что произойдёт и почему?
Ответ
ReplicaSet увидит, что подов меньше replicas, и создаст новый. У нового будет другое имя и IP. Старый перейдёт в Terminating: контейнеру пошлют SIGTERM, через terminationGracePeriodSeconds (по умолчанию 30 секунд) SIGKILL. Данные в emptyDir (временной папке пода) удалённого пода потеряются.
Что хотят услышать: reconcile-цикл, желаемое против фактического, grace period, эфемерность emptyDir.
Красный флаг: «под перезапустится сам с теми же данными и IP».
5. [middle] Под в статусе ImagePullBackOff. Твои действия?
Ответ
Начинаю с kubectl describe pod и читаю события: там точная ошибка. Проверяю тег и имя образа (опечатка), существует ли он в реестре, публичный ли, есть ли imagePullSecrets (пароль к реестру), доступна ли сеть у узла к реестру. Для kind проверяю, что образ загружен через kind load. Чиню set image или манифест.
Что хотят услышать: describe и Events, manifest unknown против unauthorized, imagePullSecrets, imagePullPolicy.
Красный флаг: «перезапущу под» без чтения событий.
6. [junior] Зачем метки и селекторы, и что будет, если селектор Deployment не совпадёт с метками шаблона?
Ответ
Метки это ключи-значения на объектах, селектор выбирает по ним поды. API не примет Deployment, у которого selector не совпадает с метками шаблона: ошибка валидации. Тем же способом Service (постоянный адрес группы подов) найдёт поды.
Что хотят услышать: matchLabels, неизменяемость селектора, связь со Service.
Красный флаг: «метки нужны только для красоты».
7. [junior] [на скорость] Чем kubectl apply отличается от kubectl create, и что делает diff?
Ответ
create создаёт и падает, если объект есть. apply декларативен и повторяем: приводит объект к файлу. kubectl diff показывает разницу до применения, --dry-run=server проверяет манифест на сервере без сохранения.
Что хотят услышать: декларативный против императивного подхода, идемпотентность, файлы из git.
Красный флаг: «всё равно, create быстрее».
8. [middle] Твой сервис использует тег latest в образе. Чем это плохо?
Ответ
Тег изменяем: сегодня и завтра под тем же именем разные образы. По умолчанию подразумевается imagePullPolicy: Always, и один узел может подтянуть новую версию, другой остаться на старой. Откат и разбор инцидента невоспроизводимы. Использую фиксированную версию и лучше digest (уникальный отпечаток образа, который нельзя перепривязать).
Что хотят услышать: воспроизводимость, разные версии на узлах, привязка к git-тегу, digest.
Красный флаг: «latest это всегда последняя стабильная версия».
9. [middle] Мы поставили replicas: 3, а пользователи жалуются, что заметки то пропадают, то появляются. Что происходит?
Ответ
Приложение хранит данные локально (файл или память), а не в общем хранилище. Запросы попадают на разные поды, у каждого свои данные, а при пересоздании пода они теряются. Реплики не сделали приложение отказоустойчивым: состояние надо вынести в БД или общий том. В нашем проекте это Postgres в 5.5 и 5.6.
Что хотят услышать: stateless против stateful, вынос состояния, emptyDir эфемерен.
Красный флаг: «увеличим реплики ещё».
10. [middle] [на скорость] Как посмотреть логи контейнера, который уже перезапустился и упал?
Ответ
kubectl logs <под> --previous покажет логи предыдущего запуска. Если в поде несколько контейнеров, добавляю -c <имя>. Если под уже пересоздан, логи потеряны: их нужно собирать централизованно (тема 8).
Что хотят услышать: --previous, -c, ограниченное время жизни логов на узле, централизованное логирование.
Красный флаг: «зайду в под через exec и посмотрю файл».
11. [middle] Как Deployment выкатывает новую версию и как откатить неудачную?
Ответ
По умолчанию стратегия RollingUpdate: Deployment создаёт новый ReplicaSet и постепенно переключает поды, параметры maxSurge и maxUnavailable (по умолчанию 25%) определяют, сколько подов можно добавить сверх и сколько временно потерять. Слежу за выкаткой: kubectl rollout status deploy/notes. Историю вижу через kubectl rollout history deploy/notes, откатываю kubectl rollout undo deploy/notes. При этом откат меняет кластер, а не git, поэтому манифест в git надо поправить тоже.
Что хотят услышать: RollingUpdate, maxSurge/maxUnavailable, rollout status/undo, git как источник правды.
Красный флаг: Удалять поды руками, чтобы «обновились».
12. [middle] Зачем в одном поде несколько контейнеров и что такое init-контейнер?
Ответ
Контейнеры одного пода делят сетевое пространство (общий localhost) и могут делить тома. Поэтому рядом с основным кладут sidecar-помощников: сбор логов, прокси. Обычно в поде один основной контейнер, лишнее не добавляю. Обычные init-контейнеры выполняются последовательно до основных и должны завершиться успешно, иначе под не стартует (нативный sidecar - это init-контейнер с restartPolicy: Always, он продолжает работать вместе с основными). Использую их для ожидания зависимости или подготовки данных. Масштабируется под целиком, а не отдельный контейнер.
Что хотят услышать: общий localhost и тома, sidecar, init выполняется до основных, масштабируется под целиком.
Красный флаг: Класть приложение и базу в один под.
13. [middle] Чем kubectl scale отличается от правки replicas в манифесте?
Ответ
kubectl scale deploy notes --replicas=5 меняет живой объект сразу, но в git остаётся старое число. Следующий kubectl apply манифеста вернёт прежнее значение, и изменение потеряется. Для временной реакции на нагрузку scale годится, но стабильное число правлю в манифесте и применяю через apply или CI. Если количество подов регулирует HPA, поле replicas из манифеста лучше убрать, чтобы apply не боролся с автоскейлером.
Что хотят услышать: императивная правка против декларативной, источник правды в git, HPA и replicas.
Красный флаг: Масштабировать только командой и забыть про манифест.
14. [junior] [часто] Зачем запускать несколько реплик приложения?
Ответ
Три причины. Отказоустойчивость: если под или узел упал, остальные продолжают отвечать, пока Kubernetes поднимает замену. Выкатки без простоя: RollingUpdate заменяет поды по одному. Масштаб: нагрузка делится между репликами через Service. Условия: приложение не хранит состояние в памяти и локальных файлах, реплики стоят на разных узлах (podAntiAffinity или topologySpreadConstraints), а PodDisruptionBudget не даёт выключить все сразу. Две реплики на одном узле при его падении пропадают обе.
Что хотят услышать: отказоустойчивость, выкатка, масштаб; условие stateless и расстановка по узлам.
Красный флаг: «реплики сами сделают сервис отказоустойчивым» без вопроса о состоянии и узлах.
Проверено на версиях
- Ответы приложения (
/healthz,POSTиGET /notes, строкаstarted, ошибка приPORT=abcс кодом выхода 2, ответ 400 на тело не в JSON) получены запускомproject/notes/versions/v4.py(STORE=file) на Mac автора; Python там 3.9.6, в образе будет 3.13. Формат выводаkubectlвзят из текущей редакции урока и знания версий, на кластере не прогонялся. - Манифест
10-deployment.yaml(обе редакции) и испорченные копии изbreak.shпровереныkubeconform -strict; самbreak.shпроверенshellcheckи прогоном с подменённымkubectl, в живом кластере не запускался. - Команда
{ wc -l < файл; } 2>/dev/null || echo 0проверена в контейнереalpine:3.22. - kind: v0.33.0
- kubectl: 1.37.1
- Kubernetes (образ узла
kindest/nodeиз 5.1): 1.37.x - Образ приложения:
ghcr.io/<github-user>/notes:0.4.0(app v4) - Python в образе: 3.13 (
python:3.13-slim) - busybox: 1.37 (в примере пода с двумя контейнерами, не применялся)
Итог урока: ты умеешь
- умею описать Deployment манифестом и применить его через
kubectl apply - умею объяснить цепочку Deployment, ReplicaSet, Pod и роль меток и селекторов
- умею смотреть логи, заходить в контейнер и открывать доступ через
port-forward - умею показать самовосстановление: удалить под и масштабировать через
scale - умею объяснить, что происходит с подом при удалении (SIGTERM, 30 секунд, SIGKILL)
- умею отличить
ImagePullBackOff,CrashLoopBackOffиPendingпо событиям и--previous - умею загрузить локальный образ в kind и объяснить
imagePullPolicy - умею объяснить, почему тег
latestне используем, аemptyDirэфемерен
Дальше: Урок 5.3: Service и DNS кластера
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.