QA Fest — конференция №1 по тестированию и автоматизации. Финальная программа уже на сайте >>
×Закрыть

Создаем приложение: Docker, VueJs и Python-Sanic. Часть 2

В предыдущей статье мы рассмотрели, как быстро поднять Docker окружение для разработки, используя возможности docker-compose. В этой статье окунемся в разработку backend-а и «контейнеризируем» API, написанное на Python.

Итак, уточним задачу, которую предстоит реализовать в API:

  1. Пусть новое приложение по ссылке /api/v1.0/user/auth принимает POST запрос с параметрами username и password и, в случае совпадения данных доступа с учетной записью в таблице users, возвращает token идентификации.
  2. Данные об авторизованном пользователе (users.id) пишем в Redis, где ключом (key) будет token, созданный в пункте 1.

Этап 1. Небольшая автоматизация рутины

Во время разработки я довольно часто пользуюсь возможностями утилиты make, позволяющей намного сократить «вербальность» некоторых команд. Для этого в корне проекта создаю Makefile с набором удобных инструкций-сокращений. Приведу список наиболее полезных для меня:

make up — просто запускает, а make upb — еще и полностью перестраивает перед запуском все существующие контейнеры.

make stop — останавливает все работающие контейнеры.

make db — подключаемся к БД PostgreSQL при помощи консольного клиента pgSQL. Обратите внимание, что мы в первой строчке Makefile «заинклюдили» все переменные окружения, которые у нас есть в .env файле. Поэтому здесь могут быть довольно экзотические инструкции, список которых ограничен лишь фантазией разработчика. К примеру, у меня ещё здесь находятся инструкции по деплою кода на продакшен при помощи ansible).

make r — быстрый способ посмотреть значения в базе данных Redis через консольную утилиту redis-cli.

make test — запускает unit/integrity tests, которые нам еще предстоит написать для нашего /api.

Последняя значимая make-инструкция — это (пример) make b c=test_api — подключиться к bash-консоли любого контейнера с явно указанным именем.

Этап 2. Создаем минимально рабочее API

Итак:

Перед тем как мы начнем реализовать логику API, нам необходимо сделать минимально работающее Sanic-приложение, контейнеризировать его и настроить проксирование всех запросов, начинающихся на /api в контейнере с nginx на наш новый контейнер. Для этого «утащим» пример hello world отсюда и слегка модифицируем его. В каталоге api нашего проекта, создаем файл run.py со следующим содержимым:

Теперь нам нужно запустить run.py внутри контейнера. Для этого создаем api/Dockerfile со следующим содержимым:

В этом файле мы указываем компоновщику, что для создания нашего контейнера нужно взять последний Docker образ Python версии 3.6.7. Далее создаем рабочий каталог /app для нашего микросервиса.

Отступление. Я взял за правило помещать весь рабочий код приложения в корневую папку /app контейнера. Аналогично будет и в примере с клиентским app, которое будет реализовано в следующей части.

Далее идет установка дополнительного пакета gcc, без которого не получится инсталлировать некоторые pip-пакеты из api/requirements.txt, которые мы указали чуть выше, при его создании. Далее ничего особенного, перейдем к настройке файла docker-compose.yml.

Этап 3. Добавляем api в docker-compose.yml

Здесь в 4-й строке мы «заставляем» docker-compose изменить контекст и пройти по указанному адресу для чтения инструкций, созданному нами во 2-м этапе «Dockerfile», который создает контейнер с работающим приложением. Особо важной для нас, как для разработчиков, является директива tty, которая позволяет подключаться (docker attach) к консоли вывода работающего процесса в контейнере. Эта жизненно необходимая для dev разработки инструкция позволит нам «дебажить» и «тюнинговать» наше приложение.

Кроме того, мы указали что наш сервис «связан» (links) c контейнерами db и redis через подсеть (network) internal.

В строчке 7 мы «пробрасываем» папку api «внутрь» контейнера (механизм volumes).

В строчке 14, как я уже рассказывал в первой части, мы пробрасываем и делаем доступными для использования приложением переменных окружения из файла .env, созданного в прошлой части. Одна из этих переменных (API_MODE), уже используется в run.py.

Этап 4. Изменяем конфигурацию nginx

Всё, что нам осталось сделать, — добавить правила проксирования для сервиса nginx. Для этого отредактируем файл nginx/server.conf, добавив:

Теперь пересоздадим контейнеры с учетом новых изменений.

Этап 5. Остановка и перестройка контейнеров

