Большинство приложений, особенно те, которые имеют дело с внешним миром, нуждаются в некоторой настройке для правильной работы. Поскольку он может работать в разных средах, невозможно предоставить их во время компиляции для обеспечения безопасности типов, но мы можем использовать там библиотеки, такие как typematcher.
В этом посте я расскажу, как настроить приложение nodejs безопасным для типов образом и запустить его как службу docker swarm.
Приложение и конфигурация
В качестве демонстрационного приложения для этого примера я собираюсь использовать это исходное репо, которое я лично использую для своих новых проектов, написанных на TypeScript.
Конфигурация загружается пакетом config, и typematcher сопоставляет конфигурацию с предопределенной структурой и типами.
Предположим, у нас есть компонент, для которого требуются некоторые параметры конфигурации, и мы определили их как:
export type UsersServiceConfig = { title: string, enabled: boolean, retries: number };
Нашему приложению, вероятно, потребуется больше таких конфигураций, чтобы мы могли объединить их в один тип конфигурации приложения:
import {UsersServiceConfig} from "./routes/users"; // this type will contain configs for all our internal components/services type ServicesConfig = { users: UsersServiceConfig }; // this is the root node of application config type Config = { services: ServicesConfig };
Далее нам нужно будет определить наши правила проверки для типов конфигурации:
import { match, isString, isNumber, isBoolean, caseId, hasFields, caseThrow, failWith } from 'typematcher'; // this will check a value to conform to UsersServiceConfig structure const isUserServiceConfig = hasFields({ title: failWith( new Error('Invalid services.users.title configuration option: string expected') )(isString), enabled: failWith( new Error('Invalid services.users.enabled configuration option: boolean expected') )(isBoolean), retries: failWith( new Error('Invalid services.users.retries configuration option: number expected') )(isNumber) }); const isServicesConfig = hasFields({ users: failWith('Invalid services.users config')(isUserServiceConfig) });
И, наконец, загрузите конфиг и экспортируйте его своим пользователям:
import * as config from 'config'; // Unfortunately there is no way yet to get full config using config package, ex: `config.get('.')` or `config.get()` // So will build final config from parts, getting one by one const conf: Config = { services: match(config.get("services"))( caseId(isServicesConfig), caseThrow(new Error('One or more services configs are invalid')) ) }; export default conf;
Если загруженная конфигурация не будет соответствовать определенным требованиям, она выдаст исключение.
Исходный файл: https://github.com/lostintime/express-ts-seed/blob/master/src/config.ts
А где сам конфиг?
Пакетconfig
использует переменные среды и правила порядка загрузки файлов, которые определяют поведение загрузки конфигурации.
По умолчанию файлы конфигов находятся в папке ./config
относительно корня проекта. Это может быть перезаписано NODE_CONFIG_DIR
переменной среды.
Правила порядка загрузки файлов определяются с использованием переменных среды NODE_ENV
, HOSTNAME
и NODE_APP_INSTANCE
, которые мы можем использовать для настройки приложения с использованием секретов докеров.
Запуск приложения в роевом кластере
Чтобы запустить режим роя в установке докера, запустите docker swarm init
. Для этого примера достаточно кластера роя с одним узлом.
Изображение приложения
Это приложение упаковывается в образ докера с использованием этого скрипта и публикуется в хабе докеров: lostintime / express-ts-seed
Подготовка конфигураций
Обычно я сохраняю файл конфигурации по умолчанию в каталоге config/
(config/default.json
) и фиксирую его в git (он не содержит никакой секретной информации, только определяет структуру), и есть еще 1 файл для каждой среды, например: development.json
, production.json
, в том же папка, игнорируемая .gitignore
и .dockerignore
, но любая другая папка, которая вам подходит - просто идеальна.
НЕ фиксируйте файлы конфигурации в git и НЕ объединяйте их с образами докеров, это плохая практика!
Для этой демонстрации я назову службу express-ts-seed
(то же самое может использоваться как HOSTNAME
) и, следуя правилам config
порядок загрузки файлов, подготовит 2 файла конфигурации:
Файл: express-ts-seed.json
{ "services": { "users": { "title": "Users Service", "enabled": false, "retries": 3, "other": 10 } } }
Файл: express-ts-seed-production.json
{ "services": { "users": { "title": "Users Service, this is overwritten in {deployment} config files", "anotherOne": "This is also added in {deployment} file", "filename": "express-ts-seed-production.json" } } }
Создание секретов докеров
$ docker secret create express-ts-seed.json express-ts-seed.json p7ebq3gfgjyeg8rc4rvrvf6uw $ docker secret create express-ts-seed-production.json express-ts-seed-production.json 9r4n47k4pn8eukyhqv793f7d3
Давайте проверим доступные секреты:
$ docker secret ls ID NAME CREATED UPDATED 9r4n47k4pn8eukyhqv793f7d3 express-ts-seed-production.json 26 seconds ago 26 seconds ago p7ebq3gfgjyeg8rc4rvrvf6uw express-ts-seed.json 53 seconds ago 53 seconds ago
Имена файлов (и секретные имена) выбираются таким образом, чтобы избежать конфликтов с другими службами, работающими в том же кластере роя, а также с использованием секретов докеров.
Создание службы докеров
Теперь мы готовы запустить наше приложение:
$ docker service create \
--name "express-ts-seed" \
--hostname "express-ts-seed" \
--env "NODE_ENV=production" \
--env "NODE_CONFIG_DIR=/run/secrets" \
--secret "source=express-ts-seed.json,target=default.json" \
--secret "source=express-ts-seed-production.json,target=production.json" \
--endpoint-mode "vip" \
--mode "replicated" \
--replicas 3 \
--update-parallelism 1 \
--update-delay 1s \
--stop-grace-period 5s \
--restart-condition "any" \
--restart-delay 10s \
--restart-max-attempts 1 \
--publish "3000:3000" \
--detach=false \
lostintime/express-ts-seed:0.2.1
pxxsnvbhmx45cuily0cylgic6
overall progress: 3 out of 3 tasks
1/3: running [==================================================>]
2/3: running [==================================================>]
3/3: running [==================================================>]
verify: Service converged
И сошлось! Что бы это ни значило :). Http: // localhost: 3000 /.
Используя настраиваемое имя target
для аргумента --secret
- наши секреты были смонтированы в /run/secrets/default.json
и /run/secrets/prduction.json
.
Установив переменные среды NODE_CONFIG_DIR=/run/secrets
и NODE_ENV=production
- мы настроили пакет config на использование /run/secrets
для поиска файлов конфигурации, и совпали 2 правила порядка загрузки файлов:
...
default.EXT
...
{deployment}.EXT
...
Это приложение предоставляет конечную точку http, которая возвращает часть своей конфигурации, которую вы, надеюсь, НИКОГДА не будете делать в своем производственном приложении: http: // localhost: 3000 / users / config.
Здесь вы можете увидеть объединенное содержимое express-ts-seed-production.json
и express-ts-seed.json
secrets. Вы можете просто использовать один файл, содержащий полную конфигурацию, но это зависит от вас.
Изменение конфигурации
А как насчет более поздних изменений конфигурации? Ну, вы не можете просто обновить или воссоздать секреты, потому что:
- секреты докеров не могут быть обновлены (что на самом деле хорошо и идеально соответствует целям неизменяемой инфраструктуры)
- секреты докеров не могут быть удалены, пока служба их использует, что опять же не так уж и плохо
Итак, как же тогда мы будем обновлять конфигурацию нашего приложения?
Создание новых секретов
У каждого секрета должно быть свое (уникальное) имя, но, к счастью, мы можем установить собственное target
секретное имя, поэтому мы собираемся монтировать разные секреты по одним и тем же путям:
$ docker secret create express-ts-seed-production-0.json express-ts-seed-production.json ys88p2d8o3biicivm58ksx3wx $ docker secret ls ID NAME CREATED UPDATED 9r4n47k4pn8eukyhqv793f7d3 express-ts-seed-production.json 27 minutes ago 27 minutes ago p7ebq3gfgjyeg8rc4rvrvf6uw express-ts-seed.json 28 minutes ago 28 minutes ago ys88p2d8o3biicivm58ksx3wx express-ts-seed-production-0.json 15 seconds ago 15 seconds ago
Обновление сервиса
Обновление env также перезапустит служебные узлы, и если новая конфигурация несовместима со старым приложением, вам, возможно, придется обновить образ службы в той же команде.
$ docker service update \
--secret-rm "express-ts-seed-production.json" \
--secret-add "source=express-ts-seed-production-0.json,target=production.json" \
--detach=false \
express-ts-seed
express-ts-seed
overall progress: 3 out of 3 tasks
1/3: running [==================================================>]
2/3: running [==================================================>]
3/3: running [==================================================>]
verify: Service converged
Удаление старых секретов
$ docker secret rm express-ts-seed-production.json express-ts-seed-production.json
Использование файла для создания
Развертывание, которое мы создали выше, можно описать в файле компоновки и развернуть как стек докеров.
Составить файл (./docker-compose.yml
):
version: '3.4' services: express-ts-seed: image: lostintime/express-ts-seed:0.2.1 hostname: "express-ts-seed" ports: - "3000:3000" secrets: - source: default_v1.json target: default.json - source: production_v1.json target: production.json environment: - "NODE_ENV=production" - "NODE_CONFIG_DIR=/run/secrets" deploy: mode: replicated replicas: 3 update_config: parallelism: 1 delay: 1s restart_policy: condition: any delay: 10s max_attempts: 1 window: 3m stop_grace_period: 5s networks: internal: driver: overlay secrets: default_v1.json: file: ./express-ts-seed.json production_v1.json: file: ./express-ts-seed-production.json
Развертывать:
$ docker stack deploy --compose-file "./docker-compose.yml" --prune express_ts_seed
При изменении файлов конфигурации - измените имя секретов в docker-compose.yml
файле, например: default_v1.json
на default_v2.json
, измените секретные ссылки в разделе службы secrets
и повторно разверните стек.
$ docker stack deploy --compose-file "./docker-compose.yml" --prune express_ts_seed
Затем вы можете удалить старый секрет:
$ docker secret rm express_ts_seed_default_v1.json
Вывод
Используя пакеты npm typematcher и config, мы можем настроить наши приложения безопасным для типов образом и подключать конфигурации с помощью секретов при развертывании приложений в docker swarm.
Тот же эффект может быть достигнут с кубернетами, поскольку секреты k8s монтируются как тома, и вы также можете выбрать расположение файлов конфигурации.
Как менее безопасный, но более гибкий вариант - аналогично можно использовать конфиги докеров:
Конфигурации работают аналогично секретам, за исключением того, что они не шифруются в состоянии покоя и монтируются непосредственно в файловую систему контейнера без использования RAM-дисков. Конфигурации могут быть добавлены или удалены из службы в любое время, и службы могут совместно использовать конфигурацию. Для максимальной гибкости вы даже можете использовать конфигурации вместе с переменными среды или метками.
Некоторые вопросы
Некоторые проблемы было бы неплохо решить:
- Нет возможности получить полную конфигурацию, например. с использованием
config.get('.')
илиconfig.get()
; - Невозможно напрямую указать настраиваемый файл конфигурации, например:
NODE_CONFIG_FILE=/path/to/file
, ноNODE_CONFIG_DIR
работал.
Ссылки
- Демо-приложение Источники
- Приложение образ докера
config
пакет npmtypematcher
пакет npm- Библиотека сопоставления шаблонов для машинописного текста сообщение в блоге
docker secret
командные документы
Первоначально размещено здесь: https://lostintimedev.com/2017/10/16/type-safe-app-config-with-docker-secrets.html