От нуля до героя: рецепты производства флаконов

Опубликовано: 2022-03-11

Как инженер по машинному обучению и эксперт по компьютерному зрению, я на удивление часто создаю API и даже веб-приложения с помощью Flask. В этом посте я хочу поделиться некоторыми советами и полезными рецептами для создания полного готового приложения Flask.

Мы рассмотрим следующие темы:

  1. Управление конфигурацией. Любое реальное приложение имеет жизненный цикл с определенными этапами — как минимум, это будет разработка, тестирование и развертывание. На каждом этапе код приложения должен работать в немного отличающейся среде, что требует наличия другого набора параметров, таких как строки подключения к базе данных, внешние ключи API и URL-адреса.
  2. Самостоятельное размещение приложения Flask с Gunicorn. Хотя Flask имеет встроенный веб-сервер, как мы все знаем, он не подходит для производства и должен быть размещен за реальным веб-сервером, способным взаимодействовать с Flask через протокол WSGI. Обычный выбор для этого — Gunicorn — HTTP-сервер Python WSGI.
  3. Обслуживание статических файлов и проксирование запросов с помощью Nginx. Будучи веб-сервером HTTP, Gunicorn, в свою очередь, является сервером приложений, не подходящим для работы в Интернете. Вот почему нам нужен Nginx в качестве обратного прокси и для обслуживания статических файлов. Если нам нужно масштабировать наше приложение на несколько серверов, Nginx также позаботится о балансировке нагрузки.
  4. Развертывание приложения внутри контейнеров Docker на выделенном сервере Linux. Контейнерное развертывание уже довольно давно является неотъемлемой частью разработки программного обеспечения. Наше приложение ничем не отличается и будет аккуратно упаковано в собственный контейнер (на самом деле несколько контейнеров).
  5. Настройка и развертывание базы данных PostgreSQL для приложения. Структурой базы данных и миграциями будет управлять Alembic, а SQLAlchemy обеспечивает объектно-реляционное сопоставление.
  6. Настройка очереди задач Celery для обработки длительных задач. Каждому приложению в конечном итоге потребуется это, чтобы разгрузить процессы, требующие больших затрат времени или вычислений — будь то отправка почты, автоматическое обслуживание базы данных или обработка загруженных изображений — с потоков веб-сервера на внешних исполнителей.

Создание приложения Flask

Начнем с создания кода приложения и ресурсов. Обратите внимание, что в этом посте я не буду рассматривать правильную структуру приложения Flask. Демонстрационное приложение состоит из минимального количества модулей и пакетов для краткости и ясности.

Сначала создайте структуру каталогов и инициализируйте пустой репозиторий Git.

 mkdir flask-deploy cd flask-deploy # init GIT repo git init # create folder structure mkdir static tasks models config # install required packages with pipenv, this will create a Pipfile pipenv install flask flask-restful flask-sqlalchemy flask-migrate celery # create test static asset echo "Hello World!" > static/hello-world.txt

Далее мы добавим код.

конфиг/__init__.py

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

 import os import sys import config.settings # create settings object corresponding to specified env APP_ENV = os.environ.get('APP_ENV', 'Dev') _current = getattr(sys.modules['config.settings'], '{0}Config'.format(APP_ENV))() # copy attributes to the module for convenience for atr in [f for f in dir(_current) if not '__' in f]: # environment can override anything val = os.environ.get(atr, getattr(_current, atr)) setattr(sys.modules[__name__], atr, val) def as_dict(): res = {} for atr in [f for f in dir(config) if not '__' in f]: val = getattr(config, atr) res[atr] = val return res

конфиг/settings.py

Это набор классов конфигурации, один из которых выбирается переменной APP_ENV . Когда приложение запускается, код в __init__.py создает экземпляр одного из этих классов, переопределяя значения полей с помощью определенных переменных среды, если они присутствуют. Мы будем использовать окончательный объект конфигурации при инициализации конфигурации Flask и Celery позже.

 class BaseConfig(): API_PREFIX = '/api' TESTING = False DEBUG = False class DevConfig(BaseConfig): FLASK_ENV = 'development' DEBUG = True SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy' CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//' CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//' class ProductionConfig(BaseConfig): FLASK_ENV = 'production' SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy' CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//' CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//' class TestConfig(BaseConfig): FLASK_ENV = 'development' TESTING = True DEBUG = True # make celery execute tasks synchronously in the same process CELERY_ALWAYS_EAGER = True

задачи/__init__.py

Пакет задач содержит код инициализации Celery. Пакет config, в котором уже будут скопированы все настройки на уровне модуля при инициализации, используется для обновления объекта конфигурации Celery на тот случай, если в будущем у нас появятся некоторые настройки, специфичные для Celery, например, запланированные задачи и тайм-ауты рабочих процессов.

 from celery import Celery import config def make_celery(): celery = Celery(__name__, broker=config.CELERY_BROKER) celery.conf.update(config.as_dict()) return celery celery = make_celery()

задачи/celery_worker.py

Этот модуль необходим для запуска и инициализации воркера Celery, который будет работать в отдельном контейнере Docker. Он инициализирует контекст приложения Flask, чтобы иметь доступ к той же среде, что и приложение. Если это не требуется, эти строки можно безопасно удалить.

 from app import create_app app = create_app() app.app_context().push() from tasks import celery

API/__init__.py

Далее идет пакет API, который определяет REST API с помощью пакета Flask-Restful. Наше приложение — всего лишь демо и будет иметь только две конечные точки:

  • /process_data — Запускает фиктивную длинную операцию на воркере Celery и возвращает идентификатор новой задачи.
  • /tasks/<task_id> — возвращает статус задачи по идентификатору задачи.
 import time from flask import jsonify from flask_restful import Api, Resource from tasks import celery import config api = Api(prefix=config.API_PREFIX) class TaskStatusAPI(Resource): def get(self, task_id): task = celery.AsyncResult(task_id) return jsonify(task.result) class DataProcessingAPI(Resource): def post(self): task = process_data.delay() return {'task_id': task.id}, 200 @celery.task() def process_data(): time.sleep(60) # data processing endpoint api.add_resource(DataProcessingAPI, '/process_data') # task status endpoint api.add_resource(TaskStatusAPI, '/tasks/<string:task_id>')

модели/__init__.py

Теперь мы добавим модель SQLAlchemy для объекта User и код инициализации ядра базы данных. Объект User не будет использоваться нашим демо-приложением каким-либо осмысленным образом, но он нам понадобится, чтобы убедиться, что миграция базы данных работает, а интеграция SQLAlchemy-Flask настроена правильно.

 import uuid from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class User(db.Model): id = db.Column(db.String(), primary_key=True, default=lambda: str(uuid.uuid4())) username = db.Column(db.String()) email = db.Column(db.String(), unique=True)

Обратите внимание, как UUID генерируется автоматически как идентификатор объекта по умолчанию.

app.py

Наконец, давайте создадим основной файл приложения Flask.

 from flask import Flask logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s]: {} %(levelname)s %(message)s'.format(os.getpid()), datefmt='%Y-%m-%d %H:%M:%S', handlers=[logging.StreamHandler()]) logger = logging.getLogger() def create_app(): logger.info(f'Starting app in {config.APP_ENV} environment') app = Flask(__name__) app.config.from_object('config') api.init_app(app) # initialize SQLAlchemy db.init_app(app) # define hello world page @app.route('/') def hello_world(): return 'Hello, World!' return app if __name__ == "__main__": app = create_app() app.run(host='0.0.0.0', debug=True)</td> </tr> <tr> <td>

Мы здесь:

  • Настройка базовой регистрации в правильном формате с указанием времени, уровня и идентификатора процесса
  • Определение функции создания приложения Flask с инициализацией API и «Hello, world!» страница
  • Определение точки входа для запуска приложения во время разработки

wsgi.py

Также нам понадобится отдельный модуль для запуска приложения Flask с помощью Gunicorn. В нем будет всего две строки:

 from app import create_app app = create_app()

Код приложения готов. Наш следующий шаг — создать конфигурацию Docker.

Создание контейнеров Docker

Для запуска нашего приложения потребуется несколько контейнеров Docker:

  1. Контейнер приложения для обслуживания шаблонных страниц и предоставления конечных точек API. Это хорошая идея разделить эти две функции в рабочей среде, но в нашем демонстрационном приложении нет шаблонных страниц. Контейнер будет запускать веб-сервер Gunicorn, который будет взаимодействовать с Flask через протокол WSGI.
  2. Рабочий контейнер Celery для выполнения длительных задач. Это тот же контейнер приложения, но с пользовательской командой запуска для запуска Celery вместо Gunicorn.
  3. Контейнер Celery beat — аналогичный описанному выше, но для задач, вызываемых по регулярному расписанию, таких как удаление учетных записей пользователей, которые никогда не подтверждали свою электронную почту.
  4. Контейнер RabbitMQ. Celery требует брокера сообщений для связи между работниками и приложением и сохранения результатов задач. RabbitMQ — распространенный выбор, но вы также можете использовать Redis или Kafka.
  5. Контейнер базы данных с PostgreSQL.

