[Перевод] Бескомпромиссный CI-конвейер для питонистов

Любому Python-проекту может пойти на пользу надёжный и стабильный конвейер непрерывной интеграции (Continuous Integration, CI). В рамках таких конвейеров выполняется сборка приложений, запуск тестов, проверка кода линтерами, контроль качества программ, анализ уязвимости приложений. Правда, построение CI-конвейеров занимает много времени, требует выполнения действий, которые, сами по себе, никакой пользы не приносят. Этот материал написан для тех Python-программистов, которым нужен полнофункциональный, настраиваемый CI-конвейер, основанный на GitHub Actions. Этот конвейер оснащён всеми мыслимыми инструментами, подключён ко всем необходимым сервисам, а подготовить его к работе можно всего за несколько минут.

973b0b27817d568456e60b980cc9a015.png

Быстрый запуск собственного конвейера

Если вы не слишком терпеливы, или просто хотите сразу перейти к делу — вот минимальная конфигурация, необходимая для того, чтобы запустить собственный конвейер:

# .github/workflows/python-pipeline.yml
name: CI Pipeline

on: [push, workflow_dispatch]

jobs:
  python-ci-pipeline:
    uses: MartinHeinz/workflows/.github/workflows/python-container-ci.yml@v1.0.0
    with:
      PYTHON_VERSION: '3.10'
      DEPENDENCY_MANAGER: 'pip'
      ENABLE_SONAR: ${​{ false }}
      ENABLE_CODE_CLIMATE: ${​{ false }}
      ENABLE_SLACK: ${​{ false }}
      ENFORCE_PYLINT: ${​{ false }}
      ENFORCE_BLACK: ${​{ false }}
      ENFORCE_FLAKE8: ${​{ false }}
      ENFORCE_BANDIT: ${​{ false }}
      ENFORCE_DIVE: ${​{ false }}

Вышеприведённый YAML-код настраивает рабочий процесс (workflow) GitHub Actions, который ссылается на рабочий процесс, подходящий для повторного использования из моего репозитория. При таком подходе не нужно копировать (а значит — и, позже, поддерживать) большие объёмы YAML-кода с описанием действий, выполняемых в GitHub Actions. Вам, чтобы этим кодом воспользоваться, нужно сохранить его, в виде .yml-файла, в директории .github/workflows/ вашего репозитория и настроить по своему желанию параметры, перечисленные в разделе конфигурационного файла with:.

Все эти параметры (конфигурационные опции) имеют адекватные значения, назначенные по умолчанию. Ни один из них не нуждается в обязательной перенастройке. Поэтому вы, если доверяете моим суждениям, можете опустить весь раздел with:. Если же вы хотите настроить их по-своему — можете поступить так, как показано выше, а так же поработать с другими параметрами, которые имеются в объявлении рабочего процесса. В файле README из репозитория можно найти пояснения по поводу того, как находить нужные значения и настраивать идентификационные данные, необходимые, например, для интеграции с сервисами Sonar или CodeClimate. Подробности об этом можно почитать и в следующих разделах этой статьи.

Рассматриваемый здесь рабочий процесс, очевидно, должен опираться на некоторые предположения относительно содержимого вашего репозитория. Поэтому ожидается, что в репозитории будет директория с исходным кодом вашего проекта и файл Dockerfile. При этом других требований к репозиторию не предъявляется. Пример того, как может быть устроен подходящий репозиторий, можно посмотреть здесь, в моём репозитории, предназначенном для тестирования конвейера.

Вы, наверное, заметили, что в вышеприведённом фрагменте кода, с использованием конструкции @v1.0.0, сделана ссылка на конкретную версию рабочего процесса. Это сделано для того чтобы предотвратить случайное использование изменённых вариантов файла, применять которые разработчик не планирует. С учётом вышесказанного — предлагаю вам иногда заглядывать в мой репозиторий на предмет поиска изменений и улучшений моего рабочего процесса и его новых релизов.

Базовые механизмы

Возможно, сейчас ваш конвейер уже работоспособен, но я предлагаю разобраться с тем, как работают его внутренние механизмы. Это даст вам возможность лучше подстроить его под свои нужды. Начнём с очевидного — с проверки репозитория, с установки кода и с запуска тестов:

