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

Бессерверные вычислительные сервисы, такие как AWS Lambda, работают так: вы предоставляете такие конфигурации, как среда выполнения, выделение памяти и регион; а облачный провайдер позаботится о запуске предоставленного вами кода. Облачные провайдеры делают это, запуская вычислительные ресурсы для выполнения заданного запроса, а затем отключая их через несколько минут.

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

Лямбда преимущества

Бессерверные вычисления приносят много преимуществ. Начиная с отсутствия необходимости в управлении и обслуживании серверов. Веб-сервер 001 больше не гудел, и никто не знает, почему!

Возможно, к еще большему преимуществу — масштабированию. Без необходимости что-либо настраивать и в зависимости от региона AWS lambda может масштабироваться до тысяч одновременных запросов в секунду. Это огромно и перекладывает всю ответственность за масштабирование на облачного провайдера.

Третьим большим преимуществом для меня является возможность масштабирования до нуля. Для меня это важный фактор в определении того, что является бессерверным, а что нет. Масштабирование до нуля означает, что вы платите только при выполнении запроса, а кроме того, минимального платежа нет вообще.

Кстати, это лишает такие сервисы, как Amazon Aurora Serverless и Serverless OpeanSearch, статус бессерверных. Я полагаю, что точная терминология для этих сервисов — автомасштабируемые сервисы, а не бессерверные.

Проблема с задержкой

Задержка исторически никогда не была сильной стороной бессерверных вычислений. Даже с учетом того, чем я собираюсь поделиться с вами здесь, если однозначная задержка мс является бизнес-требованием, контейнеры и оркестровка контейнеров, такие как Amazon ECS или Amazon EKS, — это то, что я хотел бы изучить.

Для синхронных сервисов, таких как API REST или GraphQL, задержка всегда будет важна, хотя и предоставляется в той или иной степени.

Решение собственных проблем с задержкой

Мы в Orbital используем Auth0 для аутентификации наших пользователей и для совершения безопасных вызовов к нашим внутренним службам.

Когда продукт был впервые создан, команда инженеров выбрала Rest API с использованием API Gateway с лямбда-выражением, DynamoDB и EventBridge. Наряду с пользовательским лямбда-авторизатором для проверки и авторизации входящих JWT, что имело смысл в то время.

Это, естественно, приводит к гораздо более быстрому холодному запуску, около 3–4 секунд, и в целом к ​​более высокой задержке.

Введите AppSync и JavaScript Pipeline Resolvers

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

AppSync — это сервис GraphQL, управляемый AWS, который, по сути, позволяет вам определять схемы GraphQL и разрешать поля в этой схеме различными способами, одним из которых являются прямые преобразователи JavaScript. Вы можете думать о преобразователе JavaScript как о сценарии, который запускается, когда поле запрашивается через конечную точку GraphQL.

Мне нравится все в GraphQL, и я предпочитаю его декларативный, строго типизированный стиль REST.

Удаление пользовательского авторизатора лямбда

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

Войдите в интеграцию AppSync OIDC!

Мне очень нравится эта функция, по сути, пара строк YAML в вашей конфигурации AppSync, и теперь вызовы API авторизуются для данной конечной точки OpenID Connect. В нашем случае это был Auth0, но на самом деле это может быть что угодно, что соответствует спецификации.

Вот как это выглядит при использовании бессерверной платформы V3 и плагина AppSync (с использованием версии 2.0.0-alpha.13):

appSync:
  name: ${self:service}-${opt:stage, self:custom.stage}
  logging:
    level: ALL
    retentionInDays: 14
  authentication:
    type: OPENID_CONNECT
    config:
      issuer: https://prototype.auth0.com
      clientId: someID

После этого декодированный токен доступа становится частью запроса и становится доступным для распознавателей через объект контекста в свойстве identity.

Это дико, большая часть того, что делал наш собственный лямбда-авторизатор, теперь заменена парой строк YAML!

Добавление авторизации

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

Мы оценили некоторые варианты и, учитывая, что наши API-интерфейсы GraphQL не были большим монолитом с множеством полей для разрешения, мы решили включить разрешения в токен доступа. Это сработало для нас, но не обязательно могло быть решением всех проблем с авторизацией, особенно если API слишком большой с большим количеством полей. Существует ограничение на размер JWT, поэтому обязательно имейте это в виду.

Таким образом, идея здесь проста: проверьте свойство разрешений декодированного JWT, сравните его с полем, которое пытается разрешить данный пользователь, и авторизуйте запрос, если все проверено.

Вот как выглядит этот резольвер:

import { util } from '@aws-appsync/utils'

export function request(ctx) {
  const { permissions } = ctx.identity.claims
  const { fieldName } = ctx.info
  const allowed = validatePermission(permissions, fieldName)
  if (!allowed) {
    util.unauthorized()
  }
  return {
    payload: ctx.args,
  }
}

export function response(ctx) {
  return ctx.result
}

function validatePermission(permissions, fieldName) {
  let allowed = false
  for (const permission of permissions) {
    if (permission === fieldName) allowed = true
  }
  return allowed
}

Резолверы конвейеров с JS

Преобразователи конвейеров в AppSync — довольно изящная концепция. Идея состоит в том, что вы можете объединить несколько распознавателей для выполнения данного запроса.

На мой взгляд, самым большим преимуществом является повторное использование логики и сохранение обработки запросов DRY. Таким образом, логика авторизации из более ранних версий может быть повторно использована в каждом запросе API!

Вот как это выглядит в бессерверной среде для поля getOrgById Query:

  schema: schema.graphql
  dataSources:
    mainTable:
      type: AMAZON_DYNAMODB
      config:
        tableName: ${self:custom.tableName}
        iamRoleStatements:
          - Effect: 'Allow'
            Action:
              - dynamodb:GetItem
            Resource:
              - arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/${self:custom.tableName}
    auth:
      type: NONE
  resolvers:
    Query.getOrgById:
      functions:
        - dataSource: auth
          code: src/resolvers/auth.js
        - dataSource: mainTable
          code: src/resolvers/getOrgById.js

getOrgById в JS может выглядеть примерно так:

import { util } from '@aws-appsync/utils'

export function request(ctx) {
  return dynamoDBGetItemRequest({ pk: `ORG#${ctx.prev.result.orgId}`, sk: `ORG#${ctx.prev.result.orgId}` })
}

export function response(ctx) {
  return ctx.result
}

function dynamoDBGetItemRequest(key) {
  return {
    operation: 'GetItem',
    key: util.dynamodb.toMapValues(key),
  }
}

Заключение

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

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

Теперь есть предостережение, которое вы должны иметь в виду: Не все функции JavaScript можно использовать с прямыми преобразователями.

Никакие библиотеки нельзя импортировать, и большинство методов массива JS недоступны. Это означало множество циклов fили, но, учитывая преимущества, компромисс был для нас легким делом!

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу