Сборка Django-приложения при помощи Nuitka в onefile

от автора

Привет, Хабр!

Меня зовут Данил, и я старший специалист в компании Увеон. Занимаюсь серверной частью Termidesk Assistant — это утилита для удаленных рабочих столов.

К нам в команду пришла интересная задача, нужно было собрать всю серверную часть в один исполняемый файл (.elf) и в дальнейшем на его основе сделать установочный файл (.deb), чтобы создать и запустить сервис.

Все это для того, чтобы оптимизировать наше приложение как по скорости, так и по внешним зависимостям, а также создать возможность использования Termidesk Assistant в локальных изолированных сетях.

О Nuitka мало что известно в Python-среде, особенно мало информации на русском языке, поэтому я решил взяться за написание этой статьи и расписать всё то, что успел собрать за время работы над задачей.

1.  Что же такое Nuitka?

Nuitka — транспайлер (транспилирующий компилятор), который транслирует код Python в исполняемые файлы или исходный код C/C++. То есть он переводит код Python в C++, оптимизируя его, а далее в машинный код через C++ компилятор, генерируя исполняемый файл (elf, exe и т.д.)

2. Преимущества Nuitka

При сравнении сборки Django-приложений с помощью Nuitka, Docker-контейнеров и PyInstaller, у Nuitka есть несколько потенциальных преимуществ:

  1. Производительность
    Nuitka компилирует Python-код в нативный машинный код, что может привести к повышению производительности по сравнению с интерпретируемым Python-кодом.

  2. Размер исполняемого файла
    Nuitka обычно создает меньшие по размеру исполняемые файлы по сравнению с PyInstaller, так как включает только необходимые зависимости.

  3. Защита исходного кода
    Компиляция в машинный код затрудняет обратную разработку, обеспечивая некоторую защиту вашего исходного кода.

  4. Кроссплатформенность
    Nuitka позволяет создавать исполняемые файлы для разных платформ, что может быть проще, чем управление Docker-контейнерами для разных окружений.

  5. Простота развертывания
    В отличие от Docker, не требуется устанавливать и настраивать дополнительное ПО на целевой машине.

3. Недостатки Nuitka

  1. Сложность конфигурации
    Настройка Nuitka для корректной работы с Django и всеми зависимостями может быть сложнее, чем настройка Docker или PyInstaller.

  2. Время компиляции
    Процесс компиляции с Nuitka может занимать значительно больше времени, чем создание Docker-образа или сборка с PyInstaller.

  3. Ограниченная поддержка динамических аспектов Python
    Некоторые динамические функции Python (например, динамическая загрузка модулей) могут работать некорректно или требовать дополнительной настройки.

  4. Проблемы совместимости
    Не все Python-библиотеки хорошо работают с Nuitka.

  5. Отладка
    Отлаживать скомпилированное приложение может быть сложнее (дебаг тут невозможен, только print, грубо говоря), чем интерпретируемый код или код в Docker-контейнере.

  6. Обновления
    Обновление приложения, собранного с Nuitka, требует полной пересборки, в то время как с Docker можно обновить только изменённые слои.

  7. Отсутствие изоляции
    В отличие от Docker, Nuitka не обеспечивает изоляцию окружения, что может привести к конфликтам с системными библиотеками. Для полной изоляции я использовал аргумент onefile, но оно не предоставляет изоляцию на уровне ОС и также использует системные библиотеки и драйверы, как я это обходил — чуть позже.

  8. Меньшее распространение
    Nuitka менее популярна в сообществе Django и Python вообще, чем Docker, что может затруднить поиск решений проблем и лучших практик. Поэтому я здесь 🙂

  9. Ограниченная масштабируемость
    Docker предоставляет более гибкие возможности для масштабирования приложений, особенно в контексте микросервисной архитектуры.

4. Резюмируем: сравнение Nuitka с Pyinstaller и Docker

Pyinstaller использует в своих пакетах питоновские скрипты и интерпретатор, в то время как Nuitka переводит код в машинный, что увеличивает скорость выполнения и повышает уровень защиты кода от чтения.

