О чем пойдет речь


Здесь периодически появляются посты, в которых авторы делятся своими подходами по использованию docker. Ну что же, вот вам еще один. Ниже я расскажу о нашем опыте использования docker-окружения, о неудобствах, с которыми мы столкнулись, как мы с ними боролись, и во что это вылилось. А также поделюсь небольшим, но столь полезным для нас, инструментом.


image


Как мы жили до этого


Для начала немного истории. Так сложилось, что по долгу службы, мы в той или иной степени разрабатываем и поддерживаем одновременно несколько проектов. Все они имеют разный возраст, требования и соответственно работают в разном окружении. В связи с этим при развертывании локальной копии возникали некоторые неудобства. Когда ты переключаешься на проект, с которым ранее не работал, приходится возиться с его настройкой, а также с настройкой рабочей среды. И если внутри команды это могло решиться довольно быстро, то с периодически подключаемыми внештатными разработчиками все сложнее. Было принято решение перенести разработку в 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


Заключение


Чего добились:


  1. Модульное окружение.
  2. Формирование конфигурации одной командой.
  3. Отсутствие многоразовой ручной работы.
  4. Минимальное время для включения разработчика в проект.

Над чем стоит поработать:


  1. В ddk практически отсутствует какая-либо обработка ошибок.
  2. Планировали реализовать корректную работу на MacOS, но на данный момент в нашей команде отсутствует маковод, и инструмент в этой системе не тестировался. Скорее всего, всплывут какие-то особенности, и потребуется доработка.
  3. Адреса репозиториев для пакетов и проектов передаются в качестве массива, но по факту работа ведется только с первым элементом. Необходимо реализовать корректную проверку существования репозитория и поиск по множеству адресов.
  4. Удаление лишних контейнеров.
  5. В скриптах инициализации довольно много повторяющегося кода. Может быть, имеет смысл вынести общие функции в ddk.

Для тех, у кого возникнет непреодолимое желание посмотреть, сделать лучше или просто покритиковать код, прилагаю ссылку на github. Будем рады, если инструмент окажется полезным еще кому-то, кроме нас.


Посетить github

Поделиться с друзьями
-->

