Что такое AWS Lambda?
Цитируя документацию:
AWS Lambda это вычислительный сервис, в который вы можете загружить свой код, который будет запущен на инфраструктуре AWS по вашему поручению. После загрузки кода и создания того, что мы называем лямбда-функцией, сервис AWS Lambda берёт на себя ответственность за контроль и управление вычислительными мощностями, необходимыми для выполнения данного кода. Вы можете использовать AWS Lambda следующими способами:
- Как событийно-ориентированный вычислительный сервис, когда AWS Lambda запускает ваш код при возникновении некоторых событий, таких как изменение данных в Amazon S3 или таблице Amazon DynamoDB.
- Как вычислительный сервис, который будет запускать ваш код в ответ на HTTP-запрос к Amazon API Gateway или запросам от AWS SDK.
AWS Lambda — очень крутая платформа, но поддерживает всего несколько языков: Java, Node.js и Python. Что же делать, если мы хотим выполнить некоторый код на С++? Ну, вы определённо можете слинковать код на С++ с Java-кодом, да и Python умеет это делать. Но мы посмотрим, как это сделать на Node.js. В мире Node.js интеграция с кодом на С++ традиционно происходит через аддоны. Аддон на С++ к Node.js представляет собой скомпилированный (нативный) модуль Node.js, который может быть вызван из JavaScript или любого другого Node.js-модуля.
Аддоны к Node.js это большая тема — если вы их раньше не писали, возможно, стоит почитать что-то вроде этой серии постов или более узкоспециализировано об интеграции С++ и Node.js в веб-проектах. Есть и хорошая книга на эту тему.
Аддоны в AWS Lambda
Чем же использование аддонов в AWS Lambda отличается от классического сценария их использования? Самая большая проблема состоит в том, что AWS Lambda не собирается вызывать node-gyp или любой другой инструмент сборки перед запуском вашей функции — вы должны собрать полностью функциональный бинарный пакет. Это означает, как минимум, то, что вы должны собрать ваш аддон на Linux перед деплоем в AWS Lambda. А если у вас есть какие-нибудь зависимости, то собирать нужно не просто на Linux, а на Amazon Linux. Есть и другие нюансы, о которых я расскажу дальше.
Эта статья не о построении сложных смешанных приложений на Node.js + C++ в инфраструктуре Amazon, она лишь описывает базовые техники сборки и деплоя таких программ. По остальным темам можно обратиться к документации Amazon — там есть куча примеров.
Я собираюсь написать С++ аддон, который будет содержать функцию, принимающую три числа и возвращающий их среднее значение. Да, я знаю, это вот как-раз то, что можно написать только на С++. Мы выставим данную функцию в качестве доступной для использования через AWS Lambda и протестируем её работу через AWS CLI.
Настройка рабочего окружения
Есть причина, по которой Java с её слоганом "напиши однажды, запускай везде" стала популярной — и эта причина в сложности распространения скомпилированного бинарного кода между разными платформами. Java не решила все эти проблемы идеально («напиши однажды, отлаживай везде»), но с тех пор мы прошли длинный путь. Чаще всего мы блаженно забываем о платформенно-специфичных проблемах, когда пишем код на Node.js — ведь Javascript это платформенно-независимый язык. И даже в случаях, когда Node.js приложения зависят от нативных аддонов, это легко решается на разных платформах благодаря npm и node-gyp.
Многие из этих удобств, однако, теряются при использовании Amazon Lambda — нам необходимо полностью собрать нашу Node.js-программу (и её зависимости). Если мы используем нативный аддон, это означает, что собирать всё необходимое нам придётся на той же архитектуре и платформе, где работает AWS Lambda (64-битный Linux), а кроме того нужно будет использовать ту же самую версию рантайма Node.js, который используется в AWS Lambda.
Требование №1: Linux
Мы, конечно же, можем разрабатывать / тестировать / отлаживать лямбда-функции с аддонами на OS X или Windows, но когда мы дойдём до этапа деплоя в AWS Lambda — нам понадобится zip-файл со всем содержимым модуля Node.js — включая все его зависимости. Нативный код, входящий в состав этого zip-файла, должен запускаться в инфраструктуре AWS Lambda. А это значит, что собирать его нам нужно будет только под Linux. Обратите внимание, что в этом примере я не использую никаких дополнительных библиотек — мой код на С++ полностью независимый. Как я объясню детальнее дальше — если вам нужны зависимости от внешних библиотек, понадобиться пойти немного глубже.
Я буду делать все мои эксперименты в этой статье на Linux Mint.
Требование 2: 64-bit
Это, возможно, следовало назвать требованием №1… По тем же самым причинам, о которых рассказано выше — вам нужно создать для деплоя zip-файл с бинарниками под архитектуру x64. Так что ваш старенький запылившийся 32-битный Linux на виртуалке не подойдёт.
Требование 3: Node.js версии 4.3
На момент написания данной статьи AWS Lambda поддерживает Node.js 0.10 и 4.3. Вам абсолютно точно лучше выбрать 4.3. В будущем актуальная версия может измениться — следите за этим. Я люблю использовать nvm для установки и удобного переключения между версиями Node.js. Если у вас ещё нет этого инструмента — пойдите и установите его прямо сейчас:
curl http://ift.tt/2bKJcZa | bash
source ~/.profile
А теперь установите Node.js 4.3 и node-gyp
nvm install 4.3
npm install -g node-gyp
Требование 4: инструменты для сборки C++ кода (с поддержкой С++11)
Когда вы разрабатываете аддон для Node.js v4+, вы должны использовать компилятор с поддержкой С++11. Последние версии Visual Studio (Windows) и XCode (Mac OS X) подойдут для разработки и тестирования, но, поскольку нам нужно будет собрать всё под Linux, нам понадобиться g++ 4.7 (или более свежий). Вот как установить g++ 4.9 на Mint/Ubuntu:
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
sudo apt-get update
sudo apt-get install g++-4.9
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.9 20
Создаём аддон (локально)
Нам понадобиться создать два Node.js-проекта. Один будет нашим С++ аддоном, который вообще не будет содержать в себе ничего относящегося к AWS Lambda — просто классический нативный аддон. Второй же проект будет лямбда-функций в терминах AWS Lambda — то есть модулем Node.js, который будет импортировать нативный аддон и брать на себя вызов его функционала. Если вы хотите попробовать на своей машине — весь код здесь, а конкретно этот пример — в папке lambda-cpp.
Давайте начнём с аддона.
mkdir lambda-cpp
mkdir lambda-cpp/addon
cd lambda-cpp/addon
Для создания аддона нам понадобятся три файла — код на С++, package.json чтобы сказать Node.js как обращаться с этим аддоном и binding.gyp для процесса сборки. Давайте начнём с самого простого — binding.gyp
{
"targets": [
{
"target_name": "average",
"sources": [ "average.cpp" ]
}
]
}
Это, вероятно, простейший вариант файла binding.gyp, который только возможно создать — мы задаём имя цели и исходники для компиляции. При необходимости здесь можно наворотить сложнейшие вещи, отражающие опции компилятора, пути к внешним каталогам, библиотекам и т.д. Просто помните, что всё, на что вы ссылаетесь должно быть статически слинковано в бинарник и собрано под архитектуру x64.
Теперь давайте создадим package.json, который должен определять точку входа данного аддона:
{
"name": "average",
"version": "1.0.0",
"main": "./build/Release/average",
"gypfile": true,
"author": "Scott Frees <scott.frees@gmail.com> (http://scottfrees.com/)",
"license": "ISC"
}
Ключевой момент здесь — это свойство «main», которое объясняет Node.js, что вот этот конкретный бинарник является точкой входа данного модуля и именно он должен быть загружен каждый раз, когда кто-то делает require('average').
Теперь исходный код. Давайте откроем average.cpp и создадим простой аддон с функцией, которая возвращает среднее значение всех переданных ей параметров (не будем ограничиваться лишь тремя!).
#include <node.h>
using namespace v8;
void Average(const FunctionCallbackInfo<Value>& args) {
Isolate * isolate = args.GetIsolate();
double sum = 0;
int count = 0;
for (int i = 0; i < args.Length(); i++){
if ( args[i]->IsNumber()) {
sum += args[i]->NumberValue();
count++;
}
}
Local<Number> retval = Number::New(isolate, sum / count);
args.GetReturnValue().Set(retval);
}
void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "average", Average);
}
NODE_MODULE(average, init)
Если вы не знакомы с использованием V8, пожалуйста, почитайте об этом какие-нибудь статьи или книгу — данный пост не об этом. Вкратце, макрос NODE_MODULE в конце файла указывает, какая функция должна быть вызвана, когда данный модуль будет загружен. Функция init добавляет новую функцию в список экспортируемых данным модулем — ассоциируя С++ функцию Average с вызываемой из Javascript функцией average.
Мы можем собрать всё это с помощью node-gyp configure build. Если всё прошло хорошо, то вы увидите gyp info ok в конце вывода. В качестве простого теста давайте создадим файл test.js и сделаем из него несколько вызовов:
// test.js
const addon = require('./build/Release/average');
console.log(addon.average(1, 2, 3, 4));
console.log(addon.average(1, "hello", "world", 42));
Запустите этот код с помощью команды node test.js и вы увидите в консоли ответы 2.5 и 21.5. Обратите внимание, что строки "hello" и "world" не повлияли на результаты рассчётов, поскольку аддон проверил входные параметры и использовал в рассчётах лишь числа.
Теперь нужно удалить test.js — он не будет частью нашего аддона, который мы собираемся задеплоить в AWS Lambda.
Создаём лямбда-функцию
А теперь давайте создадим, собственно, лямбда-функцию для AWS Lambda. Как вы (возможно) уже знаете, для AWS Lambda нам необходимо создать обработчик, который будет вызываться каждый раз, когда произойдёт некоторое событие. Этот обработчик получит описание данного события (которое может быть, например, операцией изменения данных в S3 или DynamoDB) в виде JS-объекта. Для этого теста мы используем простое событие, описываемое следующим JSON:
{
op1: 4,
op2: 15,
op3: 2
}
Мы можем сделать это прямо в папке аддона, но я предпочитаю создать отдельный модуль Node.js и подтянуть локальный аддон как npm-зависимость. Давайте создадим новую папку где-то рядом с lambda-cpp/addon, пусть она будет называться lambda-cpp/lambda.
cd ..
mkdir lambda
cd lambda
Теперь создадим файл index.js и напишем в нём следующий код:
exports.averageHandler = function(event, context, callback) {
const addon = require('average');
var result = addon.average(event.op1, event.op2, event.op3)
callback(null, result);
}
Заметьте, что мы сослались на внешнюю зависимость "average". Давайте создадим файл package.json, в котором опишем ссылку на локальный аддон:
{
"name": "lambda-demo",
"version": "1.0.0",
"main": "index.js",
"author": "Scott Frees <scott.frees@gmail.com> (http://scottfrees.com/)",
"license": "ISC",
"dependencies": {
"average": "file:../addon"
}
}
Когда вы выполните команду npm install, npm вытянет ваш локальный аддон и скопирует его в подпапку node_modules, а также вызовет node-gyp для его сборки. Структура ваших папок и файлов после этого будет выглядеть вот так:
/lambda-cpp -- /addon -- average.cpp -- binding.gyp -- package.json -- /lambda -- index.js -- package.json -- node_modules/ -- average/ (contains the binary addon)
Локальное тестирование
Теперь у нас есть файл index.js, экспортирующий обработчик вызовов AWS Lambda и мы можем попробовать загрузить его туда. Но давайте вначале протестируем его локально. Есть отличный модуль, который называется lambda-local — он может помочь нам с тестированием.
npm install -g lambda-local
После его установки мы можем вызвать нашу лямбда-функцию по имени обработчика "averageHandler" и передать ему наше тестовое событие. Давайте создадим файл sample.js и напишем в него:
module.exports = {
op1: 4,
op2: 15,
op3: 2
};
Теперь мы можем выполнить нашу лямбду командой:
lambda-local -l index.js -h averageHandler -e sample.js
Logs
------
START RequestId: 33711c24-01b6-fb59-803d-b96070ccdda5
END
Message
------
7
Как и ожидалось, результат равен 7 (среднее значение чисел 4, 15 и 2).
Деплой с помощью AWS CLI
Есть два способа деплоя кода в AWS Lambda — через веб-интерфейс и через утилиты командной строки (CLI). Я планирую использовать CLI, поскольку данный подход кажется мне более универсальным. Однако, всё описанное далее при желании можно сделать и через веб-интерфейс.
Если у вас ещё нет AWS-аккаунта — сейчас самое время его создать. Дальше нужно создать Администратора. Полная инструкция есть в документации Амазона. Не забудьте добавить созданному Администратору роль AWSLambdaBasicExecutionRole.
Теперь, когда у вас есть пользователь с правами Администратора, нужно получить ключ для конфигурации AWS CLI. Вы можете сделать это через IAM-консоль. Как скачать свой ключ в виде csv-файла рассказывается вот в этой инструкции.
Как только у вас будет ключ, можно устанавливать CLI. Есть несколько способов сделать это, и для установки нам в любом случае понадобится Python. Наиболее простой способ, на мой взгляд, это воспользоваться установщиком:
curl "http://ift.tt/1qk3Vnt" -o "awscli-bundle.zip"
unzip awscli-bundle.zip
sudo ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws
Дальше нужно сконфигурировать CLI. Запустите команду aws configure и введите ваш ключ и секретный код. Вы также можете выбрать регион по-умолчанию и формат вывода. Вы, скорее всего, захотите присоединить профиль к данной конфигурации (поскольку он понадобится дальее) с помощью аргумента --profile.
aws configure --profile lambdaProfile
AWS Access Key ID [None]: XXXXXXXXX
AWS Secret Access Key [None]: XXXXXXXXXXXXXXXXXXXX
Default region name [None]: us-west-2
Default output format [None]:
Вы можете проверить, что всё настроено верно, запустив команду просмотра всех лямбда-функций:
aws lambda list-functions
{
"Functions": []
}
Поскольку мы только начали работу — никаких функций пока нет. Но по крайней мере мы не увидели никаких сообщений об ошибках — это хорошо.
Упаковка лямбда-функции и аддона
Наиболее важный (и часто обсуждаемый в интернете) шаг во всём этом процессе — это убедиться в том, что весь ваш модуль будет упакован в zip-файл корректно. Вот наиболее важные вещи, которые нужно проверить:
- Файл index.js должен быть в корневой папке zip-файла. Вы не должны упаковывать саму папку /lambda-addon/lambda — только её содержимое. Другими словами — если вы распакуете созданный zip файл в текущую папку — файл index.js должен оказаться в этой же папке, а не в подпапке.
- Папка node_modules и всё её содерижмое должно быть упаковано в zip-файл.
- Вы должны собрать аддон и упаковать его в zip-файл на правильной платформе (см. выше требования — Linux, x64 и т.д.)
В папке, где находится index.js, упакуйте все файлы, которые должны быть задеплоены. Я создам zip-файл в родительской папке.
zip -r ../average.zip node_modules/ average.cpp index.js binding.gyp package.json
*Обратите внимание на ключ "-r" — нам нужно упаковать всё содержимое папки node_modules. Проверьте полученный файл командой less, должно получиться что-то такое:
less ../average.zip
Archive: ../average.zip
Length Method Size Cmpr Date Time CRC-32 Name
-------- ------ ------- ---- ---------- ----- -------- ----
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/
1 Stored 1 0% 2016-08-17 17:39 6abf4a82 node_modules/average/output.txt
478 Defl:N 285 40% 2016-08-17 19:02 e1d45ac4 node_modules/average/package.json
102 Defl:N 70 31% 2016-08-17 15:03 1f1fa0b3 node_modules/average/binding.gyp
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/build/
115 Defl:N 110 4% 2016-08-17 19:02 c79d3594 node_modules/average/build/binding.Makefile
3243 Defl:N 990 70% 2016-08-17 19:02 d3905d6b node_modules/average/build/average.target.mk
3805 Defl:N 1294 66% 2016-08-17 19:02 654f090c node_modules/average/build/config.gypi
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/build/Release/
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/build/Release/.deps/
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/build/Release/.deps/Release/
125 Defl:N 67 46% 2016-08-17 19:02 daf7c95b node_modules/average/build/Release/.deps/Release/average.node.d
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/build/Release/.deps/Release/obj.target/
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/build/Release/.deps/Release/obj.target/average/
1213 Defl:N 386 68% 2016-08-17 19:02 b5e711d9 node_modules/average/build/Release/.deps/Release/obj.target/average/average.o.d
208 Defl:N 118 43% 2016-08-17 19:02 c8a1d92a node_modules/average/build/Release/.deps/Release/obj.target/average.node.d
13416 Defl:N 3279 76% 2016-08-17 19:02 d18dc3d5 node_modules/average/build/Release/average.node
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/build/Release/obj.target/
0 Stored 0 0% 2016-08-17 19:02 00000000 node_modules/average/build/Release/obj.target/average/
5080 Defl:N 1587 69% 2016-08-17 19:02 6aae9857 node_modules/average/build/Release/obj.target/average/average.o
13416 Defl:N 3279 76% 2016-08-17 19:02 d18dc3d5 node_modules/average/build/Release/obj.target/average.node
12824 Defl:N 4759 63% 2016-08-17 19:02 f8435fef node_modules/average/build/Makefile
554 Defl:N 331 40% 2016-08-17 15:38 18255a6e node_modules/average/average.cpp
237 Defl:N 141 41% 2016-08-17 19:02 7942bb01 index.js
224 Defl:N 159 29% 2016-08-17 18:53 d3d59efb package.json
-------- ------- --- -------
55041 16856 69% 26 files
(type 'q' to exit less)
Если вы не видите содержимого папки node_modules внутри zip-файла или если файлы имеют дополнительный уровень вложенности в иерархии папок — ещё раз перечитайте всё, что написано выше!
Загрузка в AWS Lambda
Теперь мы можем создать лямбда-функцию с помощью команды «lambda create-function».
aws lambda create-function \
--region us-west-2 \
--function-name average \
--zip-file fileb://../average.zip \
--handler index.averageHandler \
--runtime nodejs4.3 \
--role arn:aws:iam::729041145942:role/lambda_execute
Большинство параметров говорят сами за себя — но если вы не знакомы с AWS Lambda, то параметр "role" может для вас выглядеть несколько загадочно. Как говорилось выше, для работы с AWS Lambda вам необходимо было создать роль, имеющую разрешение AWSLambdaBasicExecutionRole. Вы можете получить строку, начинающуюся с "arn:" для этой роли через веб-интерфейс IAM (кликнув на этой роли).
Если всё пройдёт хорошо, вы должны получить JSON с ответом, содержащим некоторую дополнительную информацию о только что задеплоеной лямбда-функции.
Тестирование с помощью AWS CLI
Теперь, когда мы задеплоили нашу лямбда-функцию, давайте протестируем её с помощью того же интерфейса командной строки. Вызовем нашу функцию, передав ей описание того же самого события, что и в прошлый раз.
aws lambda invoke \
--invocation-type RequestResponse \
--function-name average \
--region us-west-2 \
--log-type Tail \
--payload '{"op1":4, "op2":15, "op3":2}' \
--profile lambdaProfile \
output.txt
Вы получите ответ вот в такой форме:
{
"LogResult": "U1RBUlQgUmVxdWVzdElkOiAxM2UxNTk4ZC02NGMxLTExZTYtODQ0Ny0wZDZjMjJjMTRhZWYgVmVyc2lvbjogJExBVEVTVApFTkQgUmVxdWVzdElkOiAxM2UxNTk4ZC02NGMxLTExZTYtODQ0Ny0wZDZjMjJjMTRhZWYKUkVQT1JUIFJlcXVlc3RJZDogMTNlMTU5OGQtNjRjMS0xMWU2LTg0NDctMGQ2YzIyYzE0YWVmCUR1cmF0aW9uOiAwLjUxIG1zCUJpbGxlZCBEdXJhdGlvbjogMTAwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMzUgTUIJCg==",
"StatusCode": 200
}
Не очень пока что понятно, но это легко исправить. Параметр "LogResult" закодировано в base64, так что мы можем раскодировать его:
echo U1RBUlQgUmVxdWVzdElkOiAxM2UxNTk4ZC02NGMxLTExZTYtODQ0Ny0wZDZjMjJjMTRhZWYgVmVyc2lvbjogJExBVEVTVApFTkQgUmVxdWVzdElkOiAxM2UxNTk4ZC02NGMxLTExZTYtODQ0Ny0wZDZjMjJjMTRhZWYKUkVQT1JUIFJlcXVlc3RJZDogMTNlMTU5OGQtNjRjMS0xMWU2LTg0NDctMGQ2YzIyYzE0YWVmCUR1cmF0aW9uOiAwLjUxIG1zCUJpbGxlZCBEdXJhdGlvbjogMTAwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMzUgTUIJCg== | base64 --decode
START RequestId: 13e1598d-64c1-11e6-8447-0d6c22c14aef Version: $LATEST
END RequestId: 13e1598d-64c1-11e6-8447-0d6c22c14aef
REPORT RequestId: 13e1598d-64c1-11e6-8447-0d6c22c14aef Duration: 0.51 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 35 MB
Стало немного более читабельно, но всё же дало нам не очень много понимания о произошедшим. Это потому, что наша лямбда-функция не написала ничего в лог-файл. Если вы хотите увидеть результат — то можете протестировать функцию через веб-интерфейс, где легче увидеть входные и выходные параметры. А пока вы можете изменить свой файл index.js, перепаковать zip-файл, передеплоить его и вызвать свою функцию снова:
exports.averageHandler = function(event, context, callback) {
const addon = require('./build/Release/average');
console.log(event);
var result = addon.average(event.op1, event.op2, event.op3)
console.log(result);
callback(null, result);
}
После декодирования ответа вы увидите что-то вроде этого:
START RequestId: 1081efc9-64c3-11e6-ac21-43355c8afb1e Version: $LATEST
2016-08-17T21:39:24.013Z 1081efc9-64c3-11e6-ac21-43355c8afb1e { op1: 4, op2: 15, op3: 2 }
2016-08-17T21:39:24.013Z 1081efc9-64c3-11e6-ac21-43355c8afb1e 7
END RequestId: 1081efc9-64c3-11e6-ac21-43355c8afb1e
REPORT RequestId: 1081efc9-64c3-11e6-ac21-43355c8afb1e Duration: 1.75 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 17 MB
Дальнейшие планы
Итак, на данный момент у нас имеется на 100% рабочая AWS Lambda-функция, которая вызывает С++ код из аддона. Мы, конечно, пока не сделали чего-то реально полезного. Поскольку наша лямбда-функция делает некоторые рассчёты, следующим логичным шагом будет привязать её к Gateway API, чтобы входные параметры можно было брать из HTTP-запросов. О том, как это сделать, вы можете почитать в Getting Started — секцию о вызове лямбда-функций.
Я надеюсь, вы теперь убедились, что деплой С++ кода в AWS Lambda возможен и даже не слишком сложен — достаточно придерживаться описанных в начале статьи требования по сборке, и всё будет хорошо. Остальные шаги достаточно тривиальны и полностью аналогичны деплою любой лямбда-функции в AWS. Как я уже говорил, если ваш аддон требует каких-то зависимостей, их придётся статически слинковать в его бинарник.
Весь код из данной статьи доступен здесь.
Комментарии (0)