Главная/CI/CD/Практика

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

Задания раздела строятся вокруг двух правил, сквозных для всего курса:

  • измерять, а не оценивать — время, доля попаданий кэша, число блокирующих находок;
  • проверять проверку — механизм, не испытанный на случае, где он должен сработать отрицательно, обычно не работает.

Отдельно: платформы CI из этого окружения недоступны. Задания построены так, чтобы бо́льшая часть выполнялась локально теми же скриптами, что вызывает pipeline.

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

ФормулировкаЧто требуется
«измерить»Число, полученное командой
«доказать»Контрольный случай с противоположным результатом
«обосновать»Расчёт или механизм, а не предпочтение
«отметить»Явное указание, что именно не выполнялось

Задание 1. Workflow с линтером и тестами

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

Написать workflow, запускающий линтер и тесты на каждый push.

Требования:

  1. Быстрые проверки — параллельно, с fail-fast: false.
  2. Unit-тесты на матрице из двух версий Python.
  3. Объявить permissions на уровне workflow по минимуму.
  4. Добавить concurrency, отменяющую предыдущий запуск той же ветки.
  5. Вынести логику проверок в скрипты ci/*.sh и запустить их локально.
  6. Объяснить, почему fail-fast: false нужен и что происходит без него.

Проверка

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

Требования:

  1. Собрать образ один раз с load: true и проверить его.
  2. Опубликовать из того же кэша; доказать, что публикуется проверенный артефакт.
  3. Присвоить неизменяемый тег git-<sha> и подвижные указатели.
  4. Объявить packages: write на уровне задачи, а не workflow.
  5. Показать, что публикация идемпотентна: повтор на том же коммите даёт тот же digest.
  6. Объяснить, почему схема «собрать для тестов, собрать для публикации» неверна.

Проверка

bash
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

Настроить кэш и измерить время сборки до и после.

Требования:

  1. Измерить холодную сборку и сборку на чистом сборщике без внешнего кэша.
  2. Подтвердить отсутствие кэша числом строк CACHED, а не только временем.
  3. Настроить внешний кэш и измерить сокращение времени.
  4. Показать разницу mode=min и mode=max по факту выполнения pip install.
  5. Показать, что RUN --mount=type=cache в CI не помогает.
  6. Оценить окупаемость: сравнить выигрыш со временем передачи кэша.

Проверка

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

Требования:

  1. Запускать образ как есть, без подмены --entrypoint.
  2. Проверить: отвечает на запрос, работает не от root, завершается по SIGTERM.
  3. Показать, что подмена точки входа отменяет проверяемое.
  4. Вынести проверки в скрипт и запустить его локально на том же образе.
  5. Показать, что проверки на непроверенном образе дают ненулевой код.

Проверка

bash
./ci/integration-tests.sh ОБРАЗ; echo "код: $?"
docker run --rm --entrypoint sh ОБРАЗ -c 'echo это ничего не проверяет'
time docker stop КОНТЕЙНЕР

Критерий

Скрипт даёт код 0 на правильном образе и ненулевой на сломанном. docker stop укладывается в две секунды — иначе SIGTERM не обрабатывается.


Задание 5. Секреты не попадают в логи

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

Убедиться, что секреты не появляются в логах ни на одном шаге.

Требования:

  1. Передавать токен через переменную окружения и --password-stdin.
  2. Показать четыре способа обойти маскирование секретов.
  3. Найти в своём workflow все места, где секрет мог бы попасть в вывод.
  4. Добавить docker logout с if: always().
  5. Написать проверку workflow, находящую секрет в аргументе команды.
  6. Проверить эту проверку на файле с намеренным нарушением.

Проверка

bash
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

Добавить сканирование и настроить условие блокировки по уровню критичности.

Требования:

  1. Блокировать только находки с доступным исправлением.
  2. Измерить: сколько находок блокировало бы без --ignore-unfixed и сколько с ним.
  3. Сохранять полный отчёт как артефакт, не блокирующий сборку.
  4. Реализовать три кода возврата: чисто, есть находки, не проверено.
  5. Показать, что результат сканирования меняется со временем без изменения образа.
  6. Написать проверку файла исключений: даты, обоснования, просроченные записи.

Проверка

bash
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 ОБРАЗ; echo "код: $?"
./ci/scan.sh ОБРАЗ; echo "код: $?"
python3 ci/check-ignores.py .trivyignore; echo "код: $?"

Критерий

Три кода возврата различимы. Проверка исключений находит просроченную запись и запись без даты, а на чистом файле даёт ноль.

Ориентир: почему нужен третий код

Без него недоступный сканер неотличим от чистого образа: оба дают код 0, и pipeline зеленеет по причине отсутствия инструмента.

text
0 — проверено, чисто
1 — проверено, есть блокирующие находки
3 — НЕ ПРОВЕРЕНО: инструмент недоступен

Это применимо к любому шагу pipeline, а не только к сканированию.


Задание 7. SBOM как артефакт сборки

Тип: дополнительное
Материал: 16.4, 11.6

Сгенерировать SBOM и сохранить его как артефакт сборки.

Требования:

  1. Включить генерацию SBOM и provenance при публикации.
  2. Проверить наличие attestation у опубликованного образа.
  3. Объяснить, почему у локально собранного образа их нет.
  4. Показать случаи, где SBOM из сборки полнее восстановленного сканером.
  5. Сохранить SBOM отдельным файлом-артефактом и объяснить, когда это нужно.

Проверка

bash
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, оставив сборку для всех веток.

Требования:

  1. Сборка и проверки — на всех ветках и в pull request.
  2. Публикация — только при push в ветку по умолчанию и при теге версии.
  3. Integration-тесты — на pull request и main, но не на каждом push в ветку.
  4. Сканирование по расписанию для уже опубликованных образов.
  5. Обосновать каждое условие: что экономится и что теряется.
  6. Показать, что pull request из форка не получает доступ к секретам.

Проверка

bash
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 минут при неизменных зависимостях. Найти причину.

Требования:

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

Проверка

bash
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 и объяснить, что пришлось изменить.

Требования:

  1. Написать .gitlab-ci.yml, эквивалентный workflow.
  2. Объявить needs явно и объяснить, что даёт отказ от порядка стадий.
  3. Правильно разделить cache и artifacts; привести пример неверного использования с симптомом.
  4. Выбрать способ сборки образов и обосновать выбор через риск, а не удобство.
  5. Реализовать проверку защищённых переменных с понятным сообщением.
  6. Составить перечень: что перенеслось механически, что потребовало изменений.
  7. Написать проверку .gitlab-ci.yml и испытать её на файле с нарушениями.
  8. Посчитать долю механического переноса и объяснить, от чего она зависит.

Проверка

Проверка из пункта 7 должна находить:

  • задачу без needs;
  • cache, используемый для передачи результата;
  • секрет в аргументе команды;
  • публикацию без ограничения по ветке;
  • dind без внешнего кэша.

Критерий

Проверка даёт ноль критичных на правильном файле и не менее трёх на плохом. Доля механического переноса посчитана, и названо, от чего она зависит.

Ориентир: от чего зависит доля переноса

Механически переносится всё, что не описано в синтаксисе платформы: логика проверок в скриптах, Dockerfile, тесты.

Меняется обвязка: способ сборки образа, тип кэша, права токена, переиспользуемые модули, обращение с секретами.

Отсюда практический вывод, применимый до всякого переноса: чем больше логики в скриптах, тем дешевле смена платформы. Это же делает pipeline отлаживаемым локально — цикл сокращается с минут до секунд.

Признак хорошего результата: файл CI состоит преимущественно из вызовов ./ci/*.sh и объявлений условий.


Сводная таблица

ЗаданиеТипОсновной материал
1Workflow с линтером и тестамиобяз.16.2
2Сборка и публикация по commit SHAобяз.16.2
3Build cache: настроить и измеритьобяз.16.3
4Integration tests на собранном образеобяз.16.2
5Секреты не попадают в логиобяз.16.2
6Сканирование с осмысленной блокировкойдоп.16.4
7SBOM как артефакт сборкидоп.16.4
8Публикация только с mainдоп.16.1
9Сборка 8 минут при неизменных зависимостяхдиаг.16.3
10Перенос в GitLab CI16.5

Что должно получиться

После заданий 1–5 у вас есть практическое подтверждение пяти утверждений раздела:

  1. fail-fast: false нужен, чтобы видеть все проблемные варианты, а не первый.
  2. Схема «собрать для тестов, собрать для публикации» публикует непроверенный артефакт.
  3. На чистом сборщике CACHED равно нулю: локального кэша в CI не бывает.
  4. Smoke test с подменой --entrypoint не проверяет ничего.
  5. Маскирование секретов обходится любым преобразованием строки.

Задания 6–8 добавляют сканирование, SBOM и условия запуска.

Задание 9 отрабатывает диагностику на самом частом симптоме — медленной сборке при настроенном кэше. Задание 10 проверяет главное решение раздела: логика в скриптах делает pipeline и отлаживаемым локально, и переносимым между платформами.

Дальше

Практическая часть курса завершена. Соберите всё вместе: Проект 4. Production pipeline.

Проверьте себя: Quiz 16 и Checkpoint 4.

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий раздел: Docker internals →
Главное оглавление

Markdown на GitHub ↗