Естественный способ легко управлять несколькими контейнерами — использовать Docker Compose. Но сначала нам нужно создать Dockerfile для создания образа контейнера для нашего приложения. Давайте поместим его в каталог проекта.

 FROM python:3.7.2 RUN pip install pipenv ADD . /flask-deploy WORKDIR /flask-deploy RUN pipenv install --system --skip-lock RUN pip install gunicorn[gevent] EXPOSE 5000 CMD gunicorn --worker-class gevent --workers 8 --bind 0.0.0.0:5000 wsgi:app --max-requests 10000 --timeout 5 --keep-alive 5 --log-level info

Этот файл указывает Docker:

  • Установите все зависимости, используя Pipenv
  • Добавьте папку приложения в контейнер
  • Откройте TCP-порт 5000 для хоста
  • Установите команду запуска контейнера по умолчанию на вызов Gunicorn.

Давайте подробнее обсудим, что происходит в последней строке. Он запускает Gunicorn, указав рабочий класс как gevent. Gevent — это легкая параллельная библиотека для совместной многозадачности. Это дает значительный прирост производительности при связанных нагрузках ввода-вывода, обеспечивая лучшее использование ЦП по сравнению с вытесняющей многозадачностью ОС для потоков. Параметр --workers — это количество рабочих процессов. Рекомендуется установить его равным количеству ядер на сервере.

Когда у нас есть Dockerfile для контейнера приложения, мы можем создать файл docker-compose.yml , в котором будут определены все контейнеры, которые потребуются приложению для запуска.

 version: '3' services: broker-rabbitmq: image: "rabbitmq:3.7.14-management" environment: - RABBITMQ_DEFAULT_USER=rabbit_user - RABBITMQ_DEFAULT_PASS=rabbit_password db-postgres: image: "postgres:11.2" environment: - POSTGRES_USER=db_user - POSTGRES_PASSWORD=db_password migration: build: . environment: - APP_ENV=${APP_ENV} command: flask db upgrade depends_on: - db-postgres api: build: . ports: - "5000:5000" environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration api-worker: build: . command: celery worker --workdir=. -A tasks.celery --loglevel=info environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration api-beat: build: . command: celery beat -A tasks.celery --loglevel=info environment: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration

Мы определили следующие услуги:

  • broker-rabbitmq — контейнер брокера сообщений RabbitMQ. Учетные данные для подключения определяются переменными среды.
  • db-postgres — контейнер PostgreSQL и его учетные данные.
  • migration — контейнер приложения, который будет выполнять миграцию базы данных с помощью Flask-Migrate и выход. Контейнеры API зависят от него и будут запускаться впоследствии.
  • api — основной контейнер приложения
  • api-worker и api-beat — контейнеры, на которых запущены рабочие процессы Celery для задач, полученных от API, и запланированных задач.

Каждый контейнер приложения также получит переменную APP_ENV из команды docker-compose up .

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

 git add * git commit -a -m 'Initial commit' git remote add origin [email protected]:your-name/flask-deploy.git git push -u origin master

Настройка сервера

Наш код сейчас находится на GitHub, и осталось только выполнить первоначальную настройку сервера и развернуть приложение. В моем случае сервер представляет собой экземпляр AWS, работающий под управлением AMI Linux. Для других разновидностей Linux инструкции могут немного отличаться. Я также предполагаю, что у сервера уже есть внешний IP-адрес, DNS настроен с записью A, указывающей на этот IP, и для домена выданы SSL-сертификаты.

Совет по безопасности: не забудьте разрешить порты 80 и 443 для HTTP(S)-трафика, порт 22 для SSH в консоли хостинга (или с помощью iptables ) и закрыть внешний доступ ко всем остальным портам! Обязательно сделайте то же самое для протокола IPv6 !

Установка зависимостей

Во-первых, нам понадобятся Nginx и Docker, работающие на сервере, а также Git для получения кода. Давайте войдем через SSH и установим их с помощью менеджера пакетов.

 sudo yum install -y docker docker-compose nginx git

Настройка Nginx

Следующим шагом будет настройка Nginx. Основной файл конфигурации nginx.conf часто хорош как есть. Тем не менее, обязательно проверьте, соответствует ли он вашим потребностям. Для нашего приложения мы создадим новый файл конфигурации в папке conf.d Конфигурация верхнего уровня имеет директиву для включения всех файлов .conf из нее.

 cd /etc/nginx/conf.d sudo vim flask-deploy.conf

