AWS Lambda in Action. Часть 2: знакомимся с инструментами разработки и тестирования

  • Tutorial


Этот гайд — результат личного опыта разработки и тестирования Serverless-приложений, а также маневрирования между «костылями» и «велосипедами» при попытках их протестировать. Когда я только начинал заниматься разработкой Serverless-приложений, во всем приходилось разбираться руками, не было четких гайдов или удобных инструментов локальной разработки и тестирования.

Сейчас ситуация меняется: инструменты стали появляться, но найти мануалы по их использованию непросто. В этом материале я покажу на простом примере, как работать с новым функционалом. Вы сможете легко его освоить и обойти те проблемы и баги, с которыми «посчастливилось» столкнуться мне.

Вы узнаете, как вести разработку с помощью браузерной консоли AWS, SAM-CLI и IntelljIDEA. Еще я расскажу про тестирование: интеграционные, E2E и юнит-тесты. А напоследок обсудим, во сколько обойдется такое решение (спойлер: на нем можно неплохо сэкономить).

Статья будет полезна тем, кто начинает вести разработку Serverless-приложений и еще не знаком с ее инструментарием и подходами.


О чем пойдет речь


Всем привет! Меня зовут Александр Груздев, и я рад представить вторую часть статьи про использование и разработку Serverless-решения на базе AWS Lambda. В продолжении поговорим о способах ведения разработки и тестирования решения из первой части (AWS Lambda in Action на Java 11. Заезжаем с Serverless в «Production»).

С момента публикации прошлой статьи тестовое приложение было немного переработано, но основной смысл остался прежним. Это банальная HTML-форма «Contact Us», которая посредством HTTP-запросов отправляет данные с формы на API Gateway, а он в свою очередь проксирует запрос на обработку в AWS Lambda. Лямбда во время обработки записывает данные в таблицу DynamoDB и отправляет письмо через AWS SES. Диаграмма компонентов представлена ниже.

Components diagram


Содержание


  1. Разработка с использованием браузерной консоли AWS
  2. Разработка с использованием SAM-CLI
  3. Разработка с использованием IntelljIDEA
  4. Виды тестирования
  5. Стоимость Serverless-решений
  6. Полезные ссылки



1. Разработка с использованием браузерной консоли AWS



Консоль AWS


Консоль AWS дает доступ к любому AWS-сервису, будь то ваш виртуальный сервер EC2, настройки ролей/политик доступа IAM или AWS Lambda со всеми параметрами, которые можно менять в режиме реального времени без повторного развертывания через системы Continuous Delivery/Continuous Deployment.

Это значительно ускоряет процесс разработки и некоторые виды тестирования. А еще помогает наглядно управлять параметрами запуска и работы ваших приложений.

Консоль API Gateway


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

Давайте посмотрим, как выглядит эта «магическая» консоль на скриншоте вкладки Resources приложения «Contact Us»:

Gateway resources

На скриншоте видно, как запрос от клиента через API Gateway попадет в лямбду и как ответ вернется обратно клиенту при вызове методом POST ресурса/contact.

  • Method request — первый этап обработки запроса. В нашем случае не содержит никаких настроек, но может содержать, например, настройки для авторизации.
  • Integration request означает, что HTTP-запрос будет передаваться в лямбду в особом формате: он содержит всю отправленную клиентом информацию и детали о вызываемом контексте.
  • Integration response говорит о том, что HTTP-клиенту ответ будет доставлен прямиком от лямбды. Следовательно, вы сами отвечаете за формирование тела ответа в лямбде.


Disable proxy integration
Тип запроса и ответа для API Gateway можно поменять, если убрать галку Use Lambda Proxy integration в блоке Integration Request. Тогда у вас будет возможность модифицировать поступающие к лямбде данные. Например, прочитать запрос и отправить в лямбду данные в формате модели, которая ожидается на входе лямбды.


