Как стать автором
Обновить

[Python Intermediate] Урок 2. Docker и docker-compose

Время на прочтение11 мин
Количество просмотров30K

Алоха!

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

И ещё важный момент — если ты совсем не знаком с Docker, то прежде чем двигаться дальше, обязательно почитай про основы данной технологии, благо статей в интернете немало, например, вот. Или хотя бы ознакомься с моей памяткой (VK, Github). Просто чтобы понимать, о чём вообще речь.

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

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

Не переживай, супер глубокое понимание технологии в рамках данного материала не потребуется. Но если всё-таки очень хочется погрузиться, то на Хабре есть перевод серии статей от Джеффа Хейла.


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

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

Разумеется, «контейнерная» разработка, как и всё в нашем мире, имеет свои недостатки. Главный из них сопряжён с локальной отладкой приложения — при запуске через системный интерпретатор сделать это куда проще. Однако технологии тоже не стоят на месте, и такие популярные IDE, как PyCharm Professional и VS Code, уже способны справиться с данной задачей.

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

Тизер

Чтобы понимать, к чему мы вообще стремимся, предлагаю тебе сначала установить Docker и Docker Compose.

Затем стяни этот репозиторий и перейди в директорию текущего урока:

cd lesson_2

В ней создай папку secrets и положи туда два файла: event_broker_password и service_db_password. В первый файл впиши python_garden, а во второй — postgres.

Проверь, всё ли правильно получилось? (Команда cat выводит содержимое файла).

cat secrets/event_broker_password
> python_garden
cat secrets/service_db_password
> postgres

Введи в терминале из корня проекта следующую команду:

docker compose up -d

После того, как её выполнение закончится, введи:

docker compose ps

В результате у тебя должно получиться следующее:

Список запущенных контейнеров
Список запущенных контейнеров

Если ты счастливый обладатель компа с чипом M1, то смотри README.

Теперь, чтобы убедиться в правдивости напечатанных статусов, выведи логи любого сервиса при помощи команды docker compose logs <service> (имена в столбце SERVICE).

Логи консьюмера: распечатаны объекты с переменными окружения из первого урока
Логи консьюмера: распечатаны объекты с переменными окружения из первого урока

Также можешь дёрнуть апишку из предыдущего урока обычным курлом: curl http://0.0.0.0:8000.

Логи апишки: распечатан объект с переменными окружения из первого урока
Логи апишки: распечатан объект с переменными окружения из первого урока

При желании открой адрес http://0.0.0.0:8000 в браузере и убедись в наличии идентичного ответа.

Ну и напоследок небольшой бонус. Зайди на http://0.0.0.0:15672, введи в оба поля python_garden и нажми «Login».

Добро пожаловать в веб-интерфейс локального RabbitMQ! Вот так просто.

Web-интерфейс RabbitMQ
Web-интерфейс RabbitMQ

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

docker compose down
docker system prune --all

Dockerfile

Знакомство с контейнеризацией мы начнём с описания докер-файла (./Dockerfile), где будут перечислены инструкции для сборки нужного нам образа.

FROM python:3.9-slim as base
LABEL maintainer="Make <russian.it@great.again>"

# Сборка зависимостей
ARG BUILD_DEPS="curl"
RUN apt-get update && apt-get install -y $BUILD_DEPS

# Установка poetry
RUN curl -sSL https://install.python-poetry.org | POETRY_VERSION=1.2.0 POETRY_HOME=/root/poetry python3 -
ENV PATH="${PATH}:/root/poetry/bin"

# Инициализация проекта
WORKDIR /opt/lesson_2
ENTRYPOINT ["./docker-entrypoint.sh"]

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# Установка питонячьих библиотек
COPY poetry.lock pyproject.toml /
RUN poetry config virtualenvs.create false && \
    poetry install --no-interaction --no-ansi

# Копирование в контейнер папок и файлов.
COPY . .

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

🔹 Во второй строке указан ответственный за создание и поддержку образа. Обычно здесь оставляют name и email команды разработчиков.

🔹 В блоке «Сборка зависимостей» мы сначала объявляем переменную BUILD_DEPS, в которой перечисляем необходимые утилиты и функции ОС, затем запускаем обновление списков пакетов и устанавливаем перечисленные сущности.

Инструкцию ARG вместе с переменной можешь опустить, объединив со следующей. Это лишь вопрос эстетики и удобства.

RUN apt-get update && apt-get install -y curl

Главное, никогда без веской причины не запускай в докер-файле команду apt-get upgrade, которая часто встречается в различных сниппетах рядом с apt-get update.

Запомни разницу (подробности здесь):

  • apt-get update — обновляет списки пакетов, т.е. получает информацию о новейших версиях и зависимостях;

  • apt-get upgrade — на основе существующих списков (/etc/apt/sources.list) запускает обновление всех установленных в настоящее время пакетов.