В отличие от Docker, Nuitka — открытое ПО. Это вызывает большую уверенность в безопасности у наших клиентов и меньше зависимости от политической обстановки.

5. Способы интеграции Nuitka в различные процессы разработки и развертывания

  1. Использование в bash-скриптах— Автоматизация сборки проекта через shell-скрипты.- Пример:«`bash#!/bin/bashpython -m nuitka —follow-imports —standalone —output-dir=build myscript.py«`

  2. Интеграция в CI/CD пайплайны— GitFlic CI:«`yaml
    build_job:
    script:
    — pip install nuitka
    — python -m nuitka —follow-imports —standalone myapp.py
    «`
    — Jenkins:
    «`groovy
    stage(‘Build’) {
    steps {
    sh ‘pip install nuitka’
    sh ‘python -m nuitka —follow-imports —standalone myapp.py’
    }
    }
    «`

  3. Использование в setup.py— Интеграция Nuitka в процесс установки пакета:
    «`python
    from setuptools import setup
    from nuitka.distutils_based_buildsystem.BuildSystem import Nuitka

    setup(
    name=»MyApp»,
    cmdclass={«build_exe»: Nuitka},
    # другие параметры…
    )
    «`

  4. GitHub Actions workflow— Автоматизация сборки при push или pull request:
    «`yaml
    name: Build with Nuitka
    on: [push, pull_request]
    jobs:
    build:
    runs-on: ubuntu-latest
    steps:
    — uses: actions/checkout@v2
    — name: Set up Python
    uses: actions/setup-python@v2
    — name: Install dependencies
    run: |
    pip install nuitka
    — name: Build with Nuitka
    run: python -m nuitka —follow-imports —standalone myapp.py
    «`

  5. Makefile— Упрощение процесса сборки:
    «`makefile
    build:
    python -m nuitka —follow-imports —standalone myapp.py

    clean:
    rm -rf build/
    «`

  6. Docker— Использование Nuitka внутри Docker для создания компилированных приложений:
    «`dockerfile
    FROM python:3.9
    RUN pip install nuitka
    COPY . /app
    WORKDIR /app
    RUN python -m nuitka —follow-imports —standalone myapp.py
    «`

  7. Tox— Интеграция Nuitka в процесс тестирования:
    «`ini
    [testenv:build]
    deps = nuitka
    commands = python -m nuitka —follow-imports —standalone myapp.py
    «`

  8. Pre-commit hooks— Автоматическая компиляция перед коммитом:
    «`yaml
    — repo: local
    hooks:
    — id: nuitka-build
    name: Build with Nuitka
    entry: python -m nuitka —follow-imports —standalone
    language: system
    files: ^myapp\.py$
    «`

  9. Интеграция с системами управления пакетами— Создание пакетов для систем вроде apt, yum или Homebrew с использованием Nuitka для компиляции.

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

6. Переходим к практике: коротко моем опыте

6.1. Оптимизация проекта

Это было необходимо, чтобы сделать из проекта единый монолит. Если без подробностей, я заменил Memcached на python-memcached и Redis на Fakeredis. Тут подробно останавливаться не буду, потому что банально заменил зависимости проекта и добавил небольшие настройки. Далее часть чуть сложнее — прокси-сервер. У нас для этого всегда был Apache2, в связи с этой задачей я выбрал фреймворк FastAPI, если опустить эндпоинты, то вот код:
Загрузка статичных файлов:

def find_static_files():    if getattr(sys, 'frozen', False):        base_path = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))    else:        base_path = os.path.dirname(os.path.abspath(__file__))      possible_paths = [        os.path.join(base_path, 'static'),        os.path.join(base_path, 'django_termidesk_assistant', 'static'),        os.path.join(base_path, 'src', 'django_termidesk_assistant', 'static'),        os.path.join(os.path.dirname(base_path), 'static'),        '/tmp/onefile_*/static'    ]      for path in possible_paths:        if '*' in path:            import glob            matching_paths = glob.glob(path)            if matching_paths:                return matching_paths[0]        elif os.path.exists(path) and os.path.isdir(path):            return path      print("Static files not found in any of the expected locations.")    return None     def setup_static_files(app):    static_path = find_static_files()    if static_path:        if getattr(sys, 'frozen', False):            temp_dir = tempfile.mkdtemp()            temp_static_path = os.path.join(temp_dir, 'static')            shutil.copytree(static_path, temp_static_path)            static_path = temp_static_path          app.mount("/assistant/static", StaticFiles(directory=static_path), name="static")        return app    else:        print("Warning: Static files not found. Static content will not be served.")        return None     fastapi_app = setup_static_files(fastapi_app)

