Привет, Хабр!
Меня зовут Данил, и я старший специалист в компании Увеон. Занимаюсь серверной частью 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 есть несколько потенциальных преимуществ:
-
Производительность
Nuitka компилирует Python-код в нативный машинный код, что может привести к повышению производительности по сравнению с интерпретируемым Python-кодом. -
Размер исполняемого файла
Nuitka обычно создает меньшие по размеру исполняемые файлы по сравнению с PyInstaller, так как включает только необходимые зависимости. -
Защита исходного кода
Компиляция в машинный код затрудняет обратную разработку, обеспечивая некоторую защиту вашего исходного кода. -
Кроссплатформенность
Nuitka позволяет создавать исполняемые файлы для разных платформ, что может быть проще, чем управление Docker-контейнерами для разных окружений. -
Простота развертывания
В отличие от Docker, не требуется устанавливать и настраивать дополнительное ПО на целевой машине.
3. Недостатки Nuitka
-
Сложность конфигурации
Настройка Nuitka для корректной работы с Django и всеми зависимостями может быть сложнее, чем настройка Docker или PyInstaller. -
Время компиляции
Процесс компиляции с Nuitka может занимать значительно больше времени, чем создание Docker-образа или сборка с PyInstaller. -
Ограниченная поддержка динамических аспектов Python
Некоторые динамические функции Python (например, динамическая загрузка модулей) могут работать некорректно или требовать дополнительной настройки. -
Проблемы совместимости
Не все Python-библиотеки хорошо работают с Nuitka. -
Отладка
Отлаживать скомпилированное приложение может быть сложнее (дебаг тут невозможен, только print, грубо говоря), чем интерпретируемый код или код в Docker-контейнере. -
Обновления
Обновление приложения, собранного с Nuitka, требует полной пересборки, в то время как с Docker можно обновить только изменённые слои. -
Отсутствие изоляции
В отличие от Docker, Nuitka не обеспечивает изоляцию окружения, что может привести к конфликтам с системными библиотеками. Для полной изоляции я использовал аргумент onefile, но оно не предоставляет изоляцию на уровне ОС и также использует системные библиотеки и драйверы, как я это обходил — чуть позже. -
Меньшее распространение
Nuitka менее популярна в сообществе Django и Python вообще, чем Docker, что может затруднить поиск решений проблем и лучших практик. Поэтому я здесь 🙂 -
Ограниченная масштабируемость
Docker предоставляет более гибкие возможности для масштабирования приложений, особенно в контексте микросервисной архитектуры.
4. Резюмируем: сравнение Nuitka с Pyinstaller и Docker
Pyinstaller использует в своих пакетах питоновские скрипты и интерпретатор, в то время как Nuitka переводит код в машинный, что увеличивает скорость выполнения и повышает уровень защиты кода от чтения.
В отличие от Docker, Nuitka — открытое ПО. Это вызывает большую уверенность в безопасности у наших клиентов и меньше зависимости от политической обстановки.
5. Способы интеграции Nuitka в различные процессы разработки и развертывания
-
Использование в bash-скриптах— Автоматизация сборки проекта через shell-скрипты.- Пример:«`bash#!/bin/bashpython -m nuitka —follow-imports —standalone —output-dir=build myscript.py«`
-
Интеграция в 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’
}
}
«` -
Использование в setup.py— Интеграция Nuitka в процесс установки пакета:
«`python
from setuptools import setup
from nuitka.distutils_based_buildsystem.BuildSystem import Nuitkasetup(
name=»MyApp»,
cmdclass={«build_exe»: Nuitka},
# другие параметры…
)
«` -
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
«` -
Makefile— Упрощение процесса сборки:
«`makefile
build:
python -m nuitka —follow-imports —standalone myapp.pyclean:
rm -rf build/
«` -
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
«` -
Tox— Интеграция Nuitka в процесс тестирования:
«`ini
[testenv:build]
deps = nuitka
commands = python -m nuitka —follow-imports —standalone myapp.py
«` -
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$
«` -
Интеграция с системами управления пакетами— Создание пакетов для систем вроде 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/
Добавить комментарий