Проект 4. Список проверок
Проходить после реализации и до чтения SOLUTION.md.
Скрипты конвейера, вычисление тегов, сводка и отказы развёртывания проверены фактическим запуском на машине курса. Docker,
trivyиsyftотсутствуют — поэтому этапы сборки, проверки образа и сканирования дают код 3, и это ровно тот исход, который проект учит различать.
Подготовка
ci/pipeline.sh; echo "код: $?"
1. Переносимость
Что проверяется: логика в скриптах, а не в шагах YAML.
python3 -c "
import yaml
from pathlib import Path
d = yaml.safe_load(Path('.github/workflows/pipeline.yaml').read_text())
steps = d['jobs']['pipeline']['steps']
runs = [s['run'] for s in steps if isinstance(s.get('run'), str)]
via = [r for r in runs if r.startswith('ci/')]
print(f'шагов: {len(steps)}, с командами: {len(runs)}, вызывающих ci/: {len(via)}')
for r in runs:
if not r.startswith('ci/'):
print(' логика в YAML:', r.splitlines()[0][:60])"
шагов: 13, с командами: 8, вызывающих ci/: 6
логика в YAML: pip install -r requirements-dev.txt
логика в YAML: ref="$(cat artifacts/image-ref.txt)"
☐ Каждый содержательный этап — вызов скрипта
☐ ci/pipeline.sh выполняется локально без CI
☐ Оставшиеся команды в YAML — установка и публикация, а не проверки
Про две оставшиеся строки. Установка зависимостей и docker push привязаны к платформе, и выносить их в скрипт особого смысла нет. Важно, что ни одна проверка не живёт в YAML: именно проверки нужно уметь запускать локально.
2. Порядок
grep -n 'run_stage' ci/pipeline.sh
30:run_stage "линтер" ci/lint.sh
31:run_stage "тесты" ci/test.sh
32:run_stage "сборка" ci/build.sh
33:run_stage "проверка образа" ci/verify-image.sh
34:run_stage "сканирование" ci/scan.sh
35:run_stage "состав (SBOM)" ci/sbom.sh
☐ Быстрые проверки первыми
☐ Сканирование после сборки
☐ Порядок обоснован письменно
Проверка на деле: сломать линтер и замерить, за сколько падает конвейер.
echo "x=" >> app/__init__.py && time ci/pipeline.sh >/dev/null 2>&1
git checkout app/__init__.py
☐ Падение происходит за секунды, а не после сборки
Почему сканирование после сборки, а не раньше. Оно проверяет образ, а не исходники. Между ними Dockerfile, и его ошибки — лишний пакет, секрет в слое, забытый USER — исходниками не ловятся.
3. Три состояния
Главная проверка проекта.
ci/scan.sh; echo "нет сканера: $?"
PATH=/usr/bin:/bin ci/verify-image.sh >/dev/null 2>&1; echo "нет docker: $?"
[12:05:20] НЕ ПРОВЕРЕНО: trivy не установлен; образ ghcr.io/example/eventapi:g9c9b7aec7405 не проверялся
нет сканера: 3
нет docker: 3
Итог конвейера:
ci/pipeline.sh 2>/dev/null | tail -12
этап результат
───────────────────────────────
линтер пройдено
тесты пройдено
сборка НЕ ПРОВЕРЕНО
проверка образа НЕ ПРОВЕРЕНО
сканирование НЕ ПРОВЕРЕНО
состав (SBOM) НЕ ПРОВЕРЕНО
пройдено: 2, провалено: 0, не проверено: 4
ИТОГ: конвейер прошёл НЕ ПОЛНОСТЬЮ — 4 этапов не выполнялось
Это не успех. Для публикации образа требуется код 0.
☐ Отсутствие инструмента даёт 3, а не 0
☐ Итог различает три состояния
☐ Код 3 не пускает к публикации
☐ Провал (1) перевешивает непроверенное (3)
Последнее проверяется так:
printf 'тесты\t1\nсканирование\t3\n' | python3 ci/summary.py; echo "код: $?"
пройдено: 0, провалено: 1, не проверено: 1
ИТОГ: конвейер провален
код: 1
Если ваш конвейер даёт 0 при отсутствующем сканере — это не мелкая недоработка. Это состояние, при котором зелёная сборка означает «мы перестали проверять», и узнать об этом неоткуда.
4. Состав
ci/sbom.sh; echo "код: $?"
python3 -c "
import json
d = json.load(open('artifacts/sbom.json'))
print('полный:', d.get('complete'))
print('пакетов:', len(d['packages']))
print('пометка:', d['comment'][:60])"
[11:57:53] syft не установлен; собираю ЧАСТИЧНЫЙ состав из requirements.txt
пакетов: 5
[11:57:53] НЕ ПРОВЕРЕНО: полный SBOM не собран: syft отсутствует; записан частичный состав
код: 3
полный: False
пакетов: 5
пометка: ЧАСТИЧНЫЙ СОСТАВ: собран из requirements.txt, системные п
☐ SBOM создан
☐ Частичный состав помечен complete: false
☐ Код возврата отличается от успешного
Почему частичный состав — не замена полному. Он не видит системных пакетов базового образа, а уязвимости чаще находят именно там. Файл, выглядящий как SBOM и не являющийся им, хуже отсутствующего: на него сошлются.
5. Проверка образа
ci/verify-image.sh "$IMAGE"
С доступным Docker ожидается:
[12:31:02] проверяю образ ghcr.io/example/eventapi:g9c9b7aec7405
✓ пользователь задан
✓ пользователь числовой
✓ пользователь не root
✓ точка входа в exec-форме
✓ секретов в истории нет
✓ работает только для чтения
✓ нет оболочки
проверок: 7, провалов: 0
☐ Проверяется собранный образ, а не исходники
☐ Не менее пяти проверок
☐ Отсутствие Docker даёт 3, а не 0
Проверка проверок. Соберите образ с USER app вместо USER 10001 и убедитесь, что пункт «пользователь числовой» падает. Проверка, которая никогда не падает, ничего не проверяет.
6. Теги
bash -c 'source ci/lib.sh; echo "тег: $(image_tag)"'
touch проба.txt
bash -c 'source ci/lib.sh; echo "с неотслеживаемым файлом: $(image_tag)"'
rm проба.txt
bash -c 'source ci/lib.sh; echo "снова: $(image_tag)"'
тег: g9c9b7aec7405
с неотслеживаемым файлом: g9c9b7aec7405-dirty
снова: g9c9b7aec7405
☐ Тег содержит SHA коммита
☐ Грязное дерево помечается
☐ Неотслеживаемый файл тоже помечается
☐ Перезаписываемых имён (latest, prod) нет
Про неотслеживаемый файл. git diff его не видит, а в контекст сборки он попадает. Первая редакция скрипта использовала git diff и помечала такое дерево чистым: образ переставал соответствовать коммиту, а тег об этом молчал. Проверять нужно git status --porcelain.
7. Кэш
grep -n 'cache-to' ci/build.sh
--cache-to "type=registry,ref=${IMAGE_NAME}:buildcache,mode=max" \
☐ cache-from и cache-to заданы
☐ Указан mode=max
☐ Второй запуск конвейера быстрее первого
Про mode=max. Значение по умолчанию — min — экспортирует только последнюю стадию. Слой с зависимостями в кэш не попадает, и это самая частая причина «кэш настроен, а сборка медленная» (урок 16.3).
Отдельно. RUN --mount=type=cache ускоряет сборку локально и не переносится между запусками CI: cache-to экспортирует слои, а не содержимое cache mount. Это разные механизмы, и второй в конвейере не работает.
8. Развёртывание
ci/deploy.sh "ghcr.io/example/eventapi:g9c9b7aec7405"; echo "код: $?"
ci/deploy.sh "ghcr.io/example/eventapi@sha256:aaaa0000bbbb1111cccc2222dddd3333eeee4444ffff5555aaaa6666bbbb7777"; echo "код: $?"
ci/rollback.sh; echo "откат без записи: $?"
[12:05:20] ОШИБКА: развёртывание по тегу запрещено: укажите digest (ghcr.io/example/eventapi:g9c9b7aec7405)
код: 1
[12:05:20] окружение: production
[12:05:20] было: <пусто>
[12:05:20] будет: ghcr.io/example/eventapi@sha256:aaaa…7777
[12:05:20] НЕ ПРОВЕРЕНО: docker недоступен; развёртывание не выполнялось
код: 3
[12:05:21] ОШИБКА: предыдущий digest не записан: откатываться некуда
откат без записи: 1
☐ Развёртывание по тегу отвергается
☐ Развёртывание по digest принимается
☐ Предыдущий digest записывается
☐ Откат без записи отказывается работать
Почему по digest. Тег — изменяемая ссылка. Между «собрали» и «запустили» он может указывать уже на другое: сборка опубликовала повторно, два конвейера победили друг друга, pull выполнен, а container не пересоздан. Digest вычисляется из содержимого — одно значение, один набор байтов (урок 14.4).
9. Тесты конвейера
pytest -q
............................ [100%]
28 passed
☐ Скрипты конвейера покрыты тестами
☐ Проверены все три состояния сводки
☐ Проверены отказы развёртывания и отката
☐ Тесты проходят при грязном рабочем дереве
Последний пункт проверяется так:
touch грязь.txt && pytest -q; rm грязь.txt
28 passed
Почему это важно. Первая редакция тестов тегов проверяла рабочее дерево самого проекта — и падала, стоило отредактировать файл теста. Тест, зависящий от состояния репозитория, ломается от собственной правки; тесты тегов должны работать во временном репозитории.
Сводка
| № | Проверка | Требования | Отметка |
|---|---|---|---|
| 1 | Переносимость | 1 | ☐ |
| 2 | Порядок | 2 | ☐ |
| 3 | Три состояния | 3, 4 | ☐ |
| 4 | Состав | 5 | ☐ |
| 5 | Проверка образа | 6 | ☐ |
| 6 | Теги | 7 | ☐ |
| 7 | Кэш | 8 | ☐ |
| 8 | Развёртывание | 9, 10 | ☐ |
| 9 | Тесты конвейера | 11 | ☐ |
Девять из девяти — можно открывать SOLUTION.md.
Навигация
← Техническое задание
Эталонное решение →
Вернуться к проектам
Главное оглавление