Integration request sample
При создании проекта через SAM init у вас будет отдельная директория events, в которой будет пример запроса, поступающего в лямбду при включенном Integration request. Он не самый актуальный, так как AWS достаточно часто выпускает обновления, но для локальной разработки сгодится. Вернемся к этому файлу в части про консоль AWS Lambda.


Нажмите на кнопку TEST, чтобы проверить работоспособность приложения. Это откроет вам доступ к HTTP-вызовам API Gateway.

На следующем скриншоте я отправил запрос на прогрев лямбды:
Gateway test request

Кроме ответа на запрос API Gateway отображает детальные логи. Таким же образом можно сделать реальный запрос, подставив в тело запроса JSON в виде:
{
    "subject": "Email subject",
    "question": "Your question…",
    "username": "Your name…",
    "phone": "Your phone…",
    "email": "Your email…"
} 


Так вы можете вручную тестировать ваше API без каких-либо UI форм и Front-end приложений, тем самым разделив фронт- и бэкенд-разработку. В качестве альтернативы встроенной консоли можно использовать Postman или любой другой HTTP-клиент.

Postman как альтернатива
Для тестирования любых HTTP-сервисов я предпочитаю использовать приложение Postman. Оно позволяет быстро и удобно настраивать и выполнять запросы. Но обратите внимание, что использование API Gateway консоли позволяет вам делать запросы из AWS окружения, не добавляя различные настройки для авторизации в запрос и тем самым ускоряя проверку API. Также, используя консоль, вы не платите за исходящий из AWS трафик.


Консоль Lambda


Для тестирования и ведения разработки кроме запросов через API Gateway можно использовать браузерную консоль AWS Lambda.

Ее экран разбит на несколько блоков:
  • Designer показывает существующие триггеры и слои. В нашем случае триггером выступает API Gateway. Если мы на него кликнем, откроется вся необходимая информация: URL, stage, resource path.
  • Aliases (доступен при выборе конкретного алиаса лямбды) позволяет конфигурировать распределение трафика по версиям лямбды. Подробнее об этом в следующем пункте.
  • Function code — это общая информация о коде и способе запуска. Эта функция позволяет загружать напрямую zip-файл с кодом, чтобы развернуть новую версию. Если вы используете не компилируемые заранее языки, то благодаря Function code сможете поменять код в браузерном окне. Это очень удобно при разработке небольших функций на Python/NodeJs.
  • Environment variables. Думаю, с этим все понятно из названия. Любые переменные, которые могут иметь разное значение для разных окружений, нужно вынести в этот блок. В моем случае имейл, название таблицы и прочие переменные могли бы лежать тут. Кстати, можно применять настройки шифрования, если через эти переменные вы передаете конфиденциальные данные.
  • Tags — это метки, по которым можно выполнять агрегацию и отслеживать, например, расходы на конкретную команду, отдел разработки или продукт.
  • Execution role — это созданная вручную либо автоматически (как в нашем случае) роль, описывающая возможности доступа для конкретной лямбды.
  • Basic settings — конфигурация таймаута по работе лямбды и настройка количества памяти.
  • Network: по умолчанию лямбда запускается вне каких-либо приватных подсетей, так что у нее нет доступа к ресурсам внутри VPC. Если доступ нужен — настройте его по этой инструкции.
  • AWS X-Ray активирует отслеживание запросов. Это упрощает мониторинг в случае последовательных вызовов лямбд или других AWS-сервисов.
  • В Reserve concurrency можно зарезервировать определенный пул вызовов, чтобы в случае большой нагрузки на другие лямбды в аккаунте данная лямбда продолжала обрабатывать определенное значение параллельных запросов. Вдобавок эта опция ограничивает максимальное количество параллельных лямбд этим значением.
    Скрин в пункте Provisioned concurrency
  • Provisioned concurrency — это достаточно новая функциональность. По сути она заменяет наш механизм прогрева, тем самым устанавливая количество проинициализированных контейнеров лямбд до определенного значения. Она решает проблему холодного старта, но может дорого стоить — обратите на это внимание. Также включение этой функциональности аннулирует использование данной лямбды в Free Tier лицензии.
  • Asynchronous invocation позволяет настроить специальную обработку асинхронных запросов. Указываем, сколько повторных вызовов делать в случае ошибки, и как долго запрос может висеть в очереди. Дополнительно можно указать очередь для обработки запросов, которые завершились неудачно.

  • Database proxies — еще одна новая функциональность, позволяющая настроить доступ к базе через прокси в виде API Gateway + Lambda.


