Про использование Docker
и Docker-compose
последнее время написано очень много, например рекомендую недавнюю статью на Хабре, если вы до сих пор не прониклись. Это действительно очень удобно, а в связке в ansible особенно. И я его использую везде. От разработки, до автоматического интеграционного тестирования на CI
. Про использование в тестировании, тоже писали. Это здорово и удобно. Однако, для локальной разработки, для траблешутинга данных "как в продакшене" или тестирование производительности, на "объёмах близких в продакшену", хочется иметь под рукой образ, содержащий базу, "как в продакшене"!
Соответственно, хочется, чтобы каждый разработчик, приступая к работе над проектом, мог запустить его одной командой, например:
./gradlew dockerRun
и приложение поднялось бы сразу со всеми необходимыми связанными контейнерами? А главное чтобы в нём уже были бы данные для большинства кейсов разработки и багфиксинга, стандартные пользователи и большинство работающих сервисов, над которыми сразу можно было бы приступить работать, не тратя времени на экспорт-импорт каких-то там образов или демоданных!
Как приятный бонус, ну разве не здорово иметь базу данных в несколько гигабайт и возможность откатиться к её исходному (или любому другому коммиту) состоянию в течении пары секунд?
Разумеется мы поговорим о написании Dockerfile
для такого образа с данными, и некоторых подводных камнях этого процесса.
Вот на этом и сосредоточимся. В нашем приложении мы активно используем Postgres
, поэтому повествование и примеры будут о контейнере именно с ним, но это касается лишь примеров, суть изложения применима к любой другой реляционной или модной NoSQL
базе данных.
Чего хочется
Для начала определим проблему подробнее. Мы готовим образ, с данными, с которыми сможет работать каждый, кто работает с нашим приложением:
- Некоторые данные нуждаются в обфускации (например почтовые ящики или личные данные пользователей) — то есть нельзя просто восстановить дамп, требуется его обработка
- Однако мы хотим общий механизм накатывания предоставленных для сборки SQL скриптов, без дальнейшей модификации механизма сборки! Например это может использовать для семплинга (уменьшения количества данных) и уменьшения размера образа
- Хочется удобно включать в образ дополнительные настройки, не передавая их опциями во время запуска и не модифицируя каждый раз конфиг сложными регулярными выражениями
- Мы хотим включить в образ некоторые расширения конфигурации, чтобы не писать огромные мануалы как его запускать (передавая обязательные для старта опции в момент запуска)
- Данных много, поэтому хочется также включить оптимизационные настройки для увеличения производительности
- Вообще, для многих настроек, вы можете это сделать через стандартные пути расширения, выполнив ALTER SYSTEM команды. Но это только для тех настроек, которые не требуют определения только в конфигурацонном файле или параметрах запуска.
- Данные одинаковые, для разработчиков и
CI
Приступаем
Я не буду начинать с того что такое Dockerfile
, надеюсь вы уже знакомы с этим. Тех же, кто хочет получить представление, отсылаю к статье, ну или официальной документации.
Стоит отметить что официальный docker образ postgres уже имеет несколько точек расширения:
POSTGRES_*
переменные- И директорию внутри образа
/docker-entrypoint-initdb.d
куда можно положить sh скрипты илиsql
файлы, которые будут выполнены на старте. Это очень удобно, если вы скажем хотите создать дополнительных пользователей или базы данных, установить права, проинициализировать расширения.
Однако, для наших целей этого мало:
- Мы не можем включать некоторые данные, затирая их на старте:
- Во-первых это может привести к огромному размеру БД (мы хотим удалить некоторые логи или историю)
- Во вторых, пользователь может запустить образ переопределив
entrypoint
, и увидеть приватные данные, которые видеть не должен
- Дополнительно, мы можем передавать практически любые параметры в командной строке при запуске, вроде
--max_prepared_transactions=110
, но мы не можем легко положить их в образ, и сделать стандартными
- Так, например, раз мы строим образ для тестирования и имеем быстрый откат, я хочу включить в него агрессивные настройки оптимизации производительности, нежели надёжности (включая полное отключение
fsync
)
- Так, например, раз мы строим образ для тестирования и имеем быстрый откат, я хочу включить в него агрессивные настройки оптимизации производительности, нежели надёжности (включая полное отключение
Я наверное сразу покажу прототип файла (вырезаны лишь некоторые незначащие части чтобы он стал меньше, например много места занимает включение расширения pg_hint_plan
, которое ставится на Debian
из RPM
, потому что отсутствует в Deb
и официальных репозиториях):
FROM postgres:9.6
MAINTAINER Pavel Alexeev
# Do NOT use /var/lib/postgresql/data/ because its declared as volume in base image and can't be undeclared but we want persist data in image
ENV PGDATA /var/lib/pgsql/data/
ENV pgsql 'psql -U postgres -nxq -v ON_ERROR_STOP=on --dbname egais'
ENV DB_DUMP_URL 'ftp://user:password@ftp.taskdata.com/desired_db_backup/egais17qa_dump-2017-02-21-16_55_01.sql.gz'
COPY docker-entrypoint-initdb.d/* /docker-entrypoint-initdb.d/
COPY init.sql/* /init.sql/
# Later in RUN we hack config to include conf.d parts.
COPY postgres.conf.d/* /etc/postgres/conf.d/
# Unfortunately Debian /bin/sh is dash shell instead of bash (https://wiki.ubuntu.com/DashAsBinSh) and some handy options like pipefaile is unavailable
# Separate RUN to next will be in bash instead of dash. Change /bin/sh symlink as it is hardcoded https://github.com/docker/docker/issues/8100
RUN ln -sb /bin/bash /bin/sh
RUN set -euo pipefail && echo '1) Install required packages' `# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#apt-get` && apt-get update && apt-get install -y curl postgresql-plperl-9.6 && echo '3) Run postgres DB internally for init cluster:' `# Example how to run instance of service: http://stackoverflow.com/questions/25920029/setting-up-mysql-and-importing-dump-within-dockerfile` && bash -c '/docker-entrypoint.sh postgres --autovacuum=off &' && sleep 10 && echo '4.1) Configure postgres: use conf.d directory:' && sed -i "s@#include_dir = 'conf.d'@include_dir = '/etc/postgres/conf.d/'@" "$PGDATA/postgresql.conf" && echo '4.2) Configure postgres: Do NOT chown and chmod each time on start PGDATA directory (speedup on start especially on Windows):' && sed -i 's@chmod 700 "$PGDATA"@#chmod 700 "$PGDATA"@g;s@chown -R postgres "$PGDATA"@#chown -R postgres "$PGDATA"@g' /docker-entrypoint.sh && echo '4.3) RERun postgres DB for work in new configuration:' && gosu postgres pg_ctl -D "$PGDATA" -m fast -w stop && sleep 10 && bash -c '/docker-entrypoint.sh postgres --autovacuum=off --max_wal_size=3GB &' && sleep 10 && echo '5) Populate DB data: Restore DB backup:' && time curl "$DB_DUMP_URL" | gzip --decompress | grep -Pv '^((DROP|CREATE|ALTER) DATABASE|\\connect)' | $pgsql && echo '6) Execute build-time sql scripts:' && for f in /init.sql/*; do echo "Process [$f]"; $pgsql -f "$f"; rm -f "$f"; done && echo '7) Update DB to current migrations state:' && time java -jar target/egais-db-updater-*.jar -f flyway.url=jdbc:postgresql://localhost:5432/egais -f flyway.user=postgres -f flyway.password=postgres && echo '8) Vacuum full and analyze (no reindex need then):' && time vacuumdb -U postgres --full --all --analyze --freeze && echo '9) Stop postgres:' && gosu postgres pg_ctl -D "$PGDATA" -m fast -w stop && sleep 10 && echo '10) Cleanup pg_xlog required to do not include it in image!:' `# Command inspired by http://www.hivelogik.com/blog/?p=513` && gosu postgres pg_resetxlog -o $( LANG=C pg_controldata $PGDATA | grep -oP '(?<=NextOID:\s{10})\d+' ) -x $( LANG=C pg_controldata $PGDATA | grep -oP '(?<=NextXID:\s{10}0[/:])\d+' ) -f $PGDATA && echo '11(pair to 1)) Apt clean:' && apt-get autoremove -y curl && rm -rf /var/lib/apt/lists/*
Как видите, я постарался добавить комментарии прямо в файл, возможно они даже более чем исчерпывающие, но всё-таки остановимся на паре моментов подробнее.
Стоит обратить внимание
- Мы переопределяем
ENV PGDATA /var/lib/pgsql/data/
. Это ключевой момент. т.к. мы хотим, чтобы заполненные данные во время билда были включены в образ, мы не должны класть их в стандартное место, определённое как volume. - Переменная
DB_DUMP_URL
определена просто для удобства последующего редактирования. При желании её можно передавать извне, во время билда. - Далее мы запускаем
Postgres
прямо во время процесса билда:bash -c '/docker-entrypoint.sh postgres --autovacuum=off &'
для того чтобы провести некоторые нехитрые конфигурации:
- С помощью
sed
включаем в основномконфиге postgres.conf
использованиеinclude_dir
. Нам это нужно для того чтобы свести к минимуму такие манипуляции с конфигом, иначе их будет очень сложно поддерживать, но зато мы обеспечили неограниченную расширяемость конфигурации! Обратите внимание, немного выше мы используем директивуCOPY postgres.conf.d/* /etc/postgres/conf.d/
чтобы положить куски конфигов, специфичные именно для нашего билда. - Данный механизм был предложен сообществу в качестве issue для включения в базовый образ, и хотя уже вызвал вопросы как я делал (что и навело на мысль что это может быть кому-то полезно и поводом для написания статьи), пока запрос был закрыт, но я не теряю надежду на переоткрытие.
- Я также убираю (комментирую) из основного файла инструкции
chown
иchmod
, поскольку т.к. база проинициализирована, то файлы будут уже иметь в образе верных пользователей и права, но опытным путём выяснилось, что на версии docker под Windows эта операция почему-то может занимать весьма продолжительное время, вплоть до десятков минут. - Обратите пожалуйста также внимание, что мы должны сначала запустить
Postgres
, а только потом его пробовать настроить! Иначе мы получим ошибку на старте что директория для инициализации кластера не пуста! - Дальше я перезапускаю
Postgres
чтобы перечитать конфиги, которые мы подкладывали и настраивали читать. Строго говоря, этот шаг вовсе не обязательный. Однако по умолчанию он имеет очень консервативные настройки по памяти вродеshared_buffers = 128MB
, и работа со сколь-либо значительными затягивается на часы.
- С помощью
- В следующем шаге всё должно быть понятно — просто восстанавливаем дамп. А вот за ним, конструкция
/init.sql/*
применит всеSQL
скрипты из этой директории, во время создания образа (в противовес скриптам стандартного расширения). Именно здесь мы делаем необходимую обфускацию данных, сэмплирование, очистку, добавление тестовых пользователей и т.п.
- Именно это выполнение всех скриптов позволит в следующий раз не трогать процедуру билда, а лишь добавить пару файлов в эту директорию, делающих ещё что-то с вашими данными!
- Чтобы уменьшить образ а также сделать его немного более эффективным, мы выполняем на нём полный вакуум с анализом.
- Стоит заметить, именно это позволяет нам для ускорения импорта запускать
Postgres
с отключенным автовакуумом (--autovacuum=off
) - Также, для целей уменьшения образа, я дальше использую
pg_resetxlog
чтобы сбросить и не включать накопившиеся WAL. А при запуске использую--max_wal_size=3GB
чтобы увеличить размер файла и не ротэйтить их лишний раз. - Очистка APT кеша стандартна, следуя рекомендациям в лучших практиках.
- Стоит заметить, именно это позволяет нам для ускорения импорта запускать
Готовому образу остаётся только присвоить тэг и запушить в репозиторий. Чаще всего, конечно, это будет приватный репозиторий, если вы не работает над каким-то публичным сэмплом данных.
Я буду очень рад, если это поможет кому-то сделать его процесс подготовки создания тестовых образов с данными хоть чуточку проще.
Комментарии (2)
Hubbitus
11.05.2017 21:06Задача, в том числе, максимально упростить вход разработчика в приложение, чтобы все инструкции по сборке и запуску приложения, от многодневных попыток, как это было еще с год назад, сводились к:
git clone … ./gradlew dockerRun
При этом бы приложение бы сразу запустилось, но не с игрушечной базой (как обычно делают вроде in-memory H2 для тестов с парой записей), а с базой, на которой, в том числе гоняются интеграционные и перфоманс тесты на CI и чтобы воспроизвести можно было 99.9% проблем из продакшена.
Более того, я для простоты этот шаг убрал, чтобы не засорять описание, но имея стабильную master ветку, я ещё прогоняю все имеющиеся там миграции на БД (c помощью flyway) в момент билда — так, чтобы эта база запускалась бы моментально.
dmitryvim
Немного не понял самой задачи, если мы хотим иметь образ с сразу заполненной БД, то ок. Но если идёт разговор о запуске java-приложения (такое предположение я сделал из использования gradle) с инициализированной БД, то надо ли так усложнять?
Можно добавить в gradle ещё один task, который будет запускать docker-compose файл. В docker-compose прописать volume с ссылкой на нужные скрипты.
Более того, мы можем сразу положить наше приложение в docker-compose, если предварительно соберём его и положим в корень Dockerfile