От переводчика: Это седьмая статья из цикла о Node.js от команды Mozilla Identity, которая занимается проектом Persona.
- "Охотимся за утечками памяти в Node.js"
- "Нагружаем Node под завязку"
- "Храним сессии на клиенте, чтобы упростить масштабирование приложения"
- "Производительность фронтэнда. Часть 1 — конкатенация, компрессия, кэширование"
- "Пишем сервер, который не падает под нагрузкой"
- "Производительность фронтэнда. Часть 2 — кешируем динамический контент с помощью etagify"
В этой статье из цикла о Node.js мы рассмотрим модуль node-convict, который помогает управлять конфигурациями приложений Node.js. Он предоставляет прозрачные настройки по умолчанию и встроенную типизацию, чтобы было легче находить и исправлять ошибки.
Постановка задачи
Есть две основные проблемы, которые создают необходимость в конфигурации приложений:
- Большинство приложений могут работать в нескольких окружениях, имеющих разные параметры конфигурации.
- Включение учётных данных и другой конфиденциально информации в код приложения может создавать проблемы.
Эти проблемы можно решить, инициализируя некоторые переменные в зависимости от текущего окружения и используя переменные окружения для хранения конфиденциальных данных. Общепринятый в среде Node.js шаблон для реализации этого подхода состоит в создании модуля, который экспортирует конфигурацию:
var conf = {
// окружение приложения -
// "production", "development", или "test
env: process.env.NODE_ENV || "development",
// IP адрес
ip: process.env.IP_ADDRESS || "127.0.0.1",
// Порт
port: process.env.PORT || 0,
// Настройки БД
database: {
host: process.env.DB_HOST || "localhost:8091"
}
};
module.exports = conf;
Это работает неплохо, но есть ещё пара проблем:
- Что если в конфигурации указаны некорректные данные? Мы можем сберечь время и нервы, обнаруживая ошибки как можно раньше.
- Насколько легко разобраться в конфигурации администраторам, тестировщикам и другим членам большой команды, когда им надо менять настройки или искать дефекты? Более декларативный и лучше документированный формат сделал бы их жизнь легче.
Представляем convict
node-convict решает обе эти проблемы, предоставляя схему конфигурации, в которой можно задавать информацию о типах, значения по умолчанию, переменные окружения и документацию для каждой из настроек.
С использованием convict пример выше принимает такой вид:
var conf = convict({
env: {
doc: "The applicaton environment.",
format: ["production", "development", "test"],
default: "development",
env: "NODE_ENV"
},
ip: {
doc: "The IP address to bind.",
format: "ipaddress",
default: "127.0.0.1",
env: "IP_ADDRESS"
},
port: {
doc: "The port to bind.",
format: "port",
default: 0,
env: "PORT"
},
database: {
host: {
default: "localhost:8091",
env: "DB_HOST"
}
}
});
conf.validate();
module.exports = conf;
Здесь содержится практически та же самая информация, но представленная в виде схемы. Благодаря этому нам удобнее экспортировать её и отображать в удобочитаемом виде, делать валидацию. Декларативный формат делает приложение более надёжным и более дружественным ко всем членам команды.
Как устроена схема
Для каждого параметра настройки есть четыре свойства, каждое из которых помогает сделать приложение надёжнее и проще для понимания:
- Тип. В свойстве
format
указывается или один из встроенных в convict типов (ipaddress
,port
,int
и т.д.) или функция для валидации пользовательских типов. Если во время валидации параметр не проходит проверку типа, возникает ошибка. - Значения по умолчанию. Каждый параметр должен иметь значение по умолчанию.
- Переменные окружения. Если переменная, указанная в
env,
установлена, то её значение будет использовано вместо значения по умолчанию. - Документация. Свойство
doc
вполне очевидно. Преимущество включения документации в схему перед комментариями в коде состоит в том, что эту информация используется в методеconf.toSchemaString()
для более информативного вывода.
Дополнительные уровни конфигурации
Над фундаментом из значений по умолчанию можно надстраивать дополнительные уровни конфигурации с помощью вызовов conf.load()
и conf.loadFile()
. Например, можно загружать дополнительные параметры из объекта JavaScript для конкретного окружения:
var conf = convict({
// схема та же, что и в предыдущем примере
});
if (conf.get('env') === 'production') {
// в боевом окружении используем другой порт и сервер БД
conf.load({
port: 8080,
database: {
host: "ec2-117-21-174-242.compute-1.amazonaws.com:8091"
}
});
}
conf.validate();
module.exports = conf;
Или же можно создать отдельные конфигурационные файлы для каждого из окружений, и загружать их с помощью conf.loadFile()
:
conf.loadFile('./config/' + conf.get('env') + '.json');
loadFile()
также может загружать несколько файлов сразу, если передать массив аргументов:
// CONFIG_FILES=/path/to/production.json,/path/to/secrets.json,/path/to/sitespecific.json
conf.loadFile(process.env.CONFIG_FILES.split(','));
Загружать дополнительные параметры через load()
и loadFile()
полезно, когда есть настройки для каждого из окружений, которые не стоит устанавливать в переменных окружения. Отдельные декларативные конфигурационные файлы в формате JSON позволяют нагляднее представить различия между параметрами в разных окружениях. А так как файлы загружаются с помощью cjson, они могут содержать комментарии, что делает их ещё более понятными.
Обратите внимание, что переменные окружения имеют наивысший приоритет, выше, чем настройки по умолчанию и настройки, загруженные через load()
и loadFile()
. Чтобы проверить, какие именно настройки действуют, можно вызвать conf.toString()
.
«V» — значит валидация
После того, как настройки загружены, можно запустить валидацию, чтобы проверить, все ли они имеют правильный формат в соответствии со схемой. В convict есть несколько встроенных форматов, таких как url
, ports
или ipaddress
, кроме того можно использовать встроенные конструкторы JavaScript (например Number
). Если свойство format
не задано, convict проверит тип параметра на совпадение с типом значения по умолчанию (вызвав Object.prototype.toString.call). Приведённые ниже три схемы эквивалентны:
var conf1 = convict({
name: {
format: String
default: 'Brendan'
}
});
// если формат не указан, предполагаем, что тип должен быть
// такой же, как у значения по умолчанию
var conf2 = convict({
name: {
default: 'Brendan'
}
});
// более лаконичная версия
var conf3 = convict({
name: 'Brendan'
});
Формат можно задать и в виде перечисления, в явном виде задав перечень допустимых значений, например ["production", "development", "test"]
. Любое значение, которого нет в списке, не пройдёт валидацию.
Вместо встроенных типов, можно использовать собственные валидаторы. К примеру, мы хотим, чтобы параметр был строкой из 64 шестнадцатеричных цифр:
var check = require('validator').check;
var conf = convict({
key: {
doc: "API key",
format: function (val) {
check(val, 'should be a 64 character hex key').regex(/^[a-fA-F0-9]{64}$/);
},
default: '3cec609c9bc601c047af917a544645c50caf8cd606806b4e0a23312441014deb'
}
});
Вызов conf.validate()
возвратит детальную информацию о каждой ошибочной настройке, если такие есть. Это помогает избежать повторного развёртывания приложения при обнаружении каждой ошибки в конфигурации. Вот как будет выглядеть сообщение об ошибке, если мы попытаемся присвоить параметру key
из предыдущего примера значение 'foo'
:
conf.set('key', 'foo');
conf.validate();
// Error: key: should be a 64 character hex key: value was "foo"
Заключение
node-convict расширяет стандартный шаблон конфигурирования приложений Node.js, делая его более надёжным и удобным для членов команды, которым не придётся разбираться в дебрях императивного кода, чтобы проверять или изменять настройки. Схема конфигурации даёт команде проекта больше контекста для каждой настройки и позволяет делать валидацю для раннего обнаружения ошибок в конфигурации.
Продолжение следует...
Автор: ilya42