jobs:
    # ... Сокращено ради ясности изложения
    - uses: actions/checkout@v1
    - uses: actions/setup-python@v1
      id: setup-python
      with:
        python-version: ${{ inputs.PYTHON_VERSION }}
    - name: Get cache metadata
      id: cache-meta
      run: |
        # ...
        # Найти ключ и путь кеша, основываясь на сведениях об используемом менеджере зависимостей и о его lock-файле
    - name: Load cached venv
      id: cache
      uses: actions/cache@v2
      with:
        path: ${{ steps.cache-meta.outputs.cache-path }}
        key: ${{ steps.cache-meta.outputs.cache-key }}
    - name: Install Dependencies
      run: |
        # ...
        # Создать виртуальное окружение, установить зависимости, используя настроенный менеджер зависимостей
    - name: Run Tests
      run: |
        source venv/bin/activate
        pytest

Выше, ради ясности изложения, приведён сокращённый фрагмент исходного кода. Но, если вы знакомы с GitHub Actions, все шаги, выполняемые здесь, должны быть для вас вполне очевидными. Если не знакомы — не беспокойтесь, так как вам, на самом деле, не нужно это понимать. Самое важное — знать о том, что этот конвейер работает со всеми основными менеджерами зависимостей Python — то есть — с pip,  poetry и pipenv. Вам нужно лишь задать значение параметра DEPENDENCY_MANAGER, а конвейер сделает всё остальное сам. Это, понятно, предполагает наличие в репозитории файла requirements.txt, poetry.lock или Pipfile.lock.

В этом коде, кроме того, создаётся виртуальное окружение Python, используемое во всём конвейере для создания изолированного окружения, в котором выполняется сборка/тестирование проекта. Это позволяет нам кешировать все зависимости, ускоряя выполнение действий, входящих в состав конвейера. И, в качестве дополнительного плюса, зависимости, до тех пор, пока lock-файл остаётся неизменным, кешируются для всех веток репозитория.

Если говорить о шаге, на котором выполняется тестирование — то тут, для запуска набора тестов, используется pytest. Фреймворк pytest автоматически находит и использует конфигурационные файлы, имеющиеся в репозитории (понятно, что в том случае, если они есть). Точнее — порядок приоритетности их использования выглядит так: pytest.ini, pyproject.toml,  tox.ini или setup.cfg.

Качество кода

Мы уже знаем о том, как решать с помощью CI-конвейера базовые задачи. Но мы хотели бы, чтобы он помог бы нам принимать меры, обеспечивающие высокое качество кода. Существует множество инструментов, которые позволяют обеспечивать чистоту кода и возможность его поддержки. А в наш конвейер входят, в общем-то, все эти инструменты:

- name: Verify code style (Black)
  uses: psf/black@stable
  with:
    options: "--verbose ${{ inputs.ENFORCE_BLACK && '--check' || '' }}"

- name: Enforce code style (Flake8)
  run: |
    source venv/bin/activate
    flake8 ${{ inputs.ENFORCE_FLAKE8 && '' || '--exit-zero' }}

- name: Lint code
  run: |
    source venv/bin/activate
    pylint **/*.py  # ... Some more conditional arguments

- name: Send report to CodeClimate
  uses: paambaati/codeclimate-action@v3.0.0
  if: ${{ inputs.ENABLE_CODE_CLIMATE }}
  env:
    CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
  with:
    coverageLocations: |
      ${{github.workspace}}/coverage.xml:coverage.py

- name: SonarCloud scanner
  uses: sonarsource/sonarcloud-github-action@master
  if: ${{ inputs.ENABLE_SONAR }}
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Мы начинаем с запуска Black — средства для форматирования Python-кода. Рекомендуется запускать Black с помощью pre-commit-хука, или обрабатывать с его помощью файлы каждый раз, когда их сохраняют локально. Применение Black должно служить лишь способом проверки того, что ничего плохого не заберётся в код, отправленный в репозиторий.

Далее — мы запускаем Flake8 и Pylint. Они обрабатывают код с использованием дополнительных правил стилизации и линтинга. И тот, и другой инструменты поддаются настройке с помощью соответствующих конфигурационных файлов, распознаваемых автоматически. В случае с Flake8 параметры хранятся в файлах setup.cfg, tox.ini или .flake8. Для Pylint это — файлы pylintrc, .pylintrc,  pyproject.toml.

Все эти инструменты могут быть переведены в «принудительный» режим (enforcing mode). Это приведёт к тому, что выполнение конвейера завершится с ошибкой в том случае, если инструмент найдёт какую-то проблему. Соответствующие конфигурационные опции выглядят как ENFORCE_PYLINT,  ENFORCE_BLACK и ENFORCE_FLAKE8.

Помимо инструментов, специфичных для Python, в конвейер включено и два популярных универсальных инструмента, представленных внешними сервисами. Это — SonarCloud и CodeClimate. И тот и другой — это необязательные инструменты, но я настоятельно рекомендую ими пользоваться, учитывая то, что они поддерживают работу с любыми общедоступными репозиториями. При включении этих средств (с помощью опций ENABLE_SONAR и/или ENABLE_CODE_CLIMATE) сканер Sonar проведёт анализ кода и отправит результаты в SonarCloud, а CodeClimate возьмёт отчёт о покрытии кода тестами, сгенерированный в процессе работы pytest, и автоматически преобразует их в удобный для восприятия вид.

Для того чтобы получить возможность настраивать эти инструменты, включите соответствующие им поля в конфигурационный файл конвейера:

jobs:
  python-ci-pipeline:
    uses: MartinHeinz/workflows/.github/workflows/python-container-ci.yml@v1.0.0
    with:
      # ...
      ENABLE_SONAR: ${​{ true }}
      ENABLE_CODE_CLIMATE: ${​{ true }}
    secrets:
      SONAR_TOKEN: ${​{ secrets.SONAR_TOKEN }}
      CC_TEST_REPORTER_ID: ${​{ secrets.CC_TEST_REPORTER_ID }}

Для того чтобы сгенерировать и задать идентификационные данные — обратитесь к руководствам из файла README.

Упаковка кода

После того, как мы удостоверимся в том, что код соответствует нашим стандартам — пришло время его упаковать. В данном случае — упаковать в образ контейнера:

- name: Login to GitHub Container Registry
  uses: docker/login-action@v1
  with:
    # ... Задать реестр контейнеров, имя пользователя и пароль

- name: Generate tags and image meta
  id: meta
  uses: docker/metadata-action@v3
  with:
    images: |
       ${{ inputs.CONTAINER_REGISTRY }}/${{ steps.get-repo.outputs.repo }}
    tags: |
      type=ref,event=tag
      type=sha

- name: Build image
  uses: docker/build-push-action@v2
  with:
    context: .
    load: true  # Не выполнять операцию push
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
    cache-from: type=registry,ref=${{ inputs.CONTAINER_REGISTRY }}/${{ steps.get-repo.outputs.repo }}:latest
    cache-to: type=registry,ref=${{ inputs.CONTAINER_REGISTRY }}/${{ steps.get-repo.outputs.repo }}:latest,mode=max

- name: Analyze image efficiency
  uses: MartinHeinz/dive-action@v0.1.3
  with:
    image: ${{ inputs.CONTAINER_REGISTRY }}/${{ steps.get-repo.outputs.repo }}:${​{ steps.meta.outputs.version }}
    config: ${{ inputs.DIVE_CONFIG }}
    exit-zero: ${{ !inputs.ENFORCE_DIVE }}

- name: Push container image
  uses: docker/build-push-action@v2
  with:
    push: true
    # ... Остальное - то же самое, что и при сборке

Этот конвейер, по умолчанию, использует реестр контейнеров GitHub Container Registry, являющийся частью вашего репозитория. Если это — то, что вам нужно, то ничего настраивать вам не надо, кроме добавления в репозиторий файла Dockerfile.

Если вы предпочитаете пользоваться Docker Hub, или каким-то другим реестром, можете задать параметры CONTAINER_REGISTRY и CONTAINER_REPOSITORY, а так же — учётные данные в параметрах CONTAINER_REGISTRY_USERNAME и CONTAINER_REGISTRY_PASSWORD (в разделе настроек конвейера secrets). Обо всём остальном позаботится конвейер.

Помимо реализации базовой последовательности действий — вход в систему, сборка проекта, отправка его в репозиторий, этот конвейер генерирует дополнительные метаданные, прикрепляемые к образу. Сюда входит тегирование образа с помощью SHA-хеша и тега git, если он имеется.

Для того чтобы ускорить конвейер, на шаге сборки docker-образа тоже используется кеш. Это делается для того, чтобы избежать создания слоёв образа, которые не нужно перестраивать. И наконец, для обеспечения дополнительной эффективности, образ обрабатывается с помощью инструмента Dive, позволяющего оценить эффективность самого образа. Это, кроме того, даёт нам возможность размещения конфигурационных данных в .dive-ci и указания пороговых значений для метрик Dive. И, как и в случае со всеми остальными инструментами в этом конвейере, Dive тоже можно настроить на работу в «принудительном» режиме, пользуясь опцией ENFORCE_DIVE.

Безопасность

Не будем забывать и о том, что CI-конвейер должен обеспечивать отсутствие уязвимостей в коде. Для решения этой задачи в наш рабочий поток включено ещё несколько инструментов:

- name: Code security check
  run: |
    # ...
    bandit -r . --exclude ./venv  # ... Ещё немного условных аргументов
- name: Trivy vulnerability scan
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: '${{ inputs.CONTAINER_REGISTRY }}/${{ steps.get-repo.outputs.repo }}:${{ steps.meta.outputs.version }}'
    format: 'sarif'
    output: 'trivy-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab
  uses: github/codeql-action/upload-sarif@v1
  with:
    sarif_file: 'trivy-results.sarif'
- name: Sign the published Docker image
  env:
    COSIGN_EXPERIMENTAL: "true"
  run: cosign sign ${{ inputs.CONTAINER_REGISTRY }}/${{ steps.get-repo.outputs.repo }}:${{ steps.meta.outputs.version }}

Первый из них — это Python-инструмент, называемый Bandit. Он ищет в Python-коде распространённые проблемы безопасности. У него имеется набор правил, задаваемых по умолчанию, но его можно настраивать с помощью конфигурационного файла, указываемого в опции BANDIT_CONFIG. Как и другие инструменты, упомянутые выше, Bandit, по умолчанию, работает в режиме, когда конвейер, при обнаружении проблем, не останавливается. Но его можно перевести в «принудительный» режим с помощью опции ENFORCE_BANDIT.

Ещё один используемый нами инструмент, нацеленный на поиск уязвимостей, это Trivy от Aqua Security. Он сканирует образ контейнера и выдаёт список уязвимостей, найденных в самом образе. В результате можно обнаружить уязвимости, находящиеся за пределами Python-кода. Затем полученный отчёт выгружается в GitHub Code Scanning, соответствующие данные после этого появляются на закладке репозитория Security.

Отчёт о найденных уязвимостяхОтчёт о найденных уязвимостях

То, что эти инструменты позволяют обеспечить безопасность создаваемого нами приложения — это очень хорошо. Но нам ещё нужен механизм, обеспечивающий доказательство подлинности итогового образа контейнера, что позволит предотвратить так называемые «атаки на цепочку поставок». Для того чтобы это сделать, мы пользуемся инструментом cosign, который позволяет подписать образ с помощью OIDC-токена GitHub, который связывает образ с идентификационными данными пользователя, отправившего код в репозиторий. Этому инструменту не нужен какой-либо ключ для генерирования подписи, поэтому работать он будет без дополнительных настроек. Подпись, после её создания, отправляется в реестр контейнеров вместе с образом. Например — в Docker Hub:

Подпись в реестре контейнеровПодпись в реестре контейнеров

Для того чтобы проверить такую подпись, можно поступить так:

docker pull ghcr.io/some-user/some-repo:sha-1dfb324
COSIGN_EXPERIMENTAL=1 cosign verify ghcr.io/some-user/some-repo:sha-1dfb324

# Если не хочется устанавливать cosign...
docker run -e COSIGN_EXPERIMENTAL=1 --rm \
  gcr.io/projectsigstore/cosign:v1.6.0 verify \
  ghcr.io/some-user/some-repo:sha-1dfb324

...
Verification for ghcr.io/some-user/some-repo:sha-1dfb324 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - Any certificates were verified against the Fulcio roots.

Подробности о cosign и о подписании образов контейнеров можно почитать здесь.

Уведомления

Последняя небольшая возможность рассматриваемого здесь конвейера — это отправка уведомлений в Slack, выполняемая как для успешных сборок, так и для сборок, завершившихся с ошибкой. Для того чтобы она заработала бы, её нужно активировать с помощью опции ENABLE_SLACK. Всё, что нужно сделать — включить в состав настроек веб-хук канала Slack с помощью опции настройки идентификационных данных SLACK_WEBHOOK.

Для того чтобы сгенерировать этот веб-хук — загляните в README.

УведомлениеУведомление

Итоги

Пожалуй, это — всё, что нужно, чтобы заставить работать ваш полностью настроенный конвейер, охватывающий все нужды типичного Python-проекта.

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

В будущем в этом репозитории, помимо дополнительного функционала, может появиться и большее количество настроек для Python (или для других языков). Дело в том, что представленный здесь конвейер нельзя назвать подходящим для абсолютно всех типов приложений. Поэтому, если вам это интересно, заглядывайте иногда в мой репозиторий чтобы узнать о новых релизах моего CI-конвейера.

О, а приходите к нам работать?

© Habrahabr.ru