Создайте надежную непрерывную интеграцию с Docker и друзьями

Оглавление

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

В этом руководстве вы узнаете, как использовать Docker для создания надежного конвейера непрерывной интеграции для веб-приложения Flask. Вы пройдете этапы разработки и тестирования приложения локально, его контейнеризации, организации контейнеров с помощью Docker Compose и определения конвейера CI с помощью GitHub Actions. К концу этого руководства вы сможете создавать полностью автоматизированный конвейер CI для своих веб-приложений.

В этом руководстве вы узнаете:

  • Запустите сервер Redis локально в контейнере Docker
  • Доработать веб-приложение на Python , написанное на Flask
  • Создайте образов Docker и отправьте их в Docker Hub реестр
  • Управление многоконтейнерными приложениями с помощью Docker Compose
  • Реплицировать производственную инфраструктуру в любом месте
  • Определите рабочий процесс непрерывной интеграции с помощью Действий на GitHub

В идеале у вас должен быть некоторый опыт работы с веб-разработкой на Python, автоматизацией тестирования, использованием Redis с помощью Python и управление версиями исходного кода с помощью Git и GitHub. Предыдущее знакомство с Docker было бы плюсом, но не обязательно. У вас также должен быть клиент Git и учетная запись на GitHub, чтобы следовать инструкциям этого руководства и повторять их.

Примечание: Это руководство в общих чертах основано на более старом руководстве под названием Docker в действии - лучше, счастливее, продуктивнее, которое было автор: Майкл Герман, который представил свой CI workflow на PyTennessee 8 февраля 2015 года. Если вам интересно, вы можете просмотреть соответствующие слайдов, представленные на конференции.

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

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

Ознакомьтесь с архитектурой проекта

К концу этого руководства у вас будет веб-приложение Flask для отслеживания просмотров страниц, постоянно хранящееся в хранилище данных Redis. Это будет многоконтейнерное приложение, созданное с помощью Docker Compose, которое вы сможете создавать и тестировать как локально, так и в облаке, прокладывая путь к непрерывной интеграции:

The Architecture of the Page Tracker Application

Приложение состоит из двух контейнеров Docker. В первом контейнере будет запущено приложение Flask поверх Gunicorn, отвечающее на HTTP-запросы и обновляющее количество просмотров страниц. Во втором контейнере будет запущен экземпляр Redis для постоянного хранения данных о просмотре страниц в локальном томе на главном компьютере.

Для запуска этого приложения требуется только Docker, и сейчас вы его настроите.

Настройка Docker на Вашем компьютере

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

  • Docker, Inc.: Компания, стоящая за платформой и связанными с ней инструментами
  • Docker: Контейнерная платформа с открытым исходным кодом
  • Docker CLI: Клиентская программа командной строкиdocker
  • dockerd: Демон Docker , который управляет контейнерами

Существует также несколько инструментов и проектов, связанных с платформой Docker, таких как:

  • Docker Compose
  • Рабочий стол Docker
  • Движок Docker
  • Концентратор докеров
  • Режим Docker Swarm

В этом руководстве вы будете использовать все, кроме последнего, из приведенного выше списка. Кстати, не путайте устаревший Docker Classic Swarm, который был внешним инструментом, с Режимом Docker Swarm, встроенным в движок Docker начиная с версии 1.12.

Примечание: Возможно, вы слышали о Docker Machine и Docker Toolbox. Это старые инструменты, которые больше не обслуживаются.

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

У вас есть два варианта установки Docker:

  1. Механизм докера
  2. Рабочий стол Docker

Если вы чувствуете себя комфортно в терминале и цените дополнительный уровень контроля, то вам стоит обратить внимание на движок Docker с открытым исходным кодом , который предоставляет основную среду выполнения и интерфейс командной строки для управление вашими контейнерами. С другой стороны, если вы предпочитаете универсальное решение с интуитивно понятным графическим интерфейсом пользователя, то вам следует рассмотреть Рабочий стол Docker.

Примечание: В комплекте с настольным приложением поставляется Docker Compose, который понадобится вам позже, когда вы будете управление контейнерами для непрерывной интеграции.

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

Хотя технически возможно установить оба инструмента параллельно, вам, как правило, следует избегать их одновременного использования, чтобы свести к минимуму риск любых потенциальных помех между их виртуальными сетями или привязками портов. Вы можете сделать это, остановив один из них и переключив контекст между Docker Engine и Docker Desktop.

Примечание: Раньше Docker Desktop был доступен только в Windows и macOS, но теперь это изменилось, и вы можете установить его и в некоторые дистрибутивы Linux, включая Ubuntu, Debian и Fedora. Однако Linux-версия Docker Desktop работает поверх виртуальной машины , чтобы имитировать работу пользователя с ней в других операционных системах.

Чтобы убедиться, что вы успешно установили Docker в свою систему либо как движок Docker Engine, либо как приложение-оболочку Docker Desktop, откройте терминал и введите следующую команду:

$ docker --version
Docker version 23.0.4, build f480fb1

Вы должны увидеть свою версию Docker вместе с номером сборки. Если вы используете Linux, то, возможно, захотите выполнить шаги после установки, чтобы использовать команду docker, не предваряя ее символом sudo для получения административных привилегий.

Прежде чем вы сможете начать использовать Docker для обеспечения непрерывной интеграции, вам необходимо создать элементарное веб-приложение.

Разработать систему отслеживания просмотров страниц в Flask

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

A web application for tracking page views Веб-Приложение Для Отслеживания Просмотров Страниц

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

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

Скачать бесплатно: Нажмите здесь, чтобы загрузить ваше приложение Flask и связанные с ним ресурсы , чтобы вы могли определить конвейер непрерывной интеграции с Docker.

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

Прежде чем приступить к разработке приложения, вам необходимо настроить рабочую среду.

Подготовьте окружающую среду

Как и в случае с любым проектом на Python, при запуске вы должны выполнить примерно те же шаги, которые включают создание нового каталога, а затем создание и активацию изолированной виртуальной среды для вашего проекта. Вы можете сделать это непосредственно из вашего любимого редактора кода, такого как Visual Studio Code или полноценной среды IDE, такой как PyCharm, или вы можете ввести несколько команд в терминале :

S> mkdir page-tracker
PS> cd page-tracker
PS> python -m venv venv --prompt page-tracker
PS> venv\Scripts\activate
(page-tracker) PS> python -m pip install --upgrade pip

 

$ mkdir page-tracker/
$ cd page-tracker/
$ python3 -m venv venv/ --prompt page-tracker
$ source venv/bin/activate
(page-tracker) $ python -m pip install --upgrade pip

Сначала создайте новый каталог с именем page-tracker/, а затем создайте виртуальную среду Python с именем venv/ прямо внутри него. Предоставьте виртуальной среде приглашение с описанием, чтобы ее было легко распознать. Наконец, после активации вновь созданной виртуальной среды обновите pip до последней версии, чтобы избежать возможных проблем при установке пакетов Python в будущем.

Примечание: В Windows вам может потребоваться сначала запустить Windows Terminal от имени администратора и ослабить политика выполнения скрипта перед созданием виртуальной среды.

В этом руководстве вы будете использовать современный способ определения зависимостей и метаданных вашего проекта с помощью pyproject.toml файла конфигурации и инструментов настройки в качестве серверной части сборки. Кроме того, вы будете следовать инструкции src layout, поместив исходный код вашего приложения в отдельный подкаталог src/, чтобы лучше упорядочить файлы в вашем проекте. Это упростит упаковку вашего кода без автоматических тестов, которые вы добавите позже.

Продолжайте и создайте заполнитель вашего проекта Python, используя следующие команды:

page-tracker) PS> mkdir src\page_tracker
(page-tracker) PS> ni src\page_tracker\__init__.py
(page-tracker) PS> ni src\page_tracker\app.py
(page-tracker) PS> ni constraints.txt
(page-tracker) PS> ni pyproject.toml

 

(page-tracker) $ mkdir -p src/page_tracker
(page-tracker) $ touch src/page_tracker/__init__.py
(page-tracker) $ touch src/page_tracker/app.py
(page-tracker) $ touch constraints.txt
(page-tracker) $ touch pyproject.toml

Когда вы закончите, у вас должна быть следующая структура каталогов:

page-tracker/
│
├── src/
│   └── page_tracker/
│       ├── __init__.py
│       └── app.py
│
├── venv/
│
├── constraints.txt
└── pyproject.toml

Как вы можете видеть, у вас будет только один модуль Python, app, определенный в пакете с именем page_tracker, который находится внутри каталога src/. В файле constraints.txt будут указаны закрепленные версии зависимостей вашего проекта, чтобы обеспечить повторяемых установок.

Этот проект будет зависеть от двух внешних библиотек, Flask и Redis, которые вы можете объявить в своем pyproject.toml файл:

# pyproject.toml

[build-system]
requires = ["setuptools>=67.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "page-tracker"
version = "1.0.0"
dependencies = [
    "Flask",
    "redis",
]

Обратите внимание, что здесь обычно не указываются версии зависимостей. Вместо этого вы можете заморозить их вместе с любыми переходными зависимостями в требованиях или файл ограничений. Первый указывает pip, какие пакеты следует установить, а второй применяет определенные версии переходных зависимостей пакетов, напоминающие Pipenv или Poetry заблокировать файл.

Чтобы сгенерировать файл ограничений, вы должны сначала установить свой проект page-tracker в активную виртуальную среду, которая предоставит необходимые внешние библиотеки из Индекса пакетов Python (PyPI). Убедитесь, что вы создали нужную структуру папок, а затем выполните следующие команды:

(page-tracker) $ python -m pip install --editable .
(page-tracker) $ python -m pip freeze --exclude-editable > constraints.txt

Даже если вы еще не ввели ни строчки кода, Python распознает и установит заполнитель вашего пакета. Поскольку ваш пакет соответствует макету src, его удобно устанавливать в редактируемом режиме во время разработки. Это позволит вам вносить изменения в исходный код и немедленно отражать их в виртуальной среде без повторной установки. Однако вы хотите исключить редактируемый пакет из файла ограничений.

Примечание: Созданный файл ограничений очень полезен, если вы планируете поделиться своим проектом. Это означает, что кто-то другой может запустить следующую команду в своем терминале, чтобы воспроизвести ту же среду, что и у вас:

(page-tracker) $ python -m pip install -c constraints.txt .

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

Хорошо. Вы почти готовы приступить к написанию кода для вашего веб-приложения Flask. Перед этим вы на мгновение переключитесь и подготовите локальный сервер Redis для подключения по сети.

Запустите сервер Redis через Docker

Название Redis представляет собой комбинацию слов удаленный сервер словарей, который довольно точно передает свое назначение как удаленного хранилища структуры данных в памяти. Являясь хранилищем ключей и значений, Redis подобен удаленному словарю Python, к которому вы можете подключиться из любого места. Она также считается одной из самых популярных баз данных NoSQL, используемых в самых разных контекстах. Часто он используется в качестве кэша поверх реляционной базы данных.

Примечание: Хотя Redis хранит все свои данные в энергозависимой памяти, что делает его чрезвычайно быстрым, сервер оснащен множеством из вариантов сохранения. Они могут обеспечить различные уровни надежности данных в случае отключения питания или перезагрузки. Однако правильная настройка Redis часто оказывается сложной задачей, поэтому многие команды решают использовать управляемый сервис, переданный на аутсорсинг облачным провайдерам.

Установка Redis на ваш компьютер довольно проста, но запустить ее через Docker еще проще и элегантнее, при условии, что вы уже устанавливали и настраивали Docker ранее. Когда вы запускаете службу, такую как Redis, в контейнере Docker, она остается изолированной от остальной части вашей системы, не создавая помех и не перегружая системные ресурсы, такие как номера сетевых портов, которые ограничены.

Чтобы запустить Redis, не устанавливая его на свой хост-компьютер, вы можете запустить новый контейнер Docker из официального образа Redis, выполнив следующую команду:

$ docker run -d --name redis-server redis
Unable to find image 'redis:latest' locally
latest: Pulling from library/redis
26c5c85e47da: Pull complete
39f79586dcf2: Pull complete
79c71d0520e5: Pull complete
60e988668ca1: Pull complete
873c3fc9fdc6: Pull complete
50ce7f9bf183: Pull complete
Digest: sha256:f50031a49f41e493087fb95f96fdb3523bb25dcf6a3f0b07c588ad3cdb...
Status: Downloaded newer image for redis:latest
09b9842463c78a2e9135add810aba6c4573fb9e2155652a15310009632c40ea8

При этом создается новый контейнер Docker на основе последней версии образа redis с пользовательским именем redis-server, к которому вы будете обращаться позже. Контейнер работает в фоновом режиме в автономном режиме (-d). Когда вы запустите эту команду в первый раз, Docker извлекет соответствующий образ Docker из Docker Hub, который является официальным хранилищем образов Docker, похожим на PyPI.

Пока все идет по плану, ваш Redis-сервер должен быть запущен. Потому что вы запустили контейнер в автономном режиме (-d), он будет оставаться активным в фоновом режиме. Чтобы убедиться в этом, вы можете перечислить свои контейнеры Docker, используя команду docker container ls или эквивалентный псевдоним docker ps:

$ docker ps
CONTAINER ID   IMAGE   ...   STATUS              PORTS      NAMES
09b9842463c7   redis   ...   Up About a minute   6379/tcp   redis-server

Здесь вы можете видеть, что контейнер с префиксом ID, соответствующим тому, который вы получили при выполнении команды docker run, был запущен примерно минуту назад. Контейнер основан на образе redis, имеет имя redis-server и использует номер TCP-порта 6379, который является портом по умолчанию для Redis.

Далее вы попробуете подключиться к этому серверу Redis различными способами.

Проверьте подключение к Redis

На странице обзора официального образа Redis на Docker Hub вы найдете инструкции о том, как подключиться к серверу Redis, работающему в контейнере Docker. В частности, на этой странице рассказывается об использовании специального интерактивного интерфейса командной строки, Redis CLI, который поставляется с вашим образом Docker.

Вы можете запустить другой контейнер Docker из того же образа redis, но на этот раз задайте для точки входа контейнера команду redis-cli вместо двоичного файла сервера Redis по умолчанию. Когда вы настраиваете несколько контейнеров для совместной работы, вам следует использовать Docker networks, для настройки которых требуется выполнить несколько дополнительных шагов.

Сначала создайте новую пользовательскую мостовую сеть, названную в честь вашего проекта, например:

$ docker network create page-tracker-network
c942131265bf097da294edbd2ac375cd5410d6f0d87e250041827c68a3197684

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

$ docker network ls
NETWORK ID     NAME                   DRIVER    SCOPE
1bf8d998500e   bridge                 bridge    local
d5cffd6ea76f   host                   host      local
a85d88fc3abe   none                   null      local
c942131265bf   page-tracker-network   bridge    local

Затем подключите существующий контейнер redis-server к этой новой виртуальной сети и укажите ту же сеть для командной строки Redis при запуске соответствующего контейнера:

$ docker network connect page-tracker-network redis-server
$ docker run --rm -it \
             --name redis-client \
             --network page-tracker-network \
             redis redis-cli -h redis-server

Флаг --rm указывает Docker удалить созданный контейнер, как только вы его завершите, поскольку это временный или недолговечный контейнер, который вам больше не нужно запускать. Флаги -i и -t, сокращенно обозначаемые как -it, запускают контейнер в интерактивном режиме, позволяя вам вводить команды, подключаясь к стандартным потокам вашего терминала. Используя опцию --name, вы присваиваете своему новому контейнеру описательное имя.

Опция --network подключает ваш новый контейнер redis-client к ранее созданной виртуальной сети, позволяя ему взаимодействовать с контейнером redis-server. Таким образом, оба контейнера получат имен хостов, соответствующие их именам, заданным параметром --name. Обратите внимание, что, используя параметр -h, вы указываете командной строке Redis подключиться к серверу Redis, идентифицируемому по имени контейнера.

Примечание: Существует еще более быстрый способ соединить два ваших контейнера через виртуальную сеть, не создавая ее явно. Вы можете указать параметр --link при запуске нового контейнера:

$ docker run --rm -it \
             --name redis-client \
             --link redis-server:redis-client \
             redis redis-cli -h redis-server

Однако этот параметр устарел и в какой-то момент может быть удален из Docker.

При запуске вашего нового контейнера Docker вы попадете в интерактивный интерфейс командной строки Redis, который напоминает Python REPL со следующим приглашением:

redis-server:6379> SET pi 3.14
OK
redis-server:6379> GET pi
"3.14"
redis-server:6379> DEL pi
(integer) 1
redis-server:6379> KEYS *
(empty array)

После этого вы можете протестировать несколько команд Redis, например, установив пару ключ-значение, получив значение соответствующего ключа, удалив эту пару ключ-значение или восстановив список всех ключей, хранящихся в данный момент на сервере. Чтобы выйти из интерактивного интерфейса командной строки Redis, нажмите Ctrl+C на клавиатуре.

Если вы установили Docker Desktop, то в большинстве случаев он не будет перенаправлять трафик с вашего хост-компьютера на контейнеры. Между вашей локальной сетью и сетью Docker по умолчанию не будет соединения:

Docker Desktop для Mac не может перенаправлять трафик в контейнеры. (Источник)

Docker Desktop для Windows не может перенаправлять трафик в контейнеры Linux. Однако вы можете пропинговать контейнеры Windows. (Источник)

То же самое верно и для Docker Desktop в Linux. С другой стороны, если вы используете Docker Engine или запускаете контейнеры Windows на компьютере с Windows-хостом, вы сможете получить доступ к таким контейнерам по их IP-адресам.

Таким образом, иногда у вас может быть возможность связаться с сервером Redis непосредственно с вашего хост-компьютера. Сначала узнайте IP-адрес соответствующего контейнера Docker:

$ docker inspect redis-server \
  -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{println}}{{end}}'