Комментарии (22)


  1. neenik
    07.06.2017 23:33
    +6

    Какая-же боль — такое читать. Вроде давно уже космические киты бороздят просторы амазона, а тут — какие-то обвязочные шелл-скрипты, как в старо-древние времена.


    Kubernetes, nomad, ansible, в конце-концов.


    А вы, как у меня сложилось впечатление по прочтению — "принятыми соглашениями" превращаете самые простые стейтлесс контейнеры в какой-то ад.


    1. VolCh
      08.06.2017 11:22
      +2

      Docker (+swarm mode), docker-compose и bash-скрипты вполне позволяют решать задачи по разворачиванию систем в различных окружениях без умножения сущностей, без усложнения поддержки со стороны эксплуатации, слабо представляющей, что такое докер вообще. От них требуем только установить докер на новых нодах, подключить к кластеру, попрописать DNS, настроить мониторинг и бэкапы. Остальное делают разработчики: пишут докерфайлы, ямлы для композа и сварм-стэка (почти не отличающиеся). Зачем вводить новые сущности с высоким порогом входа типа кубернейтса, если вся обвязка системы из пары десятков помещается меньше чем в 1000 строк ямла и 100 строк баша для ВСНХ мыслимых окружений?


  1. delaguardo
    08.06.2017 08:09

    Хм… А можно ли посмотреть как вы ваш бинарничек собирали? Не очень хочется качать что-то исполняемое неизвестно как собранное. И заодно посмотрите вот на такой проект — https://github.com/dmitrykuzmenkov/yoda. Идеи похожи, но реализован и оформлен он получше.


    1. Simbigo
      08.06.2017 08:11

      Собирается PyInstaller'ом. В корне есть файлик make.py — он все и делает. За ссылку спасибо, глянем.


    1. Simbigo
      08.06.2017 09:55

      Я немного обманул. Сейчас заметил, что файл для сборки не попал в репу. Добавил, так что при необходимости можете подсмотреть.


  1. SirEdvin
    08.06.2017 10:38

    Для параллельной работы над несколькими проектами требуется установка всех сервисов необходимых каждому из них. В первую очередь мы создали репозиторий, в котором располагался файл конфигурации для docker-compose, а также конфигурации требуемых для работы образов. Все довольно быстро заработало и на какое-то время это нас устроило. Как оказалось, в дальнейшем данный подход решал нашу проблему частично. По мере добавления проектов в новую экосистему этот репозиторий наполнялся файлами конфигураций и различными вспомогательными скриптами. Это привело к тому что при работе над одним единственным проектом разработчику приходилось либо тащить зависимости всех проектов, либо же править docker-compose.yml, отключая лишние сервисы. В первом случае приходилось ставить лишние контейнеры, что нам казалось не лучшим решением, а во втором нужно было знать какие контейнеры требуются для работы приложения. Хотелось иметь более гибкое решение, которое позволит устанавливать только необходимые компоненты, а также, если не исключит, то минимизирует ручную работу. И вот к чему мы пришли...

    Не совсем понятно, как у вас так получилось. Один проект — один docker-compose файл, разве нет? Или у вас там космическое число микросервисов?


    1. Simbigo
      08.06.2017 10:54

      Не космическое конечно, но хватает. Даже при малом количестве неудобно было бы, переключаясь между проектами, тушить одно окружение и поднимать другое. Сейчас это выглядит также, как если бы мы запускали docker-compose up с несколькими конфигурационными файлами. Т.к. сервисы в различных проектах во многом пересекаются, пришлось бы дублировать их в каждом docker-compose файле. Сейчас общая часть вынесена в пакет, а нюансы указываются в конфиге проекта.


  1. ecto
    08.06.2017 22:19

    Мы делаем немного подругому.


    Есть принципы похожие с вами, но помойму мы гораздо проще решили.


    docker-compose.yml поддерживает инклюды.


    Опредение сервиса хранится в репе сервиса.
    В репе проекта хранится главный docker-compose.yml в котором прописаны инклюды и переопределены порты если пересекаются у сервисов.
    Там же через лейблы прописаны гит ссылки на зависимости (репы сервисов).


    Простой улилитой (рнр скрипт собраный в один исполняемый файл) https://habrahabr.ru/post/306384/
    Подтягиваются зависимости
    Ей же билдятся все зависимости (у нас билд приложение и билд имиджа два разных этапа)


    Запуск проекта выглядит приблизительно так


    git clone git://project
    cd project
    docker-project update # с этого момента инклюды в проекте становятся действительными
    docker-project build # если у вас свой билд процесс
    docker-compose up

    Из плюсов
    1 Минимальный уровень входа
    2 Нет дополнительных конфиг файлов
    3 Нет влияния на код микросервиса


    Из минусов
    1 Утилита требует установленного php-cli
    (Можно переписать на питон или лучше Го, но пока руки не доходят.)


    1. Simbigo
      08.06.2017 22:36

      В репе проекта хранится главный docker-compose.yml

      Я правильно поннимаю, что docker-compose запускает сервисы на основе этого файла? Если так, имеется ли воможность поднять окружение для нескольких проектов одновременно?


      1. ecto
        08.06.2017 22:55

        Через указывание несколько проектов


        docker-project update -f pro-a.yml
        docker-project update -f pro-b.yml
        docker-compose -f pro-a.yml -f pro-b.yml up

        Конечно, не так изящно как у вас. У нас как-то нет необходимости так проэкты запускать.
        Хотя у нас около сотни своих сервисов (несчитая публичные типа мемкеш, редис, мускуль и т.д.) и 25 проект yml файлов.


        1. Simbigo
          08.06.2017 23:05

          docker-compose -f pro-a.yml -f pro-b.yml up

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


          1. ecto
            08.06.2017 23:19

            Да, я так и понял.


            У вас идея в корне очень интересная. Я бы конфиги попробовал сделать композер совместимыми, что бы работа была более прозначной.


    1. ecto
      08.06.2017 22:44

      Сейчас заметил v3 инклюды не поддерживает пока.
      Для v3 все выше так же справедливо, однако определение сервиса придется хранить в определении проекта.
      Вы эту проблему решили генерацией конфига, как я понимаю.


  1. hippoage
    09.06.2017 09:09
    +1

    Статья интересноя, но не описано зачем это нужно. Например, в терминах задач: есть такая-то частая задача, раньше неудобно решалась так, теперь удобно решается через новый инструмент. Так можно еще одну написать :).

    Например, включение разработчика в новый проект может заключаться в git clone (с простейшей оберткой, если не хочется писать полный URL), echo 127.0.0.1 my.project.ddk >> /etc/hosts, docker-compose up -d. За запуск всего окружения отвечает docker-compose, поэтому фактически ddk расширяет его (тот же post-install скрипт — это настройка ENV переменных для докера). Я бы, на вскидку, просто копировал docker-compose из проекта в проект. Возможно, написал бы генерилку для него из каких-то параметров. Как раз интересно понять почему отказались от такого пути и перешли на текущую схему, насколько это частное/общее решение.


    1. VolCh
      09.06.2017 11:00

      Ощущение, что очень частное.


      У нас я использую docker-compose.yml в корне проекта для общего описания проекта. docker-compose.override.yml.dist и .env.dist с дефолтными настройками разработчика, которые каждый разработчик затачивает под свои нужды. Плюс docker-compose-stack.yml для docker stack deploy --compose-file docker-compose.yml --compose-file docker-compose-stack.ym (в теории, на практике приходится предварительно мержить файлы через docker-compose config --file docker-compose.yml --file docker-compose-stack.ym).


      Пока единственный недостаток значимый — при одновременной работе над двумя версиями проекта, которые отличаются от мастера лишь парой контейнеров (провайдер и клиент какого-нить API, например) из пары-тройки десятков, остальные разворачиваются в двух экземплярах. Пробовал разные решения, более-менее приемлемые нашёл только если одновременная работа затрагивает лишь один контейнер, причём заведомо (до docker-compose up) известный какой. Если их несколько, то да, думаю о написании генерилки, которая будет генерить docker-compose.yml по результатам git diff --names-only master так, чтобы шарились контейнеры из мастера через external_links, а то и вообще "bare metal" сервисы через external_hosts, как у нас шарятся в продакшене statefull сервисы не под докером вообще.


      1. Simbigo
        09.06.2017 11:29

        Изначально у нас был аналогичный подход, но потом мы «упростили» настройку для тех, кто о докере знает только как его установить. Например верстальщика устраивают дефолтные значения, так зачем ему что-то править (хотя никто не ограничивает). Достаточно запустить ddk compose и заведется все, что требуется для работы.


        1. VolCh
          09.06.2017 22:35

          Ну у нас почти то же самое: git clone; cp .dist ; docker-compose up; — запустится всё необходимое окружение локально, со смонтированными каталогами типа src. Вернее пока только наши сервисы и стандартные, в перспективе заглушки для внешних партнерских.


          Проблемы с этим сценарием в основном у тех, кому хочется странного, типа одновременно держать две версии проекта в соседних окнах браузера с шарящимися 90% контейнеров.


    1. Simbigo
      09.06.2017 11:04

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

      Простое копирование конфигурационных файлов затрудняет их модификацию в дальнейшем. Если понадобится внести изменения в какой-либо контейнер, придется пройтись по всем проектам, которые его используют.


      1. VolCh
        09.06.2017 22:43

        Кажется понял вашу особенность: у вас контейнеры шарятся между разными проектами, а не между разными версиями одного проекта. Сходу могу придумать как обойти если набор таких сервисов фиксирован и все проекты должны работать с одной версией. А вот если иногда нужно для отдельного проекта запустить свою версию конкретного "общего" контейнера, а другим оставить общую, да в одном экземпляре — тут не уверен, что чисто стандартными докеровскими инструментами можно обойтись без хитрых игр с башем или ещё чем.


    1. Simbigo
      09.06.2017 11:20

      Кстати, в post-install скриптах делается намного больше работы. В статье, действительно, пример только для конфигурации пакета, но на деле при выполнении команды project get по нужным путям раскладываются конфиги балансировщика, веб-сервера, накатывается дамп. Все это происходит без участия разработчика.


  1. Ikarr
    10.06.2017 12:35

    У меня сложилось впечатление, что вы сами себе создали проблему на ровном месте и затем героически решали её, используя как можно больше дополнительного софта и понятий. Можно же образ нужного сервиса собирать в CI, а потом подключать в docker-compose.yml конкретного проекта.


    1. VolCh
      13.06.2017 11:34

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