Допустим, мы ожидаем некий пакет версии «1.0», установленный в базовом образе, и ориентируемся на вполне конкретное поведение. А после апгрейда получаем, к примеру, версию «3.5» с совершенно иным поведением. Думаю, ты представляешь степень боли, которую нам предстоит испытать в подобном случае.

🔹 Далее принимаемся за установку poetry. Это модный и полезный менеджер зависимостей для Python. Если ты с ним ещё не знаком, то пока не заморачивайся — обсудим его как-нибудь отдельно. Просто пропускай обе инструкции.

🔹 В «Инициализации проекта» проводим подготовительные мероприятия, а именно:

  • задаём рабочую директорию для всех последующих директив;

  • указываем sh-скрипт с вариантами запуска нашего приложения (о них ниже);

  • устанавливаем две переменные среды, которые очень важны.

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

PYTHONDONTWRITEBYTECODE означает, что Python не будет пытаться создавать файлы .pyc.

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

Добавляй эти две переменные в Dockerfile всегда! Даже если пишешь на Go. Просто на всякий случай. 😁

🔹 В предпоследнем блоке устанавливаем зависимости. Для poetry, когда речь идёт о контейнере, отключаем создание виртуального окружения (virtualenvs.create false), интерактив (--no-interaction) и ANSI-output (--no-ansi).

Если же от poetry ты решил держаться подальше, то запускай:

RUN pip3 install -r requirements

🔹 Последняя директива в дополнительных комментариях не нуждается: весь проект просто копируется внутрь контейнера, в WORKDIR.

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

docker build -t python_garden .

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

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

Docker-образ, собранный по вышеописанному Dockerfile
Docker-образ, собранный по вышеописанному Dockerfile

Точка входа

Далее приступаем к описанию sh-скрипта так называемой точки входа, она же entrypoint. Точка входа — это парадные ворота в наш сервис, которые мы обозначили в докер-файле командой ENTRYPOINT ["./docker-entrypoint.sh"]. А sh-скрипт, продолжая аналогию, что-то вроде табличек-указателей или ресепшена в отеле.

Напомню: у нашего нового бэкенд-приложения из первого урока есть два режима работы — API и консьюмер.

Схема бэкенда из первого урока
Схема бэкенда из первого урока

Явно опишем оба варианта в sh-скрипте, также размещённом в корне проекта (./docker-entrypoint.sh), и не забудем оставить возможность для запуска иных команд.

#!/usr/bin/env sh

set -e

case "$1" in
    api)
        exec bash -c "uvicorn app.api.webapp:app --host 0.0.0.0 --port 8000 --reload --reload-dir app"
        ;;
    consumer)
        exec python run_consumer.py
        ;;
    *)
        exec "$@"
esac

На данном этапе у тебя может возникнуть вопрос: «А зачем такие сложности? Почему бы не указать команду запуска сервиса прямо в dockerfile через директиву CMD с возможностью переопределения?». Ты прав — если речь идёт о простом приложении с одним вариантом запуска, то смысла городить огород с точкой входа, разумеется, нет. Однако в природе такие приложения попадаются очень редко. Посуди сам, даже в Django, помимо основной команды запуска python manage.py runserver, у нас имеются следующие: создание новой миграции, применение миграций, запуск какого-нибудь вспомогательного скрипта, запуск тестов и так далее.

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

Что касается продемонстрированного выше скрипта, то он, думаю, трудностей в понимании у тебя не вызовет: имеются две строго декларированные команды и отдельная ветка для всех остальных. Тут следует остановиться подробнее разве что на exec. По рекомендации разработчиков Docker, запуск процессов внутри контейнера лучше осуществлять через неё, ибо тогда вызываемая команда получит PID 1 и будет корректно работать с сигналами. Если же exec не указать, то PID 1 достанется процессу bash.

Docker Compose

Мы почти у цели! Осталось описать инфраструктуру приложения. Для этого воспользуемся инструментом под названием Docker Compose. Если говорить упрощённо, то данная технология позволяет с помощью простых команд контролировать несколько сервисов.

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

Принимаемся за настройку базы данных.

version: "3.4"
services:
  service_db:
    container_name: python-garden_db
    image: postgres:10
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres

Здесь указаны:

  • имя контейнера;

  • образ, на основе которого будет собран контейнер;

  • соответствие портов на хост-машине портам в контейнере, то есть при подключении к порту № 1 мы будем попадать на порт № 2 в контейнере;

  • и переменные окружения, по сути являющиеся кредами для доступа к БД.

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

Теперь давай обратим внимание на RabbitMQ.

version: "3.4"
services:
  mq:
    container_name: python-garden_mq
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      RABBITMQ_DEFAULT_USER: python_garden
      RABBITMQ_DEFAULT_PASS: python_garden

Как видишь, различий немного.

Попробуем поднять нашу инфраструктуру. Для этого введи в терминале из корня проекта следующую команду:

docker compose up -d  # запуск всех описанных в compose-файлах сервисов

И посмотри результат:

docker compose ps

Если статусы у обоих сервисов — running, ты всё сделал правильно. Если нет, значит, я что-то упустил в своих объяснениях. Обязательно напиши об этом в комментариях!

Ну что? Вот мы и добрались, наконец-то, до нашего псевдобэкенда. Давай опишем его.

version: "3.4"
services:
  api:
    container_name: python-garden_api
    image: python_garden
    build:
      context: .
    ports:
      - "8000:8000"
    command: "api"
    environment:
      PYTHONUNBUFFERED: 1
      SERVICE_DB_HOST: service_db
      SERVICE_DB_NAME: postgres
      SERVICE_DB_USERNAME: postgres
    volumes:
      - .:/opt/lesson_2
  consumer:
    container_name: python-garden_consumer
    image: python_garden
    build:
      context: .
    command: "consumer"
    environment:
      PYTHONUNBUFFERED: 1
      SERVICE_DB_HOST: service_db
      SERVICE_DB_NAME: postgres
      SERVICE_DB_USERNAME: postgres
      EVENT_BROKER_HOST: mq
      EVENT_BROKER_PORT: 5672
      EVENT_BROKER_USERNAME: python_garden
    volumes:
      - .:/opt/lesson_2

Новых ключей здесь немного. Начнём с build. Это раздел с настройками для сборки образа. То есть, если у тебя на момент запуска контейнера отсутствует образ python_garden, Docker Compose при помощи параметров в разделе build соберёт его сам. Параметр context: . в данном случае сообщает, что Dockerfile располагается в текущей директории.

Ключ command переопределяет команду по умолчанию, указанную в директиве CMD. В нашем случае такая инструкция отсутствует, поэтому command её заменяет. Как ты помнишь, api и consumer у нас явно описаны в docker-entrypoint.sh.

По поводу volumes смотри уже упомянутую памятку по Docker. Внешний том нам здесь нужен для того, чтобы после каждого изменения кода не приходилось пересобирать образ. Помнишь, при сборке задействовалась инструкция COPY? После её выполнения в образе сохраняется кодовая база определённой версии, а с помощью вольюмов мы обходим это ограничение. То есть при указанном volumes образ смотрит в папку с проектом и по команде docker compose restart <service> поднимает контейнер с обновлённой кодовой базой.

Посмотреть файл с настройками Docker Compose целиком можно по ссылке.

Заключение

Дабы убедиться, что всё работает как надо, вернись в раздел «Тизер» и проделай описанные там действия.

Согласись, это очень удобно, когда ты скачиваешь проект с гитхаба и одной командой можешь запустить его вкупе с зависимыми сервисами. Только не забывай поддерживать актуальность docker-compose.yml и docker-entrypoint.sh.

Бывают случаи, когда могут понадобиться какие-то локальные доработки — например, имя контейнера не по душе или ты используешь устройство с процессором M1, тогда как коллеги сидят на православном Intel. Разработчики предусмотрели такую ситуацию, так что не вздумай ломать работающий файл docker-compose.yml! Вместо этого создай в корне проекта docker-compose.override.yml и изменяй существующие настройки, как тебе вздумается. Общий, совмещённый результат конфигурации ты можешь посмотреть с помощью команды:

docker compose config

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

Тезисы для запоминания

  1. Старайся использовать slim-образы Python, поскольку при работе с alpine ты неизбежно столкнёшься с трудностями во время установки некоторых библиотек.

  2. Не запускай в докер-файле команду apt-get upgrade.

  3. Всегда добавляй в Dockerfile переменные окружения ENV PYTHONDONTWRITEBYTECODE 1 и ENV PYTHONUNBUFFERED 1.

  4. По рекомендации разработчиков Docker, запуск процессов внутри контейнера лучше осуществлять через exec, ибо тогда вызываемая команда получит PID 1 и будет корректно работать с сигналами.

  5. Помни про volumes. Без указания внешнего тома тебе придётся постоянно пересобирать образ.

Полезные команды

Поднять в фоне все сервисы, описанные в конфигурации:

docker compose up -d

Вывести список контейнеров:

docker compose ps

Вывести логи указанного сервиса (контейнера):

docker compose logs <service>

Рестартануть сервис:

docker compose restart <service>

Посмотреть итоговую конфигурацию контейнеров:

docker compose config

Остановить все контейнеры из текущей конфигурации:

docker compose stop

Удалить все контейнеры из текущей конфигурации:

docker compose rm

Остановить и удалить все контейнеры из текущей конфигурации (последовательное выполнение двух предыдущих команд):

docker compose down

Удалить все не запущенные контейнеры и все не использующиеся образы:

docker system prune --all

Вывести список образов:

docker images

Теги:
Хабы:
Всего голосов 7: ↑7 и ↓0+7
Комментарии10

Публикации

Истории

Работа

Data Scientist
54 вакансии
Python разработчик
119 вакансий

Ближайшие события

One day offer от ВСК
Дата16 – 17 мая
Время09:00 – 18:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область