С учетом вышеописанного в статье, у нас всё готово для запуска:

В браузере вбиваем http://localhost/api, результатом должно быть {"hello":"world«}

Этап 6. Как этим пользоваться

Благодаря переменной окружения API_MODE=dev из файла .env, мы запустили приложение в режиме debug. Фреймворк sanic, как и множество других Python Web-фреймворков, поддерживает hot-reload. То есть сервер приложения автоматически будет перезапускаться, как только мы изменим один из файлов, связанных с run.py. Проверить это довольно легко: исправьте «world» на «world!» в коде run.py и тут же обновите страницу браузера. Вы увидите, что информация обновилась.

Но перезагрузки кода не достаточно. Нам необходимо «дебажить» код и где-то видеть логи приложения. Для этого существуют две команды:

Первая из них подключается к консольному выводу работающего процесса (чтобы отключиться Ctrl+C), куда мы, при помощи стандартного пакета logging (или просто print), можем выводить любую debug-информацию. Вторая команда необходима для тех случаев, когда мы ловим fatal, «не совместимый с жизнью самого процесса». Вывод этой команды показывает нам backtrace, предшествовавший ошибке.

На заметку. Я выше обращал внимание на инструкцию tty=true. Как раз она позволяет делать «безболезненный» detach по Ctrl+C, не уничтожая сам процесс.

Этап 7. Авторское отступление

Лично я, когда программирую backend, то консоль с выводом (docker attach test_api) у меня открыта в терминальном окне редактора кода. Еще, для поддержки технологии intellisense, которая есть во многих популярных редакторах, я внутри папки api создаю виртуальное окружение .venv, в которое устанавливаю те же пакеты из requirements.txt, что установлены в самом контейнере:

В корневой директории в файле .gitignore указана инструкция пропуска всех папок и файлов, начинающихся с «.», поэтому можно не переживать, что «мусор» попадет в репозиторий.

Этап 8. Реализация логики

В рамках статьи невозможно делать полный копипаст кода, который я написал (много файлов и большой объем), поэтому, как говорил в прошлой статье, будет ссылка на pull request, где можно посмотреть изменения, которые были детально описаны выше. Прокомментирую наиболее важные моменты.

Был добавлен файл api/application.py, содержащий функцию-фабрику, которую будем использовать в 2-х местах: в run.py для сервера приложения и в conftest.py — для создания экземпляра (fixture) приложения, для написания интеграционного теста. Хотел бы обратить особое внимание на функцию db_migrate. Фактически она делает то же самое, что и расширение alembic, которое помогает делать миграцию схемы БД между различными ветками кода. Я реализовал схему миграции следующим образом:

Немного пояснений. В момент перезапуска сервера система «смотрит» в БД в поисках значения таблицы version. Если такой таблицы нет, текущая версия определяется как «0» и приложение автоматически запускает все скрипты из ветки «up» до тех пор, пока значение ключа из SCHEMA не станет равным LATEST_VERSION. Последняя успешная версия фиксируется в version. Обратный процесс аналогичен. Изменяя значение LATEST_VERSION, функция миграции «спускается» вниз на любую версию. При этом она запускает все скрипты из «down», вплоть до полной очистки базы данных, при условии, если LATEST_VERSION указываем равным «0».

Еще хотелось бы обратить внимание на то, что доступ к версиям api реализован при помощи механизма blueprint, который выбран из-за возможной версионности api в будущем.

На заметку. Когда пишем интеграционные тесты, то тестируем URL локального сервера, то есть тестируем /v1.0/user/auth. Но когда в следующей статье перейдем к реализации frontend, мы будем обращаться к «api» через прокси-сервер nginx в формате /api/v1.0/user/auth.

Этап 9. Запуск

Следующая статья будет завершающей. В ней напишем WebSocket сервер fronted на VueJs и заставим всё это работать вместе.

Предыдущая статья: Создаем приложение: Docker, VueJs и Python-Sanic. Часть 1

Следующая статья: Создаем приложение: Docker, VueJs и Python-Sanic. Часть 3

LinkedIn

24 комментария

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Инструкцию links в Compose файле уже можно не использовать.
docs.docker.com/...​mpose/compose-file/#links
The —link flag is a legacy feature of Docker. It may eventually be removed.
Links are not required to enable services to communicate — by default, any service can reach any other service at that service’s name.

Спасибо, полезное замечание!