Дальше посмотрим, как вести разработку с этой консоли. Помимо изменения параметров функционал Configure Test Events можно использовать для создания тестового запроса. Доступен довольно обширный список, но мы воспользуемся тем, что я уже приложил в проекте:
ContactUs request
{
  "body": "{\"subject\": \"Question\",\"question\": \"How much does it cost\",\"username\": \"Alex\",\"phone\": \"+79999999999\",\"email\": \"alex@gmail.com\"}",
  "resource": "/{proxy+}",
  "path": "/path/to/resource",
  "httpMethod": "POST",
  "isBase64Encoded": false,
  "queryStringParameters": {
    "foo": "bar"
  },
  "pathParameters": {
    "proxy": "/path/to/resource"
  },
  "stageVariables": {
    "baz": "qux"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, sdch",
    "Accept-Language": "en-US,en;q=0.8",
    "Cache-Control": "max-age=0",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-Mobile-Viewer": "false",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "US",
    "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Custom User Agent String",
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
    "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "requestContext": {
    "accountId": "123456789012",
    "resourceId": "123456",
    "stage": "prod",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "requestTime": "09/Apr/2015:12:34:56 +0000",
    "requestTimeEpoch": 1428582896000,
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "accessKey": null,
      "sourceIp": "127.0.0.1",
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "userAgent": "Custom User Agent String",
      "user": null
    },
    "path": "/prod/path/to/resource",
    "resourcePath": "/{proxy+}",
    "httpMethod": "POST",
    "apiId": "1234567890",
    "protocol": "HTTP/1.1"
  }
}


После добавления запроса нажимаем Test и видим результат работы лямбды:
Gateway resources

Лямбда возвращает JSON, в котором присутствуют статус-код и тело ответа. Значение body пойдет напрямую клиенту, вызвавшему API Gateway. Кроме этого, на скриншоте есть характеристики вызова: время выполнения, время выполнения за которое мы будем платить, максимальное количество памяти, которое потреблялось лямбдой. Полагаясь на эти метрики, мы можем сконфигурировать оптимальное количество памяти, чтобы лямбда соответствовала вашим требованиям цены/скорости ответа.

Использование AWS консоли для тестирования Canary deployment


Теперь затронем один важный момент, связанный с разворачиванием новой версии лямбды. Я указывал такие параметры, когда я писал SAM-шаблон:
AutoPublishAlias: live
DeploymentPreference:
  Type: Canary10Percent10Minutes

Первый параметр автоматически добавляет к каждой новой версии лямбды алиас live.
Немного об алиасах
Когда вы разворачиваете новую версию лямбды, старая тоже сохраняется. Все версии остаются доступными, и вы в любой момент можете указать конкретную версию, на которую нужно перенаправлять запросы из API Gateway. Версии по умолчанию обозначаются номерами 1, 2, 3 и так далее. Еще есть метка LATEST, которая указывает на последнюю версию. Если у вас существует 2 или 3 окружения для API Gateway (Stage в терминах AWS), придется каждый раз при обновлении кода лямбды обновлять соотношение Gateway-Lambda. В API Gateway мы должны указывать конкретную версию (ARN лямбды), или по умолчанию это будет LATEST.

Алиас — еще один идентификатор (более сложно устроенный), который, как и в случае с версией, можно указать в ARN лямбды. Но его преимущество состоит в том, что можно создать по одному алиасу на каждый Stage для API Gateway и больше не менять эти зависимости. К примеру, создаем несколько окружений API Gateway: dev, qa и prod и соотносим их с соответствующими алиасами лямбды.

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


