Деплой Elixir при помощи Docker. Часть 2
В предыдущей статье мы рассмотрели процесс упаковки Elixir-приложения в Docker-образ. Один из недостатков использования Docker для Elixir-приложений заключается в том, что из-за наличия у контейнеров собственной сети, две Elixir-ноды, находящиеся в двух разных контейнерах, не могут подключаться друг к другу, даже если запускать их на одном и том же физическом сервере.
Более того, деплой контейнеров проводится на платформе Rancher, которая распределяет их между несколькими физическими машинами. По этой причине отобразить порты Docker-контейнера на сервер невозможно, ведь на каждом сервере несколько контейнеров могут запускать одно и то же приложение. Однако в Rancher имеется встроенный DNS-сервис для автоматического поиска новых нод и подключения к ним.
Как работает Rancher
Основой Rancher являются сервисы. Каждый сервис представляет собой Docker-образ с соответствующими конфигурациями (ENV, command, volumes и др.) и способен запускать несколько одинаковых контейнеров на нескольких машинах.
Для осуществления маршрутизации между этими контейнерами в Rancher существует своя оверлейная сеть и DNS-протокол обнаружения сервисов.
Например, у нас есть сервис «my-hello-service» с тремя контейнерами, запускающий функцию nslookup
в одном из контейнеров:
Поскольку все контейнеры одного сервиса могут связываться друг с другом посредством этой оверлейной сети, нужно лишь сделать так, чтобы каждая нода «видела» все другие ноды.
Динамические адреса и имена
Все вышеуказанные IP-адреса являются динамическими, а значит, срок их жизни равен сроку жизни контейнера. Адреса изменятся если перезапустить/обновить контейнер или провести масштабирование (добавить контейнер в сервис или удалить его). В связи с этим использовать статическую конфигурацию файла из sys.config
(подробнее об этом здесь) уже не получится.
Так что познакомим наше приложение с Rancher DNS и постараемся извлечь из этого максимум пользы.
Настройка ноды Elixir
Прежде чем перейти к обнаружению нод, сделаем так, чтобы Elixir-ноды смогли «видеть» друг друга. При использовании модуля mix_docker (точнее distillery) ноде по умолчанию даётся имя appname@127.0.0.1
. Но, если нужно подключиться к другим нодам сети 10.42.x.x
, это имя необходимо будет изменить. Кроме того, оно должно динамически создаваться внутри контейнера при его запуске (только в этом случае можно быть уверенным, что сеть Rancher перекроет IP-адрес).
Я достаточно долго размышлял на тему того, как это можно сделать, и, наконец, нашёл решение. В двух словах, нужно сделать так, чтобы vm.args
стали доступны переменные среды, а затем поместить эти переменные в контейнер.
ПРИМЕЧАНИЕ: прежде чем читать дальше, рекомендую сначала ознакомиться с предыдущей статьёй об Elixir и Docker.
Для начала укажем в модуле distillery путь к своему vm.args
файлу:
А сам файл vm.args
будет выглядеть так:
Синтаксис ${RANCHER_IP}
и конфигурация REPLACE_OS_VARS=true
модуля distillery позволяют задавать имя ноды динамически на основе переменной среды RANCHER_IP
.
Далее, воспользуемся встроенным в Rancher Metadata API, чтобы создать переменную RANCHER_IP
. Поместим следующий код в скрипт rel/rancher_boot.sh
, который будет выступать в качестве точки входа контейнера.
И напоследок, немного изменим Docker-файл релиза: будем использовать свои настройки вместо стандартных значений из mix_docker.
Построив образ и обновив сервис Rancher, проверим, могут ли контейнеры подключаться друг к другу. Для этого подключимся к удалённой машине с помощью SSH и запустим docker exec
:
Как видно из подсказки iex, ноде присвоен IP оверлейной сети Rancher. Теперь запустим ещё один контейнер для того же сервиса и получим его IP (в той же сети 10.42.x.x
). Чтобы убедиться, что ноды могут «видеть» друг друга, попробуйте подключить одну ноду к другой через Node.connect
. Предположим, что IP-адрес второго контейнера — 10.42.240.66
:
Для второй ноды:
Теперь в Elixir/Erlang можно осуществлять распределения с помощью модуля Node
.
Автообнаружение на основе DNS
Мы рассмотрели, как установить связь между двумя нодами Elixir, но для этого пришлось вводить IP-адрес второй ноды вручную. Такой способ определённо неуместен в продакшне, поэтому попробуем сделать это через DNS.
Эквивалентом nslookup в Elixir выступает функция :inet.gethostbyname
, а если быть точнее, то :inet_tcp.getaddrs(name)
. Вызвав эту функцию в одном из контейнеров, получим список всех IP-адресов контейнеров сервиса:
Теперь осталось только создать на каждой ноде erlang-процесс, который бы периодически вызывал эту функцию и подключался бы к другим нодам. Достаточно создать простой GenServer, который будет проверять DNS каждые 5 секунд.
Подключим его в дерево супервизоров приложения:
Ноды будут автоматически удаляться из списка сразу после отключения. Node.connect
предотвращает двойное подключение к одной и той же ноде, поэтому проверять Node.list
перед подключением не обязательно.
Ещё одна возможность, которая упростит вам жизнь, — это динамическое именование сервисов Rancher.
Как видно из примера выше, имя сервиса (а значит, и DNS-сервера) взято из ключа :rancher_service_name
конфигурации приложения. В config/prod.exs
можно сделать это имя статическим, но гораздо лучше оставить всё как есть.
Поместим следующий код в config/prod.exs:
А rel/rancher_boot.sh
приведём в соответствие со следующим:
Здесь для получения имени текущего сервиса снова используется встроенный в Rancher сервис метаданных. Такой подход позволяет использовать один и тот же образ-контейнер повторно для различных сервисов (с различными конфигурациями среды выполнения).
Победа!
Посмотреть, как в реальности работает то, что у нас получилось, можно на видео по ссылке. С левой стороны представлен живой лог одного из контейнеров, а с правой — список контейнеров, запущенных сервисом «my-hello-service». При добавлении и удалении запущенных сервисом контейнеров можно видеть, как в логе контейнера справа учитываются все изменения и обновляется список подключённых нод.