Главная/Docker и Kubernetes/Практика

Раздел 18. Практические задания

Кластер Kubernetes для заданий не требуется. Все задания либо аналитические, либо проверяют переносимость того, что у вас уже есть.

Это не упрощение. Главный вывод раздела — что готовность к Kubernetes определяется устройством образа и приложения, а не наличием манифестов (урок 18.3: около половины работы приходится на код). Проверить это можно без кластера.

Там, где кластер всё же нужен — применение манифестов, действие NetworkPolicy, поведение планировщика, — задание требует отметить шаг невыполненным, а не изобразить результат. Правило то же, что в уроке 16.4: «не проверено» должно отличаться от «проверено и работает».

Как проверять себя

ФормулировкаЧто требуется
«сопоставить»Таблица, где для каждой строки указан способ переноса
«проверить»Команда и её фактический вывод, а не суждение
«обосновать»Причина, из которой следует вывод, а не описание вывода
«отметить»Явное указание, что шаг не выполнялся, и почему
«оценить»Число с указанием, как оно получено

Задание 1. Таблица соответствий для своего стека

Тип: обязательное
Материал: 18.3

Взять собственный compose.yaml (или стек из урока 9.7) и сопоставить каждую конструкцию с объектами Kubernetes.

Требования:

  1. Разобрать compose.yaml программой, а не глазами: перечислить фактически использованные конструкции.
  2. Для каждой указать способ переноса: прямой, требует переосмысления, не переносится.
  3. Для «не переносится» назвать причину, а не только факт.
  4. Определить для каждого сервиса объект: Deployment или StatefulSet — по признаку, применимому к любому стеку.
  5. Посчитать, сколько объектов Kubernetes получится из одного compose.yaml.

Проверка

bash
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 указано, что аналога нет, и объяснено почему.

Ориентир: признак состояния
python
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.

Требования:

  1. Составить список требований, каждое из которых проверяется командой.
  2. Проверить: не root, USER числовой, работает при readOnlyRootFilesystem, корректный ENTRYPOINT, реакция на SIGTERM, конфигурация из окружения, отсутствие записи в корневую ФС.
  3. Для каждого требования показать команду и её вывод.
  4. Отделить проверенное от непроверяемого без кластера.
  5. Вывести итог: сколько требований выполнено, сколько нарушено, сколько не проверялось.

Проверка

bash
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.

Проверка:

bash
docker image inspect IMAGE --format '{{.Config.User}}' | grep -qE '^[0-9]+(:[0-9]+)?$' \
    && echo "числовой — подходит" || echo "именованный — pod не стартует"

Задание 3. Что изменится в конфигурации, а что останется

Тип: обязательное
Материал: 18.2, 18.3

Разобрать конфигурацию приложения и разделить её на переносимую без изменений и требующую переработки.

Требования:

  1. Перечислить все источники конфигурации приложения: переменные, файлы, аргументы.
  2. Для каждого указать, что с ним произойдёт при переходе.
  3. Отдельно выделить то, что уже готово благодаря правилам курса.
  4. Отдельно выделить то, что потребует изменений в коде, а не в манифестах.
  5. Оценить долю работы, приходящуюся на код.

Критерий

Список «уже готово» не пустой: конфигурация через окружение, non-root, graceful shutdown, отсутствие состояния в контейнере переносятся напрямую.

Доля работы в коде посчитана, а не оценена на глаз. Ожидаемый порядок — около половины (урок 18.3).

Ориентир: что переносится без изменений
ЧтоПочему готово
Конфигурация из переменных окруженияConfigMap подставляет их так же
Секреты из файловSecret монтируется файлом
Non-root с числовым UIDrunAsNonRoot проверяет UID
Логи в stdoutKubernetes собирает так же
Обработка SIGTERMТот же сигнал при удалении pod'а
Отсутствие состояния в контейнереPod удаляется в любой момент
ENTRYPOINT в exec-формеPID 1 получает сигналы

Всё это — правила раздела 11. Они формулировались не ради Kubernetes, но переносятся в него полностью.


Задание 4. Разделение healthcheck на пробы

Тип: дополнительное
Материал: 18.2

Взять один healthcheck из своего compose.yaml и разделить его на три пробы с разной логикой.

Требования:

  1. Написать три эндпоинта: /startupz, /healthz, /readyz.
  2. /healthz не должен проверять зависимости; /readyz должен.
  3. Показать поведение при недоступной базе: какая проба падает, какая нет.
  4. Объяснить, что произойдёт, если использовать один путь для liveness и readiness.
  5. Показать, что startup-проба снимает необходимость в большом initialDelaySeconds.

Проверка

bash
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. Если оба отвечают одинаково, разделения нет.

Объяснение последствия должно быть конкретным: при общем пути отказ базы вызывает перезапуск всех экземпляров одновременно, и после перезапуска база всё ещё недоступна.

Ориентир: разделение логики
python
@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

Заполнить чек-лист для конкретного проекта и сформулировать вывод.

Требования:

  1. Использовать веса, в которых отрицательные признаки способны перевесить положительные.
  2. Для каждого отвеченного «да» указать, чем подтверждается.
  3. Получить вывод и код возврата.
  4. Если вывод «промежуточный вариант» — назвать конкретный вариант и что он не решает.
  5. Оценить стоимость: разовую и постоянную отдельно.
  6. Проверить чек-лист на подделке: проект, где положительных признаков много, но эксплуатировать некому.

Критерий

Столбец «чем подтверждается» заполнен фактами, а не намерениями: «есть человек с опытом» подтверждается именем, а не планом нанять.

Проверка на подделке даёт вывод «Compose достаточно» несмотря на положительные признаки. Если подделка проходит — веса подобраны неверно.

Ориентир: почему отрицательные веса должны быть большими

Если сильнейший отрицательный признак слабее сильнейшего положительного, отрицательные не влияют на исход: их всегда перекроют ожидания роста.

Проверка устройства инструмента, а не его вывода:

python
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.

Требования:

  1. Перечислить виды состояния и для каждого назвать конкретный отказ.
  2. Показать отказ воспроизводимо, не рассуждением: запустить несколько экземпляров и продемонстрировать расхождение.
  3. Объяснить, почему ReadWriteOnce не решает задачу, а лишь меняет симптом.
  4. Для каждого вида состояния предложить, куда его вынести.
  5. Показать, что часть проблем видна уже в Compose при --scale, а часть — только в кластере.
  6. Отметить непроверяемое без кластера.

Проверка

bash
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 →
Главное оглавление

Markdown на GitHub ↗