Часть 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-компонент и отлаживать его.
Читайте также:
- Контейнеризация в Python. Часть 1
- Встроенная база данных Python
- Пять действительно крутых пакетов Python
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи ANCA IORDACHE: Containerized Python Development — Part 2