Для тестирования canary deployment можно воспользоваться как консолью API Gateway, так и консолью Lambda.

Для начала нужно изменить код лямбды так, чтобы она возвращала другой ответ. Например, в обработке прогрева указать в ответе Version 2 вместо Version 1. Это и будет нашим маркером.

Теперь, используя функционал API Gateway Test, мы можем убедиться, что в течении десяти минут при отправке WARM-UP запросов лишь 10 % ответов будут иметь Version 2 в теле ответа. По истечении этого времени 100 % запросов будут возвращает Version 2.

Кроме консоли API Gateway мы можем использовать консоль Lambda, где, выбрав необходимый алиас, видим политику распределения трафика, которой можно управлять.
Lambda alias



2. Локальная разработка с использованием SAM-CLI


Некоторое время назад одним из главных недостатков использования лямбды считали неудобство в разработке и тестировании на локальной машине. Сейчас ситуация сильно изменилась: появился SAM-фреймворк, который позволяет не только собирать и разворачивать решения, но и упрощает локальную разработку.

Например, вы можете вызвать вашу лямбду прямо из консоли:
sam local invoke -e ./events/warm_up_request.json ContactUsFunction

Эта команда запускается из директории с SAM-шаблоном и передает содержимое JSON-файла на вход лямбде ContactUsFunction (это логическое имя в шаблоне).

То есть, используя эту команду, SAM поднимает в докере образ lambci/lambda:java11, и в нем запускает ваш код. Для доступа к удаленным сервисам, таким как SES, используется ваша конфигурация для AWS с secret/access key, так что она должна быть актуальной. Еще важный момент: если вы не добавите заголовок для режима прогрева, будут вызваны реальные сервисы AWS SES и DynamoDB.

