Как я строю удобную инфраструктуру вокруг Python-проектов: линтеры, Poetry, CI/CD и Docker
Скорее всего, каждый из вас сталкивался с такой ситуацией, когда на просторах интернета видишь статью или видео о новой модной штуке, которую обязательно надо попробовать в своих проектах, но почему-то не пробуешь. Забываешь, или просто становится лениво разбираться.
Например, CI/CD. Как сложно в первый раз сесть и разобраться, как все это настроить. А если дело дошло до того, что уже открыта вкладка с докой Github Actions, то скорее всего вы утонете в количестве информации, так и не вычленив ничего дельного, что как-то помогло бы настроить ваш первый пайплайн. Легче уже найти какое-то видео по этой теме :)
Цель данной статьи - поделиться своими знаниями о том, как удобно организовать инфраструктуру для поддержки Python-приложений, и вынести на обсуждение использование новых библиотек, линтеров, пакетных менеджеров и т.п. Ну и конечно же не обойдусь без критики некоторых практик, которые уже я успел повидать. Ну что ж, начинаем!
Файловая структура
Я привык помещать в корневую папку проекта директорию со скромным названием app/
для хранения исходного кода. Иногда эту папку принято называть именем src/
(сокращение от source - исходный код), например, команда create-react-app
библиотеки React создает файловую структуру как раз с такой папкой.
Дальше возможны 2 варианта развития событий:
1) Маленький проект;
2) Большой проект.
В первом случае все довольно просто - можем исключить такие вещи, как тесты, паттерны, домены. Причина этого проста - писать тесты, репозитории для работы с БД и делить бизнес-логику приложения на домены займет гораздо больше времени, чем фиксить проблемы, возникающие из-за непродуманной архитектуры.
Другое дело большое количество кода. Исходя из своего скромного опыта могу сказать, что большая кодовая база без тестов и продуманной архитектуры - это ад. Для Django это еще терпимо, но для FastAPI или Flask - уже критично.
Хорошая продуманная архитектура - это удобство и стимул для других разработчиков следовать уже установленным и понятным в рамках приложения правилам, а тесты - это гарантия, что при написании новой фичи или дополнении функционала старой, все остальные компоненты системы не сломаются.
Для того, чтобы научиться строить удобную и поддерживаемую архитектуру, могу посоветовать ознакомиться с тем, что такое:
Чистая архитектура;
Domain-Driven Design;
Слоистая архитектура.
Даже в рамках их использования можно построить корявую систему, поэтому все стоит пробовать несколько раз, рефлексировать насчет удобства и поддерживаемости своего приложения.
Линтеры
Для опытных разработчиков должно быть все просто - black, flake8 и тд. Но может быть еще проще и даже быстрее - Ruff. Это супер-быстрый линтер, написанный на Rust, который объединяет в себе все привычные для python-разработчиков линтеры в один. Запустить ruff можно командой ruff check .
или засунуть в pre-commit:
default_stages: [pre-commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- id: detect-private-key
- id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.6
hooks:
# Run the linter.
- id: ruff
args: ["--fix"]
# Run the formatter.
- id: ruff-format
Пакетные менеджеры
На замену привычному pip я предпочитаю использовать poetry. Использование poetry в качестве пакетного менеджера имеет сразу несколько преимуществ:
Избавляет от проблем с кодированием файла requirements.txt при его обновлении в pycharm (эта проблема очень неприятна и постоянно ломает пайплайн);
Позволяет удобно управлять деревом зависимостей;
Имеет удобный pyproject.toml с dev-зависимостями (теперь не надо иметь несколько разных файлов зависимостей для разработки и прода).
Poetry можно установить как глобально в систему, так и с помощью самого pip (что не рекомендуется). Себе на винду я поставил poerty глобально, прокинул путь до исполняемого файла в переменные окружения и чувствую себя замечательно. Однако, в докер образах предпочитаю использовать poetry через pip.
Также в будущем обязательно буду пробовать новый модный пакетный менеджер uv, разработанный той же компанией, что и Ruff.
Docker
В основном, докер образ для python-приложения примерно похож на этот:
FROM python:3.13-alpine
RUN apk update && apk add --no-cache build-base gcc musl-dev libffi-dev
RUN pip install poetry
ENV PATH="/root/.local/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /project_root
COPY entrypoint.sh .
COPY pyproject.toml poetry.lock alembic.ini ./
RUN poetry config virtualenvs.create false
RUN poetry install --no-interaction --no-root
ENV PYTHONPATH=/project_root
RUN chmod +x entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]
Разберем этот образ, чтобы всем было понятно что, зачем и почему:
Используем образ alpine, так как это сократит кол-во используемых контейнером ресурсов, а соответственно, поможет сэкономить на ресурсах сервера при деплое;
ENV PATH="/root/.local/bin:$PATH"
- прокидывает poetry в переменные окружения системы, чтобы можно было обращаться к poetry из командной строки (все, что устанавливается через pip попадает в/root/.local/bin
);ENV PYTHONPATH=/app
- добавляет/project_root
в системный путь поиска модулей Python. Это позволяет избавить от ошибок импорта, если используются абсолютные импортыfrom app.module import some_function
;RUN poetry config virtualenvs.create false
- говорит poetry не создавать виртуальное окружение;entrypoint.sh
- bash-скрипт, который выполняется при старте контейнера (используется, когда при старте контейнера надо выполнить сразу несколько команд);RUN chmod +x entrypoint.sh
- делаем файл исполняемым, чтобы можно было его выполнить.
Ну и сам entrypoint.sh
:
#!/bin/sh
set -e
poetry run alembic upgrade head
poetry run python3 ./app/main.py
Особое внимание надо обратить на первую строку. Так как Bash не установлен, необходимо использовать именно shell (sh). Цель файла - запустить сразу две команды: провести миграции и запустить само приложение.
CI/CD
Я приведу пример с Github Actions, но Gitlab CI/CD не имеет концептуальных отличий. Пайплайн состоит из трех базовых шагов:
Прогон линтеров и тестов (можно разделить на два разных этапа);
Сборка приложения (по сути просто сбилдить все контейнеры);
Деплой обновленного приложения на сервер.
Вот сам файл-конфигурации пайплайна для пуша или пул-реквеста в ветку main:
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@main
- uses: actions/setup-python@v2
with:
python-version: '3.13'
- name: Install dependencies
run: |
sudo curl -sSL https://install.python-poetry.org | python3 -
export PATH="$HOME/.local/bin:$PATH"
poetry install --with dev
- name: Lint with ruff
run: |
poetry run ruff check .
build:
runs-on: ubuntu-latest
needs: lint
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up docker
run: |
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker run hello-world
- name: Create .env
run: |
echo "DB_USER=${{ secrets.DB_USER }}" >> .env
echo "DB_NAME=${{ secrets.DB_NAME }}" >> .env
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env
- name: Run docker compose
run: |
docker compose up -d
deploy:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
- name: Deploy
run: |
ssh -i ~/.ssh/id_rsa ${{ secrets.USER }}@${{ secrets.SERVER_IP }} << 'EOF'
cd MovieBot
git pull origin main
docker compose down
docker compose up --build -d
EOF
С линтерами всё просто. Сборка приложения требует установки docker и docker-compose, команды для установки берем и копируем с официального сайта. С деплоем все немного сложнее, но основная идея такая:
Прокидываем SSH-ключ из secrets в текущую ОС, чтобы иметь беспарольный доступ к удаленному серверу;
Подключаемся к удаленному серверу;
Прокидываем пачку команд для рестарта контейнеров на удаленный сервер.
Самый простой CI/CD готов.
Документация
Помимо очевидного совета вести README.md
для проекта, хочу обратить внимание на ведение документации эндпоинтов. Самый простой способ вести описание эндпоинтов - сразу писать веб-приложения на FastAPI, ведь автогенерируемая документация это невероятно удобно. Но если вы пишете на django, то забивать на документацию не стоит, ведь это одна из первых вещей, через которую новые разработчики будут знакомиться с проектом.
Прочее
Сделайте шаблон для переменных окружения .env-template
. Это явно не будет лишним и может немного помочь тем, кто будет пулить ваш репозиторий.
Python движется в сторону типизации, об этом говорит активное развитие библиотек Typing и Pydantic. В сторону типизации также смотрят FastAPI и Aiogram (начиная с 3 версии). Именно сейчас стоит начать пользоваться типизацией, потому что помимо реальной пользы тайпчекинга и подсказок использование типов помогает сделать код более чистым и более читаемым.
Немного про библиотеки
Мир библиотек и фреймворков не стоит на месте. Это не значит, что стоит постоянно держать руку на пульсе и мониторить все новые библиотеки. Но определенно стоит быть знакомым с новыми удобными инструментами, чтобы в будущем, будучи лидом, не выбирать стек технологий по принципу:
НУ Я ЗНАЮ ТОЛЬКО ДЖАНГО, НО ОТ ДЖАНГО НАМ МАЛО ЧТО ВООБЩЕ НАДО, НО Я ЗНАЮ ТОЛЬКО ДЖАНГО
В большинстве случаев, под вашу задачу уже написана какая-то либа, поэтому прежде чем писать свое решение, задайте себе вопрос: "А может быть для моей задачи есть библиотека?". Вот пример полезных библиотек, которые использую я:
Loguru - невероятно простая по сравнению с встроенной logging библиотека для логирования
Pydantic - быстрая библиотека для валидации на основе встроенных типов Python
Pydantic-setting - определение настроек через модели с валидацией
SQLModel - сочетание pydantic и sqlalchemy
FastAPI - микрофреймворк с очень качественной документацией, предпочитаю использовать его там, где django или слишком медленный и неповоротливый, или оверхед.
Заключение
Одними из важнейших навыков разработчика являются насмотренность и способность выбрать подходящий под текущую задачу инструмент. Невозможно стать действительно крутым специалистом, если не пробовать новые инструменты и подходы к программированию.
Предлагайте в комментариях свои любимые библиотеки, делитесь не только лучшими практиками из своего опыта, но и описывайте неудачные кейсы. Пробуйте новые инструменты, смело отказывайтесь от устаревших решений - так развивается не только ваш проект, но и ваш профессиональный опыт.
Буду рад, если вы заглянете в мою предыдущую статью про структуру FastAPI приложения.