Без API не обходится ни одно веб-приложение. Для их разработки используются разные методы. Сейчас, например, набирает популярность бессерверный подход — он экономичный, масштабируемый и относительно простой. Как ведущий провайдер бессерверных вычислений Amazon Web Services (AWS) вносит огромный вклад в бессерверную разработку. Здесь мы обсудим общие концепции реализации API с помощью AWS Lambda и других сервисов AWS.


Почему именно AWS Lambda?


AWS Lambda — это сервис AWS, который отвечает за выполнение определенных функций в ответ на триггеры, то есть события в приложении. Это могут быть HTTP-вызовы, события в других сервисах AWS, например S3, Kinesis или SNS, или повторяющиеся запланированные события. Функции выполняются в эфемерных контейнерах, подготовкой и масштабированием которых занимается AWS, так что разработчики избавлены от хлопот, связанных с инфраструктурой.


Еще одно привлекательное преимущество — оплата по мере использования. Вы платите только за общее время выполнения функций и не тратитесь в периоды простоя. Конечно, у Lambda, как и у любого другого сервиса, есть свои ограничения. Он не подходит для некоторых задач вроде очень длительных заданий, интенсивных вычислений или процессов, где мы должны контролировать среду выполнения. Но для реализации API сервис AWS Lambda обычно подходит идеально.


Роль API Gateway


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


HTTP API


API Gateway включает множество функций и интеграций. В какой-то момент в Amazon поняли, что разработчикам, которые используют бессерверные вычисления, обычно не нужно столько всего. Скорее, они предпочли бы в целом упростить процесс реализации. Наверное, поэтому в конце 2019 года AWS объявили о новых HTTP API, облегченной версии API Gateway, которая существенно упрощает разработку, повышает производительность и снижает расходы для бессерверных API. Несмотря на свою простоту, HTTP API поддерживают такие важные фичи, как настройка CORS для всех конечных точек, интеграция JWT, кастомные домены и соединения с VPC.


Принципы бессерверных API


Чтобы разобраться с главными принципами реализации бессерверных API, рассмотрим лаконичный пример простого приложения виртуальной доски, состоящий из двух конечных точек: POST для записи сообщений и GET для извлечения трех последних сообщений. Рассмотрим и другие возможные функции (параметры пути, CORS и авторизаторы), но постараемся не усложнять финальную реализацию.


AWS DynamoDB


Наш проект будет полностью бессерверным, поэтому используем AWS DynamoDB для хранения сообщений. Эта база данных отвечает принципам бессерверной архитектуры — простота использования и оплата только за запросы. DynamoDB — это база данных NoSQL «ключ-значение» от AWS, где данные хранятся на серверах AWS и полностью управляются Amazon.


AWS Serverless Application Model


Для реализации вам понадобится аккаунт AWS, а также установленный и настроенный фреймворк AWS Serverless Application Model (SAM). SAM — это инструмент для создания, обновления и администрирования бессерверных приложений и всех ресурсов, которые нужны ему для работы. С AWS SAM мы не создаем каждый сервис вручную на веб-консоли — мы просто описываем все, что нам нужно, в специальном файле шаблона.
После установки CLI переходим в нужный каталог и выполняем команду:


$ sam init -r nodejs12.x -n whiteboard
Инициализация нового проекта


Выбираем первую опцию и нажимаем Quick Start from Scratch (Быстрый запуск с нуля). Создается каталог нашей доски с минимумом установочных файлов.


Определяем необходимые ресурсы


Сначала открываем файл template.yml и удаляем все, что находится под разделом Resources. Прежде чем перейти к самому API, создаем дополнительные ресурсы. Определяем таблицу DynamoDB, где будут храниться сообщения:


Resources:

  BoardMessagesTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: board-messages-table
        AttributeDefinitions:
          - AttributeName: partKey
            AttributeType: S
          - AttributeName: createdAt
            AttributeType: N
        KeySchema:
          - AttributeName: partKey
            KeyType: HASH
          - AttributeName: createdAt
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5