172.17.0.2
172.18.0.2

Если вы видите более одного IP-адреса, это означает, что ваш контейнер подключен к нескольким сетям. Контейнеры автоматически подключаются к сети Docker по умолчанию при запуске.

Обратите внимание на один из этих адресов, который может у вас отличаться. Теперь вы можете использовать этот IP-адрес в качестве значения параметра -h вместо связанного имени контейнера в redis-cli. Вы также можете использовать этот IP-адрес для подключения к Redis с помощью netcat или клиента Telnet, например, Замазка или команда telnet:

$ telnet 172.17.0.2 6379
Trying 172.17.0.2...
Connected to 172.17.0.2.
Escape character is '^]'.
SET pi 3.14
+OK
GET pi
$4
3.14
DEL pi
:1
KEYS *
*0
^]
telnet> Connection closed.

Не забудьте указать номер порта, который по умолчанию равен 6379, на котором Redis прослушивает входящие соединения. Здесь вы можете вводить команды Redis открытым текстом, поскольку сервер использует незашифрованный протокол, если вы явно не включите поддержку TLS в конфигурации.

Наконец, вы можете воспользоваться сопоставлением портов, чтобы сделать Redis доступным за пределами контейнера Docker. Во время разработки вы захотите подключиться к Redis напрямую, а не через виртуальную сеть из другого контейнера, поэтому вам пока не нужно подключать его к какой-либо сети.

Чтобы использовать сопоставление портов, остановите и удалите существующий redis-server, а затем запустите новый контейнер с параметром -p, указанным ниже:

$ docker stop redis-server
$ docker rm redis-server
$ docker run -d --name redis-server -p 6379:6379 redis

Число слева от двоеточия (:) обозначает номер порта на главном компьютере или на вашем компьютере, в то время как число справа обозначает подключенный порт внутри контейнера Docker, который должен быть запущен. Использование одного и того же номера порта с обеих сторон эффективно перенаправляет его, так что вы можете подключиться к Redis так, как если бы он был запущен локально на вашем компьютере:

$ telnet localhost 6379
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
INCR page_views
:1
INCR page_views
:2
INCR page_views
:3
^]
telnet> Connection closed.

После подключения к Redis, которое теперь отображается на локальном хосте и на порту по умолчанию, вы используете команду INCR, чтобы увеличить количество просмотров страниц. Если базовый ключ еще не существует, то Redis инициализирует его значением 1.

Примечание: Если у вас локально установлен Redis или какой-либо системный процесс также использует порт 6379 на вашем хост-компьютере, вам нужно будет по-другому сопоставить номера ваших портов используя незанятый порт. Например, вы могли бы сделать следующее:

$ docker run -d --name redis-server -p 9736:6379 redis

Это позволит вам подключиться к localhost через порт 9736, если он еще не занят другой службой. Неважно, какой порт вы используете, главное, чтобы он был доступен.

Теперь, когда вы знаете, как подключиться к Redis из командной строки, вы можете двигаться дальше и посмотреть, как сделать то же самое с помощью программы на Python.

Подключение к Redis из Python

На данный момент у вас есть сервер Redis, работающий в контейнере Docker, доступ к которому вы можете получить на локальном хостинге, используя номер порта Redis по умолчанию. Если вы хотите узнать больше о своем контейнере или любом другом ресурсе Docker, вы всегда можете получить ценную информацию, просмотрев объект, который находится под рукой:

$ docker inspect redis-server
[
    {
        "Id": "09b9842463c78a2e9135add810aba6...2a15310009632c40ea8",
        ⋮
        "NetworkSettings": {
            ⋮
            "Ports": {
                "6379/tcp": null
            },
            ⋮
            "IPAddress": "172.17.0.2",
            ⋮
        }
    }
]

В данном случае вы запрашиваете информацию о контейнере redis-server, которая включает в себя множество деталей, таких как конфигурация сети контейнера. Команда docker inspect по умолчанию возвращает данные в формате JSON, которые вы можете дополнительно отфильтровать, используя шаблоны Go.

Затем откройте терминал, активируйте виртуальную среду вашего проекта и запустите новый Python REPL:

S> venv\Scripts\activate
(page-tracker) PS> python

 

$ source venv/bin/activate
(page-tracker) $ python

Предполагая, что вы ранее установили пакет redis в этой виртуальной среде, вы должны иметь возможность импортировать клиент Redis для Python и вызвать один из его методов:

>>> from redis import Redis
>>> redis = Redis()
>>> redis.incr("page_views")
4
>>> redis.incr("page_views")
5

Когда вы создаете новый экземпляр Redis без указания каких-либо аргументов, он попытается подключиться к серверу Redis, работающему на локальном хосте, и порту по умолчанию 6379. В этом случае вызов .incr() подтверждает, что вы успешно установили соединение с Redis, находящимся в вашем контейнере Docker, поскольку он запомнил последнее значение ключа page_views.

Если вам нужно подключиться к Redis, расположенному на удаленном компьютере, укажите пользовательский хост и номер порта в качестве параметров:

>>> from redis import Redis
>>> redis = Redis(host="127.0.0.1", port=6379)
>>> redis.incr("page_views")
6

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

Другим способом подключения к Redis является использование специально отформатированной строки, которая представляет собой URL-адрес:

>>> from redis import Redis
>>> redis = Redis.from_url("redis://localhost:6379/")
>>> redis.incr("page_views")
7

Это может быть особенно удобно, если вы хотите сохранить конфигурацию Redis в файле или переменной среды.

Отлично! Вы можете использовать один из этих фрагментов кода и интегрировать его в свое веб-приложение Flask. В следующем разделе вы увидите, как это сделать.

Внедрить и запустить приложение Flask локально

Вернитесь в свой редактор кода, откройте модуль app в вашем проекте page-tracker и напишите следующие несколько строк кода на Python:

# src/page_tracker/app.py

from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis()

@app.get("/")
def index():
    page_views = redis.incr("page_views")
    return f"This page has been seen {page_views} times."

Вы начинаете с импорта Flask и Redis из соответствующих сторонних библиотек вашего проекта, перечисленных в качестве зависимостей. Затем вы создаете экземпляр приложения Flask и клиент Redis, используя аргументы по умолчанию, что означает, что клиент попытается подключиться к локальному серверу Redis. Наконец, вы определяете функцию контроллера для обработки запросов HTTP GET, поступающих на корневой адрес веб-сервера (/).

Ваша конечная точка увеличивает количество просмотров страниц в Redis и отображает соответствующее сообщение в веб-браузере клиента. Вот и все! У вас есть полноценное веб-приложение, которое может обрабатывать HTTP-трафик и сохранять состояние в удаленном хранилище данных, используя менее десяти строк кода.

Чтобы проверить, работает ли ваше приложение Flask должным образом, выполните следующую команду в терминале:

(page-tracker) $ flask --app page_tracker.app run
 * Serving Flask app 'page_tracker.app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production
⮑ deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

Вы можете запустить эту команду в любом месте вашей файловой системы, если вы активировали правильную виртуальную среду и установили свой пакет page-tracker. При этом должен быть запущен Сервер разработки Flask на локальном хосте и порту 5000 с отключенным режимом отладки.

Если вы хотите получить доступ к своему серверу с другого компьютера в той же сети, вы должны привязать его ко всем сетевым интерфейсам, используя специальный адрес 0.0.0.0 вместо локальный хост по умолчанию, который представляет интерфейс обратной связи:

(page-tracker) $ flask --app page_tracker.app run --host=0.0.0.0 \
                                                  --port=8080 \
                                                  --debug
 * Serving Flask app 'page_tracker.app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production
⮑ deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:8080
 * Running on http://192.168.0.115:8080
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 123-167-546

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

После запуска сервера вы можете перейти по ссылке, отображаемой в терминале, и просмотреть страницу с количеством просмотров в вашем веб-браузере. Каждый раз, когда вы обновляете эту страницу, счетчик должен увеличиваться на единицу.

Отличная работа! Вам удалось создать простое приложение Flask, которое отслеживает количество просмотров страниц с помощью Redis. Далее вы узнаете, как протестировать и обезопасить свое веб-приложение.

Протестируйте и обезопасьте свое веб-приложение

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

Покройте Исходный Код модульными тестами

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

Когда дело доходит до написания модульных тестов, те, кто работает на Python, довольно часто выбирают pytest вместо модуля стандартной библиотеки unittest . Благодаря относительной простоте pytest, с этим фреймворком тестирования можно быстро начать. Продолжайте и добавьте pytest в качестве необязательной зависимости к вашему проекту:

# pyproject.toml

