Раздел 18. Практические задания
Кластер Kubernetes для заданий не требуется. Все задания либо аналитические, либо проверяют переносимость того, что у вас уже есть.
Это не упрощение. Главный вывод раздела — что готовность к Kubernetes определяется устройством образа и приложения, а не наличием манифестов (урок 18.3: около половины работы приходится на код). Проверить это можно без кластера.
Там, где кластер всё же нужен — применение манифестов, действие NetworkPolicy, поведение планировщика, — задание требует отметить шаг невыполненным, а не изобразить результат. Правило то же, что в уроке 16.4: «не проверено» должно отличаться от «проверено и работает».
Как проверять себя
| Формулировка | Что требуется |
|---|---|
| «сопоставить» | Таблица, где для каждой строки указан способ переноса |
| «проверить» | Команда и её фактический вывод, а не суждение |
| «обосновать» | Причина, из которой следует вывод, а не описание вывода |
| «отметить» | Явное указание, что шаг не выполнялся, и почему |
| «оценить» | Число с указанием, как оно получено |
Задание 1. Таблица соответствий для своего стека
Тип: обязательное
Материал: 18.3
Взять собственный compose.yaml (или стек из урока 9.7) и сопоставить каждую конструкцию с объектами Kubernetes.
Требования:
- Разобрать
compose.yamlпрограммой, а не глазами: перечислить фактически использованные конструкции. - Для каждой указать способ переноса: прямой, требует переосмысления, не переносится.
- Для «не переносится» назвать причину, а не только факт.
- Определить для каждого сервиса объект: Deployment или StatefulSet — по признаку, применимому к любому стеку.
- Посчитать, сколько объектов Kubernetes получится из одного
compose.yaml.
Проверка
python3 mapping.py compose.yaml
python3 -c "
import yaml
c = yaml.safe_load(open('compose.yaml'))
print('сервисов:', len(c['services']))
print('с томами:', [n for n, s in c['services'].items() if s.get('volumes')])"
Критерий
Признак состояния определяется по устройству тома, а не по имени образа — иначе правило не работает на чужом стеке. Для depends_on, networks и profiles указано, что аналога нет, и объяснено почему.
Ориентир: признак состояния
def is_stateful(svc: dict) -> bool:
"""Именованный том — состояние. Bind mount — нет."""
for v in svc.get("volumes") or []:
source = v if isinstance(v, str) else v.get("source", "")
if source and not source.startswith((".", "/")):
return True
return False
Проверка «это postgres или redis» работала бы на вашем примере и не работала бы на чужом.
Задание 2. Готовность образа к Kubernetes
Тип: обязательное
Материал: 18.1, Раздел 11
Проверить образ из проекта 2 на пригодность к запуску в Kubernetes.
Требования:
- Составить список требований, каждое из которых проверяется командой.
- Проверить: не
root,USERчисловой, работает приreadOnlyRootFilesystem, корректныйENTRYPOINT, реакция наSIGTERM, конфигурация из окружения, отсутствие записи в корневую ФС. - Для каждого требования показать команду и её вывод.
- Отделить проверенное от непроверяемого без кластера.
- Вывести итог: сколько требований выполнено, сколько нарушено, сколько не проверялось.
Проверка
docker image inspect IMAGE --format '{{.Config.User}}'
docker run --rm --read-only --tmpfs /tmp IMAGE python -c "print('ok')"
docker run -d --name t IMAGE; docker stop -t 30 t; docker inspect t --format '{{.State.ExitCode}}'
Критерий
Каждое требование подтверждено выводом команды. Числовой USER отличается от именованного: Kubernetes проверяет runAsNonRoot по идентификатору, и имя пользователя ему неизвестно.
Итог содержит три числа, а не два: «не проверялось» — отдельная категория.
Ориентир: почему `USER app` недостаточно
runAsNonRoot: true требует, чтобы kubelet мог убедиться, что UID не равен нулю. Если в образе указано имя, а не число, kubelet не разрешает имя в UID и pod не стартует с ошибкой container has runAsNonRoot and image has non-numeric user.
Проверка:
docker image inspect IMAGE --format '{{.Config.User}}' | grep -qE '^[0-9]+(:[0-9]+)?$' \
&& echo "числовой — подходит" || echo "именованный — pod не стартует"
Задание 3. Что изменится в конфигурации, а что останется
Тип: обязательное
Материал: 18.2, 18.3
Разобрать конфигурацию приложения и разделить её на переносимую без изменений и требующую переработки.
Требования:
- Перечислить все источники конфигурации приложения: переменные, файлы, аргументы.
- Для каждого указать, что с ним произойдёт при переходе.
- Отдельно выделить то, что уже готово благодаря правилам курса.
- Отдельно выделить то, что потребует изменений в коде, а не в манифестах.
- Оценить долю работы, приходящуюся на код.
Критерий
Список «уже готово» не пустой: конфигурация через окружение, non-root, graceful shutdown, отсутствие состояния в контейнере переносятся напрямую.
Доля работы в коде посчитана, а не оценена на глаз. Ожидаемый порядок — около половины (урок 18.3).
Ориентир: что переносится без изменений
| Что | Почему готово |
|---|---|
| Конфигурация из переменных окружения | ConfigMap подставляет их так же |
| Секреты из файлов | Secret монтируется файлом |
| Non-root с числовым UID | runAsNonRoot проверяет UID |
Логи в stdout | Kubernetes собирает так же |
Обработка SIGTERM | Тот же сигнал при удалении pod'а |
| Отсутствие состояния в контейнере | Pod удаляется в любой момент |
ENTRYPOINT в exec-форме | PID 1 получает сигналы |
Всё это — правила раздела 11. Они формулировались не ради Kubernetes, но переносятся в него полностью.
Задание 4. Разделение healthcheck на пробы
Тип: дополнительное
Материал: 18.2
Взять один healthcheck из своего compose.yaml и разделить его на три пробы с разной логикой.
Требования:
- Написать три эндпоинта:
/startupz,/healthz,/readyz. /healthzне должен проверять зависимости;/readyzдолжен.- Показать поведение при недоступной базе: какая проба падает, какая нет.
- Объяснить, что произойдёт, если использовать один путь для liveness и readiness.
- Показать, что startup-проба снимает необходимость в большом
initialDelaySeconds.
Проверка
curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/healthz
curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/readyz
docker compose stop db && curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/healthz
Критерий
При остановленной базе /healthz отвечает 200, /readyz — 503. Если оба отвечают одинаково, разделения нет.
Объяснение последствия должно быть конкретным: при общем пути отказ базы вызывает перезапуск всех экземпляров одновременно, и после перезапуска база всё ещё недоступна.
Ориентир: разделение логики
@app.get("/healthz")
async def healthz() -> dict[str, str]:
"""Только процесс. Зависимости НЕ проверяются намеренно."""
return {"status": "alive"}
@app.get("/readyz")
async def readyz(response: Response) -> dict[str, object]:
"""Готовность обслуживать: зависимости, прогрев, очередь."""
checks = {"db": await check_db(), "cache": await check_cache()}
if not all(checks.values()):
response.status_code = 503
return {"ready": all(checks.values()), "checks": checks}
Разница в одной строке кода и очень большая в поведении под отказом.
Задание 5. Чек-лист принятия решения
Тип: дополнительное
Материал: 18.4
Заполнить чек-лист для конкретного проекта и сформулировать вывод.
Требования:
- Использовать веса, в которых отрицательные признаки способны перевесить положительные.
- Для каждого отвеченного «да» указать, чем подтверждается.
- Получить вывод и код возврата.
- Если вывод «промежуточный вариант» — назвать конкретный вариант и что он не решает.
- Оценить стоимость: разовую и постоянную отдельно.
- Проверить чек-лист на подделке: проект, где положительных признаков много, но эксплуатировать некому.
Критерий
Столбец «чем подтверждается» заполнен фактами, а не намерениями: «есть человек с опытом» подтверждается именем, а не планом нанять.
Проверка на подделке даёт вывод «Compose достаточно» несмотря на положительные признаки. Если подделка проходит — веса подобраны неверно.
Ориентир: почему отрицательные веса должны быть большими
Если сильнейший отрицательный признак слабее сильнейшего положительного, отрицательные не влияют на исход: их всегда перекроют ожидания роста.
Проверка устройства инструмента, а не его вывода:
plus = [w for _, _, w, _ in QUESTIONS if w > 0]
minus = [w for _, _, w, _ in QUESTIONS if w < 0]
assert abs(min(minus)) >= max(plus), "отрицательные признаки не способны перевесить"
Это проверка чек-листа, а не проекта. Её стоит написать до того, как заполнять ответы, — иначе веса подгонятся под желаемый исход.
Задание 6. Сервис с состоянием в локальном каталоге ★
Тип: повышенной сложности
Материал: весь раздел
Дан сервис, хранящий состояние в локальном каталоге контейнера: загруженные файлы, кэш обработки, сессии пользователей, счётчик в файле.
Объяснить и показать, что именно сломается при масштабировании в Kubernetes.
Требования:
- Перечислить виды состояния и для каждого назвать конкретный отказ.
- Показать отказ воспроизводимо, не рассуждением: запустить несколько экземпляров и продемонстрировать расхождение.
- Объяснить, почему
ReadWriteOnceне решает задачу, а лишь меняет симптом. - Для каждого вида состояния предложить, куда его вынести.
- Показать, что часть проблем видна уже в Compose при
--scale, а часть — только в кластере. - Отметить непроверяемое без кластера.
Проверка
docker compose up -d --scale app=3
for i in $(seq 6); do curl -s localhost:8080/counter; done
docker compose exec --index 1 app cat /data/counter
docker compose exec --index 2 app cat /data/counter
Критерий
Расхождение показано числами: три экземпляра дали три разных значения счётчика вместо одного общего.
Про ReadWriteOnce сказано точно: том подключается к одному узлу, поэтому второй pod либо не стартует (Multi-Attach error), либо оказывается на том же узле — и тогда два процесса пишут в один файл без блокировок. Ни один из исходов не является решением.
Отмечено, что поведение планировщика и Multi-Attach error без кластера не воспроизводились.
Разбор
Четыре вида состояния и четыре разных отказа:
| Состояние | Отказ при масштабировании | Куда выносить |
|---|---|---|
| Счётчик в файле | Каждый экземпляр считает своё | Redis, база |
| Загруженные файлы | Файл доступен только через тот экземпляр, что его принял | Объектное хранилище |
| Кэш обработки | Кэш не разделяется; работа выполняется трижды | Redis, memcached |
| Сессии | Пользователь «разлогинивается» при попадании на другой экземпляр | Хранилище сессий или токены без состояния |
Существенное различие между двумя первыми: счётчик даёт неверный результат, а файлы — нестабильный. Второе хуже: ошибка воспроизводится через раз и выглядит как случайная.
Почему ReadWriteOnce не решение. Режим означает, что том подключается к одному узлу. Возможны два исхода:
- pod'ы попали на разные узлы — второй не стартует. Сообщение зависит от хранилища: у узлового (local-path, hostPath) это
FailedScheduling: didn't match PersistentVolume's node affinityещё на этапе планирования; у сетевого (EBS, Cinder) —Multi-Attach errorпри подключении. Масштабирования нет в обоих случаях; - pod'ы попали на один узел — том подключился один раз, но два процесса пишут в один файл. Без блокировок это повреждение данных.
Первый исход — отказ, заметный сразу. Второй — тихий, и он опаснее. Выбор между ними делает планировщик, то есть исход недетерминирован.
ReadWriteMany доступен не во всех хранилищах и не отменяет необходимости в блокировках.
Что видно уже в Compose. Расхождение счётчика воспроизводится через --scale на одной машине: каждый контейнер получает свою копию каталога, если том не общий. Это же показывает, что проблема не в Kubernetes — он лишь делает её неизбежной.
Что без кластера не проверить:
| Проверка | Причина |
|---|---|
Multi-Attach error | Требует сетевого хранилища; на узловом отказ наступает раньше, при планировании |
| Распределение pod'ов планировщиком | Требует кластера |
Поведение ReadWriteMany | Зависит от класса хранилища |
Практический вывод раздела: состояние в локальном каталоге — главное препятствие для масштабирования, и оно находится в коде, а не в конфигурации. Ни один манифест его не устранит.
Сводная таблица
| № | Задание | Тип | Основной материал |
|---|---|---|---|
| 1 | Таблица соответствий для своего стека | обяз. | 18.3 |
| 2 | Готовность образа к Kubernetes | обяз. | 18.1 |
| 3 | Что изменится в конфигурации | обяз. | 18.2 |
| 4 | Разделение healthcheck на пробы | доп. | 18.2 |
| 5 | Чек-лист принятия решения | доп. | 18.4 |
| 6 | Состояние в локальном каталоге | ★ | весь раздел |
Что должно получиться
После заданий 1–3 у вас есть ответ на вопрос «что нам придётся сделать», выраженный в списке работ, а не в ощущении.
Задание 2 обычно даёт неожиданный результат: образ, собранный по правилам раздела 11, проходит проверку почти полностью. Это и есть главное практическое утверждение раздела — курс готовит к Kubernetes, не будучи курсом по нему.
Задание 4 переносит в код различение, которое до сих пор было теоретическим: liveness и readiness отвечают на разные вопросы, и одинаковый ответ на оба — ошибка.
Задание 5 даёт инструмент для разговора, в котором обычно побеждает не аргумент, а настойчивость.
Задание 6 показывает границу: состояние в локальном каталоге не устраняется манифестами. Это работа в приложении, и она нужна независимо от того, будет кластер или нет.
Что этот раздел меняет в работе
До него Kubernetes был отдельной темой, к которой «надо будет когда-нибудь подступиться». После — понятно, что подступаться нужно не к нему, а к собственному приложению: убрать состояние из контейнера, разделить пробы, вынести конфигурацию, научиться переживать отказ зависимости.
Всё перечисленное полезно и без кластера. Это и есть проверка искренности требования: если работа полезна только вместе с Kubernetes, она, скорее всего, не нужна.
Обратное тоже верно и стоит того, чтобы быть сказанным прямо: вывод «Compose достаточно» — полноценный результат раздела, а не признак неготовности.
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий раздел: Ограничения Docker →
Главное оглавление