Редирект:

@fastapi_app.exception_handler(404) async def custom_404_handler(request: Request, exc: Exception):    return RedirectResponse(url="/assistant/")

Генерация сертификатов для https:

def generate_ssl_certificates():    try:        subprocess.run(['openssl', 'genrsa', '-out', 'privkey.pem', '2048'], check=True)        subprocess.run(['openssl', 'req', '-new', '-x509', '-key', 'privkey.pem',                        '-out', 'fullchain.pem', '-days', '365', '-subj', "/CN=localhost"], check=True)    except subprocess.CalledProcessError as e:        print(f"Failed to generate SSL certificates: {e}")        raise

Запуск http сервера:

def run_http_server():    config = Config(fastapi_app, host="0.0.0.0", port=80)    server = Server(config=config)    try:        server.run()    except Exception as e:        print(f"Server error on port 80: {e}")

Запуск https сервера:

def run_https_server():    generate_ssl_certificates()      certfile = "fullchain.pem"    keyfile = "privkey.pem"      config = Config(fastapi_app, host="0.0.0.0", port=443,                    ssl_keyfile=keyfile, ssl_certfile=certfile)    server = Server(config=config)    try:        server.run()    except Exception as e:        print(f"Server error on port 443: {e}")

Дальше запускаем в отдельных процессах, чтобы не было конфликтов (пробовал асинхрон — не вышло):

multiprocessing.freeze_support() http_process = multiprocessing.Process(target=run_http_server) https_process = multiprocessing.Process(target=run_https_server) daphne_process = multiprocessing.Process(target=run_daphne)   http_process.start() https_process.start() daphne_process.start()

run_daphne — скрипт запуска внутреннего сервера соответственно.

6.2. GitFlic-пайплайны, связанные со сборкой в Nuitka

Далее идет процесс сборки пакетов на CI/CD сервере.

6.2.1. Сборка elf-файла

build_nuitka_elf:  image: "python:3.8-buster"  stage: build  tags:    - at-docker  before_script:    - python -m venv venv    - source venv/bin/activate    - pip install --upgrade pip    - pip install --upgrade setuptools wheel    - apt -y update >/dev/null    - apt install -y gettext patchelf >/dev/null    - export PATH=$PATH:/usr/sbin    - pip install -r ${CI_PROJECT_DIR}/src/django_termidesk_assistant/requirements.txt    - pip install uvicorn==0.17.6 fastapi==0.78.0 starlette==0.19.1    - cd ${CI_PROJECT_DIR}/src/django_termidesk_assistant    - python manage.py makemigrations >/dev/null    - python manage.py migrate >/dev/null    - python manage.py createcachetable >/dev/null    - python manage.py collectstatic --noinput >/dev/null    - python manage.py compilemessages >/dev/null  script:    - cd ${CI_PROJECT_DIR}    - export PYTHONPATH=$PYTHONPATH:${CI_PROJECT_DIR}/src/django_termidesk_assistant    - export DJANGO_SETTINGS_MODULE=django_termidesk_assistant.settings    - pip install nuitka==1.5.4    - python -m nuitka      --remove-output      --follow-imports      --include-module=django_termidesk_assistant.settings      --include-package=django_termidesk_assistant      --include-package=termidesk_assistant      --include-package=signalling      --include-module=termidesk_assistant.middleware      --include-package=django      --include-module=django.core.management      --include-package=channels      --include-package=channels_redis      --include-package=fakeredis      --include-package=asgiref      --include-package=django.templatetags.i18n      --include-package=daphne      --include-package=rest_framework      --include-module=django_termidesk_assistant.asgi      --include-package=django_structlog      --include-package=gettext      --include-package=uvicorn      --include-package=fastapi      --include-package=starlette      --include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/termidesk_assistant=termidesk_assistant      --include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/static=static      --include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/locale=locale      --include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/var/db=var/db      --include-data-dir=${CI_PROJECT_DIR}/src/django_termidesk_assistant/var/log=var/log      --output-dir=${CI_PROJECT_DIR}/dist      --onefile      --onefile-tempdir-spec=/tmp/onefile_%PID%_%TIME%      --output-filename=termidesk_assistant_server.bin      ${CI_PROJECT_DIR}/src/django_termidesk_assistant/run_servers.py  artifacts:    expire_in: 1 day    paths:      - dist/

