Как люди обычно конфигурируют приложения? Некоторые, например, передают аргументы через командную строку. Ещё можно задать переменные окружения или просто указать на файл с настройками. Да чего уж там, пока никто не видит, опцию-другую можно и прямо в код прошить.
Но как быть с такой ситуацией. Есть у нас распределённое приложение: куча хостов, сервисы создаются, приходят в онлайн, уходят, и все с разными версиями и непонятными адресами. И вдруг нам нужно их всех переконфигурировать. Ничего такая задачка? С монолитным приложением можно было бы просто зайти на хост и поколдовать там. А как быть с распределённым?
Раздаём конфигурацию с 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 делать это за нас, и тогда всё облако можно будет централизованно реконфигурировать практически без задержки, не меняя при этом ни строчки существующего кода.
				

