Статья из серии "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.