Объявление таблицы DynamoDB


AWS создает таблицу DynamoDB, где атрибутом partKey будет ключ партиции, одинаковый для всех записей, а атрибутом createdAt будет ключ диапазона для сортировки по метке времени. Можно добавить в записи другие ключи и значения, но определять их необязательно.


Теперь в том же файле, сразу под предыдущим определением, объявим HTTP API, с которым будут связаны все будущие конечные точки и функции.


BoardHttpApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: Test
      CorsConfiguration: True

Объявление HTTP API


Это очень короткое и простое определение — название этапа и конфигурация CORS, которые, впрочем, необязательны. Видите, каким простым и лаконичным может быть создание API? При необходимости, конечно, можно добавить самые разные свойства, например ссылку на функцию авторизации, определение домена, параметры логирования и другие.


Определяем функции обработчиков API


Наконец, определив все API, давайте объявим две функции, связанные с конкретными конечными точками.


PostMessageFunction:
      Type: AWS::Serverless::Function
      Properties:
        Handler: src/handlers/postMessage.handler
        Runtime: nodejs12.x
        MemorySize: 128
        Timeout: 5
        Events:
          PostMessage:
            Type: HttpApi
            Properties:
              ApiId: !Ref BoardHttpApi
              Method: POST
              Path: /messages
        Policies:
          - AmazonDynamoDBFullAccess

  GetMessagesFunction:
      Type: AWS::Serverless::Function
      Properties:
        Handler: src/handlers/getMessages.handler
        Runtime: nodejs12.x
        MemorySize: 128
        Timeout: 5
        Events:
          GetMessages:
            Type: HttpApi
            Properties:
              ApiId: !Ref BoardHttpApi
              Method: GET
              Path: /messages
        Policies:
          - AmazonDynamoDBFullAccess

Объявление обработчиков для запросов POST и GET


Тут все понятно: две функции, одна из которых будет вызываться при запросе POST к пути /messages, а другая — при запросе GET к тому же пути. У обеих функций есть ограничения на 128 МБ оперативки и пятисекундный таймаут. Код функций находится в файлах postMessage.js и getMessage.js в каталоге /src/handlers/. Создадим их прямо сейчас. (Мы предоставили полный доступ к DynamoDB в разделе Policies каждой функции, чтобы упросить код. В реальном проекте доступ нужно будет настроить более тонко).


Написание функций


Идем в каталог /src/handlers и создаем файлы со следующим содержимым:


postMessage.js


const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB();

exports.handler = async (event) => {
  const { body } = event;
  try {
    const { author, text } = JSON.parse(body);
    if (!author || !text) {
      return {
        statusCode: 403,
        body: 'author and text are required!'
      }
    }

    await dynamodb.putItem({
      TableName: 'board-messages-table',
      Item: {
        msgId: { S: 'board' },
        author: { S: author },
        text: { S: text },
        createdAt: { N: String(Date.now()) } // still expects string!
      }
    }).promise();
    return {
       statusCode: 200,
       body: 'Message posted on board!',
    }
  } catch (err) {
    return {
       statusCode: 500,
       body: 'Something went wrong :(',
    }
  }
};

Код обработчика запросов POST


Эта функция будет выполняться в ответ на запросы POST. Она вычленяет из тела запроса автора и текст и сохраняет эти данные в базе данных. Еще она заполняет атрибут partKey одинаковым значением для всех записей. Обычно это не лучший метод, но для нашего примера подходит, потому что позволяет выполнять сортировку по ключам диапазона среди всех элементов с одинаковым ключом партиции. Кстати, DynamoDB всегда ожидает строку, даже если тип атрибута — число.


getMessages.js


const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB();

