Источник: Nuances of Programming
Разработка в Python в локальных средах может стать нелёгкой задачей, если одновременно работать более чем над одним проектом. Бутстрэппинг (начальная загрузка) проекта может потребовать некоторого времени: нужно согласовать версии, а также настроить зависимости и конфигурацию. Раньше мы устанавливали все требования к проектам напрямую в нашу локальную среду разработки и фокусировались на написании кода. Однако при ведении нескольких проектов в одной среде уже возникают сложности, так как мы можем столкнуться с конфликтами конфигурации или зависимостей. Более того, если мы начинаем работать совместно с коллегами, то наши среды разработки уже приходится координировать. Поэтому нам нужно определить среду проекта так, чтобы её могли с лёгкостью использовать другие.
Для этого можно создать изолированную среду для каждого проекта при помощи контейнеров и Docker Compose — инструмента композиции контейнеров, который будет ими управлять. Эту тему мы раскроем в серии статей, первую часть которой вы сейчас и читаете. Каждая из них будет посвящена отдельному аспекту всего этого процесса.
В текущей части мы рассмотрим, как помещать в контейнер Python-службу или инструмент и познакомимся с наилучшими подходами для осуществления данного процесса.
Требования
Для простоты работы с материалами этой и двух последующих статей нужно установить минимальный набор инструментов, которые позволят управлять помещёнными в контейнеры средами локально:
Контейнеризация сервера Python
В качестве примера мы возьмём простой сервер Flask, который можно запустить автономно, не устанавливая другие компоненты.
Перед запуском этой программы сначала нужно убедиться в наличии всех необходимых зависимостей. Один из способов управления ими — применение установщика пакетов, например pip. Для этого нужно создать файл requirements.txt и записать в него зависимости. Ниже приведён пример такого файла для нашего простого server.py:
requirements.txt
Flask==1.1.1
Структура проекта:
app
├─── requirements.txt
└─── src
└─── server.py
Мы создаём выделенную директорию для исходного кода, чтобы изолировать его от других файлов конфигурации. Позже вы увидите, зачем мы это делаем. Для выполнения программы осталось только установить интерпретатор Python и запустить его.
Можно запустить программу локально, но так мы отклонимся от нашей задачи по контейнеризации разработки, которая подразумевает чистую стандартную среду разработки, позволяющую легко переключаться между проектами с разными конфликтующими требованиями. Я расскажу, как легко поместить этот сервис Python в контейнер.
Dockerfile
Чтобы осуществить работу нашего Python-кода в контейнере мы упакуем его как образ Docker, а затем запустим на основе этого образа контейнер:
Для генерации образа Docker нужно создать Dockerfile, содержащий необходимые для сборки образа инструкции. Затем Dockerfile обрабатывается сборщиком Docker, который и сгенерирует нужный образ. После этого с помощью простой команды docker run мы создаём и запускаем контейнер с сервером Python.
Анализ Dockerfile
Ниже приведён пример Dockerfile, содержащего инструкции по сборке образа для нашего сервиса hello world:
Для каждой инструкции или команды из Dockerfile сборщик Docker генерирует слой образа и накладывает его на предыдущие. Таким образом, получающийся в результате образ Docker представляет собой простой только читаемый стэк, состоящий из разных слоёв. В выводе команды сборки мы видим, как по очереди выполняются инструкции Dockerfile:
Затем можно проверить образ в локальном хранилище образов:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myimage latest 70a92e92f3b5 8 seconds ago 991MB
В процессе разработки нам может потребоваться повторно собрать образ для Python-сервиса, на что желательно потратить как можно меньше времени. Далее мы проанализируем некоторые лучшие практики, которые могут нам в этом помочь.
Лучшие практики разработки Dockerfile
Базовый образ
Первая инструкция из Dockerfile определяет базовый образ, поверх которого мы добавляем новые слои для приложения. Выбор базового слоя весьма важен, поскольку поставляемые им возможности могут влиять на качество надстроенных слоёв.
По возможности старайтесь работать с официальными образами, которые, как правило, часто обновляются и имеют меньше проблем с безопасностью.
Выбор базового образа также влияет на размер итогового. Если для вас размер имеет первостепенное значение, то можно выбрать какой-нибудь очень маленький нетребовательный к ресурсам образ. Такие образы обычно основываются на дистрибутиве Alpine и имеют соответствующий тег. Тем не менее для приложений Python в большинстве случаев отлично подходит slim-вариант официального Python-образа Docker (например, python:3.8-slim)
Порядок инструкций влияет на использование кэша сборки
При частой сборке образа мы определённо будем использовать механизм кэширования для ускорения. Как я упоминала ранее, инструкции Dockerfile выполняются в заданном порядке. Для каждой инструкции сборщик сначала проверяет свой кэш на наличие образа для повторного использования. При обнаружении изменения в слое этот и все последующие слои пересобираются. Чтобы кэширование было эффективным, нужно поместить инструкции часто изменяемых слоёв после тех, которые меняются редко.
Посмотрим на пример Dockerfile, чтобы понять, как порядок инструкций влияет на кэширование. Ниже я привела интересующие нас строки:
В процессе разработки зависимости нашего приложения изменяются не так часто, как Python-код. В связи с этим мы устанавливаем их в слое, предшествующем слою кода. То есть мы копируем файл зависимостей, устанавливаем их, а затем копируем исходный код. Это главная причина изолирования исходного кода в отдельную директорию, о котором было сказано в начале статьи.
Многоэтапные сборки
Хотя это может и не быть существенным в разработке, мы кратко расскажем о подобных сборках, поскольку они интересны в плане итоговой отправки контейнеризованного приложения уже по её завершении.
Многоэтапные сборки используются для очистки итогового образа от ненужных файлов и пакетов ПО, чтобы отправлять только необходимые для выполнения кода файлы. Вот небольшой пример многоэтапного Dockerfile:
Обратите внимание, что здесь мы используем двухэтапную сборку, где только первый этап называем builder — сборщик. Название этапу мы задаём, добавляя AS <НАЗВАНИЕ> к инструкции FROM и используем это название в инструкции COPY, где хотим скопировать в итоговый образ только необходимые файлы. Результат — облегчённый образ:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myimage latest 70a92e92f3b5 2 hours ago 991MB
multistage latest e598271edefa 6 minutes ago 197MB
…
В этом примере мы установили зависимости в локальную директорию user и скопировали эту директорию в итоговый образ с помощью опции pip -user. Однако для выполнения этих действий есть и другие решения вроде virtualenv или сборки в виде пакетов wheel с последующим их копированием и установкой в итоговый образ.
Запуск контейнера
После написания Dockerfile и сборки образа, мы запускаем контейнер с нашим сервисом:
Что дальше?
Мы показали, как помещать в контейнер сервер на Python для облегчения разработки. Контейнеризация позволяет не только добиваться одинаковых результатов на разных платформах, но также избегать конфликтов зависимостей и поддерживать в чистоте стандартную среду разработки. Контейнеризованная среда легко управляется и удобна при совместной работе с другими разработчиками: они смогут без проблем развёртывать её в своих стандартных средах, не внося изменений.
В следующей статье вы узнаете, как настроить основанный на контейнерах многосервисный проект, где Python-компонент соединён с внешними компонентами, а также научитесь управлять жизненным циклом всех компонентов проекта при помощи Docker Compose.
Читайте также:
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи ANCA IORDACHE: Containerized Python Development — Part1