Раздел 16. Практические задания
Задания раздела строятся вокруг двух правил, сквозных для всего курса:
- измерять, а не оценивать — время, доля попаданий кэша, число блокирующих находок;
- проверять проверку — механизм, не испытанный на случае, где он должен сработать отрицательно, обычно не работает.
Отдельно: платформы CI из этого окружения недоступны. Задания построены так, чтобы бо́льшая часть выполнялась локально теми же скриптами, что вызывает pipeline.
Как проверять себя
| Формулировка | Что требуется |
|---|---|
| «измерить» | Число, полученное командой |
| «доказать» | Контрольный случай с противоположным результатом |
| «обосновать» | Расчёт или механизм, а не предпочтение |
| «отметить» | Явное указание, что именно не выполнялось |
Задание 1. Workflow с линтером и тестами
Тип: обязательное
Материал: 16.2
Написать workflow, запускающий линтер и тесты на каждый push.
Требования:
- Быстрые проверки — параллельно, с
fail-fast: false. - Unit-тесты на матрице из двух версий Python.
- Объявить
permissionsна уровне workflow по минимуму. - Добавить
concurrency, отменяющую предыдущий запуск той же ветки. - Вынести логику проверок в скрипты
ci/*.shи запустить их локально. - Объяснить, почему
fail-fast: falseнужен и что происходит без него.
Проверка
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yaml'))" && echo валиден
./ci/lint.sh; echo "код: $?"
./ci/test.sh; echo "код: $?"
Критерий
Скрипты выполняются локально и дают те же коды, что в workflow. Объяснено: без fail-fast: false отказ одного варианта матрицы отменяет остальные, и вы не узнаете, на каких версиях проблема.
Задание 2. Сборка и публикация по commit SHA
Тип: обязательное
Материал: 16.2, 14.4
Добавить сборку образа и публикацию по тегу с commit SHA.
Требования:
- Собрать образ один раз с
load: trueи проверить его. - Опубликовать из того же кэша; доказать, что публикуется проверенный артефакт.
- Присвоить неизменяемый тег
git-<sha>и подвижные указатели. - Объявить
packages: writeна уровне задачи, а не workflow. - Показать, что публикация идемпотентна: повтор на том же коммите даёт тот же digest.
- Объяснить, почему схема «собрать для тестов, собрать для публикации» неверна.
Проверка
docker build --target production -t a . && docker build --target production -t b .
docker inspect a --format '{{.Id}}'; docker inspect b --format '{{.Id}}'
docker inspect ОБРАЗ --format '{{index .RepoDigests 0}}'
Критерий
Id двух сборок совпадают. Объяснено: между двумя независимыми сборками может обновиться базовый образ или подтянуться другая зависимость — публикуется не то, что проверяли.
Ориентир для самопроверки
Признак правильной схемы в workflow: два вызова build-push-action, у первого load: true, push: false, у второго push: true, и у обоих один и тот же cache-from.
Вторая сборка при этом занимает секунды: она экспортирует уже готовый результат.
Задание 3. Build cache: настроить и измерить
Тип: обязательное
Материал: 16.3
Настроить кэш и измерить время сборки до и после.
Требования:
- Измерить холодную сборку и сборку на чистом сборщике без внешнего кэша.
- Подтвердить отсутствие кэша числом строк
CACHED, а не только временем. - Настроить внешний кэш и измерить сокращение времени.
- Показать разницу
mode=minиmode=maxпо факту выполненияpip install. - Показать, что
RUN --mount=type=cacheв CI не помогает. - Оценить окупаемость: сравнить выигрыш со временем передачи кэша.
Проверка
docker buildx rm t; docker buildx create --name t --use --driver docker-container
docker buildx build --progress=plain . 2>&1 | grep -c CACHED
docker buildx build --progress=plain --cache-from type=registry,ref=... . 2>&1 | grep -c CACHED
Критерий
На чистом сборщике CACHED равно нулю. С внешним кэшем — больше нуля. Разница mode=min и mode=max подтверждена наличием строк Downloading в выводе, а не только временем.
Ориентир для самопроверки
Сборщик нужно пересоздавать перед каждым измерением — иначе второй замер получает кэш от первого, и весь эксперимент показывает не то, что заявлено.
Признак правильного измерения: первая строка результата — CACHED 0.
Задание 4. Integration tests на собранном образе
Тип: обязательное
Материал: 16.2, 15.5
Запустить integration tests на собранном образе внутри pipeline.
Требования:
- Запускать образ как есть, без подмены
--entrypoint. - Проверить: отвечает на запрос, работает не от root, завершается по
SIGTERM. - Показать, что подмена точки входа отменяет проверяемое.
- Вынести проверки в скрипт и запустить его локально на том же образе.
- Показать, что проверки на непроверенном образе дают ненулевой код.
Проверка
./ci/integration-tests.sh ОБРАЗ; echo "код: $?"
docker run --rm --entrypoint sh ОБРАЗ -c 'echo это ничего не проверяет'
time docker stop КОНТЕЙНЕР
Критерий
Скрипт даёт код 0 на правильном образе и ненулевой на сломанном. docker stop укладывается в две секунды — иначе SIGTERM не обрабатывается.
Задание 5. Секреты не попадают в логи
Тип: обязательное
Материал: 16.2, 14.2
Убедиться, что секреты не появляются в логах ни на одном шаге.
Требования:
- Передавать токен через переменную окружения и
--password-stdin. - Показать четыре способа обойти маскирование секретов.
- Найти в своём workflow все места, где секрет мог бы попасть в вывод.
- Добавить
docker logoutсif: always(). - Написать проверку workflow, находящую секрет в аргументе команды.
- Проверить эту проверку на файле с намеренным нарушением.
Проверка
grep -n 'password\|secrets\.' .github/workflows/ci.yaml
python3 ci/lint-workflow.py .github/workflows/ci.yaml bad.yaml
Критерий
Проверка даёт ноль критичных на правильном файле и не менее двух на плохом. Четыре способа обхода маскирования показаны с фактическим результатом.
Ориентир: способы обхода
Маскирование ищет точное совпадение строки. Обходят его:
| Действие | Почему видно |
|---|---|
base64 | Другая строка |
Подстрока ${SECRET:0:10} | Не совпадает целиком |
Разворот rev | То же |
Смена регистра tr a-z A-Z | То же |
| Разбиение переносом строки | Совпадения нет ни в одной строке |
Вывод: маскирование — последний рубеж, а не защита. Секрет не должен попадать в вывод вовсе.
Задание 6. Сканирование с осмысленной блокировкой
Тип: дополнительное
Материал: 16.4
Добавить сканирование и настроить условие блокировки по уровню критичности.
Требования:
- Блокировать только находки с доступным исправлением.
- Измерить: сколько находок блокировало бы без
--ignore-unfixedи сколько с ним. - Сохранять полный отчёт как артефакт, не блокирующий сборку.
- Реализовать три кода возврата: чисто, есть находки, не проверено.
- Показать, что результат сканирования меняется со временем без изменения образа.
- Написать проверку файла исключений: даты, обоснования, просроченные записи.
Проверка
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 ОБРАЗ; echo "код: $?"
./ci/scan.sh ОБРАЗ; echo "код: $?"
python3 ci/check-ignores.py .trivyignore; echo "код: $?"
Критерий
Три кода возврата различимы. Проверка исключений находит просроченную запись и запись без даты, а на чистом файле даёт ноль.
Ориентир: почему нужен третий код
Без него недоступный сканер неотличим от чистого образа: оба дают код 0, и pipeline зеленеет по причине отсутствия инструмента.
0 — проверено, чисто
1 — проверено, есть блокирующие находки
3 — НЕ ПРОВЕРЕНО: инструмент недоступен
Это применимо к любому шагу pipeline, а не только к сканированию.
Задание 7. SBOM как артефакт сборки
Тип: дополнительное
Материал: 16.4, 11.6
Сгенерировать SBOM и сохранить его как артефакт сборки.
Требования:
- Включить генерацию SBOM и provenance при публикации.
- Проверить наличие attestation у опубликованного образа.
- Объяснить, почему у локально собранного образа их нет.
- Показать случаи, где SBOM из сборки полнее восстановленного сканером.
- Сохранить SBOM отдельным файлом-артефактом и объяснить, когда это нужно.
Проверка
docker buildx imagetools inspect ОБРАЗ --format '{{json .SBOM}}' | head -c 300
docker buildx imagetools inspect ОБРАЗ --format '{{json .Provenance}}' | head -c 300
syft ОБРАЗ -o spdx-json | head -20
Критерий
Attestation присутствует у опубликованного образа и отсутствует у локального. Объяснено: attestations живут в registry и теряются при --load.
Задание 8. Публикация только с main
Тип: дополнительное
Материал: 16.1, 16.2
Настроить публикацию только для ветки main, оставив сборку для всех веток.
Требования:
- Сборка и проверки — на всех ветках и в pull request.
- Публикация — только при push в ветку по умолчанию и при теге версии.
- Integration-тесты — на pull request и
main, но не на каждом push в ветку. - Сканирование по расписанию для уже опубликованных образов.
- Обосновать каждое условие: что экономится и что теряется.
- Показать, что pull request из форка не получает доступ к секретам.
Проверка
python3 -c "
import yaml
wf = yaml.safe_load(open('.github/workflows/ci.yaml'))
for name, job in wf['jobs'].items():
print(name, job.get('if', '—'))
"
Критерий
Для каждой задачи условие запуска объявлено явно и обосновано. Сканирование по расписанию присутствует с объяснением, зачем оно нужно при неизменном коде.
Задание 9. Диагностика: сборка 8 минут при неизменных зависимостях
Тип: диагностическое
Материал: 16.3
Pipeline собирает образ 8 минут при неизменных зависимостях. Найти причину.
Требования:
- Назвать шесть механизмов, дающих такой результат.
- Для каждого предложить проверку, подтверждающую или опровергающую его.
- Воспроизвести хотя бы два механизма.
- Определить, какой из них наиболее вероятен, и обосновать.
- Исправить и измерить результат.
- Объяснить, почему «кэш настроен» не означает «кэш работает».
Проверка
docker buildx build --progress=plain . 2>&1 | grep -E 'CACHED|Downloading' | head -20
grep -n 'cache-from\|cache-to\|mode=' .github/workflows/ci.yaml
grep -n 'COPY' Dockerfile
Критерий
Названы шесть механизмов с проверкой для каждого. Два воспроизведены. Исправление подтверждено измерением, а не заявлено.
Ориентир: шесть механизмов
| Механизм | Проверка |
|---|---|
mode=min для multi-stage: стадия с зависимостями не сохраняется | Есть ли Downloading в выводе при неизменном requirements.txt |
cache-from не указан или указан не тот ref | Число строк CACHED равно нулю |
COPY . . перед установкой зависимостей: любое изменение обесценивает слой | Порядок инструкций в Dockerfile |
Расчёт на --mount=type=cache: он в CI не восстанавливается | Есть ли Downloading при настроенном внешнем кэше |
Кэш пишется только на main, ветка читает устаревший | Где выполняется шаг cache-to |
Общий scope для всех веток: ветки затирают кэш друг друга | Значение scope в cache-to |
Наиболее вероятен первый: mode=min — значение по умолчанию, и его пропускают чаще прочего.
«Кэш настроен» означает, что в файле есть строки cache-from и cache-to. «Кэш работает» означает, что в выводе сборки есть строки CACHED и нет Downloading. Это разные утверждения, и второе проверяется командой.
Задание 10. Перенос в GitLab CI ★
Тип: итоговое
Материал: 16.5, весь раздел
Перенести тот же pipeline в GitLab CI и объяснить, что пришлось изменить.
Требования:
- Написать
.gitlab-ci.yml, эквивалентный workflow. - Объявить
needsявно и объяснить, что даёт отказ от порядка стадий. - Правильно разделить
cacheиartifacts; привести пример неверного использования с симптомом. - Выбрать способ сборки образов и обосновать выбор через риск, а не удобство.
- Реализовать проверку защищённых переменных с понятным сообщением.
- Составить перечень: что перенеслось механически, что потребовало изменений.
- Написать проверку
.gitlab-ci.ymlи испытать её на файле с нарушениями. - Посчитать долю механического переноса и объяснить, от чего она зависит.
Проверка
Проверка из пункта 7 должна находить:
- задачу без
needs; cache, используемый для передачи результата;- секрет в аргументе команды;
- публикацию без ограничения по ветке;
dindбез внешнего кэша.
Критерий
Проверка даёт ноль критичных на правильном файле и не менее трёх на плохом. Доля механического переноса посчитана, и названо, от чего она зависит.
Ориентир: от чего зависит доля переноса
Механически переносится всё, что не описано в синтаксисе платформы: логика проверок в скриптах, Dockerfile, тесты.
Меняется обвязка: способ сборки образа, тип кэша, права токена, переиспользуемые модули, обращение с секретами.
Отсюда практический вывод, применимый до всякого переноса: чем больше логики в скриптах, тем дешевле смена платформы. Это же делает pipeline отлаживаемым локально — цикл сокращается с минут до секунд.
Признак хорошего результата: файл CI состоит преимущественно из вызовов ./ci/*.sh и объявлений условий.
Сводная таблица
| № | Задание | Тип | Основной материал |
|---|---|---|---|
| 1 | Workflow с линтером и тестами | обяз. | 16.2 |
| 2 | Сборка и публикация по commit SHA | обяз. | 16.2 |
| 3 | Build cache: настроить и измерить | обяз. | 16.3 |
| 4 | Integration tests на собранном образе | обяз. | 16.2 |
| 5 | Секреты не попадают в логи | обяз. | 16.2 |
| 6 | Сканирование с осмысленной блокировкой | доп. | 16.4 |
| 7 | SBOM как артефакт сборки | доп. | 16.4 |
| 8 | Публикация только с main | доп. | 16.1 |
| 9 | Сборка 8 минут при неизменных зависимостях | диаг. | 16.3 |
| 10 | Перенос в GitLab CI | ★ | 16.5 |
Что должно получиться
После заданий 1–5 у вас есть практическое подтверждение пяти утверждений раздела:
fail-fast: falseнужен, чтобы видеть все проблемные варианты, а не первый.- Схема «собрать для тестов, собрать для публикации» публикует непроверенный артефакт.
- На чистом сборщике
CACHEDравно нулю: локального кэша в CI не бывает. - Smoke test с подменой
--entrypointне проверяет ничего. - Маскирование секретов обходится любым преобразованием строки.
Задания 6–8 добавляют сканирование, SBOM и условия запуска.
Задание 9 отрабатывает диагностику на самом частом симптоме — медленной сборке при настроенном кэше. Задание 10 проверяет главное решение раздела: логика в скриптах делает pipeline и отлаживаемым локально, и переносимым между платформами.
Дальше
Практическая часть курса завершена. Соберите всё вместе: Проект 4. Production pipeline.
Проверьте себя: Quiz 16 и Checkpoint 4.
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий раздел: Docker internals →
Главное оглавление