Без 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 тысяч запросов в месяц бесплатно — вполне достаточно для маленьких проектов или стартапов.
D1abloRUS
Можно было бы и про cold start добавить…