Статья из серии "2х минутные заметки разработчика".


Облака повсюду. Пожалуй так стоит начать эту заметку. Как только пользователь открывает браузер, он уже начинает использовать облачные технологии. Браузер делает запросы на свои серверы, подтягивает нужные данные, и пользователь испытывает тот же опыт что и раньше, несмотря на то, что уже 10 раз менял свое устройство, будь то компьютер, смартфон, смарт-ТВ или холодильник. Браузер распознал пользователя, и пользователь видит знакомый интерфейс.

Облака все больше проникают в повседневную жизнь пользователя. Поэтому все больше разработчиков берут облака в свой арсенал инструментов, обустраивая эту самую жизнь пользователя.

Проблема

Одной из популярных задач, которую приходится решать разработчикам, является динамическая настройка того, что должен видеть пользователь в тот или иной момент. Иногда вам нужно изменить содержимое сайта или веб-приложения без физического изменения самого кода. А чтобы это можно было сделать, код изначально должен поддерживать возможность динамической загрузки разного контента.

Самый распространенный случай, когда необходимо динамически менять контент, — это AB-тестирование. В код вводится условие, что если определенный флаг (называемый фиче-флагом) установлен в значение true, то пользователь видит один сценарий, иначе другой.

Более того, один из сценариев может быть не совсем рабочим, и тогда нужно быстро запретить его видеть, а значит, это может быть опасно, так как пользователи могут видеть нерабочий код в течение длительного времени, потому что нужно собирать проект заново, и доставлять сборку в продакшен каждый раз, когда что-то ломается.

Удаленное переключение фиче-флага решает эту проблему.

Делаем конфиг сервер

Конфиг сервер - это как раз таки тот переключатель, который позволяет обновить сценарий пользователя без ре-деплоя приложения.

Как бы выглядела наивная реализация этого паттерна?

Вероятно, вам нужно иметь базу данных для хранения динамических конфигов, поэтому вам нужен бэкенд-сервер, который поможет вам манипулировать этими конфигами + UI-сервис для удобной работы.

Помимо трех сервисов, нужно думать еще и об авторизации, так как просто так конфиги не изменить, а также об управлении нагрузкой, так как запросы к конфиг-серверу практически дублируют запросы к самому приложению.

Результат - много работы.

Более удачный способ

Давайте попробуем использовать облака из введения, чтобы найти лучшую реализацию.

В итоге имеем - БД и сервер заменены на две лямбда-функции. Писать и читать конфиг. Остальная работа ложится на плечи AWS.

UI сервис переезжает, например, на github или любую другую систему контроля версий. Более того, в этом случае мы также делегируем авторизацию и управление доступом github и снимаем с себя большой пласт работы.

Давайте подробнее рассмотрим этот подход.

Начнем с развертывания API Gateway. Вы можете использовать шаблон, который я подготовил для таких целей. Шаблон написан на serverless фреймворке и позволяет одной командой развернуть простой API Gateway + DynamoDB.

Далее нам нужно его немного изменить. Так как наш Gateway будет работать в обе стороны (на запись и чтение), нам нужно правильно настроить разрешения, чтобы конечное приложение, использующее конфиг, могло его только читать, но никак не могло перезаписывать. Для этих целей мы будем использовать два разных API-ключа. В целом рекомендуется использовать разные API ключи для каждой операции, чтобы отслеживать, кто и когда обращается к вашему сервису.

apiGateway:
    apiKeys:
      - ${self:custom.env.repoApiKey}
      - ${self:custom.env.appApiKey}

В параметрах нашего обработчика пишем, что функция чтения конфига приватная и теперь AWS при вызове этой лямбды будет ожидать заголовок x-api-key, значение для которого мы можем найти в AWS SSM, если, конечно, мы использовали приведенный выше шаблон. Шаблон содержит инструкции по созданию такого ключа API и сохранению его в SSM под именем /cfg-srv-exmpl/dev/apis/rest-api/app-api-key.