exports.handler = async () => {
  try {
    const result = await dynamodb.query({
      TableName: 'board-messages-table',
      KeyConditionExpression: 'partKey = :partKey',
      ScanIndexForward: false,
      Limit: 3,
      ExpressionAttributeValues: {':partKey': { S: 'board'}}
    }).promise();

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(result.Items),
    }
  } catch (err) {
    console.log(err);
    return {
      statusCode: 500,
      body: 'Something went wrong :(',
    }
  }
};

Код обработчика запросов GET


В этой функции мы сначала получаем записи, у которых partKey равен board, потом задаем для ScanIndexForward значение false, чтобы последние сообщения отображались сверху, и наконец с помощью свойства Limit указываем, что хотим увидеть только три сообщения.


Развёртывание


Деплоить с AWS SAM легко — достаточно одной команды с несколькими введенными значениями. Переходим в root-каталог проекта и выполняем команду:


$ sam deploy --guided

Команда развертывания


Нас попросят ввести имя приложения и выбрать регион AWS, а еще подтвердить несколько действий:



Указание и подтверждение параметров


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



Список создаваемых ресурсов их статусы


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



URL для конечной точки root API


Тестируем API


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


curl -d '{"author":"name", "text":"Message text"}' -H "Content-Type: application/json" -X POST https://your-api-id.execute-api.your-region.amazonaws.com/test/messages

Выполнение запроса POST в curl


Отправляем несколько запросов с разными сообщениями. Если все нормально, в консоли отобразится Message posted on board! (Сообщение опубликовано на доске) без ошибок.


Чтобы извлечь последнее сообщение, выполним простую команду, даже короче предыдущей:


curl https://your-api-id.execute-api.your-region.amazonaws.com/test/messages

Выполнение запроса GET в curl


Вот и все. Мы создали простой HTTP API с AWS Lambda и AWS SAM. Конечно, в реальном проекте нужно будет больше функций и конфигураций, но принцип будет тот же: определяем ресурсы, определяем конфигурации, пишем код и запускаем деплой.


Подключение мониторинга Thundra


Будет разумно организовать мониторинг — особенно для бессерверных приложений, где легко запутаться с дебагом и трейсингом.


Можно подключить мониторинг Thundra к только что созданным функциям Lambda (см. краткое руководство). После подключения Thundra вам нужно будет инструментировать функции Lambda postMessage и getMessages, чтобы просмотреть подробную информацию о каждом вызове и получить общую картину приложения.


Выбираем функции в списке, нажимаем сначала кнопку Instrument, а затем OK.



Подтверждение инструментирования функции Lambda


Пробуем сделать несколько запросов к API, возвращаемся на дашборд Thundra, нажимаем на имя функции и выбираем вызовы из списка. Здесь мы видим время, производительность, входные и выходные данные функции и т. д. Это очень полезно при отладке API в реальных проектах.



Сведения об одном вызове


Если вы используете бессерверные вычисления в сложных проектах с разными сервисами AWS или других вендоров, вам определенно пригодится функция Unique Tracing, которая существенно упрощает устранение неполадок и отладку приложения.


Стоит ли использовать HTTP API?


С помощью HTTP API с AWS Lambda можно создавать высокопроизводительные и экономичные API. Хоть это и облегченная версия API Gateway REST API, она обладает всем необходимым функционалом и покрывает 90% нужд разработчиков. HTTP API не поддерживают некоторые полезные функции, вроде кэширования, валидации схем и трансформации ответов.


Кэширование вам, скорее всего, не пригодится, потому что HTTP API работают гораздо быстрее старых REST API, а валидацию и трансформацию можно выполнять на уровне кода функции.
Если у ваших разработчиков нет других причин отказаться от HTTP API, можете с уверенностью их использовать. Обычно бессерверная транзакция начинается с вызова API, а отслеживать асинхронный поток событий не так-то просто. В таких ситуациях используйте комплексную функцию распределенного трейсинга в Thundra. Thundra предоставляет 250 тысяч запросов в месяц бесплатно — вполне достаточно для маленьких проектов или стартапов.