спасибо за статью. но как человек, считающий что асинхронность — это паралельность для «бедных», не могу не спросить ;)
1. зачем асинхронность на сыром фреймворке вместо фласк или даже джанго? в чем польза от велосипедов с тем же рестом(если для обучения — не вопрос, но если вы это хотите в прод...)
2. у вас примере ручные миграции бд... возникает вопрос — что опять?!!! т.е мы и не такое видели, но смысл? код вида

            _user = await conn.fetchrow('''
            SELECT * FROM users WHERE email=$1
            ''', res['email'])
отдает мхом из 90х ;)
3. docker-compose.yml это все забавно для локалхоста, но чисто интересно, как это в «продакшен» деплоить?

в общем не понял, где плюсы такого решения... если они есть, с интересом о них узнаю :) а пока видиться только переусложненная схема вида «смотрите, как я могу» )))

Очень хорошие вопросы, и много! Возможно не очевидно, но в статьях я пытаюсь построить систему на базе микросервисов, исходя из этого:
1. Асинхронность питона позволяет , сильно повысить производительность вебсервера, и упростить код выбросив местами Celery (или Rq).

2. Я много писал под Sqlalchemy на Flask используюя в качестве миграции Alembic. В микросервисной архитектуре, где всё приложение максимум 500 строк кода, нужно оптимизировать по максимум всё что можно. Все что сложнее, считаю избыточным и не нужным, это ИМХО.

3. Лучше деплоиться при помощи ansible. У меня в плейбуке ansible находится всё, что нужно для работы продакшен сервера, вплоть до настройки опенвпн-сервера и браундмена который держит открытыми только ssh(22 порт) и 80. Схема не переусложненная, и стоит один раз сесть и разобраться как это работает, чтобы понять насколько docker это эврика :)

1. воу, воу, полегче ) вы пишете про обычный веб апи, в котором сколько бы реквест времени ни потребовал, вы все равно что-то ответите клиенту... то что сервер при этом асинхронный, помогает только когда реквесты сильно зависят от ввода-вывода... что никак не заменяет celery в вашем деплое, если вам нужны будут какие-либо бекграунд таски...
2.1.

микросервисной архитектуре

вы тут себе идола не сотворили? ;) панацеей это считалось 10 лет назад, но нет не все так просто )
2.2.

нужно оптимизировать по максимум всё что можно

вот тут хочется узнать критерий оптимизации :) если по кол-ву написанного кода, то вы сольетесь любому ORM. если по безопасности использования — тоже, потому как sql injection никто не отменял, параметры приводить в нужно типу — будут не покрытые тестами велосипеды(банально, как вы boolen будете из базы в веб преобразовывать и обратно? true/True/1??? а тот кто будет писать с вами точно выберет такой же вариант как вы? и вы же задокументируете все корнер кейсы? ))) Про скорость запросов тоже не надо — дочитав доку по вашему орму вы должны понимать какой запрос получится в базу уйдет(в крайнем случае под дебагом посмотрите), что в итоге сводиться к следующему — или вы понимаете как с базой работать или не в орме дело ))) в общем, ваш критерий не очевиден...
3.

Лучше деплоиться при помощи ansible

это очень смело ))) как человек, деплоивший через ansible и SaltStack 5 лет назад, ответственно заявляю — в клауде они не нужны, оба ))) ансибл вообще, тупо замена баша архитектурно, что естественно большой шаг вперед, если до этого было на баше или вообще «руками» на сервере... но как бы в 3м тысячелетии это несколько устаревший подход... для деплоя «не в клауде» оно явно лучше, чем ничего, но осилившие SaltStack знают, что ансибл на пару тысяч серваков не заскейлится ))) большая его беда — он держит коннект во время всех операций, которые транзакционными не являются и когда коннект таки порвется, то что делать? куда бежать? ;) в общем, ансибл — тулза для оптимистов с 10-20ю серваками :) в AWS клауде я лично попробовал Terraform(так сяк, но для AWS не все запилено и мы его выкинули из своих проектов) и Cloudformation(его сильно допилили за последние пару лет, и если еспользовать yaml а не дефолтный ранее json, то на него можно без слез смотреть). Ваша фраза про докер намекает, что вам показалось, будто я не осилил эту эврику ))) но начав возиться с ним 5 лет назад, я претендую на некоторое понимание ))) и моя заметка была про то что docker-compose является базовой вещью для «наколеночных тестов» и на 100не нод вам не поможет... а вот всякие кубернетисы и жменя вариантов в клауде — вполне...

P.S. прошу не считать данный опус критикой :) все выше написанное IMHO, за которым стоят некоторые аргументы но, возможно,не всем они важны...

