Часть 1, Часть 2

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

Управление конфигурацией с Docker Compose

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

  • UI, работающего на сервисе nginx;
  • Логики  —  центрального в статьях компонента на Python.
  • Данных в БД MySQL. 

Мы разделяем приложение на уровни: так можно с лёгкостью изменять или добавлять новые уровни без необходимости перерабатывать весь проект.

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

Project
├─── web
└─── app
└─── db

В первой части мы увидели контейнеризацию компонента на Python. То же самое относится и к другим компонентам проекта, но мы пропустим эти детали, поскольку можем обратиться к шаблонам, реализующим необходимую нам структуру. Пример  —  nginx-flask-mysql из репозитория awesome-compose. Ниже вы видите обновлённую структуру проекта с файлом Dockerfile. Предположим, что у веб- и db-компонентов похожие структуры: 

Project
├─── web
├─── app
│ ├─── Dockerfile
│ ├─── requirements.txt
│ └─── src
│ └─── server.py
└─── db

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

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

Compose-файл

Посмотрим на файл Compose и разберёмся, как с его помощью можно управлять сервисами проекта. 

Ниже  —  пример для нашего случая. В нём мы определяем список сервисов. В разделе db указывается базовый образ непосредственно, поскольку для него не применяется никакой конкретной конфигурации, а веб-сервиса и сервиса приложения будут содержать образ, собранный на основе их Dockerfile. Мы можем настроить поля build  —  сборка или image  —  образ в зависимости от того, откуда будем получать образ сервиса. В поле build указывается путь к Dockerfile:

docker-compose.yaml

version: "3.7"
services:
  db:
    image: mysql:8.0.19
    command: '--default-authentication-plugin=mysql_native_password'
    restart: always
    environment:
      - MYSQL_DATABASE=example
      - MYSQL_ROOT_PASSWORD=password
app:
    build: app
    restart: always
web:
    build: web
    restart: always
    ports:
      - 80:80

Для инициализации БД мы передаем переменные среды с именем этой БД и паролем, а для веб-сервиса  —  отображаем порт контейнера в localhost, чтобы получить возможность обращаться к веб-интерфейсу проекта. 

Теперь разберём развёртывание проекта при помощи Docker Compose. Нам осталось поместить docker-compose.yaml в корневую директорию и назначить команду для развёртывания:

Project
├─── docker-compose.yaml
├─── web
├─── app
└─── db

Docker Compose позаботится об извлечении образа MySQL из Docker Hub и запуске контейнера db, а для веб-сервиса и сервиса приложения он соберёт образы локально, запустив контейнеры из них. Он также берёт на себя создание предустановленной сети по умолчанию и помещение в неё контейнеров, предоставляя возможность коммуникации между сервисами.

Да, всё это запускается одной командой:

$ docker-compose up -d
Creating network "project_default" with the default driver
Pulling db (mysql:8.0.19)…
…
Status: Downloaded newer image for mysql:8.0.19
Building app
Step 1/6 : FROM python:3.8
---> 7f5b6ccd03e9
Step 2/6 : WORKDIR /code
---> Using cache
---> c347603a917d
Step 3/6 : COPY requirements.txt .
---> fa9a504e43ac
Step 4/6 : RUN pip install -r requirements.txt
---> Running in f0e93a88adb1
Collecting Flask==1.1.1
…
Successfully tagged project_app:latest
WARNING: Image for service app was built because it did not already exist. To rebuild this image you must use docker-compose build or docker-compose up --build.
Building web
Step 1/3 : FROM nginx:1.13-alpine
1.13-alpine: Pulling from library/nginx
…
Status: Downloaded newer image for nginx:1.13-alpine
---> ebe2c7c61055
Step 2/3 : COPY nginx.conf /etc/nginx/nginx.conf
---> a3b2a7c8853c
Step 3/3 : COPY index.html /usr/share/nginx/html/index.html
---> 9a0713a65fd6
Successfully built 9a0713a65fd6
Successfully tagged project_web:latest
Creating project_web_1 … done
Creating project_db_1 … done
Creating project_app_1 … done

Проверка выполнения контейнеров:

$ docker-compose ps
  Name         Command                        State  Ports
-------------------------------------------------------------------------
project_app_1  /bin/sh -c python server.py    Up
project_db_1   docker-entrypoint.sh --def ... Up     3306/tcp, 33060/tcp
project_web_1  nginx -g daemon off;           Up     0.0.0.0:80->80/tcp

Для остановки и удаления всех контейнеров выполните:

$ docker-compose down
Stopping project_db_1 ... done
Stopping project_web_1 ... done
Stopping project_app_1 ... done
Removing project_db_1 ... done
Removing project_web_1 ... done
Removing project_app_1 ... done
Removing network project-default

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

$ docker-compose build
$ docker-compose up -d

Как видите, docker-compose позволяет с лёгкостью управлять жизненным циклом проекта.

Лучшие подходы в Docker Compose

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

Разделение сети

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

Однако, если нужно, чтобы обращаться к БД мог только Python-сервис, нам нужно описать в Compose-файле отдельную сеть для каждой пары компонентов. Тогда веб-компонент к БД обращаться не сможет.

Тома в Docker

При каждой остановке контейнеров мы удаляем их и теряем данные, хранившиеся в предыдущих сеансах. Чтобы избежать потери даннных и сохранить данные БД между разными контейнерами, мы можем использовать именованные тома  —  docker volumes. Для этого нужно просто определить такой том в Compose-файле и указать точку его монтирования в сервисе db:

version: "3.7"
services:
  db:
    image: mysql:8.0.19
    command: '--default-authentication-plugin=mysql_native_password'
    restart: always
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - backend-network
    environment:
      - MYSQL_DATABASE=example
      - MYSQL_ROOT_PASSWORD=password

app:
    build: app
    restart: always
    networks:
      - backend-network
      - frontend-network

web:
    build: web
    restart: always
    ports:
      - 80:80
    networks:
      - frontend-network
volumes:
  db-data:
networks:
  backend-network:
  frontend-network:

В docker-compose при желании можно удалять именованные тома.

Docker Secrets

Как видно из Сompose-файла, мы храним пароль дляdb в виде простого текста. Такой подход небезопасен. Можно использовать секреты Docker, которые хранят пароль, а при необходимости безопасно предоставлять его сервисам. Ниже показано, как можно определить секреты и организовать на них ссылку в сервисах. Пароль в нашем случае хранится локально в файле project/db/password.txt и монтируется в контейнеры так: /run/secrets/<secret-name>.

version: "3.7"
services:
  db:
    image: mysql:8.0.19
    command: '--default-authentication-plugin=mysql_native_password'
    restart: always
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - backend-network
    environment:
      - MYSQL_DATABASE=example
      - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password

app:
    build: app
    restart: always
    secrets:
      - db-password
    networks:
      - backend-network
      - frontend-network

web:
    build: web
    restart: always
    ports:
      - 80:80
    networks:
      - frontend-network
volumes:
  db-data:
secrets:
  db-password:
    file: db/password.txt
networks:
  backend-network:
  frontend-network:

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

Что дальше?

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

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи ANCA IORDACHE: Containerized Python Development — Part 2

Предыдущая статьяЗапуск DBT в Azure Functions с помощью Snowflake
Следующая статьяПод покровом капустного листа: шаблон Декоратор