[build-system]
requires = ["setuptools>=67.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "page-tracker"
version = "1.0.0"
dependencies = [
    "Flask",
    "redis",
]

[project.optional-dependencies]
dev = [
    "pytest",
]

Вы можете сгруппировать необязательные зависимости, которые каким-то образом связаны под общим названием. Здесь, например, вы создали группу под названием dev для сбора инструментов и библиотек, которые вы будете использовать в процессе разработки. Сохраняя pytest отдельно от основных зависимостей, вы сможете устанавливать ее по требованию только при необходимости. В конце концов, нет смысла связывать ваши тесты или связанный с ними фреймворк тестирования со встроенным дистрибутивом .

Не забудьте переустановить свой пакет Python с дополнительными зависимостями, чтобы получить pytest в виртуальной среде вашего проекта:

(page-tracker) $ python -m pip install --editable ".[dev]"

Вы можете использовать квадратные скобки для перечисления имен необязательных групп зависимостей, определенных в вашем файле pyproject.toml. В этом случае вы запрашиваете установку зависимостей для целей разработки, включая платформу тестирования. Обратите внимание, что рекомендуется заключать квадратные скобки в кавычки (""), чтобы предотвратить возможное расширение имени файла в командной строке.

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

page-tracker/
│
├── src/
│   └── page_tracker/
│       ├── __init__.py
│       └── app.py
│
├── test/
│   └── unit/
│       └── test_app.py
│
├── venv/
│
├── constraints.txt
└── pyproject.toml

Вы поместили свой тестовый модуль в папку test/unit/, чтобы все было упорядочено. Платформа pytest обнаружит ваши тесты, если вы добавите к ним слово test. Хотя вы можете изменить это, обычно соблюдается соглашение по умолчанию при зеркальном отображении каждого модуля Python с соответствующим тестовым модулем. Например, вы закроете модуль app с помощью test_app в своей папке test/unit/.

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

Вы можете получить тестовый клиент и удобно поместить его в тестовое устройство, чтобы сделать его доступным для ваших тестовых функций:

# test/unit/test_app.py

import pytest

from page_tracker.app import app

@pytest.fixture
def http_client():
    return app.test_client()

Сначала вы импортируете пакет pytest, чтобы воспользоваться преимуществами его @fixture декоратор для вашей пользовательской функции. Тщательно выбирайте имя вашей функции, потому что оно также станет именем устройства, которое вы сможете передавать в качестве аргумента отдельным тестовым функциям. Вы также импортируете приложение Flask из своего пакета page_tracker, чтобы получить соответствующий экземпляр тестового клиента.

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

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

 # src/page_tracker/app.py

+from functools import cache

 from flask import Flask
 from redis import Redis

 app = Flask(__name__)
-redis = Redis()

 @app.get("/")
 def index():
-    page_views = redis.incr("page_views")
+    page_views = redis().incr("page_views")
     return f"This page has been seen {page_views} times."

+@cache
+def redis():
+    return Redis()

По сути, вы перемещаете код создания клиента Redis из глобальной области в новую redis() функцию, которую функция вашего контроллера вызывает во время выполнения при каждом входящем запросе. Это позволит вашему тестовому набору в нужный момент заменить возвращенный экземпляр Redis имитационным аналогом. Но, чтобы гарантировать, что в памяти есть только один экземпляр клиента, фактически превращая его в одноэлементный, вы также кэшируете результат вашей новой функции.

Теперь вернитесь к своему тестовому модулю и выполните следующий модульный тест:

# test/unit/test_app.py

import unittest.mock

import pytest

from page_tracker.app import app

@pytest.fixture
def http_client():
    return app.test_client()

@unittest.mock.patch("page_tracker.app.redis")
def test_should_call_redis_incr(mock_redis, http_client):
    # Given
    mock_redis.return_value.incr.return_value = 5

    # When
    response = http_client.get("/")

    # Then
    assert response.status_code == 200
    assert response.text == "This page has been seen 5 times."
    mock_redis.return_value.incr.assert_called_once_with("page_views")

Вы обертываете свою тестовую функцию с помощью Python's @patch decorator, чтобы ввести в нее имитируемый клиент Redis в качестве аргумента. Вы также указываете pytest, чтобы ввести свой HTTP-тестовый клиентский инструмент в качестве еще одного аргумента. Тестовая функция имеет описательное название, которое начинается с глагола should и следует шаблону Given-When-Then. Оба этих соглашения, обычно используемые в разработке, основанной на поведении, позволяют интерпретировать ваш тест как поведенческие спецификации.

В вашем тестовом примере вы сначала настраиваете макетный клиент Redis так, чтобы он всегда возвращал 5 всякий раз, когда вызывается его метод .incr(). Затем вы отправляете поддельный HTTP-запрос к корневой конечной точке (/) и проверяете статус ответа сервера и тело. Поскольку mocking помогает вам протестировать поведение вашего модуля, вы только проверяете, что сервер вызывает правильный метод с ожидаемым аргументом, полагаясь на то, что клиентская библиотека Redis работает правильно.

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

(page-tracker) $ python -m pytest -v test/unit/

Вы запускаете pytest как модуль Python из своей виртуальной среды, проинструктировав его сканировать каталог test/unit/, чтобы найти там тестовые модули. Переключатель -v увеличивает детализацию отчета о тестировании, так что вы можете увидеть более подробную информацию об отдельных тестовых примерах.

Просмотр зеленого отчета после прохождения всех ваших модульных тестов может принести удовлетворение. Это дает вам некоторую уверенность в вашем коде, но вряд ли этого достаточно, чтобы давать какие-либо гарантии. Множество мемов иллюстрируют важность выполнения интеграционных тестов даже после завершения модульных тестов.

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

Проверьте взаимодействие Компонентов С Помощью Интеграционных Тестов

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

Вы можете повторно использовать pytest для реализации и запуска интеграционных тестов. Однако вы установите дополнительный pytest-timeout плагин, который позволит вам принудительно завершать работу с тестовыми примерами, выполнение которых занимает слишком много времени:

# pyproject.toml

[build-system]
requires = ["setuptools>=67.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "page-tracker"
version = "1.0.0"
dependencies = [
    "Flask",
    "redis",
]

[project.optional-dependencies]
dev = [
    "pytest",
    "pytest-timeout",
]

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

Не забудьте переустановить свой пакет с дополнительными зависимостями еще раз, чтобы сделать доступным плагин pytest-timeout:

(page-tracker) $ python -m pip install --editable ".[dev]"

Прежде чем двигаться дальше, добавьте еще одну вложенную папку для ваших интеграционных тестов и укажите файл conftest.py в вашей папке test/:

page-tracker/
│
├── src/
│   └── page_tracker/
│       ├── __init__.py
│       └── app.py
│
├── test/
│   ├── integration/
│   │   └── test_app_redis.py
│   │
│   ├── unit/
│   │   └── test_app.py
│   │
│   └── conftest.py
│
├── venv/
│
├── constraints.txt
└── pyproject.toml

В conftest.py вы разместите общие параметры, которые будут использоваться разными типами тестов совместно.

Хотя в вашем веб-приложении есть только один компонент, вы можете рассматривать Redis как еще один компонент, с которым должен работать Flask. Таким образом, интеграционный тест может выглядеть аналогично вашему модульному тесту, за исключением того, что клиент Redis больше не будет подвергаться издевательствам:

# test/integration/test_app_redis.py

import pytest

@pytest.mark.timeout(1.5)
def test_should_update_redis(redis_client, http_client):
    # Given
    redis_client.set("page_views", 4)

    # When
    response = http_client.get("/")

    # Then
    assert response.status_code == 200
    assert response.text == "This page has been seen 5 times."
    assert redis_client.get("page_views") == b"5"

Концептуально ваш новый тестовый пример состоит из тех же шагов, что и предыдущий, но он взаимодействует с реальным сервером Redis. Вот почему вы даете тесту не более 1.5 секунд на завершение с помощью декоратора @pytest.mark.timeout. Функция тестирования принимает в качестве параметров два параметра:

  1. Клиент Redis, подключенный к локальному хранилищу данных
  2. Тестовый клиент Flask, подключенный к вашему веб-приложению

Чтобы сделать доступным и второй модуль в вашем интеграционном тесте, вы должны переместить модуль http_client() из модуля test_app в файл conftest.py:

# test/conftest.py

import pytest
import redis

from page_tracker.app import app

@pytest.fixture
def http_client():
    return app.test_client()

@pytest.fixture(scope="module")
def redis_client():
    return redis.Redis()

Поскольку этот файл расположен на один уровень выше в иерархии папок, pytest подберет все параметры, определенные в нем, и сделает их видимыми во всех ваших вложенных папках. Помимо знакомого http_client() fixture, который вы перенесли из другого модуля Python, вы определяете новый fixture, который возвращает клиент Redis по умолчанию. Обратите внимание, что вы предоставляете ему область действия module для повторного использования одного и того же экземпляра клиента Redis для всех функций в тестовом модуле.

Чтобы выполнить интеграционный тест, вам нужно будет дважды проверить, запущен ли сервер Redis локально на порту по умолчанию 6379, а затем запустить pytest как и раньше, но указать на папку с ваши интеграционные тесты:

(page-tracker) $ python -m pytest -v test/integration/

Поскольку ваш интеграционный тест подключается к реальному серверу Redis, он перезапишет значение, которое вы, возможно, ранее сохранили под ключом page_views. Однако, если сервер Redis не запущен во время выполнения ваших интеграционных тестов или если Redis запущен в другом месте, то ваш тест завершится неудачей. Этот сбой может быть вызван неправильными причинами, что приводит к ложноотрицательной ошибке, поскольку ваш код на самом деле может работать так, как ожидалось.

Чтобы устранить эту проблему, остановите Redis сейчас и повторно запустите интеграционный тест:

(page-tracker) $ docker stop redis-server
redis-server
(page-tracker) $ python -m pytest -v test/integration/
⋮
========================= short test summary info ==========================
FAILED test/integration/test_app_redis.py::test_should_update_redis -
⮑redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379.
⮑Connection refused
============================ 1 failed in 0.19s =============================

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

# test/unit/test_app.py

import unittest.mock

from redis import ConnectionError

# ...

@unittest.mock.patch("page_tracker.app.redis")
def test_should_handle_redis_connection_error(mock_redis, http_client):
    # Given
    mock_redis.return_value.incr.side_effect = ConnectionError

    # When
    response = http_client.get("/")

    # Then
    assert response.status_code == 500
    assert response.text == "Sorry, something went wrong \N{pensive face}"

Вы задаете имитируемый .incr() побочный эффект метода, чтобы при вызове этого метода возникало исключение redis.ConnectionError , который вы наблюдали, когда интеграционный тест завершился неудачей. Ваш новый модульный тест, который является примером отрицательного теста, ожидает, что Flask ответит кодом состояния HTTP 500 и описательным сообщением. Вот как вы можете выполнить этот модульный тест:

# src/page_tracker/app.py

from functools import cache

from flask import Flask
from redis import Redis, RedisError

app = Flask(__name__)

@app.get("/")
def index():
    try:
        page_views = redis().incr("page_views")
    except RedisError:
        app.logger.exception("Redis error")
        return "Sorry, something went wrong \N{pensive face}", 500
    else:
        return f"This page has been seen {page_views} times."

@cache
def redis():
    return Redis()

Вы перехватываете класс исключений верхнего уровня, redis.RedisError, который является предком всех типов исключений, создаваемых клиентом Redis. Если что-то пойдет не так, вы вернете ожидаемый код состояния HTTP и сообщение. Для удобства вы также регистрируете исключение с помощью регистратора, встроенного в Flask.

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

Отлично! Благодаря тестированию вы внесли изменения в свои модульные тесты, внедрили интеграционный тест и исправили дефект в коде после того, как узнали о нем. Тем не менее, когда вы развертываете свое приложение в удаленной среде, как вы узнаете, что все компоненты подходят друг к другу и все работает так, как ожидалось?

В следующем разделе вы смоделируете реальный сценарий, выполнив сквозной тест на вашем реальном сервере Flask, а не на тестовом клиенте.

Протестируйте реальный сценарий от начала до конца (E2E)

Комплексное тестирование, также известное как тестирование широкого стека, включает в себя множество видов тестов, которые могут помочь вам проверить систему в целом. Они протестировали весь программный стек, смоделировав работу реального пользователя с приложением. Таким образом, для комплексного тестирования требуется среда развертывания, максимально приближенная к производственной среде. Обычно также требуется специальная команда инженеров-тестировщиков.

Примечание: Поскольку комплексные тесты требуют больших затрат на обслуживание и, как правило, занимают много времени на настройку и запуск, они находятся на вершине пирамиды тестирования Google . Другими словами, вам следует стремиться к большему количеству интеграционных тестов и даже к большему количеству модульных тестов в ваших проектах.

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

page-tracker/
│
├── src/
│   └── page_tracker/
│       ├── __init__.py
│       └── app.py
│
├── test/
│   ├── e2e/
│   │   └── test_app_redis_http.py
│   │
│   ├── integration/
│   │   └── test_app_redis.py
│   │
│   ├── unit/
│   │   └── test_app.py
│   │
│   └── conftest.py
│
├── venv/
│
├── constraints.txt
└── pyproject.toml

Тестовый сценарий, который вы собираетесь реализовать, будет похож на ваш интеграционный тест. Однако основное отличие заключается в том, что вы будете отправлять фактический HTTP-запрос через сеть на веб-сервер в реальном времени, а не полагаться на тестовый клиент Flask. Для этого вы будете использовать стороннюю библиотеку requests, которую вы должны сначала указать в своем файле pyproject.toml в качестве еще одной необязательной зависимости:

# pyproject.toml

[build-system]
requires = ["setuptools>=67.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "page-tracker"
version = "1.0.0"
dependencies = [
    "Flask",
    "redis",
]

[project.optional-dependencies]
dev = [
    "pytest",
    "pytest-timeout",
    "requests",
]

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

(page-tracker) $ python -m pip install --editable ".[dev]"

Теперь вы можете использовать установленную библиотеку requests в своем комплексном тестировании:

 1# test/e2e/test_app_redis_http.py
 2
 3import pytest
 4import requests
 5
 6@pytest.mark.timeout(1.5)
 7def test_should_update_redis(redis_client, flask_url):
 8    # Given
 9    redis_client.set("page_views", 4)
10
11    # When
12    response = requests.get(flask_url)
13
14    # Then
15    assert response.status_code == 200
16    assert response.text == "This page has been seen 5 times."
17    assert redis_client.get("page_views") == b"5"

Этот код практически идентичен вашему интеграционному тесту, за исключением строки 12, которая отвечает за отправку HTTP-запроса GET. Ранее вы отправляли этот запрос на корневой адрес тестового клиента, обозначенный символом косой черты (/). Теперь вы не знаете точный домен или IP-адрес сервера Flask, который, возможно, запущен на удаленном хосте. Следовательно, ваша функция получает URL-адрес Flask в качестве аргумента, который pytest вводится как фиксированный.

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

# src/page_tracker/app.py

import os
from functools import cache

from flask import Flask
from redis import Redis, RedisError

app = Flask(__name__)

@app.get("/")
def index():
    try:
        page_views = redis().incr("page_views")
    except RedisError:
        app.logger.exception("Redis error")
        return "Sorry, something went wrong \N{pensive face}", 500
    else:
        return f"This page has been seen {page_views} times."

@cache
def redis():
    return Redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379"))

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

Чтобы расширить pytest с помощью пользовательских аргументов командной строки, вы должны отредактировать conftest.py и подключиться к анализатору аргументов фреймворка следующим образом:

# test/conftest.py

import pytest
import redis

from page_tracker.app import app

def pytest_addoption(parser):
    parser.addoption("--flask-url")
    parser.addoption("--redis-url")

@pytest.fixture(scope="session")
def flask_url(request):
    return request.config.getoption("--flask-url")

@pytest.fixture(scope="session")
def redis_url(request):
    return request.config.getoption("--redis-url")

@pytest.fixture
def http_client():
    return app.test_client()

@pytest.fixture(scope="module")
def redis_client(redis_url):
    if redis_url:
        return redis.Redis.from_url(redis_url)
    return redis.Redis()

Вы определяете два необязательных аргумента, --flask-url и --redis-url, используя синтаксис, аналогичный модулю Python argparse. Затем вы помещаете эти аргументы в фиксированных параметров сеанса, которые вы сможете внедрять в свои тестовые функции и другие фиксированные параметры. В частности, ваше существующее приложение redis_client() теперь использует дополнительный URL-адрес Redis.

Примечание: Поскольку в ваших сквозных тестах и интеграционных тестах используется одно и то же устройство redis_client(), вы сможете подключиться к удаленному серверу Redis, указав параметр --redis-url в обоих типах тестов.

Вот как вы можете запустить свой сквозной тест с помощью pytest, указав URL-адрес веб-сервера Flask и соответствующего сервера Redis:

(page-tracker) $ python -m pytest -v test/e2e/ \
  --flask-url http://127.0.0.1:5000 \
  --redis-url redis://127.0.0.1:6379

В этом случае вы можете получить доступ как к Flask, так и к Redis через localhost (127.0.0.1), но ваше приложение может быть развернуто в географически распределенной среде, состоящей из нескольких удаленных компьютеров. Когда вы будете выполнять эту команду локально, убедитесь, что Redis запущен, и сначала запустите свой сервер Flask отдельно:

(page-tracker) $ docker start redis-server
(page-tracker) $ flask --app page_tracker.app run

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

Выполнить статический анализ кода и проверку безопасности

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

Вы будете использовать следующие автоматизированные инструменты, поэтому, пожалуйста, добавьте их в свой файл pyproject.toml в качестве необязательных зависимостей:

# pyproject.toml

[build-system]
requires = ["setuptools>=67.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "page-tracker"
version = "1.0.0"
dependencies = [
    "Flask",
    "redis",
]

[project.optional-dependencies]
dev = [
    "bandit",
    "black",
    "flake8",
    "isort",
    "pylint",
    "pytest",
    "pytest-timeout",
    "requests",
]

Не забудьте после этого переустановить и закрепить свои зависимости:

 

(page-tracker) $ python -m pip install --editable ".[dev]"
(page-tracker) $ python -m pip freeze --exclude-editable > constraints.txt

Это позволит использовать в вашей виртуальной среде несколько утилит командной строки. Прежде всего, вам следует очистить свой код, последовательно отформатировав его, отсортировав операторы import и проверив соответствие PEP 8:

(page-tracker) $ python -m black src/ --check
would reformat /home/realpython/page-tracker/src/page_tracker/app.py

Oh no! 💥 💔 💥
1 file would be reformatted, 1 file would be left unchanged.

(page-tracker) $ python -m isort src/ --check
ERROR: /home/.../app.py Imports are incorrectly sorted and/or formatted.

(page-tracker) $ python -m flake8 src/
src/page_tracker/app.py:23:1: E302 expected 2 blank lines, found 1

Используется black для обозначения любых несоответствий форматирования в вашем коде, isort для обеспечения того, чтобы ваши import инструкции были организованы в соответствии с официальными рекомендациями, и flake8 для проверки любых другие нарушения стиля PEP 8.

Если после запуска этих инструментов вы не видите никаких результатов, это означает, что исправлять нечего. С другой стороны, если появляются предупреждения или ошибки, вы можете исправить любые обнаруженные проблемы вручную или позволить этим инструментам делать это автоматически, когда вы снимаете флаг --check:

(page-tracker) $ python -m black src/
reformatted /home/realpython/page-tracker/src/page_tracker/app.py

All done! ✨ 🍰 ✨
1 file reformatted, 1 file left unchanged.

(page-tracker) $ python -m isort src/
Fixing /home/realpython/page-tracker/src/page_tracker/app.py

(page-tracker) $ python -m flake8 src/

Без флажка --check, как black, так и isort переформатируйте поврежденные файлы на месте без запроса. Выполнение этих двух команд также обеспечивает соответствие требованиям PEP 8, поскольку flake8 больше не возвращает никаких нарушений стиля.

Примечание: Полезно поддерживать порядок в коде, следуя общим правилам оформления кода в вашей команде. Таким образом, когда один человек обновляет исходный файл, членам команды не придется разбираться с изменениями в несущественных частях кода, таких как пробелы.

Как только все будет вычищено, вы можете доработать свой код, чтобы найти потенциальные запахи кода или способы их улучшения:

(page-tracker) $ python -m pylint src/

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

  • E: Ошибки
  • W: Предупреждения
  • C: Нарушения Конвенции
  • R: Предложения по рефакторингу

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

# src/page_tracker/app.py

import os
from functools import cache

from flask import Flask
from redis import Redis, RedisError

app = Flask(__name__)

@app.get("/")
def index():
    try:
        page_views = redis().incr("page_views")
    except RedisError:
        app.logger.exception("Redis error")  # pylint: disable=E1101
        return "Sorry, something went wrong \N{pensive face}", 500
    else:
        return f"This page has been seen {page_views} times."

@cache
def redis():
    return Redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379"))

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

Примечание: Если вы собираетесь использовать pylint как часть вашего автоматизированного конвейера непрерывной интеграции, то вы можете указать, когда он должен завершиться с помощью код ошибки, который обычно останавливает последующие этапы конвейера. Например, вы можете настроить его так, чтобы он всегда возвращал нулевой нейтральный код выхода:

(page-tracker) $ python -m pylint src/ --exit-zero

Это никогда не остановит работу конвейера, даже если pylint обнаружит какие-то проблемы в коде. В качестве альтернативы, используя --fail-under, вы можете указать произвольный порог оценки, при достижении которого pylint завершится с кодом ошибки.

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

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

Для проверки вашего кода вы можете использовать bandit,, который вы установили ранее в качестве дополнительной зависимости:

(page-tracker) $ python -m bandit -r src/

Когда вы указываете путь к папке, а не к файлу, вы также должны включить флаг -r для его рекурсивного сканирования. На данный момент bandit не должен обнаружить никаких проблем в вашем коде. Но если вы запустите его снова после добавления следующих двух строк в нижней части вашего приложения Flask, то инструмент сообщит о проблемах с разными уровнями серьезности:

# src/page_tracker/app.py

# ...

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

Это название-основная идиома - распространенный шаблон, встречающийся во многих приложениях Flask, поскольку он делает разработку более удобной, позволяя запускать модуль Python напрямую. С другой стороны, он предоставляет доступ к отладчику Flask, позволяя выполнять произвольный код и привязываться ко всем сетевым интерфейсам через адрес 0.0.0.0 ваш сервис будет доступен для общественного транспорта.

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

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

Настройте настройки вашего веб-приложения Flask

В этом разделе вы запустите свое веб-приложение для отслеживания страниц как контейнер Docker, который может взаимодействовать с Redis, запущенным в другом контейнере. Такая настройка полезна для разработки и тестирования, а также для развертывания вашего приложения в удаленной среде. Даже если вы не установили Python или Redis на свой компьютер, вы все равно сможете запустить свой проект через Docker.

Понимать терминологию Docker

Настройка приложения включает в себя создание Файла настройки, который представляет собой декларативное описание желаемого состояния вашей среды. Он предоставляет шаблон для среды выполнения, конфигурацию и все зависимости и библиотеки, необходимые для запуска вашего приложения.

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

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

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

Примечание: Контейнеры Docker напоминают виртуальные машины, такие как Vagrant или VirtualBox, но они гораздо легче и быстрее настраиваются. В результате вы можете одновременно запускать на вашем хост-компьютере гораздо больше контейнеров, чем виртуальных машин.

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

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

  1. Веб-служба
  2. Служба Redis

Вы уже знаете, как запускать Redis через Docker. Теперь пришло время изолировать ваше веб-приложение Flask в контейнере Docker, чтобы упростить процесс разработки и развертывания обоих сервисов.

Изучите анатомию файла Dockerfile

Для начала вы определите относительно короткий файл Dockerfile, который будет применим на этапе разработки. Создайте файл с именем Dockerfile в корневой папке вашего проекта на том же уровне, что и подпапка src/ и файл конфигурации pyproject.toml в файловой иерархии:

page-tracker/
│
├── src/
│   └── page_tracker/
│       ├── __init__.py
│       └── app.py
│
├── test/
│
├── venv/
│
├── constraints.txt
├── Dockerfile
└── pyproject.toml

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

Файл Dockerfile - это обычный текстовый документ, в котором перечислены шаги, необходимые для сборки изображения. Он соответствует определенному формату, который определяет фиксированный набор инструкций, которые вы должны использовать.

Примечание: Вы должны размещать каждую инструкцию в вашем файле Dockerfile в отдельной строке, но нередко можно увидеть, что действительно длинные строки несколько раз разбиваются на строки символ продолжения (\). На самом деле, чтобы воспользоваться преимуществами механизма кэширования, о котором вы сейчас узнаете, часто требуется выполнить более одной операции в одной строке.

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

  1. Скорость: Docker может пропускать слои, которые не менялись с момента последней сборки, и загружать их из кэша, что значительно ускоряет сборку изображений.
  2. Размер: Несколько изображений могут иметь общие слои, что уменьшает их индивидуальный размер. Кроме того, меньшее количество слоев приводит к уменьшению размера изображения.

Теперь, когда вы знакомы с многоуровневой структурой файла Dockerfile, вы можете начать добавлять к нему инструкции, одновременно изучая рекомендации по созданию эффективных образов Docker.

Выберите базовый образ Docker

Первая инструкция в каждом файле Dockerfile, FROM, всегда должна определять базовый образ, из которого будет создан ваш новый образ. Это означает, что вам не нужно начинать с нуля, но вы можете выбрать подходящее изображение, которое уже создано. Например, вы можете использовать изображение, поставляемое с интерпретатором Python:

# Dockerfile

FROM python:3.11.2-slim-bullseye

Здесь вы используете официальный образ Python с именем python, который размещен на Docker Hub. Официальные изображения создаются и поддерживаются официальными разработчиками соответствующего языка или технологии. Они не принадлежат какому-либо конкретному пользователю или команде в Docker Hub, но доступны в глобальном пространстве имен, неявно называемом library/, в отличие от более специализированных вариантов, таких как circleci/python.

Вы также можете указать необязательную метку или название тега после двоеточия (:), чтобы сузить выбор конкретной версии базового изображения. Вы можете просмотреть все доступные теги данного изображения Docker, нажав на вкладку Теги на соответствующей странице Docker Hub.

Примечание: Теги не являются обязательными, но их включение в инструкцию FROM считается оптимальной практикой. Вы должны быть максимально конкретны, чтобы избежать нежелательных сюрпризов. Если вы опустите этот тег, то Docker выдаст изображение с тегом latest, которое может содержать неподходящую операционную систему или неожиданные изменения в среде выполнения, влияющие на ваше приложение.

Тег 3.11.2-slim-bullseye означает, что ваш базовый образ будет представлять собой уменьшенный вариант Debian Bullseye, содержащий только самое необходимое, что позволит вам позже установить любые дополнительные пакеты по мере необходимости. Это уменьшает размер изображения и ускоряет его загрузку. Разница в размере между обычным и тонким вариантами этого изображения составляет целых восемьсот мегабайт!

Тег также указывает на то, что ваш базовый образ будет поставляться с уже установленным Python 3.11.2, так что вы можете начать использовать его прямо сейчас.

Следующая задача, которую вы, возможно, захотите выполнить сразу после создания базового образа, - это установить на него самые последние обновления для системы безопасности и исправления ошибок, который, возможно, был выпущен с момента публикации изображения на Docker Hub:

# Dockerfile

FROM python:3.11.2-slim-bullseye

RUN apt-get update && \
    apt-get upgrade --yes

В Debian вы используете команду apt-get, чтобы получить список последних пакетов и обновить все пакеты, для которых доступны обновления. Обратите внимание, что обе команды выполняются как часть одной инструкции RUN чтобы минимизировать количество уровней в файловой системе, вы не занимаете слишком много места на диске.

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

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

Изолируйте свой образ Docker

Еще одна полезная практика при работе с Dockerfiles - создать и переключиться на обычного пользователя без административных привилегий, как только они вам больше не понадобятся. По умолчанию Docker выполняет ваши команды от имени суперпользователя, что может быть использовано злоумышленником для получения неограниченного доступа к вашей хост-системе. Да, Docker предоставляет доступ на корневом уровне к контейнеру и вашему хост-компьютеру!

Вот как вы можете избежать этой потенциальной угрозы безопасности:

# Dockerfile

FROM python:3.11.2-slim-bullseye

RUN apt-get update && \
    apt-get upgrade --yes

RUN useradd --create-home realpython
USER realpython
WORKDIR /home/realpython

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

Даже если в вашем контейнере Docker будет запущено одно приложение Flask, рассмотрите возможность создания выделенной виртуальной среды внутри самого контейнера. Хотя вам не нужно беспокоиться об изоляции нескольких проектов на Python друг от друга, а Docker обеспечивает достаточный уровень изоляции от вашего хост-компьютера, вы все равно рискуете вмешаться в работу собственных системных инструментов контейнера.

К сожалению, для бесперебойной работы многих дистрибутивов Linux требуется глобальная установка Python. Если вы начнете устанавливать пакеты непосредственно в глобальную среду Python, то откроете дверь для потенциальных конфликтов версий. Это может даже привести к поломке вашей системы.

Примечание: Если вы все еще сомневаетесь в создании виртуальной среды внутри контейнера Docker, то, возможно, это предупреждающее сообщение изменит ваше мнение:

WARNING: Running pip as the 'root' user can result in broken permissions
and conflicting behaviour with the system package manager. It is
⮑recommended to use a virtual environment instead:
⮑https://pip.pypa.io/warnings/venv

Вы можете увидеть это после попытки установить пакет Python с помощью системной глобальной команды pip в Debian или производном дистрибутиве, таком как Ubuntu.

Самый надежный способ создать и активировать виртуальную среду в вашем образе Docker - это напрямую изменить ее PATH переменную окружения:

# Dockerfile

FROM python:3.11.2-slim-bullseye

RUN apt-get update && \
    apt-get upgrade --yes

RUN useradd --create-home realpython
USER realpython
WORKDIR /home/realpython

ENV VIRTUALENV=/home/realpython/venv
RUN python3 -m venv $VIRTUALENV
ENV PATH="$VIRTUALENV/bin:$PATH"

Сначала вы определяете вспомогательную переменную VIRTUALENV с указанием пути к виртуальной среде вашего проекта, а затем используете модуль Python venv для создания этой среды. Однако вместо того, чтобы активировать новую среду с помощью сценария оболочки, вы обновляете переменную PATH, переопределяя путь к исполняемому файлу python.

Почему? Это необходимо, потому что активация вашей среды обычным способом была бы временной и не повлияла бы на контейнеры Docker, созданные на основе вашего образа. Более того, если вы активировали виртуальную среду с помощью инструкции Dockerfile RUN, то она будет работать только до следующей инструкции в вашем Dockerfile, поскольку каждая из них запускает новый сеанс оболочки.

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

Кэшируйте зависимости вашего проекта

Установка зависимостей в Dockerfile выглядит несколько иначе, чем при локальной работе на вашем хост-компьютере. Обычно вы устанавливаете зависимости, а затем сразу же после этого свой пакет Python. Напротив, когда вы создаете образ в Docker, стоит разделить этот процесс на два этапа, чтобы использовать кэширование слоев и сократить общее время, необходимое для создания образа.

Сначала, COPY перенесите два файла с метаданными проекта с вашего хост-компьютера в образ Docker:

# Dockerfile

FROM python:3.11.2-slim-bullseye

RUN apt-get update && \
    apt-get upgrade --yes

RUN useradd --create-home realpython
USER realpython
WORKDIR /home/realpython

ENV VIRTUALENV=/home/realpython/venv
RUN python3 -m venv $VIRTUALENV
ENV PATH="$VIRTUALENV/bin:$PATH"

COPY --chown=realpython pyproject.toml constraints.txt ./

Вы копируете только файлы pyproject.toml и constraints.txt, которые содержат информацию о зависимостях проекта, в домашний каталог вашего пользователя realpython в образе Docker. По умолчанию файлы принадлежат суперпользователю, поэтому вы можете захотеть изменить их владельца с помощью --chown на обычного пользователя, которого вы создали ранее. Параметр --chown аналогичен команде chown, которая расшифровывается как изменить владельца.

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

Примечание: Вам следует скопировать в свой Dockerfile только те отдельные файлы, которые вам нужны в данный момент. В противном случае даже малейшее изменение в несвязанном файле приведет к перестройке остальных слоев. В качестве альтернативы вы могли бы определить файл .dockerignore, который работает аналогично аналогу .gitignore, но безопаснее четко указать, что именно вы хотите скопировать.

Еще один фрагмент головоломки, который легко упустить, - это когда вы забываете обновить сам pip перед попыткой установки зависимостей вашего проекта. В редких случаях старая версия pip может фактически препятствовать установке последних версий других пакетов! В вашем случае также стоит обновить setuptools, который вы используете в качестве серверной части сборки, чтобы получить новейшие исправления безопасности.

Вы можете объединить следующие две команды в одну RUN инструкцию для установки ваших зависимостей:

# Dockerfile

FROM python:3.11.2-slim-bullseye

RUN apt-get update && \
    apt-get upgrade --yes

RUN useradd --create-home realpython
USER realpython
WORKDIR /home/realpython

ENV VIRTUALENV=/home/realpython/venv
RUN python3 -m venv $VIRTUALENV
ENV PATH="$VIRTUALENV/bin:$PATH"

COPY --chown=realpython pyproject.toml constraints.txt ./
RUN python -m pip install --upgrade pip setuptools && \
    python -m pip install --no-cache-dir -c constraints.txt ".[dev]"

Вы обновляете pip и setuptools до их самых последних версий. Затем вы устанавливаете сторонние библиотеки, необходимые для вашего проекта, включая необязательные зависимости для разработки. Вы ограничиваете их версии, чтобы обеспечить согласованность среды, и указываете pip отключить кэширование с помощью --no-cache-dir. Вам не понадобятся эти пакеты вне вашей виртуальной среды, поэтому нет необходимости их кэшировать. Таким образом, вы уменьшите размер своего изображения в Docker.

Примечание: Поскольку вы установили зависимости, не устанавливая свой пакет page-tracker в образе Docker, они останутся в кэшированном слое. Таким образом, любые изменения в вашем исходном коде не потребуют переустановки этих зависимостей.

Ваш Dockerfile растет и становится все более сложным, но не волнуйтесь. Осталось выполнить всего несколько шагов, так что вы почти у цели.

Запуск тестов как часть процесса сборки

Наконец, пришло время скопировать ваш исходный код в образ Docker и запустить ваши тесты вместе с линтеры и другие инструменты статического анализа:

# Dockerfile

FROM python:3.11.2-slim-bullseye

RUN apt-get update && \
    apt-get upgrade --yes

RUN useradd --create-home realpython
USER realpython
WORKDIR /home/realpython

ENV VIRTUALENV=/home/realpython/venv
RUN python3 -m venv $VIRTUALENV
ENV PATH="$VIRTUALENV/bin:$PATH"

COPY --chown=realpython pyproject.toml constraints.txt ./
RUN python -m pip install --upgrade pip setuptools && \
    python -m pip install --no-cache-dir -c constraints.txt ".[dev]"

COPY --chown=realpython src/ src/
COPY --chown=realpython test/ test/

RUN python -m pip install . -c constraints.txt && \
    python -m pytest test/unit/ && \
    python -m flake8 src/ && \
    python -m isort src/ --check && \
    python -m black src/ --check --quiet && \
    python -m pylint src/ --disable=C0114,C0116,R1705 && \
    python -m bandit -r src/ --quiet

После копирования папок src/ и test/ с вашего хост-компьютера вы устанавливаете пакет page-tracker в виртуальную среду. Включив средства автоматического тестирования в процесс сборки, вы гарантируете, что если какой-либо из них вернет ненулевой код состояния завершения , то сборка вашего образа Docker завершится ошибкой. Это именно то, чего вы хотите при реализации конвейера непрерывной интеграции.

Обратите внимание, что вам пришлось отключить проблемы с низкой степенью серьезности pylint C0114, C0116 и R1705, которые сейчас не имеют большого значения. В противном случае они помешали бы успешному созданию вашего образа Docker.

Причина объединения отдельных команд в одну RUN инструкцию заключается в том, чтобы уменьшить количество кэшируемых слоев. Помните, что чем больше у вас слоев, тем больше будет результирующее изображение в Docker.

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

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

Укажите команду для запуска в контейнерах Docker

Последним шагом является указание команды для выполнения внутри каждого нового Контейнера Docker, созданного на основе вашего образа Docker. На этом этапе вы можете запустить свое веб-приложение поверх встроенного сервера разработки Flask:

# Dockerfile

FROM python:3.11.2-slim-bullseye

RUN apt-get update && \
    apt-get upgrade --yes

RUN useradd --create-home realpython
USER realpython
WORKDIR /home/realpython

ENV VIRTUALENV=/home/realpython/venv
RUN python3 -m venv $VIRTUALENV
ENV PATH="$VIRTUALENV/bin:$PATH"

COPY --chown=realpython pyproject.toml constraints.txt ./
RUN python -m pip install --upgrade pip setuptools && \
    python -m pip install --no-cache-dir -c constraints.txt ".[dev]"

COPY --chown=realpython src/ src/
COPY --chown=realpython test/ test/

RUN python -m pip install . -c constraints.txt && \
    python -m pytest test/unit/ && \
    python -m flake8 src/ && \
    python -m isort src/ --check && \
    python -m black src/ --check --quiet && \
    python -m pylint src/ --disable=C0114,C0116,R1705 && \
    python -m bandit -r src/ --quiet

CMD ["flask", "--app", "page_tracker.app", "run", \
     "--host", "0.0.0.0", "--port", "5000"]

Здесь вы используете одну из трех форм команды CMD, которая напоминает синтаксис функции Python subprocess.run(). Обратите внимание, что вы должны привязать хост к адресу 0.0.0.0, чтобы сделать ваше приложение доступным из-за пределов контейнера Docker.

Теперь вы можете создать образ Docker на основе существующего файла Dockerfile и запустить контейнеры Docker, созданные на его основе. Следующая команда превратит ваш файл Dockerfile в изображение Docker с именем page-tracker:

$ docker build -t page-tracker .

Он будет искать файл Dockerfile в текущем рабочем каталоге, обозначенный точкой (.), и помечать полученное изображение меткой по умолчанию latest. Таким образом, полное название изображения будет выглядеть следующим образом page-tracker:latest.

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

К счастью, есть лучший способ упорядочить ваш файл Dockerfile, позволяющий создавать изображение в несколько этапов, которые вы сейчас изучите.

Реорганизуйте свой файл Dockerfile для многоступенчатых сборок

Файл Dockerfile, который вы создали до сих пор, довольно прост и должен подойти для разработки. Сохраните его, так как он понадобится вам позже для выполнения сквозного теста с помощью Docker Compose. Вы можете продублировать этот файл сейчас и присвоить ему другое имя. Например, вы можете добавить суффикс .dev к одной из двух копий:

page-tracker/
│
├── src/
│   └── page_tracker/
│       ├── __init__.py
│       └── app.py
│
├── test/
│
├── venv/
│
├── constraints.txt
├── Dockerfile
├── Dockerfile.dev
└── pyproject.toml

Теперь отредактируйте файл с именем Dockerfile и держите его открытым, пока вы разбиваете процесс сборки на этапы.

Примечание: Чтобы указать пользовательское имя файла вместо используемого по умолчанию Dockerfile при создании изображения, используйте -f или --file вариант:

$ docker build -f Dockerfile.dev -t page-tracker .

Это имя файла может быть любым по вашему желанию. Просто убедитесь, что вы правильно указали его в команде docker build.

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

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

$ docker images
REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
page-tracker   prod      9cb2e3233522   5 minutes ago    204MB
page-tracker   dev       f9918cb213dc   5 minutes ago    244MB
(...)

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

Каждый этап в файле Dockerfile начинается со своей собственной инструкции FROM, так что у вас их будет две. Первый этап будет выглядеть почти так же, как и ваш текущий файл Dockerfile, за исключением того, что вы дадите этому этапу имя builder, к которому вы сможете обратиться позже:

 # Dockerfile

-FROM python:3.11.2-slim-bullseye
+FROM python:3.11.2-slim-bullseye AS builder

 RUN apt-get update && \
     apt-get upgrade --yes

 RUN useradd --create-home realpython
 USER realpython
 WORKDIR /home/realpython

 ENV VIRTUALENV=/home/realpython/venv
 RUN python3 -m venv $VIRTUALENV
 ENV PATH="$VIRTUALENV/bin:$PATH"

 COPY --chown=realpython pyproject.toml constraints.txt ./
 RUN python -m pip install --upgrade pip setuptools && \
     python -m pip install --no-cache-dir -c constraints.txt ".[dev]"

 COPY --chown=realpython src/ src/
 COPY --chown=realpython test/ test/

 RUN python -m pip install . -c constraints.txt && \
     python -m pytest test/unit/ && \
     python -m flake8 src/ && \
     python -m isort src/ --check && \
     python -m black src/ --check --quiet && \
     python -m pylint src/ --disable=C0114,C0116,R1705 && \
-    python -m bandit -r src/ --quiet
+    python -m bandit -r src/ --quiet && \
+    python -m pip wheel --wheel-dir dist/ . -c constraints.txt

-CMD ["flask", "--app", "page_tracker.app", "run", \
-     "--host", "0.0.0.0", "--port", "5000"]

Поскольку вы будете переносить свое упакованное приложение для отслеживания страниц из одного образа в другой, вы должны добавить дополнительный шаг по созданию дистрибутивного пакета, используя формат Python wheel. Команда pip wheel создаст файл с именем, похожим на page_tracker-1.0.0-py3-none-any.whl, в подпапке dist/. Вы также удаляете инструкцию CMD из этого этапа, поскольку она станет частью следующего этапа.

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

# Dockerfile

FROM python:3.11.2-slim-bullseye AS builder

# ...

FROM python:3.11.2-slim-bullseye

RUN apt-get update && \
    apt-get upgrade --yes

RUN useradd --create-home realpython
USER realpython
WORKDIR /home/realpython

ENV VIRTUALENV=/home/realpython/venv
RUN python3 -m venv $VIRTUALENV
ENV PATH="$VIRTUALENV/bin:$PATH"

COPY --from=builder /home/realpython/dist/page_tracker*.whl /home/realpython

RUN python -m pip install --upgrade pip setuptools && \
    python -m pip install --no-cache-dir page_tracker*.whl

CMD ["flask", "--app", "page_tracker.app", "run", \
     "--host", "0.0.0.0", "--port", "5000"]

Вы начинаете с выполнения привычных шагов по обновлению системных пакетов, созданию пользователя и созданию виртуальной среды. Затем выделенная строка отвечает за копирование вашего файла wheel со стадии builder. Вы устанавливаете его с помощью pip, как и раньше. Наконец, вы добавляете инструкцию CMD для запуска вашего веб-приложения с помощью Flask.

Когда вы создадите образ с помощью такого многоэтапного Dockerfile, вы заметите, что выполнение первого этапа занимает больше времени, так как требуется установить все зависимости, запустить тесты и создать файл wheel. Однако создание второго этапа будет немного быстрее, потому что для этого нужно просто скопировать и установить готовый файл wheel. Также обратите внимание, что этап builder является временным, поэтому после него в ваших изображениях Docker не останется никаких следов.

Хорошо. Наконец-то вы готовы к созданию своего многоэтапного образа Docker!

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

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

Существует несколько различных стратегий управления версиями изображений в Docker. Например, некоторые популярные из них включают:

  • В семантическом управлении версиями используются три числа, разделенные точкой, для обозначения основных, второстепенных и исправленных версий.
  • Git commit hash использует SHA-1 хэш Git commit, привязанный к исходному коду вашего изображения.
  • Временная метка использует временную информацию, такую как Время Unix, чтобы указать, когда был создан образ.

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

В этом руководстве вы будете придерживаться подхода Git commit hash, поскольку он обеспечивает уникальные и неизменяемые метки для ваших изображений в Docker. Найдите минутку, чтобы инициализировать локальный репозиторий Git в вашей папке page-tracker/ и определить шаблоны .gitignore с файлами , соответствующие вашей рабочей среде. Вы можете еще раз проверить, находитесь ли вы в нужной папке, распечатав свой рабочий каталог с помощью команды pwd:

PS> pwd

Path
----
C:\Users\realpython\page-tracker

PS> git init
Initialized empty Git repository in C:/Users/realpython/page-tracker/.git/

PS> curl.exe -sL https://www.gitignore.io/api/python,pycharm+all > .gitignore

 

$ pwd
/home/realpython/page-tracker

$ git init
Initialized empty Git repository in /home/realpython/page-tracker/.git/

$ curl -sL https://www.gitignore.io/api/python,pycharm+all > .gitignore

Здесь вы загружаете содержимое из gitignore.io с помощью curl, запрашивая, чтобы Git исключал из отслеживания шаблоны файлов, связанные с Python и PyCharm. Флажок -L необходим для отслеживания перенаправлений, поскольку веб-сайт недавно переехал на другой адрес с более длинным доменом. В качестве альтернативы вы могли бы взять один из шаблонов из GitHub's gitignore репозиторий, который используют некоторые редакторы кода.

Как только ваш локальный репозиторий Git будет инициализирован, вы можете выполнить свой первый коммит и получить соответствующее значение хэша, например, с помощью команды git rev-parse:

$ git add .
$ git commit -m "Initial commit"
[master (root-commit) dde1dc9] Initial commit
 11 files changed, 535 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Dockerfile
 create mode 100644 Dockerfile.dev
 create mode 100644 constraints.txt
 create mode 100644 pyproject.toml
 create mode 100644 src/page_tracker/__init__.py
 create mode 100644 src/page_tracker/app.py
 create mode 100644 test/conftest.py
 create mode 100644 test/e2e/test_app_redis_http.py
 create mode 100644 test/integration/test_app_redis.py
 create mode 100644 test/unit/test_app.py

$ git rev-parse HEAD
dde1dc9303a2a9f414d470d501572bdac29e4075

Если вам не нравится длинный вывод, то вы можете добавить флаг --short в команду, которая выдаст вам сокращенную версию того же хэша фиксации:

$ git rev-parse --short HEAD
dde1dc9

По умолчанию он возвращает самый короткий префикс, который может однозначно идентифицировать этот конкретный коммит без двусмысленности.

Теперь, когда у вас есть хэш Git commit, вы можете использовать его в качестве тега для вашего изображения в Docker. Чтобы создать изображение, запустите команду docker build, указав при этом параметр -t или --tag, чтобы присвоить вашему новому изображению метку. Конечная точка указывает на ваш текущий рабочий каталог как на место поиска файла Dockerfile:

$ docker build -t page-tracker:$(git rev-parse --short HEAD) .

Первая часть до двоеточия, page-tracker, - это мнемоническое название вашего образа Docker. Обратите внимание, что в реальной жизни вы, вероятно, добавили бы какой-нибудь суффикс, чтобы указать роль этого сервиса. Например, поскольку это веб-приложение Flask, вы могли бы назвать свое изображение page-tracker-web или что-то в этом роде. То, что следует за двоеточием, является фактическим тегом, который в данном случае является хэшем Git-коммита из текущего коммита.

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

Еще один момент, на который стоит обратить внимание, заключается в том, что Docker хранит только одну копию вашего изображения. У него есть уникальный идентификатор, например 9cb2e3233522, на который могут ссылаться несколько меток:

$ docker images
REPOSITORY     TAG                    IMAGE ID       CREATED       SIZE
page-tracker   dde1dc9                9cb2e3233522   1 hour ago    204MB
page-tracker   prod                   9cb2e3233522   1 hour ago    204MB
page-tracker   dev                    f9918cb213dc   1 hour ago    244MB
(...)

В этом и заключается преимущество добавления тегов к изображениям в Docker. Это позволяет вам ссылаться на одно и то же изображение с помощью разных меток, таких как page-tracker:prod или page-tracker:dde1dc9, сохраняя при этом уникальную идентификацию. Каждая метка состоит из названия хранилища, о котором вы узнаете в следующем разделе, и определенного имени тега.

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

Но как перенести контейнер в удаленную среду? Об этом вы узнаете в следующем разделе.

Переместите изображение в реестр Docker

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

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

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

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

Примечание: Репозиторий в вашей учетной записи Docker Hub представляет собой коллекцию изображений Docker, которые пользователи могут загружать. Каждое хранилище может содержать несколько помеченных версий одного и того же изображения. В этом отношении репозиторий Docker Hub аналогичен репозиторию GitHub, но в нем содержатся изображения Docker, а не код. Частные репозитории позволяют ограничить доступ только авторизованным пользователям, в то время как общедоступные репозитории доступны всем.

Зачем вам вообще использовать реестр Docker?

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

Зарегистрируйте учетную запись на Docker Hub прямо сейчас, если вы еще этого не сделали. Обратите внимание, что вам нужно будет указать свое уникальное имя пользователя в дополнение к адресу электронной почты и паролю, как на GitHub:

Docker Hub Sign-Up Form Форма регистрации в Docker Hub

Важно выбрать хорошее и запоминающееся имя пользователя, потому что оно станет вашим отличительным признаком в Docker Hub. Чтобы избежать конфликтов имен между изображениями, принадлежащими разным пользователям, Docker Hub распознает каждое хранилище по комбинации имени пользователя и названия хранилища. Например, если ваше имя пользователя realpython, то один из ваших репозиториев может быть идентифицирован строкой realpython/page-tracker, которая напоминает название репозитория на GitHub.

Первое, что вам следует сделать после регистрации и входа в свою новую учетную запись Docker Hub в веб-браузере, - это создать хранилище для ваших изображений. Нажмите на плитку Создать репозиторий или перейдите на вкладку Хранилища на панели навигации вверху и нажмите Создать репозиторий<кнопка>. Затем назовите свой репозиторий page-tracker, если хотите, дайте ему содержательное описание и выберите опцию Приватный, чтобы он был доступен только вам:

Docker Hub Create Repository Form Форма создания репозитория в Docker Hub

После этого вы увидите инструкции с командами терминала, которые позволят вам загружать изображения Docker в ваш репозиторий. Но сначала вам нужно будет войти в Docker Hub из командной строки, указав свое имя пользователя и пароль:

$ docker login -u realpython
Password:

Аутентификация с помощью docker login требуется, даже если вы собираетесь работать только со своими общедоступными репозиториями.

Примечание: Если вы включили двухфакторную аутентификацию в настройках безопасности вашей учетной записи Docker Hub, вам потребуется создать токен доступа с соответствующими разрешениями для входа в систему с помощью docker в командной строке. Когда он попросит вас ввести пароль, просто укажите свой токен.

В противном случае, если у вас не настроена двухфакторная аутентификация, вы сможете войти в систему, используя свой пароль Docker Hub. Тем не менее, все равно стоит сгенерировать токен для повышения безопасности, как объясняется в документации .

Когда вы отправляете код в удаленный репозиторий с помощью Git, вы должны сначала клонировать его откуда-либо или вручную установить источник по умолчанию, который настраивает метаданные локального репозитория. В отличие от этого, в Docker Hub или любом другом реестре Docker процесс сопоставления локального образа с его удаленным аналогом немного отличается — вы используете теги. В частности, вы помечаете созданный образ, используя имя пользователя вашего Docker Hub и имя репозитория в качестве префикса.

Во-первых, вы должны указать исходную метку, например page-tracker:dde1dc9, локального образа Docker, который вы хотите опубликовать. Чтобы найти точную метку вашего изображения page-tracker, которое вы только что создали, проверьте текущий хэш Git-коммита или перечислите существующие docker images.

Как только вы узнаете, как вы пометили свое изображение, используйте тот же хэш фиксации для создания целевой метки, которая будет идентифицировать ваш репозиторий в реестре Docker. Не забудьте заменить realpython на ваше собственное имя пользователя в Docker Hub, прежде чем создавать новый тег:

$ docker tag page-tracker:dde1dc9 realpython/page-tracker:dde1dc9

Это добавит новую метку realpython/page-tracker:dde1dc9 к вашему локальному изображению, помеченному как page-tracker:dde1dc9. Полная форма целевой метки выглядит следующим образом:

registry/username/repository:tag

Часть реестра может быть опущена, если вы хотите перейти к концентратору Docker по умолчанию. В противном случае это может быть адрес домена, например docker.io, или IP-адрес с необязательным номером порта вашего частного экземпляра реестра. Имя пользователя и репозиторий должны соответствовать тем, которые вы создали в Docker Hub или в любом другом реестре, который вы используете. Если вы не укажете тег, то Docker неявно применит тег latest, который может быть не определен.

Вы можете пометить одно и то же изображение несколькими тегами: 

$ docker tag page-tracker:dde1dc9 realpython/page-tracker:latest

Как только вы правильно пометите изображения, вы сможете отправить их в нужный реестр с помощью docker push:

$ docker push realpython/page-tracker:dde1dc9
$ docker push realpython/page-tracker:latest

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

Когда вы обновляете свой профиль на Docker Hub, в нем должны отображаться два тега, которые вы только что поместили в свой репозиторий:

Tagged Docker Images on Docker Hub Изображения Docker с тегами на Docker Hub

Теперь, когда вы добавляете соавторов в свой личный репозиторий, они смогут загружать изображения. Имейте в виду, что для этого требуется обновленный тарифный план подписки на Docker Hub. Альтернативой было бы создание токена доступа с правами доступа только для чтения ко всем вашим репозиториям или создание общедоступного репозитория вместо этого.

Хорошо. Наконец-то пришло время запустить ваше веб-приложение Flask с привязкой к докеру, запустив его в контейнере Docker.

Запуск контейнера Docker

Если вы запускаете в чистой среде Docker, возможно, на другом компьютере, вы можете загрузить свой образ, загрузив его из Docker Hub, при условии, что у вас есть разрешение на чтение этого хранилища: 

$ docker pull realpython/page-tracker
Using default tag: latest
latest: Pulling from realpython/page-tracker
f1f26f570256: Pull complete
2d2b01660885: Pull complete
e4e8e4c0b0e1: Pull complete
1ba60f086308: Pull complete
3c2fccf90be1: Pull complete
15e9066b1610: Pull complete
e8271c9a01cc: Pull complete
4f4fb700ef54: Pull complete
bb211d339643: Pull complete
8690f9a37c37: Pull complete
7404f1e120d1: Pull complete
Digest: sha256:cc6fe40a1ac73e6378d0660bf386a1599880a30e422dc061680769bc4d501164
Status: Downloaded newer image for realpython/page-tracker:latest
docker.io/realpython/page-tracker:latest

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

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

Вот команда для запуска нового контейнера Docker на основе вашего нового образа: 

$ docker run -p 80:5000 --name web-service realpython/page-tracker
 * Serving Flask app 'page_tracker.app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production
⮑ deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://172.17.0.3:5000
Press CTRL+C to quit

Когда вы разрабатываете свой проект локально, часто бывает удобно использовать переадресацию портов для доступа к веб-серверу через локальный хост вашего хост-компьютера. В этом случае опция -p позволит вам перейти по адресу http://localhost:80 или просто по адресу http://localhost, не зная точного IP-адреса запущенного контейнера Docker. Порт 80 - это порт протокола HTTP по умолчанию, что означает, что вы можете не указывать его при вводе адреса в веб-браузере.

Кроме того, такое сопоставление портов гарантирует отсутствие коллизии сетевых портов в точке http://localhost:5000, если вы не остановили свой локальный экземпляр Flask. Помните, что ранее вы запустили один из них для выполнения сквозного тестирования. Он будет занимать порт Flask по умолчанию, 5000, если процесс все еще выполняется где-то в фоновом режиме.

примечание: это также полезно, чтобы дать вашему контейнера Docker описательное имя, например web-service, так что вы можете перезапустить или удалить его по имени, не отрываясь соответствующий контейнер идентификатор. Если вы этого не сделаете, то Docker присвоит вашему контейнеру странное имя, например admiring_jang или frosty_almeida, выбранное случайным образом.

Подумайте о том, чтобы добавить флаг --rm, чтобы автоматически удалять контейнер при его остановке, если вы не хотите делать это вручную.

Как вы можете видеть в приведенном выше выводе, сервер Flask запущен на всех сетевых интерфейсах (0.0.0.0) в своем контейнере, как вы и указали на уровне CMD в вашем файле Dockerfile.

Перейдите по адресу http://localhost в своем веб-браузере или воспользуйтесь инструментом командной строки, например, curl, чтобы получить доступ к отслеживанию страниц с привязкой:

PS> curl.exe http://localhost
Sorry, something went wrong 😔
$ curl http://localhost
Sorry, something went wrong 😔

Вы увидите ожидаемое сообщение об ошибке из-за сбоя подключения к Redis, но, по крайней мере, вы можете получить доступ к своему приложению Flask, запущенному в контейнере Docker. Чтобы устранить ошибку, вам нужно будет указать правильный URL-адрес Redis с помощью переменной окружения, которую вы передадите в контейнер web-service.

Остановите этот контейнер сейчас, нажав Ctrl+C или Cmd+C на вашей клавиатуре. Затем найдите идентификатор контейнера и удалите связанный с ним контейнер:

$ docker ps -a
CONTAINER ID   IMAGE                     COMMAND                  CREATED
dd446a1b72a7   realpython/page-tracker   "flask --app page_tr…"   1 minute ago

$ docker rm dd446a1b72a7

Флажок -a гарантирует отображение всех контейнеров, включая остановленные. В противном случае вы бы не увидели свой.

Правильный способ подключить веб-приложение Flask к Redis через Docker - это создать выделенную виртуальную сеть. Сначала перечислите доступные сети, чтобы проверить, созданы ли они уже page-tracker-network

$ docker network ls
NETWORK ID     NAME                   DRIVER    SCOPE
46e9ff2ec568   bridge                 bridge    local
4795b850cb58   host                   host      local
f8f99d305c5e   none                   null      local
84b134794660   page-tracker-network   bridge    local

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

$ docker network create page-tracker-network

Аналогичным образом вы можете создать том, чтобы сервер Redis постоянно хранил свои данные на вашем хост-компьютере. Таким образом, вы можете перезапустить или даже удалить и создать новый контейнер с нуля, и Redis получит доступ к своему предыдущему состоянию. Вот как вы можете создать именованный том с помощью Docker: 

$ docker volume create redis-volume

Затем остановите и удалите все контейнеры Redis, которые могут находиться поблизости, и запустите новый. На этот раз вы подключите контейнер к page-tracker-network и привяжете его папку /data к тому с именем redis-volume, который вы только что создали: 

$ docker run -d \
             -v redis-volume:/data \
             --network page-tracker-network \
             --name redis-service \
             redis:7.0.10-bullseye

Когда вы посмотрите на официальное изображение Redis в Docker на GitHub, вы увидите слой, который определяет точка монтирования в папке /data. Redis будет время от времени сохранять свое состояние в этой папке. Подключив каталог с вашего хоста к этой точке монтирования, вы сможете сохранить это состояние даже при перезапуске контейнера.

Присвоив своему контейнеру описательное имя redis-service, вы сможете подключиться к нему из другого контейнера в той же сети. Вот как: 

$ docker run -d \
             -p 80:5000 \
             -e REDIS_URL=redis://redis-service:6379 \
             --network page-tracker-network \
             --name web-service \
             realpython/page-tracker

Вы запускаете новый контейнер, созданный на основе page-tracker изображения, с достаточным количеством параметров. Вот краткое описание отдельных флагов и опций в приведенной выше команде docker run:

  • -d: Запустите контейнер в фоновом режиме, отдельно от терминала. Это означает, что вы не увидите никаких выходных данных с сервера Flask и не сможете остановить контейнер с помощью Ctrl+C или Cmd+C больше нет.
  • -p 80:5000: Укажите порт контейнера 5000 на порту 80 хост-компьютера, чтобы вы могли получить доступ к своему веб-приложению через localhost.
  • -e REDIS_URL=...: Задайте переменной окружения контейнера адрес сервера Redis, работающего в другом контейнере в той же сети.
  • --network page-tracker-network: Укажите виртуальную сеть, которую будет использовать контейнер. Это позволит другим контейнерам в той же сети взаимодействовать с этим контейнером с помощью абстрактных имен, а не IP-адресов.
  • --name web-service: Присвойте контейнеру осмысленное имя, чтобы было проще ссылаться на него из команд Docker.

Теперь, когда вы получаете доступ к своему веб-приложению Flask либо в веб-браузере, либо в терминале, вы должны соблюдать правильное поведение:

PS> curl.exe http://localhost
This page has been seen 1 times.

PS> curl.exe http://localhost
This page has been seen 2 times.

PS> curl.exe http://localhost
This page has been seen 3 times.

 

$ curl http://localhost
This page has been seen 1 times.

$ curl http://localhost
This page has been seen 2 times.

$ curl http://localhost
This page has been seen 3 times. 

Каждый раз, когда вы отправляете запрос, сервер выдает разное количество просмотров страниц. Обратите внимание, что вы получаете доступ к серверу через localhost. Если вы запустили redis-service до web-service, то IP-адрес контейнера, скорее всего, изменился.

Ух ты! Пришлось изрядно потрудиться, чтобы запустить две службы. Как вы можете видеть, управление образами Docker, контейнерами, томами, сетями, портами и переменными среды может показаться сложным, если выполнять его вручную. И это только начало! Представьте, какие усилия требуются для управления сложным приложением с десятками сервисов, готовых к работе мониторинг, балансировка нагрузки, автоматическое масштабирование и многое другое.

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

Управление контейнерами с помощью Docker Compose

Большинство реальных приложений состоят из множества компонентов, которые естественным образом преобразуются в контейнеры Docker. Например, более сложное веб-приложение может содержать следующее:

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

Чтобы помочь управлять и, в некоторой степени, организовывать несколько контейнеров Docker для таких приложений, вы можете использовать Docker Compose. Это инструмент, который работает поверх Docker и упрощает запуск многоконтейнерных приложений Docker. Docker Compose позволяет вам определить ваше приложение с точки зрения взаимозависимых служб, а также их конфигурацию и требования. Затем он скоординирует их и запустит как одно согласованное приложение.

Примечание: Контейнерная оркестровка автоматизирует развертывание, масштабирование и управление конфигурацией распределенного приложения. Хотя Docker Compose может помочь в элементарной форме оркестровки, в более сложных и крупномасштабных системах лучше использовать такие инструменты, как Kubernetes.

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

Настройка Docker Compose на Вашем компьютере

Если вы следовали инструкциям по настройке Docker Desktop, то у вас уже должен быть установлен Docker Compose. Для подтверждения этого запустите в своем терминале следующую команду: 

$ docker compose version
Docker Compose version v2.17.2

Использование Docker Desktop, который объединяет Docker Compose и несколько других компонентов, в настоящее время является рекомендуемым способом установки Docker Compose на macOS и Windows. Если вы используете Linux, то можете попробовать альтернативный способ, установив плагин Compose вручную или из репозитория пакетов вашего дистрибутива. К сожалению, этот метод может не работать с последней и рекомендуемой версией Docker Compose.

Примечание: В прежние времена Docker Compose был независимым проектом, который поддерживался отдельно от Docker. Первоначально он был реализован в виде скрипта на Python, который в конечном итоге был переписан на Go.

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

Как только вы подтвердите, что Docker Compose доступен в вашем терминале, все готово к работе!

Определите многоконтейнерное приложение Docker

Поскольку вы будете создавать многоконтейнерное приложение Docker, которое в будущем потенциально может расшириться и включать в себя гораздо больше сервисов, стоит изменить структуру папок в вашем проекте. Создайте новую подпапку с именем web/ в корневой папке вашего проекта, где вы будете хранить все файлы, относящиеся к веб-сервису Flask.

Ваша виртуальная среда также относится к этой новой подпапке, поскольку другие сервисы могут быть реализованы на совершенно незнакомом языке программирования, таком как C++ или Java. К сожалению, перемещение папки venv/, скорее всего, приведет к нарушению абсолютных путей, жестко заданных в соответствующих сценариях активации. Поэтому на всякий случай удалите старую виртуальную среду и создайте новую во вложенной папке web/:

(page-tracker) PS> deactivate
PS> cd page-tracker\
PS> rmdir venv\ /s
PS> python -m venv web\venv\ --prompt page-tracker
PS> web\venv\Scripts\activate
(page-tracker) PS> python -m pip install --upgrade pip

 

(page-tracker) $ deactivate
$ cd page-tracker/
$ rm -rf venv/
$ python3 -m venv web/venv/ --prompt page-tracker
$ source web/venv/bin/activate
(page-tracker) $ python -m pip install --upgrade pip

Затем переместите приложение Flask в новую подпапку web/, оставив только папку .git/, .gitignore и любые другие файлы конфигурации, связанные с редактором. Вы можете сохранить их в корневой папке проекта, поскольку они являются общими для всех возможных сервисов в вашем проекте. После этого структура вашего проекта должна выглядеть следующим образом:

page-tracker/
│
├── web/
│   │
│   ├── src/
│   │   └── page_tracker/
│   │       ├── __init__.py
│   │       └── app.py
│   │
│   ├── test/
│   │   ├── e2e/
│   │   │   └── test_app_redis_http.py
│   │   │
│   │   ├── integration/
│   │   │   └── test_app_redis.py
│   │   │
│   │   ├── unit/
│   │   │   └── test_app.py
│   │   │
│   │   └── conftest.py
│   │
│   ├── venv/
│   │
│   ├── constraints.txt
│   ├── Dockerfile
│   ├── Dockerfile.dev
│   └── pyproject.toml
│
├── .git/
│
├── .gitignore
└── docker-compose.yml

Одним из новых дополнений к файловому дереву, представленному выше, является файл docker-compose.yml, расположенный на верхнем уровне, который вы сейчас будете записывать.

Docker Compose использует формат YAML для декларативного описания служб вашего приложения, которые станут контейнерами Docker, их сетями, томами, сопоставлениями портов, переменными среды и многим другим. Раньше вам приходилось кропотливо определять каждый элемент архитектуры вашего приложения вручную, но с Docker Compose вы можете определить все это в одном файле. Инструмент может даже извлекать или создавать изображения для вас!

Примечание: Если вы никогда раньше не использовали YAML, но знакомы с JSON, то его синтаксис должен быть вам знаком, поскольку YAML является надмножеством JSON. Ознакомьтесь с YAML: Недостающая батарея в Python для получения более подробной информации.

В окне настройки Создайте файл, в котором вы будете определять свои службы, сети и тома. Вот полный файл docker-compose.yml, который отражает все, что вы определили для своего приложения для отслеживания страниц вручную в предыдущих разделах:

 1# docker-compose.yml
 2
 3services:
 4  redis-service:
 5    image: "redis:7.0.10-bullseye"
 6    networks:
 7      - backend-network
 8    volumes:
 9      - "redis-volume:/data"
10  web-service:
11    build: ./web
12    ports:
13      - "80:5000"
14    environment:
15      REDIS_URL: "redis://redis-service:6379"
16    networks:
17      - backend-network
18    depends_on:
19      - redis-service
20
21networks:
22    backend-network:
23
24volumes:
25  redis-volume:

Теперь вы разберете это построчно:

  • Строка 3 знаменует собой начало описания двух ваших сервисов, redis-service и web-service, которые представляют собой многоконтейнерное приложение Docker. Обратите внимание, что вы можете масштабировать каждую службу, поэтому фактическое количество контейнеров Docker может быть больше, чем заявленное здесь количество служб.
  • Строки с 4 по 9 определяют конфигурацию для redis-service, включая образ Docker для запуска, сеть для подключения и том для монтирования.
  • Строки с 10 по 19 сконфигурируйте web-service, указав папку с файлом Dockerfile, который нужно создать, порты, которые нужно предоставить, переменные среды, которые нужно установить, и сети, к которым нужно подключиться. Инструкция depends_on требует, чтобы redis-service был доступен до запуска web-service.
  • Строки 21 и 22 определяют виртуальную сеть для ваших двух сервисов. Это объявление не является строго необходимым, поскольку Docker Compose автоматически создаст и подключит ваши контейнеры к новой сети. Однако явное объявление сети дает вам больше контроля над ее настройками и диапазоном адресов, если это необходимо.
  • Строки 24 и 25 определяют постоянный том для вашего сервера Redis.

Некоторые значения в приведенном выше файле конфигурации заключены в кавычки, в то время как другие - нет. Это мера предосторожности против известной причуды в более старой спецификации формата YAML, которая рассматривает определенные символы как специальные, если они появляются в строках без кавычек. Например, двоеточие (:) может заставить некоторые анализаторы YAML интерпретировать литерал как шестидесятеричное число вместо строки.

Примечание: Этот файл соответствует самой последней и рекомендуемой спецификации Compose, которая больше не требует поля верхнего уровня version, в отличие от предыдущих версий схемы. Прочтите о управлении версиями файлов Compose в официальной документации, чтобы узнать больше.

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

$ docker stop -t 0 web-service redis-service
$ docker container rm web-service redis-service
$ docker network rm page-tracker-network
$ docker volume rm redis-volume

Это приведет к удалению двух ваших контейнеров, сети и тома, которые вы создали ранее. Обратите внимание, что вы можете сократить команду docker container rm до более короткого псевдонима docker rm.

Чтобы удалить контейнер корректно, вы должны сначала остановить его. По умолчанию команда docker stop будет ждать десять секунд, прежде чем уничтожить контейнер, что даст ему достаточно времени для выполнения всех необходимых действий по очистке перед выходом. Поскольку вашему приложению Flask не нужно ничего делать после того, как оно перестанет работать, вы можете установить этот тайм-аут равным нулю секунд, используя параметр -t, который немедленно завершит работу перечисленных контейнеров.

Чтобы удалить все связанные теги изображений Docker, вы должны сначала найти их общий идентификатор:

$ docker images
REPOSITORY                TAG              IMAGE ID       CREATED      SIZE
page-tracker              dde1dc9          9cb2e3233522   1 hour ago   204MB
page-tracker              latest           9cb2e3233522   1 hour ago   204MB
realpython/page-tracker   dde1dc9          9cb2e3233522   1 hour ago   204MB
realpython/page-tracker   latest           9cb2e3233522   1 hour ago   204MB
(...)

В этом случае короткий идентификатор, общий для всех тегов изображения page-tracker, равен 9cb2e3233522, который вы можете использовать для снятия тега и удаления базового изображения Docker: 

$ docker rmi -f 9cb2e3233522
Untagged: page-tracker:dde1dc9
Untagged: page-tracker:latest
Untagged: realpython/page-tracker:dde1dc9
Untagged: realpython/page-tracker:latest
Deleted: sha256:9cb2e3233522e020c366880867980232d747c4c99a1f60a61b9bece40...

Команда docker rmi является псевдонимом docker image rm и docker image remove.

Примечание: Если вы хотите начать с нуля, используя новую среду Docker, и не боитесь потерять данные, вы можете сократить все системные ресурсы с помощью следующей команды:

$ docker system prune --all --volumes

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

После подтверждения удаления ваших ресурсов Docker вы можете мгновенно восстановить работу своего приложения для отслеживания страниц с помощью одной инструкции. Выполните следующую команду в той же папке, что и ваш файл docker-comopse.yml, чтобы не указывать путь к нему:

$ docker compose up -d
(...)
[+] Running 4/4
 ⠿ Network page-tracker_backend-network    Created                      0.1s
 ⠿ Volume "page-tracker_redis-volume"      Created                      0.0s
 ⠿ Container page-tracker-redis-service-1  Started                      1.0s
 ⠿ Container page-tracker-web-service-1    Started                      1.3s

При первом запуске этой команды может потребоваться больше времени, поскольку Docker Compose должен загрузить образ Redis из Docker Hub и снова создать другой образ из вашего файла Docker. Но после этого все должно быть почти мгновенно.

В приведенном выше выводе вы можете видеть, что Docker Compose создал запрошенную сеть, том и два контейнера. Обратите внимание, что он всегда добавляет к таким именам ресурсов префикс вашего проекта Docker Compose , который по умолчанию соответствует имени папки, содержащей ваш файл docker-compose.yml. В этом случае имя проекта будет page-tracker. Эта функция помогает предотвратить конфликт имен ресурсов в разных проектах Docker Compose.

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

Плагин Docker Compose предоставляет несколько полезных команд для управления вашим многоконтейнерным приложением. Вот лишь некоторые из них:

$ docker compose ps
NAME                           COMMAND                  SERVICE        ...
page-tracker-redis-service-1   "docker-entrypoint.s…"   redis-service  ...
page-tracker-web-service-1     "flask --app page_tr…"   web-service    ...

$ docker compose logs --follow
(...)
page-tracker-web-service-1    |  * Running on all addresses (0.0.0.0)
page-tracker-web-service-1    |  * Running on http://127.0.0.1:5000
page-tracker-web-service-1    |  * Running on http://172.20.0.3:5000
page-tracker-web-service-1    | Press CTRL+C to quit

$ docker compose stop
[+] Running 2/2
 ⠿ Container page-tracker-web-service-1    Stopped                     10.3s
 ⠿ Container page-tracker-redis-service-1  Stopped                      0.4s

$ docker compose restart
[+] Running 2/2
 ⠿ Container page-tracker-redis-service-1  Started                      0.4s
 ⠿ Container page-tracker-web-service-1    Started                      0.5s

$ docker compose down --volumes
[+] Running 4/4
 ⠿ Container page-tracker-web-service-1    Removed                      6.0s
 ⠿ Container page-tracker-redis-service-1  Removed                      0.4s
 ⠿ Volume page-tracker_redis-volume        Removed                      0.0s
 ⠿ Network page-tracker_backend-network    Removed                      0.1s

Например, вы можете перечислить контейнеры в вашем проекте Docker Compose, не показывая никаких других контейнеров. Используя соответствующую команду, вы можете просматривать их текущие выходные данные, останавливать, запускать и перезапускать их все. Когда вы закончите работу над проектом, вы можете удалить его, и Docker Compose удалит связанные контейнеры и сети. Однако это не затронет постоянное хранилище данных, если только вы явно не запросите это с помощью флага --volumes.

Одна вещь, на которую вы, возможно, обратили внимание в журналах, на которые Flask уже давно жалуется, - это использование небезопасного, неэффективного и нестабильного веб-сервера разработки для запуска вашего приложения. Теперь вы можете исправить это с помощью Docker Compose.

Замените Веб-Сервер Разработки Flask на Gunicorn

Docker позволяет переопределить команду по умолчанию или точку входа, указанную в файле Docker, при запуске нового контейнера. Например, команда по умолчанию в образе redis запускает сервер Redis. Однако ранее вы использовали этот же образ для запуска redis-cli в другом контейнере. Аналогичным образом, вы можете указать пользовательскую команду для ваших изображений Docker в файле docker-compose.yml. Вы будете использовать эту функцию для запуска Flask через веб-сервер производственного уровня.

Примечание: Иногда требуется исследовать существующий контейнер. Чтобы запустить команду в запущенном контейнере вместо запуска нового, вы можете использовать команду docker exec:

$ docker exec -it -u root page-tracker-web-service-1 /bin/bash
root@6e23f154a5b9:/home/realpython#

Запустив исполняемый файл Bash, /bin/bash и указав пользователя с параметром -u, вы фактически получаете доступ к контейнеру, как если бы входили в систему удаленный сервер по SSH. Флаги -it необходимы для запуска интерактивного сеанса терминала. В противном случае команда была бы немедленно завершена.

Существует несколько вариантов замены встроенного веб-сервера разработки Flask, которые официальная документация рекомендует при развертывании в рабочей среде. Одним из наиболее популярных вариантов является Gunicorn (Зеленый единорог), который представляет собой чисто Python-реализацию протокола Web Server Gateway Interface (WSGI). Чтобы начать его использовать, вы должны добавить пакет gunicorn в качестве еще одной зависимости в свой проект:

# web/pyproject.toml

[build-system]
requires = ["setuptools>=67.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "page-tracker"
version = "1.0.0"
dependencies = [
    "Flask",
    "gunicorn",
    "redis",
]

[project.optional-dependencies]
dev = [
    "bandit",
    "black",
    "flake8",
    "isort",
    "pylint",
    "pytest",
    "pytest-timeout",
    "requests",
]

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

Как обычно, переустановите свой пакет page-tracker локально и закрепите его зависимости в файле ограничений. Имейте в виду, что вам может потребоваться сначала активировать вашу виртуальную среду, поскольку ранее вы повторно создали ее в подпапке web/:

(page-tracker) $ python -m pip install --editable "web/[dev]"
(page-tracker) $ python -m pip freeze --exclude-editable > web/constraints.txt

Обратите внимание, что эти команды будут выглядеть немного по-другому, когда вы будете выполнять их из корневой папки вашего проекта. В таком случае вы должны заменить точку (.),, которая указывает на текущий рабочий каталог, на путь к вашей подпапке web/.

Теперь, когда вы установили Gunicorn, вы можете начать его использовать. Измените docker-compose.yml, добавив новый атрибут command под вашим ключом web-service:

# docker-compose.yml

services:
  redis-service:
    image: "redis:7.0.10-bullseye"
    networks:
      - backend-network
    volumes:
      - "redis-volume:/data"
  web-service:
    build: ./web
    ports:
      - "80:8000"
    environment:
      REDIS_URL: "redis://redis-service:6379"
    networks:
      - backend-network
    depends_on:
      - redis-service
    command: "gunicorn page_tracker.app:app --bind 0.0.0.0:8000"

networks:
    backend-network:

volumes:
  redis-volume:

Эта команда будет иметь приоритет над командой Dockerfile по умолчанию, которая используется на сервере разработки Flask. С этого момента Docker Compose будет запускать ваше веб-приложение с помощью Gunicorn. Чтобы показать разницу, вы запустите сервер на порту 8000 вместо 5000, таким образом, вы также измените отображение портов.

Указав порт 80 на главном компьютере, вы все равно сможете получить доступ к приложению по адресу http://localhost, не указывая номер порта.

Не забудьте зафиксировать свои изменения, чтобы сохранить вашу работу в локальном репозитории Git:

$ git add .
$ git commit -m "Refactor folders and add Docker Compose"

Всегда рекомендуется делать небольшие и частые коммиты, чтобы вы могли отслеживать постепенные изменения с течением времени и иметь более полную историю своей работы. Если вы не уверены, как описать свой коммит, то попробуйте объяснить , почему вы внесли определенное изменение, поскольку Git уже отслеживает , что изменилось.

Хорошо. Если вы сейчас попытаетесь перезапустить приложение Docker Compose, то это приведет к сбою, потому что Docker не найдет запрошенный gunicorn исполняемый файл во время запуска контейнера. Вы добавили дополнительную зависимость, которая отсутствует в образе Docker, созданном ранее. Поэтому вам нужно указать Docker Compose, чтобы перестроить ваш образ. Вы можете сделать это с помощью любой из следующих команд:

  1. docker compose build
  2. docker compose up --build

В первом случае вы бы явно указали Docker создать образ заранее. Всякий раз, когда вы меняете зависимости вашего проекта или Dockerfile, вам придется запускать docker compose build снова, чтобы применить эти изменения.

Во втором случае docker compose up --build даст команду Docker создавать образ "на лету" при каждом запуске контейнеров. Это особенно полезно, если вы пытаетесь быстро выполнить итерацию изменений в исходном коде или файле Dockerfile.

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

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

Запуск сквозных тестов для Служб

При первой попытке вы выполните комплексные тесты локально с вашего компьютера. Обратите внимание, что для того, чтобы это сработало, все необходимые службы должны быть доступны из вашей локальной сети. Хотя это и не идеально, поскольку вы не хотите делать общедоступными какие-либо конфиденциальные сервисы, такие как база данных, но вскоре вы узнаете о лучшем способе. Тем временем вы можете обновить свою конфигурацию docker-compose.yml, чтобы перенаправить порт Redis:

# docker-compose.yml

services:
  redis-service:
    image: "redis:7.0.10-bullseye"
    ports:
      - "6379:6379"
    networks:
      - backend-network
    volumes:
      - "redis-volume:/data"
  web-service:
    build: ./web
    ports:
      - "80:8000"
    environment:
      REDIS_URL: "redis://redis-service:6379"
    networks:
      - backend-network
    depends_on:
      - redis-service
    command: "gunicorn page_tracker.app:app --bind 0.0.0.0:8000"

networks:
    backend-network:

volumes:
  redis-volume:

Если у вас есть существующий контейнер Docker для redis-service, то вам нужно сначала удалить этот контейнер, даже если он в данный момент остановлен, чтобы отразить новые правила переадресации портов. К счастью, Docker Compose автоматически обнаружит изменения в вашем файле docker-compose.yml и при необходимости заново создаст ваши контейнеры, когда вы выполните команду docker compose up

$ docker compose up -d
[+] Running 2/2
 ⠿ Container page-tracker-redis-service-1  Started                      1.0s
 ⠿ Container page-tracker-web-service-1    Started                      1.2s

$ docker compose ps
NAME                           ...   PORTS
page-tracker-redis-service-1   ...   0.0.0.0:6379->6379/tcp
page-tracker-web-service-1     ...   0.0.0.0:80->8000/tcp

После перечисления ваших новых контейнеров вы должны увидеть, что порт 6379 контейнера Redis перенаправляется на хост-компьютер. Таким образом, теперь вы можете запускать свои комплексные тесты с помощью pytest, установленного в виртуальной среде на вашем компьютере разработчика:

(page-tracker) $ python -m pytest web/test/e2e/ \
  --flask-url http://localhost \
  --redis-url redis://localhost:6379

Благодаря сопоставлению портов вы можете использовать localhost для подключения к контейнерам, не зная их индивидуальных IP-адресов.

Примечание: Если ваш тест пройдет успешно,

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

Чтобы имитировать сбой, вы можете временно приостановить работу контейнеров на время выполнения теста:

$ docker compose pause
[+] Running 2/0
 ⠿ Container page-tracker-web-service-1    Paused                       0.0s
 ⠿ Container page-tracker-redis-service-1  Paused                       0.0s

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

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

$ docker compose unpause
[+] Running 2/0
 ⠿ Container page-tracker-web-service-1    Unpaused                     0.0s
 ⠿ Container page-tracker-redis-service-1  Unpaused                     0.0s

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

Сначала откройте свой файл docker-compose.yml и удалите переадресацию портов из Redis, так как вы больше не хотите предоставлять его внешнему миру. Затем добавьте новый сервис, основанный на вашем старом Dockerfile.dev, который объединяет платформу тестирования, тестовые инструменты и ваш тестовый код. Для выполнения сквозного теста вы будете использовать соответствующий образ Docker:

 1# docker-compose.yml
 2
 3services:
 4  redis-service:
 5    image: "redis:7.0.10-bullseye"
 6    networks:
 7      - backend-network
 8    volumes:
 9      - "redis-volume:/data"
10  web-service:
11    build: ./web
12    ports:
13      - "80:8000"
14    environment:
15      REDIS_URL: "redis://redis-service:6379"
16    networks:
17      - backend-network
18    depends_on:
19      - redis-service
20    command: "gunicorn page_tracker.app:app --bind 0.0.0.0:8000"
21  test-service:
22    profiles:
23      - testing
24    build:
25      context: ./web
26      dockerfile: Dockerfile.dev
27    environment:
28      REDIS_URL: "redis://redis-service:6379"
29      FLASK_URL: "http://web-service:8000"
30    networks:
31      - backend-network
32    depends_on:
33      - redis-service
34      - web-service
35    command: >
36      sh -c 'python -m pytest test/e2e/ -vv
37      --redis-url $$REDIS_URL
38      --flask-url $$FLASK_URL'
39
40networks:
41    backend-network:
42
43volumes:
44  redis-volume:

Большая часть файла docker-compose.yml остается неизменной, поэтому вы можете обратить свое внимание на выделенные строки:

  • Строка 22 определяет список профилей, к которым будет относиться ваш новый сервис. Будет только один профиль под названием testing, который вы включите для запуска тестов.
  • В строках с 24 по 26 укажите путь к каталогу, содержащему ваш файл Dockerfile для создания. Поскольку у файла нестандартное имя, вы указываете его явно.
  • Строки с 27 по 29 определяют две переменные среды, которые ваш тест будет использовать для подключения к Redis и Flask, работающим за сервером Gunicorn. Обратите внимание, что вы используете имена служб Docker Compose в качестве имен хостов.
  • Линии 30 и 31 подключают службу к той же сети, что и две другие службы.
  • Строки с 32 по 34 убедитесь, что Redis и Flask запущены до начала сквозного тестирования.
  • Строки с 35 по 38 определяют команду, которая будет выполняться при запуске вашей службы. Обратите внимание, что вы используете многострочное сворачивание букв в YAML (>), чтобы отформатировать длинную команду оболочки более удобным для чтения способом.

Поскольку Docker Compose имеет доступ к командной строке вашего хост-компьютера, он попытается интерполировать любую ссылку на переменную окружения, такую как $REDIS_URL или $FLASK_URL, которая отображается в вашем docker-compose.yml как только файл будет обработан. К сожалению, эти переменные, скорее всего, еще не определены. Вы указываете их в разделе environment вашего сервиса, что означает, что ваш контейнер получит эти переменные позже.

Чтобы отключить преждевременную замену переменных среды с помощью Docker Compose, вы заменяете знак доллара двумя знаками доллара ($$). Это, в свою очередь, создает литеральные строки $REDIS_URL и $FLASK_URL в команде, которая будет выполнена в результирующем контейнере. Чтобы интерполировать эти переменные при запуске контейнера, вы должны заключить всю команду в одинарные кавычки (') и передать ее в командную строку (sh).

Когда вы запускаете многоконтейнерное приложение с помощью Docker Compose, запускаются только основные службы, которые не относятся ни к одному профилю. Если вы также хотите запустить службы, которые были назначены одному или нескольким профилям, то вы должны перечислить эти профили, используя опцию --profile:

$ docker compose --profile testing up -d
[+] Running 3/3
 ⠿ Container page-tracker-redis-service-1  Running                      0.0s
 ⠿ Container page-tracker-web-service-1    Running                      0.0s
 ⠿ Container page-tracker-test-service-1   Started                      0.6s

$ docker compose ps -a
NAME                           ...   SERVICE             STATUS       ...
page-tracker-redis-service-1   ...   redis-service       running      ...
page-tracker-test-service-1    ...   test-service        exited (0)   ...
page-tracker-web-service-1     ...   web-service         running      ...

Обратите внимание, что это параметр команды docker compose, а не ее подкоманды up, поэтому следите за порядком аргументов. В выходных данных показана запущенная дополнительная служба, но когда вы изучите ее, вы заметите, что test-service быстро завершается с нулевым статусом "успешно".

Чтобы получить более подробную информацию об этой службе, вы можете просмотреть ее журналы:

$ docker compose logs test-service
============================= test session starts ==========================
platform linux -- Python 3.11.2, pytest-7.2.2, pluggy-1.0.0 -- /home/realp..
cachedir: .pytest_cache
rootdir: /home/realpython
plugins: timeout-2.1.0
collecting ... collected 1 item

test/e2e/test_app_redis_http.py::test_should_update_redis ... PASSED [100%]

============================== 1 passed in 0.10s ===========================

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

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

Определите конвейер непрерывной интеграции на основе Docker

Цель непрерывной интеграции (CI) заключается в обеспечении более быстрого и предсказуемого выпуска программного обеспечения путем как можно более частой интеграции изменений кода, внесенных несколькими разработчиками в команде. В те времена интеграция была важным мероприятием, на выполнение которого часто уходили недели или даже месяцы, иногда с привлечением специальной команды инженеров по интеграции.

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

Слово непрерывный подразумевает, что интеграция должна быть частой, чтобы минимизировать объем изменений и снизить риск внесения дефектов в кодовую базу. Для команд стало стандартной практикой интегрировать работу отдельных разработчиков по крайней мере один раз в день, а лучше несколько раз в день.

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

Чтобы внедрить непрерывную интеграцию в свой проект, вам необходимы следующие элементы:

  • Система контроля версий
  • Стратегия ветвления
  • Автоматизация сборки
  • Автоматизация тестирования
  • Сервер непрерывной интеграции
  • Частые интеграции

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

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

Примечание: Несмотря на давнюю традицию использования термина master для обозначения основной ветки, GitHub недавно объявил что название филиала по умолчанию будет изменено на main, чтобы лучше отражать его назначение и избегать оскорбительных выражений.

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

Хотя вы найдете несколько различных подходов к обеспечению непрерывной интеграции с GitHub Flow, вот шаги, которым вы будете следовать для своего приложения Docker:

  1. Загрузите последнюю версию основной строки на свой компьютер.
  2. Создайте функциональную ветвь из основной строки.
  3. Откройте запрос на обновление, чтобы заблаговременно получить отзывы от других пользователей.
  4. Продолжайте работать над своей функциональной веткой.
  5. Почаще извлекайте основную строку, объединяя ее с вашей функциональной веткой и разрешая любые потенциальные конфликты локально.
  6. Создайте, доработайте и протестируйте код в своей локальной ветке.
  7. Вносите изменения всякий раз, когда локальная сборка и тесты завершаются успешно.
  8. При каждом нажатии проверяйте соответствие автоматических тестов, выполняемых на сервере CI, вашей ветви функций.
  9. Воспроизведите и исправьте все выявленные проблемы локально, прежде чем повторно запускать код.
  10. Когда вы закончите и все тесты пройдут успешно, попросите одного или нескольких коллег просмотреть ваши изменения.
  11. Применяйте их отзывы до тех пор, пока рецензенты не одобрят ваши обновления и все тесты не пройдут на сервере CI после отправки ваших последних изменений.
  12. Закройте запрос на извлечение, объединив функциональную ветвь с основной линией.
  13. Проверьте автоматические тесты, запущенные на сервере CI, на соответствие основной линии с учетом изменений из вашей ветви функций.
  14. Исследуйте и устраняйте любые проблемы, которые могут быть обнаружены, например, из-за новых обновлений, внесенных в основную линию другими пользователями в период между вашим последним нажатием и слиянием.

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

Примечание: Команды разработчиков программного обеспечения часто сочетают непрерывную интеграцию с непрерывной поставкой, образуя единый процесс, известный как CI/CD. Непрерывная поставка - это расширение непрерывной интеграции, которое добавляет дополнительные шаги для развертывания проверенной и интегрированной сборки в производственной среде.

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

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

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

У вас есть множество вариантов настройки сервера непрерывной интеграции для вашего приложения Docker, как онлайн, так и автономного. Популярными вариантами являются CircleCI, Дженкинс и Трэвис. В этом руководстве вы будете использовать GitHub Actions, которое представляет собой бесплатное CI-решение, предоставляемое GitHub.

Отправить код в репозиторий на GitHub

Чтобы воспользоваться преимуществами GitHub Actions, вы должны сначала создать репозиторий на GitHub. Зарегистрироваться если у вас еще нет учетной записи, то войдите в систему и создайте новый репозиторий с именем page-tracker.

Публичные репозитории могут использовать GitHub Actions без ограничений, в то время как частные репозитории получают две тысячи минут и пятьсот мегабайт хранилища в месяц на бесплатном уровне. Однако задания, выполняемые в Windows, будут занимать в два раза больше минут, чем в Linux, а задания для macOS - в десять раз больше! Более подробную информацию о выставлении счетов за действия на GitHub можно найти в официальной документации.

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

Сохраняйте предлагаемые значения по умолчанию без инициализации вашего нового репозитория с помощью файлов-заполнителей GitHub, потому что вы будете продвигать существующий проект. Затем перейдите в терминал и измените рабочий каталог на тот, в котором находится ваш проект page-tracker. У него уже должен быть инициализирован локальный репозиторий Git, который вы вскоре подключите к GitHub. Но сначала внесите все ожидающие изменения в свой локальный репозиторий:

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   docker-compose.yml

no changes added to commit (use "git add" and/or "git commit -a")

$ git commit -am "Add a test-service to Docker Compose"

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

$ git remote add origin git@github.com:realpython/page-tracker.git
$ git push -u origin master

Обязательно замените realpython на ваше имя пользователя на GitHub. Первая команда добавит удаленный репозиторий на GitHub, который вы только что создали, в ваш локальный репозиторий под псевдонимом origin. Вторая команда перенесет содержимое вашего локального репозитория на GitHub.

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

Научитесь говорить на языке действий на GitHub

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

  1. Раннеры, размещенные на GitHub: Ubuntu Linux, Windows, macOS
  2. Автономные исполнители: Локальные серверы, которыми вы владеете и обслуживаете

В этом руководстве вы будете использовать только последнюю версию Ubuntu Linux runner, предоставленную GitHub. Обратите внимание, что одно и то же задание можно выполнить в нескольких программах запуска, например, для проверки кросс-платформенной совместимости.

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

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

  1. Пользовательская команда оболочки или скрипт
  2. Действие на GitHub, определенное в другом репозитории GitHub

Существует множество предопределенных действий на GitHub, которые вы можете просмотреть и найти на торговой площадке GitHub. Сообщество предоставляет и поддерживает их. Например, есть один для создания и распространения образов Docker, принадлежащий организации Docker на GitHub. Из-за множества конкурирующих плагинов иногда существует несколько способов достичь желаемого результата с помощью действий на GitHub.

Как и во многих инструментах, связанных с DevOps в наши дни, GitHub использует формат YAML для настройки рабочих процессов. Он ищет специальную папку .github/workflows/ в корневой папке вашего репозитория, куда вы можете поместить несколько файлов YAML, каждый из которых соответствует своему рабочему процессу. Кроме того, вы можете включить туда другие файлы, такие как файлы конфигурации или пользовательские скрипты для выполнения в программе запуска.

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

page-tracker/
│
├── web/
│
├── .git/
│
├── .github/
│   └── workflows/
│       └── ci.yml
│
├── .gitignore
└── docker-compose.yml

Хотя вы можете использовать любой редактор кода, который вам нравится, для написания файла рабочего процесса для действий на GitHub, в этом случае рассмотрите возможность использования веб-редактора GitHub. Он обеспечивает не только общую подсветку синтаксиса YAML, но и проверку схемы и интеллектуальные рекомендации для доступных атрибутов действий на GitHub. Поэтому вы можете сначала загрузить свой код на GitHub и отредактировать свой файл ci.yml непосредственно там, используя встроенный редактор.

Чтобы открыть редактор, встроенный в GitHub, перейдите в веб-браузере к файлу ci.yml и нажмите или щелкните значок карандаша. Теперь вы можете приступить к написанию своего файла рабочего процесса GitHub Actions.

Создайте рабочий процесс с помощью действий на GitHub

Во время редактирования файла ci.yml присвойте своему новому рабочему процессу описательное имя и определите события, которые должны его запустить:

# .github/workflows/ci.yml

name: Continuous Integration

on:
  pull_request:
    branches:
      - master
  push:
    branches:
      - master

Этот рабочий процесс запускается двумя событиями:

  1. Открытие или изменение запроса на извлечение в master ветви
  2. Ввод кода или объединение ветки с веткой master

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

Задача вашего рабочего процесса непрерывной интеграции - создать образ Docker, выполнить комплексные тесты с помощью Docker Compose и отправить созданный образ в Docker Hub, если все пройдет нормально. Благодаря встроенному файлу Dockerfile модульные тесты, различные инструменты статического анализа кода и проверки безопасности интегрированы в одну команду. Таким образом, вам не нужно писать много YAML для вашего рабочего процесса CI.

Почти каждое задание в рабочем процессе GitHub Action начинается с извлечения кода из репозитория GitHub:

# .github/workflows/ci.yml

name: Continuous Integration

on:
  pull_request:
    branches:
      - master
  push:
    branches:
      - master

jobs:
  build:
    name: Build Docker image and run end-to-end tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code from GitHub
        uses: actions/checkout@v3

Вы указываете задание, обозначенное как build, которое будет выполняться в последней версии Ubuntu runner, предоставленной GitHub. Первый шаг, чтобы проверить один коммит, который срабатывает рабочего процесса с использованием actions/checkout на GitHub действий. Поскольку действия на GitHub на самом деле являются замаскированными репозиториями на GitHub, вы можете указать тег Git или хэш фиксации после знака at (@), чтобы выбрать конкретную версию действия.

В качестве следующего шага в процессе непрерывной интеграции вы хотите создать образы Docker для своих веб-сервисов и тестовых сервисов перед выполнением сквозных тестов с помощью Docker Compose. Вместо того, чтобы использовать существующее действие, на этот раз вы запустите команду оболочки в runner:

# .github/workflows/ci.yml

name: Continuous Integration

on:
  pull_request:
    branches:
      - master
  push:
    branches:
      - master

jobs:
  build:
    name: Build Docker image and run end-to-end tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code from GitHub
        uses: actions/checkout@v3
      - name: Run end-to-end tests
        run: >
          docker compose --profile testing up
          --build
          --exit-code-from test-service

Как и в случае с вашим файлом docker-compose.yml, вы используете многострочное литеральное сворачивание YAML (>), чтобы разбить длинную команду на несколько строк для улучшения читаемости. Вы запрашиваете, чтобы Docker Compose перестраивал ваши изображения с флагом --build и останавливал все контейнеры, когда завершается test-service. В противном случае ваша работа может продолжаться бесконечно. Это также возвращает исполнителю код завершения test-service, что может привести к прерыванию последующих шагов.

Эти два шага всегда будут выполняться в ответ на события, перечисленные в верхней части файла, которые либо открывают запрос на извлечение, либо объединяют функциональную ветвь с основной. Кроме того, вы захотите перенести свой новый образ Docker в Docker Hub, когда все тесты пройдут успешно после слияния ветки с основной линией. Таким образом, вы будете выполнять следующие шаги условно только тогда, когда push событие запустит ваш рабочий процесс.

Но как получить безопасный доступ к Docker Hub, не раскрывая свои секреты с помощью действий на GitHub? Сейчас вы узнаете.

Получите доступ к Docker Hub с помощью секретов действий на GitHub

Ранее, когда вы загружали один из своих образов Docker в реестр Docker из терминала, вам приходилось входить в Docker Hub, вызывая docker login и вводя свое имя пользователя и пароль. Кроме того, если вы включили двухфакторную аутентификацию, то вам нужно было сгенерировать персональный токен доступа с достаточными разрешениями и предоставить его вместо своего пароля.

Действия по удалению изображения из автоматизированного рабочего процесса аналогичны, поэтому сначала вам придется пройти аутентификацию. Вы можете сделать это с помощью команды shell или предопределенного действия на GitHub, например docker/login-action:

# .github/workflows/ci.yml

name: Continuous Integration

on:
  pull_request:
    branches:
      - master
  push:
    branches:
      - master

jobs:
  build:
    name: Build Docker image and run end-to-end tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code from GitHub
        uses: actions/checkout@v3
      - name: Run end-to-end tests
        run: >
          docker compose --profile testing up
          --build
          --exit-code-from test-service
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        if: ${{ github.event_name == 'push' }}
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

Вы выполняете этот шаг условно, получая тип события из github контекста, используя выражение JavaScript, заключенное в знак доллара, за которым следуют двойные фигурные скобки. Затем вы предоставляете свои секретные учетные данные Docker Hub с помощью другого предопределенного secrets контекста и двух пользовательских констант, которые вы собираетесь определить сейчас.

Откройте настройки вашего репозитория на GitHub , щелкнув вкладку со значком шестеренки на панели инструментов вверху, найдите и раскройте Секреты и переменные в разделе Безопасность, а затем нажмите Действия. Это приведет вас к панели, которая позволяет вам определять переменных среды, а также зашифрованные секреты для ваших программ запуска действий на GitHub. Теперь укажите ваши DOCKERHUB_USERNAME и DOCKERHUB_TOKEN секреты:

GitHub Actions Repository Secrets Секреты репозитория действий на GitHub

Обратите внимание, что эти секреты зашифрованы, и GitHub больше не покажет их вам, поэтому убедитесь, что вы сохранили их в надежном месте. Но если вы приложите достаточно усилий, то сможете восстановить их — например, с помощью команды shell в вашем рабочем процессе.

После авторизации в Docker Hub вы можете пометить и опубликовать свой новый образ Docker, используя другое действие на GitHub из marketplace:

# .github/workflows/ci.yml

name: Continuous Integration

on:
  pull_request:
    branches:
      - master
  push:
    branches:
      - master

jobs:
  build:
    name: Build Docker image and run end-to-end tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code from GitHub
        uses: actions/checkout@v3
      - name: Run end-to-end tests
        run: >
          docker compose --profile testing up
          --build
          --exit-code-from test-service
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        if: ${{ github.event_name == 'push' }}
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Push image to Docker Hub
        uses: docker/build-push-action@v4.0.0
        if: ${{ github.event_name == 'push' }}
        with:
          context: ./web
          push: true
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/page-tracker:${{ github.sha }}
            ${{ secrets.DOCKERHUB_USERNAME }}/page-tracker:latest

Это действие также выполняется условно, когда вы объединяете ветвь объекта с основной линией. В разделе with вы указываете путь к своему файлу Dockerfile, запрашиваете действие для отправки изображения и перечисляете теги для вашего изображения. Обратите внимание, что вы снова используете контекст github для получения хэша текущей фиксации, хотя и в длинной форме.

Примечание: Пакеты GitHub - это еще один сервис, интегрированный в GitHub. Он может служить заменой Docker Hub. Он поддерживает различные типы пакетов, включая образы Docker, что позволяет хранить исходный код и двоичные пакеты в одном месте. Пользователь docker/build-push-action может использовать свой токен GitHub для отправки пакетов на GitHub.

На этом этапе ваш рабочий процесс непрерывной интеграции настроен и готов к работе. Если вы еще не использовали редактор кода, встроенный в GitHub, не забудьте зафиксировать его и отправить в свой локальный репозиторий, чтобы изменения вступили в силу:

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .github/

nothing added to commit but untracked files present (use "git add" to track)

$ git add .github/
$ git commit -m "Add a continuous integration workflow"
$ git push

В следующем разделе вы включите несколько правил защиты ветвей, чтобы никто не мог отправлять свой код непосредственно в ветвь master. В результате событие push в вашем рабочем процессе будет применяться только к объединению функциональной ветви с основной линией посредством запроса на извлечение.

Включить правила защиты филиалов

Перейдите в настройки вашего репозитория снова нажмите Ветви в разделе Код и автоматизация раздел и нажмите кнопку с надписью Добавить правило защиты филиалов. Затем введите название вашей магистрали в поле Шаблон названия ответвления. Если вы следовали правилам именования, использованным в этом руководстве, то вам следует ввести master в поле ввода:

GitHub Repository's Protected Branch Защищенная ветка репозитория GitHub

Затем включите опцию чуть ниже, которая гласит: Требуется запрос на удаление перед объединением. Для этого автоматически потребуется одобрение по крайней мере от одного проверяющего. Вы можете пока снять этот флажок, если у вас нет другой учетной записи на GitHub. В противном случае вы не сможете объединить свой запрос на удаление без его одобрения кем-либо еще:

Require a Pull Request Before Merging

Прокрутив немного вниз, вы увидите опцию с надписью Требуется выполнить проверку состояния перед объединением. Выберите ее, чтобы открыть еще больше возможностей. Когда вы это сделаете, установите другой флажок, чтобы Требовать обновления веток перед объединением, что предотвратит закрытие запроса на извлечение, когда в вашей master ветке появятся новые коммиты. Наконец, введите название вашей работы, build, в поле поиска ниже:

Require Status Checks to Pass Before Merging

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

Чтобы обеспечить соблюдение этих правил, не допуская каких-либо обходных действий для администраторов и других пользователей с повышенными привилегиями, вы можете выбрать опцию внизу с надписью Не разрешать обход вышеуказанных настроек:

Don't Allow Bypassing the Above Settings

Хорошо. Все готово! Как насчет того, чтобы протестировать рабочий процесс непрерывной интеграции с вашим приложением Docker?

Интегрировать изменения из ветви объектов

Следуйте инструкциям Конвейера непрерывной интеграции на основе Docker, описанным ранее в этом руководстве. Начните с создания отдельной ветки функций, измените код таким образом, чтобы он нарушал ваши тесты, зафиксируйте изменения и отправьте их на GitHub:

S> git checkout -b feature/replace-emoji-face
Switched to a new branch 'feature/replace-emoji-face'

PS> cd web\src\page_tracker

PS> (Get-Content app.py).replace('pensive', 'thinking') | Set-Content app.py

PS> git commit -am "Replace the emoji in an error message"
[feature/replace-emoji-face 9225d18] Replace the emoji in an error message
 1 file changed, 1 insertion(+), 1 deletion(-)

PS> git push --set-upstream origin feature/replace-emoji-face
⋮
remote: Create a pull request for 'feature/replace-emoji-face' on GitHub...
remote:      https://github.com/realpython/page-tracker/pull/new/feature...
⋮

 

$ git checkout -b feature/replace-emoji-face
Switched to a new branch 'feature/replace-emoji-face'

$ sed -i 's/pensive/thinking/g' web/src/page_tracker/app.py

$ git commit -am "Replace the emoji in an error message"
[feature/replace-emoji-face 9225d18] Replace the emoji in an error message
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git push --set-upstream origin feature/replace-emoji-face
⋮
remote: Create a pull request for 'feature/replace-emoji-face' on GitHub...
remote:      https://github.com/realpython/page-tracker/pull/new/feature...
⋮

Вы создаете и переключаетесь на новую локальную ветку с именем feature/replace-emoji-face, а затем меняете эмодзи в своем сообщении об ошибке с задумчивое лицо на задумчивое лицо без обновления соответствующего модульного теста. После фиксации и отправки ветки на GitHub вы можете открыть новый запрос на перенос из вашей функциональной ветки в master, перейдя по ссылке в выделенной строке. Как только вы это сделаете, запустится ваш рабочий процесс непрерывной интеграции.

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

GitHub Status Check Failed With a Conflict

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

Кроме того, на скриншоте выше показан гипотетический конфликт между вашей функциональной ветвью и целевой магистралью. Это означает, что кто-то другой модифицировал тот же файл, что и вы, и успешно интегрировал свои изменения в ветку master, пока вы обновляли эмодзи.

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

Даже без каких-либо конфликтов, если основная строка на несколько коммитов опережает вашу функциональную ветку, вам все равно придется объединить последние изменения из master в свою ветку независимо от результатов тестирования. Это связано с еще одним правилом защиты филиалов, которое вы ввели ранее:

GitHub Status Check Passed With an Outdated Branch

Кнопка Запрос на получение слияния будет недоступна до тех пор, пока вы не предпримете действия для устранения всех этих проблем.

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

 # web/test/unit/test_app.py

 # ...

 @unittest.mock.patch("page_tracker.app.redis")
 def test_should_handle_redis_connection_error(mock_redis, http_client):
     # Given
     mock_redis.return_value.incr.side_effect = ConnectionError

     # When
     response = http_client.get("/")

     # Then
     assert response.status_code == 500
-    assert response.text == "Sorry, something went wrong \N{pensive face}"
+    assert response.text == "Sorry, something went wrong \N{thinking face}"

Как только вы запустите тесты локально и будете уверены в правильности своего кода, сделайте еще один коммит в той же ветке и отправьте его на GitHub. Перед этим стоит дважды проверить текущую ветку:

$ git branch
* feature/replace-emoji-face
  master
$ git add web/test/unit/test_app.py
$ git commit -m "Fix the failing unit test"
$ git push

Запрос на обновление должен зафиксировать ваше изменение и запустить новую сборку CI. Как только все правила защиты будут выполнены, вы, наконец, сможете объединить свою функциональную ветвь с защищенной основной линией, нажав зеленую кнопку:

GitHub Status Checks Passed

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

Обратите внимание, что слияние запустит еще одну сборку CI для ветки master, чтобы проверить, хорошо ли ваши изменения интегрируются с остальной кодовой базой. Всегда есть вероятность, что что-то пойдет не так. С другой стороны, если сборка CI завершится успешно, то рабочий процесс пометит изображение тегом и отправит его в ваш репозиторий Docker Hub:

Docker Image Tagged and Pushed By a GitHub Action Изображение Docker помечено тегом и перемещено с помощью действия на GitHub

Каждый раз, когда рабочий процесс CI завершается успешно, загруженное изображение Docker помечается текущим хэшем фиксации Git и меткой latest.

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

Следующие шаги

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

Вот несколько идей, которые вам следует рассмотреть:

  • Автоматизируйте развертывание в облаке для непрерывной доставки.
  • Переход к непрерывному развертыванию с полной автоматизацией процессов.
  • Внедрите балансировщик нагрузки и реплики ваших сервисов для лучшей масштабируемости.
  • Защищайте хранилища конфиденциальных данных с помощью токена аутентификации.
  • Настройте постоянное ведение журнала и мониторинг ваших служб.
  • Внедрите сине-зеленые развертывания для минимизации времени простоя.
  • Добавить переключатели функций для экспериментов с выпусками canary и A/B-тестированием.

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

Заключение

Теперь у вас есть четкое представление о том, как создавать, развертывать и управлять многоконтейнерными веб-приложениями в контейнерной среде. Вы рассказали о разработке, тестировании, обеспечении безопасности, настройке и оркестровке веб-приложения Flask, подключенного к серверу Redis. Вы также увидели, как определить конвейер непрерывной интеграции с помощью Docker, GitHub Actions и различных других инструментов.

В этом руководстве вы должны:

  • Запустите сервер Redis локально в контейнере Docker
  • Доработанное веб-приложение на Python , написанное на Flask
  • Создал образов Docker и поместил их в Docker Hub реестр
  • Организованные многоконтейнерные приложения с помощью Docker Compose
  • Реплицировал производственную инфраструктуру в любом месте
  • Определен рабочий процесс непрерывной интеграции с использованием Действий на GitHub

 

Back to Top