Как люди обычно конфигурируют приложения? Некоторые, например, передают аргументы через командную строку. Ещё можно задать переменные окружения или просто указать на файл с настройками. Да чего уж там, пока никто не видит, опцию-другую можно и прямо в код прошить.
Но как быть с такой ситуацией. Есть у нас распределённое приложение: куча хостов, сервисы создаются, приходят в онлайн, уходят, и все с разными версиями и непонятными адресами. И вдруг нам нужно их всех переконфигурировать. Ничего такая задачка? С монолитным приложением можно было бы просто зайти на хост и поколдовать там. А как быть с распределённым?
Раздаём конфигурацию с Consul
Consul — это такой масштабируемый и высоко-надёжный инструмент, который делает жизнь разработчика распределённых приложений легче по многим фронтам. Он поможет сервисам найти друг друга, проверит, отзываются ли они на запросы, поработает DNS сервисом и даже предоставит хранилище типа ключ-значение всем желающим. А такое хранилище — очень удобное место для хранения разного рода настроек. К тому же, в комплекте с Consul можно получить набор утилит, которые умеют синхронизировать это хранилище и с локальными конфигурационными файлами, и с переменными окружения. На лету и без SMS. То есть мы можем привязать конфигурацию отдельных сервисов к Consul-агенту, и раздавать им настройки из общего хранилища.
Установка
Consul работает на всех основных платформах и скачивается в виде архива из одного-единственного экзешника. Даже устанавливать ничего не надо. Образ для Docker тоже имеется. После скачивания и распаковки можно запустить его с такими параметрами:
1 2 3 4 |
./consul agent -dev #==> Starting Consul agent... #==> Starting Consul agent RPC... #==> Consul agent running! |
Это запустит агент Consul на локальной машие в режиме сервера. -dev
флаг укажет ему работать в тестовом режиме, в котором Consul не оставит после себя никаких следов.
У Consul-агента есть и вэб-UI, который можно найти на порту 8500:
Хранилище ключ-значение
Как я уже упомянул, одна из фич консула — это хранилище вида ключ-значение (key-value store). Ключи — обычные строки — можно упорядочить в иерархию, как в файловой системе, просто разделяя компоненты пути косой чертой. Например, db/config/max-connections
:
Манипуляция данными
Создавать, читать и удалять данные можно как через вэб-UI, так и через RESTful API, утилиту consul kv
, или многочисленные API клиенты.
Чтобы прочитать одно значение через HTTP, нужно сделать обычный GET запрос по адресу /v1/kv
+ ключ:
1 2 3 4 5 6 7 8 9 10 11 |
curl http://localhost:8500/v1/kv/db/config/max-connections #[ # { # "LockIndex": 0, # "Key": "db/config/max-connections", # "Flags": 0, # "Value": "NTAwMA==", # "CreateIndex": 5007, # "ModifyIndex": 5007 # } #] |
В ответном JSON значение будет закодировано в Base64, но оно точно такое же, что и в UI. Я проверял.
1 2 |
echo 'NTAwMA==' | base64 --decode #5000 |
Простым GET запросом можно получить вообще всё:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
curl http://localhost:8500/v1/kv/?recurse #[ # { # "LockIndex": 0, # "Key": "db/", # "Flags": 0, # "Value": null, # "CreateIndex": 5003, # "ModifyIndex": 5003 # }, # { # "Key": "db/config/", # "Value": null, # ... # }, # { # "Key": "db/config/max-connections", # "Value": "NTAwMA==", # ... # }, # { # "Key": "db/config/timeout", # "Value": "NTAwMA==", # ... # } #] |
Для создания новой записи нужен уже PUT запрос:
1 2 |
curl -X PUT http://localhost:8500/v1/kv/web/config/experimental -d 'enabled' #true |
Команда выше создаст пару ключ-значение, в которой ключ — web/config/experimental
, а значение — enabled
.
PUT запрос отвечает и за редактирование. Предсказуемо, удалить что-нибудь можно через HTTP DELETE.
Если к HTTP душа не лежит, можно манипулировать значениями через consul kv
. Там и синтаксис лаконичнее, и Base64 нет:
1 2 3 4 5 6 7 |
./consul kv get db/config/timeout #5000 ./consul kv get -recurse #db/: #db/config/: #db/config/max-connections:5000 #db/config/timeout:5000 |
Long polling для получения обновлений
Допустим, решили мы сделать приложение, которые получает свои настройки через HTTP из Consul. Если настройки могут меняться, а приложение способно применить их без перезагрузки, то стоит делать такие запросы регулярно, чтобы не пропустить обновления. Но насколько регулярно их стоит делать? Всё-таки выбор правильного интервала — та ещё задача. Слишком частые запросы могут отправить сервер в страну вечной охоты, особенно если клиентов много. Слишком редкие могут сильно опоздать с новостями.
Но есть и другой способ — long polling. Мы делаем серверу настроек запрос на обновлённое значение, а он ответит на него только тогда, когда значение действительно изменится.
Как это сделать. Отправим-ка ещё один запрос в Consul, но в этот раз посмотрим, какие HTTP заголовки придут нам в ответе.
1 2 3 4 5 6 7 8 |
curl -v http://localhost:8500/v1/kv/db/config/max-connections #> GET /v1/kv/db/config/max-connections HTTP/1.1 #> ... #< HTTP/1.1 200 OK #< Content-Type: application/json #< X-Consul-Index: 5007 #< X-Consul-Knownleader: true #< ... |
Вместе со стандартными заголовками Consul прислал несколько собственных, в том числе и X-Consul-Index
. Если значение этого заголовка указать в запросе (например GET ../config/max-connections?index=5007
), то сервер ответит тогда, когда запрашиваемое значение изменится, либо произойдёт таймаут. По-умолчанию, это пять минут, но можно передать и своё значение через URL в wait=
параметре. Максимальное значение таймаута — 10 минут.
1 2 |
curl http://localhost:8500/v1/kv/db/config/max-connections?index=5007 #...wait for up to 10m |
С long polling можно свести количество запросов к минимуму, и при этом получать обновления практически в реальном времени.
Обновляем конфигурационные файлы напрямую из Consul
Допустим, у нас есть какой-нибудь config.json
с такими настройками внутри:
1 2 3 4 |
{ "max-connections": 5000, "timeout": 5000 } |
Удивительное совпадение, но в Consul key-value хранилище были точно такие же ключи. Вот было бы здорово, если бы этот файл генерировался напрямую оттуда. Оказывается, это можно очень легко сделать при помощи consul-template
.
consul-template — это ещё одна скачиваемая утилита от того же разработчика. Она умеет делать классную штуку — получать на вход шаблон файла настроек и выдавать на выходе его заполненную версию. К тому же её можно запустить в цикле, и тогда она будет тихо ждать где-нибудь в фоне, и как только в KV хранилище что-то изменится, тут же обновит файл настроек. Практически без задержки.
Простые подстановки
Возвращаясь назад к config.json
файлу, создадим-ка шаблон для него, например, config.tpl
, и положим в него следующее:
1 2 3 4 5 |
cat config.tpl #{ # "max-connections": {{ key "db/config/max-connections" }}, # "timeout": {{ key "db/config/timeout" }}, #} |
То есть это практически такой же config.json
, но значения в нём заменены на плейсхолдеры.
Теперь, если натравить на него consul-template
:
1 2 3 4 5 6 |
./consul-template -template "config.tpl:config.json" -once cat config.json #{ # "max-connections": 5001, # "timeout": 5000, #} |
Получится актуальный файл настроек. Сильное колдунство: шаблонизатор подключился к локальному Consul-агенту, выяснил текущие значения настроек, создал новый файл, и завершился. Кстати, подключиться он мог и к удалённому Consul серверу.
Если убрать -once
параметр, то consul-template
останется работать в фоне и поддерживать файл настроек в актуальном состоянии. Просто и при этом гениально. Можно запускать сервисы пачками, конфигурировать всех одновременно, и даже не знать, сколько их там всего.
Сложные подстановки
Но кроме подстановки значений consul-template
может делать задачи и посложнее. Например, что если мы хотим создать файл настроек, в котором лежат все параметры, найденные в KV?
Можно использовать функцию ls
, чтобы получить список всех ключей (например, внутри db/config/
) и затем просто пропустить их через цикл:
1 2 3 4 |
{ {{ range ls "db/config" }} "{{ .Key }}": {{ .Value }},{{ end }} } |
Получится практически тот же самый config.json
, но с лишней запятой на последней строке. Её можно было бы убрать использовав {{ if }}
, но для JavaScript лишняя запятая не проблема, так что зачем усложнять пример.
Кроме того, что consul-template
рендерит файлы настроек, его ещё можно попросить и запускать сервис, которому тот был предназначен. И не только запускать, но и отправлять сигнал на перезапуск, если настройки изменились. Сигнал, кстати, можно подстроить под конкретный сервис.
Итого
Конфигурация коллекции сервисов, которые живут где-то в облаке, — весьма нетривиальная задача. То есть задать начальную конфигурацию, конечно, не проблема, но изменить её на лету сразу в нескольких местах — та ещё задача. А Consul и consul-template могут её решить. Можно использовать Consul Key-Value хранилище как репозиторий настроек и научить сервисы их запрашивать. Или вообще можно попросить consul-template делать это за нас, и тогда всё облако можно будет централизованно реконфигурировать практически без задержки, не меняя при этом ни строчки существующего кода.