Давайте разберем построчно (ну, почти).

Выбираем образ докера python:3.8-buster, так как версии выше вызывают ошибку glibc на Astra Linux: glibc_2.33′ not found. К тому же наша кодовая база написана для python 3.7, потому что в официальных репозиториях Astra Linux SE последняя доступная версия Python это 3.7.3, как и включенный в ОС интерпретатор.

Далее мы активируем виртуальное окружение и ставим patchelf, необходимый для сборки elf-файлов.

Можно заметить, что uvicorn, fastapi и starlette ставятся отдельно, я специально их вынес из requirements.txt, потому что они нужны исключительно для сборки в Nuitka и кастомизированного прокси-сервера, а у нас еще есть другие версии Termidesk Assistant.

Потом мы проходимся по стандартным Django-командам, которые нужны перед запуском любого Django-приложения.

Давайте перейдем к сборке. Мы ставим Nuitka версии 1.5.4, поскольку эмпирически было выяснено, что она оптимально подходит для python 3.8 и тех библиотек, что у нас поставлены для кодовой базы python 3.7. Пройдемся по самим аргументам Nuitka:
—remove-output — удаление C-файлов из python-модуля после сборки.

—follow-imports — компиляция зависимостей вместе с проектом.

—include-module и —include-package — если есть ошибка во время сборки, то указываем нужный нам импорт. В моем случае через —include-package я включал также внутренние приложения проекта.

—include-data-dir — все нужные для приложения папки, в основном это статические файлы.

—output-dir — папка, в которой будет лежать наш билд-файл внутри докер-контейнера.

—onefile — формат билда, в нашем случае это единый файл без зависимых папок и файлов.

—onefile-tempdir-spec — путь до папки, в которой будут храниться временные файлы во время запуска билда, например, файлы кэша.

—output-filename — имя билд-файла, ограничений по названию нет.

Далее ставим путь до скрипта входа, в моем случае это скрипт запуска процессов серверов.

Далее в artifacts указываем хранение файла сборки на CI/CD — сервере и папку, в которой будет лежать файл внутри zip-файла.

6.2.2. Сборка deb-файла

