тестирование python приложения в docker


В этой статье речь пойдет о том, как я тестирую свой python код внутри docker контейнера с Alpine Linux. Для тех, кто не знает, что такое docker, поясню, что это инструмент для управления контейнерами, который использует “слоёные” файловые системы и написан на Go.

Для меня основное преимущество docker перед привычной виртуализацией, например Qemu/KVM, в его легковесности. За счет AuFS/OverlayFS базовый образ системы и контейнер занимают до нескольких сотен мегабайт, а новые контейнеры используют тот же образ, что и старые, что позволяет экономить еще больше места. Docker есть в репозиториях многих дистров, так что вы его сможете легко поставить.

Что касается моего приложения, то оно написано на Python и использует Flask. Оно пока в активной разработке и выложить исходники я не могу. Приложение реализует RESTful API для gdnsd. Gdnsd — это авторитативный DNS сервер, который умеет балансировку и геораспределение. Сначала я тестировал со стандартным, встроенным во Flask web сервером, но после добавления в приложение поддержки uwsgi, я решил тестировать с uwsgi. Первым делом, в голову пришло поднять виртуалку и тестить в ней, но это показалось слишком громоздким, поэтому я засунул все внутрь docker.

В качестве базовой системы был выбран Alpine Linux за его крошечный размер — 5 МБ. Я был приятно удивлен, когда обнаружил все необходимые мне пакеты под Alpine.

Вот мой Dockerfile, который расположен прямо в директории приложения

FROM alpine

RUN apk add --update python uwsgi-python py-pip mercurial gdnsd && \
    rm -rf /var/cache/apk/*

COPY . /opt/gdnsd-api

RUN pip install -r /opt/gdnsd-api/requirements.txt

RUN hg init /etc/gdnsd && \
    mkdir -p /opt/config/example && \
    cp /opt/gdnsd-api/wsgi/api.ini /opt/config/ && \
    sed -i 's|pyargv.*|pyargv=/opt/config/api.json|' /opt/config/api.ini && \
    cp /opt/gdnsd-api/example/api.json /opt/config/ && \
    sed -i 's|example|/opt/config/example|' /opt/config/api.json && \
    cp /opt/gdnsd-api/example/config.json /opt/config/example && \
    hg init /opt/config/example

EXPOSE 5000
ENTRYPOINT ["uwsgi", "--ini", "/opt/config/api.ini", "--plugin-dir", \
    "/usr/lib/uwsgi", "--plugin", "python", "--protocol", "http", \
    "--socket", "0.0.0.0:5000", "--py-autoreload", "1"]
  • FROM alpine из какого образа создавать docker image
  • RUN apk add ... устанавливает нужные для работы приложения пакеты
  • COPY . /opt/gdnsd-api копируем код приложения внутрь image
  • RUN pip install -r /opt/gdnsd-api/requirements.txt устанавливает все зависимости приложения
  • дальше мы копируем примеры конфигов и правим пути в скопированных конфигах
  • EXPOSE 5000 говорит демону docker, о том, какие порты слушает контейнер
  • ENTRYPOINT команда, которая будет выполняться в контейнере, запущенном из построенного image

Рассмотрим ENTRYPOINT подробнее. Я запускаю uwsgi с модифицированным ранее конфигом /opt/config/api.ini, говорю, что нужно искать плагины в /usr/lib/uwsgi и подгружать оттуда python плагин, потому что это не делается автоматически. Используем протокол http, чтоб не устанавливать еще и nginx. Слушаем порт 5000 на всех интерфейсах и перезагружаем приложение при изменении кода - py-autoreload.

Вот сам api.ini для наглядности

[uwsgi]
module = app
callable = app
master = true
processes = 1
socket = 127.0.0.1:8000
chdir = /opt/gdnsd-api
wsgi-file = app.py
uid = root
gid = root
pyargv = /opt/config/example/api.json
buffer-size = 32768

Когда мы вдумчиво написали Dockerfile, можем создать image

docker build -t myapp .

Создание моего image, после выкачки базового образа alpine и установки пакетов занимает около 15 секунд.

Образ готов и весит всего 73МБ вместе с приложением и его зависимостями, а не 188 как чистая Ubuntu.

docker images
REPOSITORY      TAG       IMAGE ID         CREATED       VIRTUAL SIZE
myapp           latest    410d923182b5     5 hours ago   72.68 MB
ubuntu          14.04     36248ae4a9ac     4 days ago    187.9 MB
alpine          latest    3571dd565f47     4 days ago    4.79 MB

Теперь запустим контейнер из свежесозданного образа

docker run -v /my-app:/opt/gdnsd-api:ro -p 127.0.0.1:5000:5000 \
--name myapp myapp

С помощью -v (volume) мы прокинем код нашего приложения из хост системы в контейнер, при чем в режиме только для чтения (:ro). Т.к. разработку я веду на хосте (в ОС ноутбука), а не в контейнере, то мне удобней, когда контейнер получает изменения сразу после сохранения файла на хосте. Деплоить приложение внутрь контейнера после каждого изменения слишком накладно для тестирования, а для распростраения приложения — в самый раз. Подробнее про docker volumes вы можете почитать вот здесь. -p редиректит порт 5000 из контенера в хост систему.

Получить shell в запущенном контейнере можно вот так:

docker exec -ti myapp sh

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