Статьи

Развертывание приложений Java / Scala на уровне кластера с помощью Docker, Chef и Amazon OpsWorks

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

MQPerf-развертывания-блог-логотипы-1-300x216

Конкретная проблема, которую я хочу решить, заключается в следующем: у меня есть два демона Scala, которые я хочу запускать на нескольких узлах (в зависимости от конфигурации каждый узел может запускать один из демонов или оба). Я хочу достаточно быстрый способ развертывания модифицированных двоичных файлов в кластере. Я также не хочу тратить слишком много времени на настройку серверов. (Мои дни Gentoo давно прошли.)

Конечное решение, к которому я пришел, включает Docker , OpsWorks , Chef и Vagrant . Но, принимая вещи шаг за шагом.

Кстати – как бы вы решили вышеуказанную проблему? Пожалуйста, прокомментируйте.

Упаковка приложения Java / Scala

Сначала я должен быть в состоянии упаковать и загрузить двоичные файлы. Здесь Docker идеален. Я написал простой Dockerfile, который:

  • основан на надежном образе Ubuntu + java7 – не нужно устанавливать Java на серверах!
  • копирует фляги с моего диска в образ
  • указывает точку входа для запуска Java с скопированными банками

Полный файл Docker доступен здесь: https://gist.github.com/adamw/166b82ec04c9c0f67453 .

Имея такой образ, я могу отправить его в (общедоступный или частный) реестр Docker , где он будет доступен для загрузки узлами кластера.

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

Если вы посмотрите на Dockerfile, вы можете заметить, что есть две банки. Это сделано, чтобы минимизировать размер образа Docker, который должен быть загружен после каждого изменения кода. Первый jar содержит только зависимости (библиотека Scala, библиотеки журналов, фреймворки и т. Д.). Вторая банка содержит скомпилированный код приложения. Когда образ Docker создается из файла Docker, создается серия промежуточных образов, по одному после каждого шага. Для тех же команд, включая те же файлы, новые изображения не создаются, но изображения повторно используются из кэша Docker.

Зависимости меняются редко, поэтому, как правило, dep-jar не изменяется, и, следовательно, кэшированная версия используется повторно (и промежуточное изображение загружается один раз). С другой стороны, код приложения меняется всегда. Важно, чтобы файл jar зависимостей был сначала добавлен к изображению, чтобы промежуточное изображение содержало deps, но не код приложения (который изменяется). В конце, обычно требуется загрузка всего 2-3 МБ.

Здесь есть одна вещь, которую стоит отметить. При определении, можно ли повторно использовать изображение после команды ADD (которая копирует файл с локального диска в образ), Docker просто проверяет последнюю измененную временную метку файла. Это приведет к повторному добавлению зависимостей fat-jar каждый раз, когда он перестраивается, даже если он идентичен. Поэтому я создал простой сценарий bash, который копирует фат-jar рядом с Dockerfile (откуда они загружаются как часть контекста Docker), только если их контрольная сумма md5 изменилась: https://gist.github.com/adamw/ ba5d8b79ff553fba83fd .

Как создать такие две отдельные банки с SBT ? Достаточно просто. Просто используйте плагин SBT Assembly и измените его настройки:

1
assemblyOption in assembly ~= { _.copy(includeBin = true, includeScala = false, includeDependency = false) }

После этого цель assemblyPackageDependency создаст jar-файл только для зависимостей, а Assembly – jar-файл только для приложения.

Настройка серверов

С образом Docker, содержащим наше приложение, ожидающее в облаке (на концентраторе Docker), теперь пришло время настроить серверы, на которых демон Docker будет запускать контейнеры.

Для подготовки серверов я выбрал Chef с Amazon OpsWorks по нескольким причинам: есть возможность четко разделить и организовать экземпляры EC2 с помощью стеков и уровней, серверы поставляются с готовой интеграцией с Chef и это очень легко использовать пользовательские рецепты шеф-повара. Ручная настройка экземпляра не нужна вообще!

Приведенные ниже шаги являются частью краткого изложения, частично продолжением того, что описано в блоге ShopIgniter .

Настройка Chef (запущенная OpsWorks) будет минимальной и включает только то, что требуется для запуска Docker.

Во-первых, нам нужно создать AMI на основе Ubuntu 12.04 с обновленным ядром (14.04 еще не работает с OpsWorks) – подробности см. В блоге ShopIgniter.