build_nuitka_deb_package:  image: "python:3.8-buster"  stage: package  tags:    - at-docker  dependencies:    - build_nuitka_elf  before_script:    - apt-get update    - apt-get install -y dpkg-dev  script:    - |      if [ -z "${CI_COMMIT_TAG}" ]; then        export PACKAGE_VERSION=$(date +%Y%m%d%H%M%S)      else        export PACKAGE_VERSION=${CI_COMMIT_TAG}      fi    - export PACKAGE_NAME="termidesk-assistant-server${PACKAGE_VERSION}"    - mkdir -p ${PACKAGE_NAME}/DEBIAN    - mkdir -p ${PACKAGE_NAME}/usr/local/bin    - mkdir -p ${PACKAGE_NAME}/etc/systemd/system    - mkdir -p ${PACKAGE_NAME}/var/lib/termidesk-assistant    - mkdir -p ${PACKAGE_NAME}/var/log/termidesk-assistant    - cp dist/termidesk_assistant_server.bin ${PACKAGE_NAME}/usr/local/bin/    - chmod +x ${PACKAGE_NAME}/usr/local/bin/termidesk_assistant_server.bin      - |      cat << EOF > ${PACKAGE_NAME}/DEBIAN/control      Package: termidesk-assistant-server      Version: ${PACKAGE_VERSION}      Maintainer: Release Team <release@uveon.ru>      Architecture: amd64      Section: non-free/admin      Priority: optional      Description: Termidesk Assistant Server      Homepage: http://uveon.ru/      EOF      - |      cat << EOF > ${PACKAGE_NAME}/etc/systemd/system/termidesk-assistant.service      [Unit]      Description=Termidesk Assistant Server      After=network.target        [Service]      ExecStart=/usr/local/bin/termidesk_assistant_server.bin      Restart=on-failure      RestartSec=5           User=root      Group=root      Environment=PATH=/usr/bin:/usr/local/bin      WorkingDirectory=/var/lib/termidesk-assistant           StandardOutput=append:/var/log/termidesk-assistant/service.log      StandardError=append:/var/log/termidesk-assistant/service.log        [Install]      WantedBy=multi-user.target      EOF      - |      cat << EOF > ${PACKAGE_NAME}/DEBIAN/postinst      #!/bin/bash      set -e        # Log file for installation      LOG_FILE="/var/lib/termidesk-assistant/install.log"        log() {          echo "\$(date): \$1" >> \$LOG_FILE      }        log "Starting postinst script"        # Create necessary directories      mkdir -p /var/lib/termidesk-assistant /var/log/termidesk-assistant      chown root:root /var/lib/termidesk-assistant /var/log/termidesk-assistant      chmod 755 /var/lib/termidesk-assistant /var/log/termidesk-assistant      log "Created necessary directories"        # Reload systemd to recognize the new service      systemctl daemon-reload      log "Systemd reloaded"        # Enable the service to start on boot      systemctl enable termidesk-assistant.service      log "Service enabled"        # Start the service      systemctl start termidesk-assistant.service      log "Service start command issued"        # Check if the service is running      sleep 5  # Give the service a moment to start      if systemctl is-active --quiet termidesk-assistant.service; then          log "Service is running successfully"      else          log "Service failed to start. Check systemctl status termidesk-assistant.service for more info"          systemctl status termidesk-assistant.service >> \$LOG_FILE 2>&1          journalctl -u termidesk-assistant.service --no-pager >> \$LOG_FILE      fi        # Check if the service is enabled      if systemctl is-enabled --quiet termidesk-assistant.service; then          log "Service is enabled for autostart"      else          log "WARNING: Service is not enabled for autostart"      fi        log "Postinst script completed"        exit 0      EOF      - chmod 755 ${PACKAGE_NAME}/DEBIAN/postinst      - dpkg-deb --build ${PACKAGE_NAME}  artifacts:    expire_in: 1 day    paths:      - ./*.deb

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

dependencies:    - build_nuitka_elf

Кратко, так выглядит зависимость второго пайплайна от первого:

Далее ставим необходимый пакет dpkg-dev для сборки deb-файла.
Перейдем к части script пайплайна:

Вводим переменную PACKAGE_NAME, основанную на имени приложения, тега коммита и времени. Создаем необходимые для deb-файла директории, в том числе и логи. Копируем наш elf-файл из предыдущего пайплайна.

А теперь пишем файлы, без которых наш результат будет нежизнеспособен:

сontrol — описание deb-файла.

service — окружение, файл, на котором будет основан deb-файл, пользователь и группы пользователей, которым дан доступ, рабочая папка и т.д.

postinst — команды, которые должны быть исполнены после установки, в моем случае это создание папки логов, запуск самого сервиса приложения, создание и запись в лог сборки файла.

В artifacts указываем аналогично хранение в один день и сохранение файла с расширением .deb.

7. Резюмируем

В этой статье мы разобрали преимущества и недостатки Nuitka перед ее аналогами, такими как Pyinstaller и Docker. Выяснили, какие существуют варианты интеграции Nuitka в ваше приложение. Пошагово рассмотрели, как собрать исполняемый и установочный файлы на моем примере с Termidesk Assistant.

Если у вас остались какие-то вопросы, пишите в комментариях, с радостью отвечу!


ссылка на оригинал статьи https://habr.com/ru/articles/872510/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *