В предыдущей статьемы рассмотрели, как быстро поднять Docker окружение для разработки, используя возможности docker-compose. В этой статье окунемся в разработку backend-а и «контейнеризируем» API, написанное на Python.
Итак, уточним задачу, которую предстоит реализовать в API:
- Пусть новое приложение по ссылке /api/v1.0/user/authпринимает POST запрос с параметрами usernameи password и, в случае совпадения данных доступа с учетной записью в таблице users, возвращает token идентификации.
- Данные об авторизованном пользователе (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
Здесь в
Кроме того, мы указали что наш сервис «связан» (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, содержащий функцию-фабрику, которую будем использовать в
Немного пояснений. В момент перезапуска сервера система «смотрит» в БД в поисках значения таблицы 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 и заставим всё это работать вместе.