О чем пойдет речь
Здесь периодически появляются посты, в которых авторы делятся своими подходами по использованию docker. Ну что же, вот вам еще один. Ниже я расскажу о нашем опыте использования docker-окружения, о неудобствах, с которыми мы столкнулись, как мы с ними боролись, и во что это вылилось. А также поделюсь небольшим, но столь полезным для нас, инструментом.
Как мы жили до этого
Для начала немного истории. Так сложилось, что по долгу службы, мы в той или иной степени разрабатываем и поддерживаем одновременно несколько проектов. Все они имеют разный возраст, требования и соответственно работают в разном окружении. В связи с этим при развертывании локальной копии возникали некоторые неудобства. Когда ты переключаешься на проект, с которым ранее не работал, приходится возиться с его настройкой, а также с настройкой рабочей среды. И если внутри команды это могло решиться довольно быстро, то с периодически подключаемыми внештатными разработчиками все сложнее. Было принято решение перенести разработку в docker-окружение. Здесь мы не стали ничего выдумывать, а пошли общепринятым путем. Каждый сервис поднимался в отдельном контейнере. Для связки использовали docker-compose.
Для параллельной работы над несколькими проектами требуется установка всех сервисов необходимых каждому из них. В первую очередь мы создали репозиторий, в котором располагался файл конфигурации для docker-compose, а также конфигурации требуемых для работы образов. Все довольно быстро заработало и на какое-то время это нас устроило. Как оказалось, в дальнейшем данный подход решал нашу проблему частично. По мере добавления проектов в новую экосистему этот репозиторий наполнялся файлами конфигураций и различными вспомогательными скриптами. Это привело к тому что при работе над одним единственным проектом разработчику приходилось либо тащить зависимости всех проектов, либо же править docker-compose.yml, отключая лишние сервисы. В первом случае приходилось ставить лишние контейнеры, что нам казалось не лучшим решением, а во втором нужно было знать какие контейнеры требуются для работы приложения. Хотелось иметь более гибкое решение, которое позволит устанавливать только необходимые компоненты, а также, если не исключит, то минимизирует ручную работу. И вот к чему мы пришли...
ddk
ddk (Docker Development Kit) — инструмент, призванный упростить настройку окружения и автоматизировать развертывание среды разработки для проектов, работающих в docker-окружении. Звучит, наверное, сильно. На деле же, ddk является некой оберткой над git и docker и предоставляет ряд дополнительных команд для удобного управления пакетами, файлами конфигураций и проектами. В некотором роде, это менеджер зависимостей окружения для проектов и сервисов docker-compose.
Изначально ddk — это набор python-скриптов, но конечный пользователь получает единственный исполняемый файл, с которым и работает. Теперь, помимо установки самого docker'а и docker-compose, разработчику необходимо проинициализоровать ddk, создав конфигурационный файл. Эта задача решается вызовом команды init.
cd /var/projects/ddk
ddk init
После этого подключение к новому проекту выглядит следующим образом:
ddk project get my.project.ru
ddk compose --up
Также, при необходимости, перенаправляем новый домен на localhost.
echo 127.0.0.1 my.project.ddk >> /etc/hosts
Первая команда клонирует проект и выполняет его инициализацию. Вторая генерирует конфигурацию для docker-compose и запускает необходимые сервисы. В процессе выполнения будут загружены все недостающие компоненты. По завершению сборки разработчик получает полностью рабочую локальную копию проекта, которая доступна по адресу my.project.ddk.
Немного о том, как это работает.
При использовании ddk рабочей считается та директория, в которой расположен конфигурационный файл, сгенерированный командой init. Сам же исполняемый файл может располагаться в любом удобном месте. Поиск конфигурации осуществляется, начиная с текущей директории, а затем ddk поднимается по дереву каталогов пока не обнаружит искомый файл или не достигнет корня файловой системы. Схожим образом работают git и docker-compose. После того, как файл конфигурации найден, ddk формирует некоторые каталоги для хранения пакетов и исходного кода проектов, разрешает и устанавливает зависимости. Установка компонентов осуществляется простым клонированием git-репозитория, адрес которого определяется путем конкатенации имени компонента и префикса из конфигурационного файла.
# "project-repo-prefix": ["git@github.com/vendor-name/"]
ddk project get my.project.ru
git clone git@github.com/vendor-name/my.project.ru.git
Само собой, ddk не является простым шорткатом для git clone, и имеет дополнительные функциональные возможности, из-за которых он и задумывался. О том, как, зачем и почему — чуть ниже, а здесь добавлю лишь то, что в итоге сформируется директория, в которой будут собраны все проекты, а также необходимые для их работы конфигурационные файлы. Данная директория может быть без проблем перемещена в другой каталог или на другую машину.
ddk-пакеты
Первое чего хотелось добиться — сделать все окружение максимально модульным. Мы выделили описание каждого сервиса в отдельные конфигурационные файлы и вынесли их в самостоятельные репозитории. Коллега назвал их пакетами. Эти самые пакеты и легли в основу работы нашего инструмента. При сборке docker-compose.yml ddk проходит по всем требуемым пакетам и генерирует на их основе итоговый конфигурационный файл.
Как правило, нет необходимости устанавливать отдельные пакеты самостоятельно, так как при сборке автоматически подгружаются все недостающие компоненты. Тем не менее имеется возможность для их установки и обновления.
ddk package install package-name
ddk package update
Теперь о содержимом. В корне всегда находится конфигурационный файл ddk.json, в котором указываются имя контейнера и используемый docker-образ. Ниже приведен пример пакета с минимальной конфигурацией.
{
"container_name": "memcached.ddk",
"image": "memcached:latest"
}
Как вы, наверное, заметили, фактически, это часть конфигурации из docker-compose.yml представленная в формате JSON. Такой подход дает возможность установить любые параметры, поддерживаемые docker-compose. Вот пример более сложного пакета, который использует отдельный Dockerfile и монтирует директории.
{
"build": "${PACKAGE_PATH}",
"container_name": "nginx.ddk",
"volumes": [
"${SHARE_PATH}/var/www:/var/www",
"${PACKAGE_PATH}/storage/etc/nginx/conf.d:/etc/nginx/conf.d:ro",
"${PACKAGE_PATH}/storage/etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro",
"${PACKAGE_PATH}/storage/var/log/nginx:/var/log/nginx"
]
}
Листинг директории пакета:
storage/
etc/
nginx/
conf.d/
site.ddk.sample
nginx.conf
ddk.json
Dockerfile
Ключи, имеющие префикс "ddk-" используются для указания специальных директив. На данный момент единственным поддерживаемым ключом является "ddk-post-install", который хранит список команд, выполняющихся после установки и обновления пакета.
{
"ddk-post-install": [
"echo 'Done'"
]
}
Один из вариантов использования данной опции приведен в разделе "Соглашения"
Проекты
Теперь рассмотрим, как использовать ddk на примере конкретного проекта. Для того, чтобы развернуть существующий проект достаточно вызвать команду get
.
ddk project get project-id
Данная команда клонирует проект в директорию share/var/www, после чего производится поиск конфигурационного файла (по умолчанию в корне проекта), и запускаются все необходимые команды из секции on-init. На этом этапе выполняется настройка индивидуальных параметров проекта (генерация .env, установка прав на файлы, конфигурация базы данных и т.п.).
Помимо команд для инициализации, файл ddk.json содержит список пакетов, от которых зависит работа проекта. Если какой-то из пакетов отсутствует, он будет автоматически установлен. Ниже приведен пример конфигурации проекта.
{
"packages": [
"mysql5.5",
"memcached",
"apache-php5.5"
],
"on-init": [
"${PROJECT_PATH}/init.sh ${PACKAGES_PATH} ${PROJECT_DIR}"
]
}
Несмотря на то, что секция on-init позволяет передать несколько команд мы, как правило, указываем лишь одну. В примере выше вы можете видеть, что при инициализации проекта запустится скрипт развертывания, который и выполнит основную конфигурацию. Такой подход оказался удобнее, так как дает большую гибкость и позволяет добавить интерактив в процесс инициализации проекта.
При необходимости расширить конфигурацию какого-либо пакета, можно сделать это, указав его в виде объекта. Данный объект должен иметь атрибут name, содержащий название пакета. Все остальные атрибуты будут восприняты как конфигурация.
{
"packages": [
{
"name": "nginx",
"depends_on": [
"php-fpm7.1"
],
"environment": [
"SOME_VAR=Hello"
]
}
]
}
Таким образом мы имеем возможность влиять на работу сервисов, не меняя оригинальную конфигурацию пакета.
Соглашения
В процессе работы в docker-окружении мы выработали несколько соглашений, которых и стараемся придерживаться.
Во-первых, при монтировании каких-либо файлов и директорий пакета либо проекта, их структура должна совпадать со структурой внутри контейнера. Т.е. package-name/storage соответствует корневой директории контейнера package-name. Директория share также соответствует корневой директории контейнеров. Именно поэтому все проекты располагаются в share/var/www. Данное правило прослеживается и в приведенных выше примерах.
Следующий пункт заключается в том, что при установке пакетов, в контейнерах которых предполагается модификация файловой системы, создается специальный пользователь, учетные данные которого соответствуют данным пользователя хост-системы. Другими словами, мы мапим логин, идентификатор пользователя и идентификатор группы с хост-системы в контейнер. В дальнейшем все команды в контейнере рекомендуется выполнять, используя эти данные. Такой подход позволяет избежать проблем с правами доступа при обращении к файлам вне контейнера. Если хотя бы один из проектов сконфигурирован подобным образом, будет создана директория share/home/<user-dir>, которая монтируется в контейнер и используется в качестве домашнего каталога. Ниже пример того, как мы это реализовали.
{
"container_name": "php71-fpm.ddk",
"command": "map-user.sh",
"env_file": [
"${PACKAGE_PATH}/env/user.env"
],
"ddk-post-install": [
"mkdir -p ${PACKAGE_PATH}/env",
"echo USER_NAME=`whoami` > ${PACKAGE_PATH}/env/user.env",
"echo USER_ID=`id -u` >> ${PACKAGE_PATH}/env/user.env",
"echo GROUP_ID=`id -g` >> ${PACKAGE_PATH}/env/user.env"
]
}
Как вы видите, после установки пакета генерируется файл с данными пользователя. При старте контейнера скрипт map-user.sh проверяет и при необходимости создает учетную запись, используя полученные данные.
Щепотка магии
Последнее, что требуется сделать это запустить все необходимые сервисы, используя обычный docker-compose. Для генерации параметров запуска предназначена команда compose. При ее вызове, ddk проходит по всем активным проектам, собирает данные о пакетах и их параметрах, объединяет всю полученную информацию с конфигурациями самих пакетов и на основе этих данных генерирует итоговый docker-compose.yml. Данный файл и используется при запуске.
ddk compose
docker-compose up -d
Если при формировании конфигурации указать соответствующую опцию, можно обойтись одной командой.
ddk compose --up
Hello, world
Желающие увидеть ddk в действии могут развернуть демо-проект.
Качаем последнюю сборку:
wget https://github.com/simbigo/ddk/raw/master/dist/ddk
chmod +x ddk
Настраиваем будущий домен:
echo 127.0.0.1 hello.ddk >> /etc/hosts
Разворачиваем проект:
./ddk init
./ddk project get hello
./ddk compose --up
После успешной сборки всех образов, проект доступен по адресу http://hello.ddk
Заключение
Чего добились:
- Модульное окружение.
- Формирование конфигурации одной командой.
- Отсутствие многоразовой ручной работы.
- Минимальное время для включения разработчика в проект.
Над чем стоит поработать:
- В ddk практически отсутствует какая-либо обработка ошибок.
- Планировали реализовать корректную работу на MacOS, но на данный момент в нашей команде отсутствует маковод, и инструмент в этой системе не тестировался. Скорее всего, всплывут какие-то особенности, и потребуется доработка.
- Адреса репозиториев для пакетов и проектов передаются в качестве массива, но по факту работа ведется только с первым элементом. Необходимо реализовать корректную проверку существования репозитория и поиск по множеству адресов.
- Удаление лишних контейнеров.
- В скриптах инициализации довольно много повторяющегося кода. Может быть, имеет смысл вынести общие функции в ddk.
Для тех, у кого возникнет непреодолимое желание посмотреть, сделать лучше или просто покритиковать код, прилагаю ссылку на github. Будем рады, если инструмент окажется полезным еще кому-то, кроме нас.
Автор: Simbigo