Большинство приложений, особенно те, которые имеют дело с внешним миром, нуждаются в некоторой настройке для правильной работы. Поскольку он может работать в разных средах, невозможно предоставить их во время компиляции для обеспечения безопасности типов, но мы можем использовать там библиотеки, такие как 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.jsonsecrets. Вы можете просто использовать один файл, содержащий полную конфигурацию, но это зависит от вас.

Изменение конфигурации

А как насчет более поздних изменений конфигурации? Ну, вы не можете просто обновить или воссоздать секреты, потому что:

  • секреты докеров не могут быть обновлены (что на самом деле хорошо и идеально соответствует целям неизменяемой инфраструктуры)
  • секреты докеров не могут быть удалены, пока служба их использует, что опять же не так уж и плохо

Итак, как же тогда мы будем обновлять конфигурацию нашего приложения?

Создание новых секретов

У каждого секрета должно быть свое (уникальное) имя, но, к счастью, мы можем установить собственное 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 работал.

Ссылки

Первоначально размещено здесь: https://lostintimedev.com/2017/10/16/type-safe-app-config-with-docker-secrets.html