getConfig:
  handler: src/handlers/get-config.handler
  events:
    - http:
        method: get
        path: config
        cors: true
        private: true

Сама лямбда очень проста. Мы получаем id нашего конфига из параметров запроса и просто читаем конфиг с этим id из базы данных. При этом авторизация проходит в фоновом режиме. Если функция была вызвана, значит, AWS проверил заголовки запроса и все в порядке.

export const handler: APIGatewayProxyHandler = async (event) => {
    const id = event.queryStringParameters.id;
    const json = (await getConfigs()).find((i) => i.id == id);

    return HttpResponse.success(json);
}

Теперь давайте перейдем к части на запись.

  • Создаем репозиторий на github, где будет храниться наш конфиг.

  • Заходим в настройки и переходим на вкладку Webhooks.

  • В "Payload URL" вставляем URL из хранилища SSM под именем /cfg-srv-exmpl/dev/apis/rest-api/api-url, опять же, если вы использовали шаблон, то под этим именем там будет адрес самого API Gateway.

  • Не забудьте указать «application/json» для типа контента, а также секрет. Секрет — это наш второй сгенерированный ключ API.

Github будет использовать этот секрет для получения хэша sha256. Далее, как только что-то обновится в конфиг-репозитории, гитхаб сделает POST-запрос на указанный нами в Payload URL адрес, сгенерирует хеш и поместит его в заголовок X-hub-signature-256 и так как только мы знаем этот секрет, никто другой не может сделать такой же запрос.

Затем в нашей функции мы получаем этот запрос. Мы можем понять, что он был сделан github с нашим секретом (пример), мы делаем дополнительный запрос через github API, чтобы получить конфиг и записываем его в базу, поэтому после каждого изменения конфига в github, он будет синхронизирован с базой данных, и приложение будет использовать актуальную версию конфига.

Давайте еще раз пройдемся по шагам.

  • Разворачиваем API Gateway с двумя функциями (приватной и публичной) с подключением к БД.

  • Убеждаемся, что создаются два ключа API.

  • Используем один ключ для чтения в приватной функции

  • Делаем веб-хук для репозитория с конфигом, и подписываем каждый запрос вторым API ключом

  • При изменении конфига делается запрос к веб-хуку, функция проверяет подпись и если все ок, то конфиг обновляется в базе

  • Любое приложение, которое знает ключ API для чтения, может прочитать файл конфигурации и внедрить его в свой код.

Плюсы:

  • минимум усилий, почти все разворачивается в одну строчку

  • система авторизации для чтения конфига из коробки

  • система контроля версий для конфига из коробки

  • авторизованные изменения конфигурации из коробки

  • управление нагрузкой из коробки

Пример реализованного конфиг-сервера

Теперь мы можем, наконец, решить нашу проблему с динамическим контентом. Давайте использовать NextJs в качестве примера.

export async function getServerSideProps() {
  const config = await fetch('https://api-gateway-url/dev/config?id=project', {
      headers: {
        'x-api-key': 'some-api-key-hash'
      }
    }).then(res => res.json())

    return {
      props: {
        itemsPerPage: config.itemsPerPage
      }
    }
}

Как только мы изменим itemsPerPage в нашей конфигурации, эти изменения будут автоматически применены к нашему приложению без необходимости повторного деплоя кода в продакшен.


Если вам понравилась данная заметка то можно также ознакомится с оригинальным блогом на github. Там я выпускаю заметки немного чаще и они также бывают не совсем техническими. Поэтому тыкайте на глаза и звезды :)

Прочие вопросы можно задать в twitter.

Комментарии (3)


  1. kein
    16.08.2022 09:01
    +1

    Можно еще использовать AWS AppConfig. У него свои ограничения есть, но многим подходит.


  1. WondeRu
    16.08.2022 09:14

    А можно просто json-файл с конфигурацией на s3 положить)


    1. Werawoolf Автор
      16.08.2022 09:21

      Согласен) но кажется доступом легче в гитхабе управлять, так как все разработчики уже там