Про ansible интересно — как раз собирался все перевести из баша на ansible. Но возможно, ваш опыт действительно больше релевантен к «сотням серверов»? ;) Big scale, high load и все такое — у меня например докер вместе с compose крутится с 2015 года на парочке серверов в продакшне без всяких проблем. Я бы даже сказал что docker там используется как продвинутый virtualenv и не более — без всяких автоскейлингов и прочих свармов из «3-го тысячелетия» :)

Все что вы пишете, абсолютно справедливо в контексте большого проекта, но.... Но в цикле статей, я пишу про фулстек разработку, одиночки, если хотите, это будни фриланса, где нет облачных деплоев, а есть digitalocean/vultr и это спошь и рядом. Далее: Если Ansible не подходит для вашего проекта (вы его вообще с bash по функциональности приравняли) , то это не значит, что он для оптимистов которых обманули «творцы». Мне приходилось делать один заказ, который и по сей день деплоит код на 150+ компьютеров с опцией async: 50, да, это не сверхсложная система перелинкованных сервисов, а специфическое ПО, которое банально продублирвоано на всех серверах, но к этому просто голову надо прикладывать и понимать для чего оно нужно.

Автор сам себе devops. Спасибо за статью.

Дякую за статтю, цікаво подивитись на повний солюшн
В докерфайлі краще навіть так:
RUN apt-get update && apt-get -y install gcc

В данном конкретном случае возможно и да — но общая практика писать каждый RUN своей отдельной строчкой по причине кэширования при билде контейнера. Когда нужно часто перестраивать контейнер и возникает потребность поменять какую-либо зависимость, то при длинном списке в одну строку apt-get -y install x1 x2 x3 будет перестраиваться все и это может быть очень долго

нет такой общей практики нигде. Большинство официальных докерфайлов выглядит вот так: github.com/...​/master/bionic/Dockerfile

Что подтверждает полностью то что я написал выше про кэширование. Но я предпочитаю копипастить RUN  в каждой строке вместо табов и обратных слэшэй

угу, і 40 слоїв образа замість 5-10

Поведайте чем это плохо?

1. не відповідає загальній практиці офіційних докерфайлів :)
2. ліміт в 42 слоя (він ще є доречі?)
3. швидкодія білда. Так, якщо пакет добавиться у список, необхідно перезібрати все. Але це не так часто робиться. З іншої сторони образ не повинен довго жити, до пакетів виходять оновлення безпеки, і все-одно весь той список потрібно збирати, щоб мати образ з актуальними патченими версіями. І без різниці, це apt-get install, чи pip install чи будь-який інших менеджер пакетів.
4. Для зменшення розміру також досить часто в ту саму команду RUN установки пакетів в кінці добавляється ; apt-get clean для мінімізації розміру слоя

1. Not true, дивiться вище де про «большинство официальных докерфайлов» — one dependency per line
2. Не знав :) Але наче вже не актуально github.com/shykes/docker/pull/66
3. Це якщо ви devops, а якщо я дев й менi треба тут й зараз — неактуально
4. Розмiр — ок аргумент, хоча дуже залежить з огляду на вартiсть стораджу яка прямуэ до нуля з кожним роком

1. так якраз зверху йде одним RUN-ом все, як і у всіх офіційних докерфайлах
3. якщо вам потрібна установка 20 пакетів apt-get’ом, то в одному рані, можна через бекслеш і з нового рядка кожен пакет воно збереться набагато швидше, ніж 20 рядків RUN apt-get install ...
4. суть в швидкодії і в швидкості доставки.

не бачу змісту продовжувати ці дебати, ми все-одно залишимось кожен при своїй думці :)

ми про одне й те й саме насправдi)

Нет никаких проблем в том, что лично вы предпочитаете копипастить. Я только хочу сказать, чтобы публика не входила в заблуждение, что ваше утверждение «общая практика писать отдельной строчкой» — неправда, такой практики нет

«Общая практика» == «Большинство официальных докерфайлов», не вижу никаких противоречий.

эм, мы наверное о разных вещах говорим. Вы понимаете различие в семантике между «5 строчек, разделенных обратным слешом и переводом строки» и «5 строчек, разделенных переводом строки, каждая начинающаяся командой RUN»?

да, судя по всему о разных

Да, действительно — был не прав. «Общей» практикой таковое не является. Тем не менее, вполне имеет место быть при активной разработке приложения/контейнера.

Спасибо. Ждем финальную часть.

Подписаться на комментарии