Вот файл конфигурации сайта Flask для Nginx, включая батареи. Он имеет следующие особенности:

  1. SSL настроен. У вас должны быть действительные сертификаты для вашего домена, например, бесплатный сертификат Let's Encrypt.
  2. Запросы www.your-site.com перенаправляются на your-site.com
  3. HTTP-запросы перенаправляются на безопасный порт HTTPS.
  4. Обратный прокси настроен для передачи запросов на локальный порт 5000.
  5. Статические файлы обслуживаются Nginx из локальной папки.
 server { listen 80; listen 443; server_name www.your-site.com; # check your certificate path! ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; # redirect to non-www domain return 301 https://your-site.com$request_uri; } # HTTP to HTTPS redirection server { listen 80; server_name your-site.com; return 301 https://your-site.com$request_uri; } server { listen 443 ssl; # check your certificate path! ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; # affects the size of files user can upload with HTTP POST client_max_body_size 10M; server_name your-site.com; location / { include /etc/nginx/mime.types; root /home/ec2-user/flask-deploy/static; # if static file not found - pass request to Flask try_files $uri @flask; } location @flask { add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; proxy_read_timeout 10; proxy_send_timeout 10; send_timeout 60; resolver_timeout 120; client_body_timeout 120; # set headers to pass request info to Flask proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; proxy_redirect off; proxy_pass http://127.0.0.1:5000$uri; } }

После редактирования файла запустите sudo nginx -s reload и посмотрите, нет ли ошибок.

Настройка учетных данных GitHub

Рекомендуется иметь отдельную учетную запись VCS «развертывание» для развертывания проекта и системы CI/CD. Таким образом, вы не рискуете, раскрывая учетные данные своей учетной записи. Для дополнительной защиты репозитория проекта вы также можете ограничить разрешения такой учетной записи доступом только для чтения. Для репозитория GitHub вам понадобится учетная запись организации. Чтобы развернуть наше демонстрационное приложение, мы просто создадим открытый ключ на сервере и зарегистрируем его на GitHub, чтобы получить доступ к нашему проекту без необходимости каждый раз вводить учетные данные.

Чтобы создать новый ключ SSH, запустите:

 cd ~/.ssh ssh-keygen -b 2048 -t rsa -f id_rsa.pub -q -N "" -C "deploy"

Затем войдите на GitHub и добавьте свой открытый ключ из ~/.ssh/id_rsa.pub в настройках учетной записи.

Развертывание приложения

Последние шаги довольно просты — нам нужно получить код приложения с GitHub и запустить все контейнеры с помощью Docker Compose.

 cd ~ git clone https://github.com/your-name/flask-deploy.git git checkout master APP_ENV=Production docker-compose up -d

Может быть хорошей идеей опустить -d (который запускает контейнер в отсоединенном режиме) для первого запуска, чтобы увидеть вывод каждого контейнера прямо в терминале и проверить возможные проблемы. Другой вариант — после этого проверить каждый отдельный контейнер с помощью docker logs докеров. Давайте посмотрим, все ли наши контейнеры работают с docker ps.

image_alt_text

Здорово. Все пять контейнеров работают. Docker Compose автоматически присваивает имена контейнерам на основе службы, указанной в docker-compose.yml. Теперь пришло время, наконец, проверить, как работает вся конфигурация! Лучше всего запускать тесты с внешнего компьютера, чтобы убедиться, что сервер имеет правильные сетевые настройки.

 # test HTTP protocol, you should get a 301 response curl your-site.com # HTTPS request should return our Hello World message curl https://your-site.com # and nginx should correctly send test static file: curl https://your-site.com/hello-world.txt

Вот и все. У нас есть минималистичная, но полностью готовая к работе конфигурация нашего приложения, работающего на экземпляре AWS. Надеюсь, это поможет вам быстро приступить к созданию реального приложения и избежать некоторых распространенных ошибок! Полный код доступен в репозитории GitHub.

Заключение

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

В этой статье не рассматриваются:

  • Непрерывная интеграция и непрерывное развертывание
  • Автоматическое тестирование
  • Доставка журналов
  • API-мониторинг
  • Масштабирование приложения на несколько серверов
  • Защита учетных данных в исходном коде

Однако вы можете узнать, как это сделать, используя некоторые другие замечательные ресурсы в этом блоге. Например, чтобы изучить ведение журнала, см. Ведение журнала Python: подробное руководство, а общий обзор CI/CD и автоматизированного тестирования см. в разделе Как создать эффективный конвейер начального развертывания. Я оставляю их выполнение в качестве упражнения вам, читатель.

Спасибо за прочтение!