Лог вызова тут
F:\aws\projects\contact-us-sam-app>sam local invoke -e ./events/warm_up_request.json ContactUsFunction
Invoking com.gralll.sam.App::handleRequest (java11)
Fetching lambci/lambda:java11 Docker container image......
Mounting F:\aws\projects\contact-us-sam-app\.aws-sam\build\ContactUsFunction as /var/task:ro,delegated inside runtime container
?[32mSTART RequestId: a86d65fa-1f19-15c5-93b8-d87631c80ee2 Version: $LATEST?[0m
2020-01-06 19:09:17 a86d65fa-1f19-15c5-93b8-d87631c80ee2 INFO  App - Request was received
2020-01-06 19:09:17 a86d65fa-1f19-15c5-93b8-d87631c80ee2 DEBUG App - {
  "body" : "{\"subject\": \"Question\",\"question\": \"How much does it cost\",\"username\": \"Alex\",\"phone\": \"+79999999999\",\"email\": \"alex@gmail.com\"}",
  "resource" : "/{proxy+}",
  "requestContext" : {
    "resourceId" : "123456",
    "apiId" : "1234567890",
    "resourcePath" : "/{proxy+}",
    "httpMethod" : "POST",
    "requestId" : "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "extendedRequestId" : null,
    "accountId" : "123456789012",
    "identity" : {
      "apiKey" : null,
      "apiKeyId" : null,
      "userArn" : null,
      "cognitoAuthenticationType" : null,
      "caller" : null,
      "userAgent" : "Custom User Agent String",
      "user" : null,
      "cognitoIdentityPoolId" : null,
      "cognitoIdentityId" : null,
      "cognitoAuthenticationProvider" : null,
      "sourceIp" : "127.0.0.1",
      "accountId" : null,
      "accessKey" : null
    },
    "authorizer" : null,
    "stage" : "prod",
    "path" : "/prod/path/to/resource",
    "protocol" : "HTTP/1.1",
    "requestTime" : "09/Apr/2015:12:34:56 +0000",
    "requestTimeEpoch" : 1428582896000,
    "elb" : null
  },
  "multiValueQueryStringParameters" : { },
  "multiValueHeaders" : {
    "X-WARM-UP" : [ "13" ]
  },
  "pathParameters" : {
    "proxy" : "/path/to/resource"
  },
  "httpMethod" : "POST",
  "stageVariables" : {
    "baz" : "qux"
  },
  "path" : "/path/to/resource",
  "isBase64Encoded" : false,
  "requestSource" : "API_GATEWAY"
}
2020-01-06 19:09:17 a86d65fa-1f19-15c5-93b8-d87631c80ee2 INFO  App - Lambda was warmed up
?[32mEND RequestId: a86d65fa-1f19-15c5-93b8-d87631c80ee2?[0m
?[32mREPORT RequestId: a86d65fa-1f19-15c5-93b8-d87631c80ee2     Init Duration: 1984.55 ms       Duration: 42.22 ms      Billed Duration: 100 ms Memory Size: 256 MB     Max Memory Used: 102 MB ?[0m
{"statusCode":201,"multiValueHeaders":{"Access-Control-Allow-Origin":["*"]},"body":{"response":"Lambda was warmed up. V1"},"isBase64Encoded":false}


Кроме лога лямбды в консоль пишется служебная информация по статусу вызова и ответ в формате JSON.

API Gateway, как и лямбду, можно запустить локально.
Выполняем команду:
sam local start-api

Лог консоли при старте
F:\aws\projects\contact-us-sam-app>sam local start-api
Mounting ContactUsFunction at http://127.0.0.1:3000/contact [POST, OPTIONS]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-01-06 22:20:30  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)


После запуска сервера мы можем выполнять запросы. Я буду использовать Postman и отправлю запрос на прогрев лямбды.

В консоли видим
Invoking com.gralll.sam.App::handleRequest (java11)
Fetching lambci/lambda:java11 Docker container image......
Mounting F:\aws\projects\contact-us-sam-app\.aws-sam\build\ContactUsFunction as /var/task:ro,delegated inside runtime container
?[32mSTART RequestId: 27868750-3637-1f80-3d80-53a724da16ef Version: $LATEST?[0m
2020-01-06 19:28:09 27868750-3637-1f80-3d80-53a724da16ef INFO  App - Request was received
2020-01-06 19:28:09 27868750-3637-1f80-3d80-53a724da16ef DEBUG App - {
  "body" : "{\r\n  \"subject\": \"Question\",\r\n  \"question\": \"How much does it cost\",\r\n  \"username\": \"Alex\",\r\n  \"phone\": \"+79999999999\",\r\n  \"email\": \"alex@gmail.com\"\r\n}",
  "resource" : "/contact",
  "requestContext" : {
    "resourceId" : "123456",
    "apiId" : "1234567890",
    "resourcePath" : "/contact",
    "httpMethod" : "POST",
    "requestId" : "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "extendedRequestId" : null,
    "accountId" : "123456789012",
    "identity" : {
      "apiKey" : null,
      "apiKeyId" : null,
      "userArn" : null,
      "cognitoAuthenticationType" : null,
      "caller" : null,
      "userAgent" : "Custom User Agent String",
      "user" : null,
      "cognitoIdentityPoolId" : null,
      "cognitoIdentityId" : null,
      "cognitoAuthenticationProvider" : null,
      "sourceIp" : "127.0.0.1",
      "accountId" : null,
      "accessKey" : null
    },
    "authorizer" : null,
    "stage" : "Prod",
    "path" : "/contact",
    "protocol" : null,
    "requestTime" : null,
    "requestTimeEpoch" : 0,
    "elb" : null
  },
  "multiValueQueryStringParameters" : null,
  "multiValueHeaders" : {
    "Accept" : [ "application/json" ],
    "Accept-Encoding" : [ "gzip, deflate, br" ],
    "Accept-Language" : [ "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7" ],
    "Cache-Control" : [ "no-cache" ],
    "Connection" : [ "keep-alive" ],
    "Content-Length" : [ "150" ],
    "Content-Type" : [ "text/plain;charset=UTF-8" ],
    "Host" : [ "127.0.0.1:3000" ],
    "Origin" : [ "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop" ],
    "Postman-Token" : [ "5cadaccd-6fae-5bc2-b4ef-63900a1725ff" ],
    "Sec-Fetch-Mode" : [ "cors" ],
    "Sec-Fetch-Site" : [ "cross-site" ],
    "User-Agent" : [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" ],
    "X-Forwarded-Port" : [ "3000" ],
    "X-Forwarded-Proto" : [ "http" ],
    "X-Warm-Up" : [ "123" ]
  },
  "pathParameters" : null,
  "httpMethod" : "POST",
  "stageVariables" : null,
  "path" : "/contact",
  "isBase64Encoded" : false,
  "requestSource" : "API_GATEWAY"
}
2020-01-06 19:28:09 27868750-3637-1f80-3d80-53a724da16ef INFO  App - Lambda was warmed up
?[32mEND RequestId: 27868750-3637-1f80-3d80-53a724da16ef?[0m
?[32mREPORT RequestId: 27868750-3637-1f80-3d80-53a724da16ef     Init Duration: 1951.87 ms       Duration: 42.91 ms      Billed Duration: 100 ms Memory Size: 256 MB     Max Memory Used: 102 MB ?[0m
2020-01-06 22:28:11 127.0.0.1 - - [06/Jan/2020 22:28:11] "POST /contact HTTP/1.1" 201 -


Логи, конечно, форматированы не лучшим образом. Надеюсь, в будущем это исправят.

Важно: при изменении кода лямбды делать рестарт для API не обязательно. Достаточно пересобрать лямбду командой sam build.
Немного о багах
Когда я взял за основу запроса и ответа Java-модели из aws-serverless-java-container-core, то столкнулся с проблемой: локальный сервер при запуске всегда возвращал 502 статус. В логах я видел:
Invalid API Gateway Response Keys: {'base64Encoded'} in {'statusCode': 200, 'headers': {'Content-Type': 'text/plain'}, 'body': 'Hello World', 'base64Encoded': False} 

Это случалось из-за того, что локальный API Gateway использовал отличный от AWS метод сериализации и не обрабатывал аннотации JsonProperty. По итогу переменная из 'isBase64Encoded' превратилась в 'base64Encoded', что, собственно, и нарушало валидацию ответа. Разобравшись с этим, я решил просто использовать свою собственную Java-модель ContactUsProxyResponse вместо AwsProxyRequest.



3. Локальные вызовы из IDEA


Все описанное в пункте выше — функциональность инструмента SAM. Он управляется из командной строки и его можно интегрировать в любой интерфейс. Как раз это и было сделано в плагине для IntelljIDEA AWS Toolkit.

Данный плагин добавляет дополнительные конфигурации для запуска лямбды прямиком из IDE. Так выглядит окно конфигурации.


Вы должны либо выбрать шаблон с описанием лямбды, либо настроить параметры вручную.

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

Еще плагин позволяет запускать конфигурацию прямо из окна редактора Java кода либо yaml-шаблона. Скриншоты ниже:




На этом, собственно, и все. Запустив настроенную конфигурацию, вы просто автоматизируете частые вызовы лямбды из консоли. Согласитесь, достаточно удобно?


4. Тестирование


Здесь поговорим о видах тестов для лямбд. Так как в примере я использовал Java, то и тесты будут включать довольно стандартные инструменты для юнит и интеграционного тестирования: Junit 4, Mockito и PowerMockito.

Юнит-тесты


Для покрытия кода лямбды юнит-тестами не нужно обладать специфичными знаниями. Достаточно применить стандартные Java-практики: замокать все зависимости в классе и постараться протестировать все возможные случаи обработки запроса.

Я не стал добавлять все тестовые сценарии и ограничился двумя позитивными и двумя негативными сценариями.

Первый позитивный тест проверяет, что при наличии заголовка X-WARM-UP запросы в DbService и EmailService отсутствуют. Второй кейс проверяет, что эти сервисы будут вызваны, если запрос реальный. Это простейшие тесты, и я опустил в них часть проверок.

Негативные сценарии представляют собой проверку ответа в случае клиентской или серверной ошибки обработки.

Интеграционные тесты


Что касается интеграционных тестов, то я решил проверить DbService и его работу с таблицей DynamoDB.

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

Для установки этого решения достаточно выполнить команды:
pip install localstack
localstack start

Либо можно воспользоваться docker-compose файлом.
docker-compose для поднятия только DynamoDB
version: '2.1'
services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack
    ports:
      - "4567-4599:4567-4599"
      - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
    environment:
      - SERVICES=dynamodb
      - DEBUG=${DEBUG- }
      - DATA_DIR=${DATA_DIR- }
      - PORT_WEB_UI=${PORT_WEB_UI- }
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
      - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - "${TMPDIR:-/tmp/localstack}:/tmp/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"


Команда start по умолчанию поднимает все доступные AWS-сервисы. Чтобы поднять конкретно DynamoDB достаточно задать переменную окружения:
set SERVICES=dynamodb


Для Junit существует специальный LocalstackTestRunner, который позволяет запустить необходимые сервисы, используя конфигурацию @LocalstackDockerProperties.
В итоге написание теста для DbService выглядит таким образом:
  • Добавляем над тестовым классом
    @RunWith(LocalstackTestRunner.class)
    @LocalstackDockerProperties(services = { "dynamodb" })
    

  • Создаем таблицу
    java
    @Before
    public void setUp() {
        AmazonDynamoDB clientDynamoDB = TestUtils.getClientDynamoDB();
        dynamoDB = new DynamoDB(clientDynamoDB);
        dbService = new DbService(dynamoDB);
    
        dynamoDB.createTable(
                new CreateTableRequest()
                        .withTableName("ContactUsTable")
                        .withKeySchema(new KeySchemaElement("Id", KeyType.HASH))
                        .withAttributeDefinitions(new AttributeDefinition("Id", ScalarAttributeType.S))
                        .withProvisionedThroughput(new ProvisionedThroughput(10L, 10L)));
    }
                    


  • Описываем тестовый сценарий
    java
    // given
    ContactUsRequest contactUsRequest = new ContactUsRequest("subject", "name", "+79991234545", "123@mail.ru", "Qeustion");
    
    // when
    dbService.putContactUsRequest("123", contactUsRequest);
    
    // then
    Item item = dynamoDB.getTable("ContactUsTable").getItem(new PrimaryKey("Id", "123"));
    assertEquals(contactUsRequest.getSubject(), item.get("Subject"));
    assertEquals(contactUsRequest.getUsername(), item.get("Username"));
    assertEquals(contactUsRequest.getPhone(), item.get("Phone"));
    assertEquals(contactUsRequest.getEmail(), item.get("Email"));
    assertEquals(contactUsRequest.getQuestion(), item.get("Question"));
                  


  • Не забываем удалить таблицу после теста
    java
    @After
    public void tearDown() {
        dynamoDB.getTable("ContactUsTable").delete();
    }
                   



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

E2E тесты


Для воспроизведения E2E сценариев лучше поднять тестовые AWS-сервисы конкретно под запуск тестов, если, конечно, есть такая возможность. Это можно сделать с помощью дополнительного шаблона CloudFormation, в котором ресурсы будут копиями основного шаблона.

Тест можно провести, вызвав лямбду через AWS-CLI и обработав результат любым доступным инструментом. Еще можно сделать вызов в юнит-тестах, но без создания моков.

Тесты производительности


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

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

Не забывайте про CloudWatch. С его помощью можно построить довольно показательные дашборды и на их основе определить, какие параметры лямбды и траты вы можете себе позволить.


5. Стоимость Serverless-решений


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

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

Ниже привожу приблизительные подсчеты стоимости лямбды и самого дешевого сервера EC2, чтобы вы поняли, о каких значениях идет речь:


Для расчета брались следующие параметры для лямбды:

  • Время выполнения: 150 миллисекунд
  • Цена: 0,00001667$ за 1ГБ‑с
  • Память: 256 МБ

Для EC2 t2.nano:

  • Цена: 4,75$ за месяц
  • Память: 512 МБ
  • vCPU: 1
  • Максимальная нагрузка: 5% CPU в месяц

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

Все эти подсчеты довольно абстрактны и не учитывают множество факторов: скидки за предоплату или большое количество запросов, время выполнения лямбды и так далее. Но они показывают, что при нагрузке менее 1 запроса в секунду лямбда стоит меньше, чем самый дешевый EC2 сервер.

Кстати, не забудьте прибавить к стоимости EC2 цену ALB, если у вас есть необходимость держать несколько реплик приложения или elastic IPs.


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

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


Полезные ссылки


DINS
Компания

Комментарии 7

    +1
    Спасибо за статью
    Есть пара вопросов:
    1 Подходят ли лямбды реализации редко запускаемых шедулеров?
    2 Как вы считаете насколько оправдано разворачивать свою инфраструктуру для лямбд? Или разумнее использовать провайдера?
    Спасибо
      0
      Насчет шедулеров, это прямо одно из основных направлений я бы сказал. CloudWatch может работать по принципу cron job и триггерить запуск лямбды. Также для лямбды доступно огромное количество триггеров, который могут даже существовать параллельно.
      Насчет своей инфраструктуры для лямбд, мне кажется зависит от специализации. Если огромная компания имеет такую потребность то наверно стоит оценить такую возможность. Естественно тут все упирается в рабочие ресурсы и производительность. Возможно вы потратите больше на зп разработчикам, чем провайдеру. Ну и чтобы добиться того же перформанса что имеется у aws/google придется попотеть))
      +1
      Александр, спасибо большое за две статьи.
      Я недавно работаю с AWS и для меня это было очень полезно!
      Хотел уточнить такой момент.
      В Вашем примере, мы отлавливаем все исключительнные ситуации и всегда возращаем соотвествующий респонс из лямбды (400 либо 500).
      Но если нам будет необхомидо протестить, например, такой кейс — при определенных ошибках лямбда должна сделать рестрат запроса, т.е. попыться обработаь тот же запрос повторно.
      Например, лямбда ходит в некоторый внешний сервис и он, например, временно не доступен или выкинул какую-то ошибку и нам необходимо повторить запрос чуть позже еще раз.
      Правильно ли я понимаю, что:
      Во-первых, в этом случае нам необходимо, чтобы лябмбда брасала определенный exception и не обробытывала его, тогда будет ее рестарт (что определяется настройками retry attempts);
      Во-вторых, в тесте соответственно, проверить, что такой exception босается.

      Спасибо,
      Виталий
        0
        Почти все верно, но по большей степени для асинхронных вызовов. Для них настраивается Retry pollicy (по дефолту 2 раза). Для синхронных запросов, как в моем случае, retry — это обязанность API Gateway либо консьюмера Api Gateway. Можно сделать на стороне JS к примеру.
        +1
        Спасибо! Шикарная статья.
          0
          Надеюсь, что действительно это как-то поможет распространить применение serverless ))
          0
          Отличная статья, спасибо!
          Инфраструктуру намного удобнее писать на CDK, чем на CFN или SAM. Да, там пока нет инструментов для локального тестирования, но сама разработка шаблонов инфраструктуры настолько проще и приятнее (особенно если использовать TS в VSCode), что попробовав однажды обратно дороги уже нет. Некоторый недостаток инструментов деплоя (например, он не умеет собирать пакеты Python по requirements.txt как SAM) легко обходится простейшим Makefile.

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

          Самое читаемое