Во-вторых, мы будем использовать собственные рецепты шеф-повара; для этого вам нужно создать специальный репозиторий (например, на GitHub). Рецепты довольно простые и простые: https://gist.github.com/adamw/792f8c22abb09699b6d5 .

Подводя их итог:

  • docker::setup устанавливает Docker
  • docker::kill_containers убивает и удаляет все запущенные контейнеры
  • docker::myapp извлекает образ myapp из реестра Docker и запускает контейнер с параметрами командной строки и переменными среды, как указано, например, в разделе для приложения в файле конфигурации Chef-JSON (здесь наше приложение принимает одну команду -line параметр и требует учетные данные AWS в среде):
01
02
03
04
05
06
07
08
09
10
{
  "myapp": {
    "image": "adamw/myapp:latest",
    "cmdline": [ "com.softwaremill.myapp.Main", "10" ],
    "env": {
      "AWS_ACCESS_KEY_ID": “...",
      "AWS_SECRET_ACCESS_KEY": “..."
    }
  }
}

Настройка OpsWorks

Чтобы настроить OpsWorks, нам нужно создать стек, используя пользовательскую поваренную книгу Chef и настраиваемую конфигурацию JSON, например, как указано выше (для каждого приложения / типа контейнера, который мы хотим запустить, нам нужен раздел в конфигурации JSON ). Во-вторых, для каждого приложения (контейнера), который мы хотим развернуть, нам нужно создать слой. Поскольку слои будут запускать только Docker, мы не используем какой-либо из предварительно настроенных слоев, а используем «пользовательский».

Слой будет содержать наши пользовательские рецепты: на этапе Setup нам нужно использовать рецепт docker::setup , а на этапе Deploy docker::myapp рецепты docker::kill_containers и docker::myapp .

Теперь при каждом запуске фазы Deploy на слое Docker будет извлекать изображения и запускать указанные контейнеры! Создавая слои с соответствующими рецептами, мы можем запустить любую комбинацию контейнеров на любом узле.

opsworks1-300x233

Запуск фазы развертывания

Чтобы запустить фазу Deploy одним щелчком мыши, нам нужно создать фиктивное приложение OpsWorks: просто выберите «Тип: Другое» и «Тип репозитория: Другое». Теперь, каждый раз, когда вы хотите развернуть приложение на своих серверах (запустить обновленные контейнеры Docker), просто разверните это фиктивное приложение на нужных вам экземплярах или слоях.

Это также можно сделать с помощью вызова API (как и все в AWS)! Таким образом, весь процесс создания приложения, создания образа Docker, его отправки и запуска развертывания в OpsWorks может быть довольно легко автоматизирован – например, после успешной сборки.

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

Тестирование шеф-повара на месте

Хотя рецепты шеф-повара весьма минимальны, все же полезно иметь возможность проверить их на месте. Это легко возможно с Vagrant . Используя Vagrant, мы можем легко создать виртуальную машину с установленным Chef, который запускает наши рецепты – и, в результате, контейнеры Docker. Vagrantfile для этого конкретного случая находится здесь: https://gist.github.com/adamw/bf6fa803b6b13fd7430b .

Vagrantfile содержит ссылку на рецепты Chef, которые мы разрабатываем (через chef.cookbooks_path ), и имеет ту же конфигурацию JSON, которую мы используем в OpsWorks.

После выдачи vagrant up мы запустим виртуальную машину. Изменив рецепты или загрузив новый контейнер, мы можем легко перезапустить рецепты Chef с помощью vagrant provision --provision-with chef_solo .

Подводя итоги

В итоге мы получаем следующее разделение интересов:

  • Docker – запуск приложений в изолированных контейнерах со всеми необходимыми зависимостями
  • Chef – настройка докера, запуск и связывание контейнеров с заданными параметрами / средой на определенных узлах
  • OpsWorks – управление экземплярами, запуск развертывания
  • Vagrant – локальное тестирование всей установки

Хотя, безусловно, есть вещи, которые нужно упростить в целом процессе, описанном выше (и я надеюсь, что проект Atomic сделает именно это!), В конце концов, развертывание новой версии вашего модифицированного приложения в кластере легко и без проблем, обеспечивая отличный среда разработки.