📜 ⬆️ ⬇️

Using C ++ in AWS Lambda

In this article I plan to describe the process of creating and deploying AWS lambda functions that will call native code from the C ++ addon. As you can see, this process is not much different from creating normal AWS Lambda functions on Node.js — all you have to do is configure your environment according to AWS requirements.

What is AWS Lambda?



Citing documentation:
')
AWS Lambda is a computational service into which you can upload your code, which will be run on the AWS infrastructure on your behalf. After downloading the code and creating what we call a lambda function, AWS Lambda takes responsibility for monitoring and managing the computing power required to execute this code. You can use AWS Lambda in the following ways:

  • As an event-oriented computing service, when AWS Lambda runs your code when certain events occur, such as data changes in Amazon S3 or Amazon DynamoDB table.
  • As a computing service that will run your code in response to an HTTP request to Amazon API Gateway or requests from AWS SDK.


AWS Lambda is a very cool platform, but it supports only a few languages: Java, Node.js, and Python. What to do if we want to execute some code in C ++? Well, you can definitely link C ++ code with Java code, and Python can do it. But we will see how to do this on Node.js. In the world of Node.js, integration with C ++ code traditionally occurs through addons. C ++ addon to Node.js is a compiled (native) Node.js module that can be called from JavaScript or any other Node.js module.


Addons to Node.js are a big topic - if you haven’t written them before, you may want to read something like this series of posts or be more specialized about integrating C ++ and Node.js in web projects. There is a good book on this topic.

Addons in AWS Lambda



What is the use of add-ons in AWS Lambda differs from the classic scenario of their use? The biggest problem is that AWS Lambda is not going to call node-gyp or any other build tool before running your function — you must build a fully functional binary package. This means, at a minimum, that you must build your addon on Linux before deploying it to AWS Lambda. And if you have any dependencies, then you need to build not just on Linux, but on Amazon Linux. There are other nuances, which I will discuss next.

This article is not about building complex mixed applications on Node.js + C ++ in the Amazon infrastructure, it only describes the basic techniques of building and deploying such programs. For other topics, you can refer to the Amazon documentation - there are a bunch of examples.

I'm going to write a C ++ addon that will contain a function that takes three numbers and returns their average value. Yes, I know, this is something that can only be written in C ++ . We will set this feature as available for use through AWS Lambda and test its operation via the AWS CLI.

Setting up the working environment


There is a reason why Java with its slogan " write once, run everywhere " has become popular - and this reason is in the difficulty of distributing compiled binary code between different platforms. Java did not solve all these problems perfectly (“write once, debug everywhere”), but since then we have come a long way. Most often, we blissfully forget about platform-specific problems when we write code on Node.js - after all, Javascript is a platform-independent language. And even in cases where Node.js applications depend on native addons, this is easily solved on different platforms thanks to npm and node-gyp .

Many of these amenities, however, are lost when using Amazon Lambda - we need to fully build our Node.js program (and its dependencies). If we use a native addon, this means that we will have to collect everything we need on the same architecture and platform where AWS Lambda (64-bit Linux) works, and in addition, we will need to use the same version of Node.js runtime that is used in AWS Lambda.

Requirement # 1: Linux


Of course, we can develop / test / debug lambda functions with addons on OS X or Windows, but when we get to the deployment stage in AWS Lambda, we will need a zip file with all the contents of the Node.js module - including all its dependencies . The native code that is part of this zip file must be run on the AWS Lambda infrastructure. And this means that we will need to collect it only under Linux. Please note that in this example I do not use any additional libraries - my C ++ code is completely independent. As I will explain in more detail later, if you need dependencies on external libraries, you need to go a little deeper.

I will do all my experiments in this article on linux mint.

Requirement 2: 64-bit


This, perhaps, should have been called requirement # 1 ... For the very same reasons described above, you need to create a zip file for binary files for x64 architecture. So your old dusty 32-bit Linux on a virtual machine will not work.

Requirement 3: Node.js version 4.3


At the time of this writing, AWS Lambda supports Node.js 0.10 and 4.3. Absolutely better for you to choose 4.3. In the future, the current version may change - follow this. I love using nvm to install and conveniently switch between Node.js versions. If you do not already have this tool - go and install it right now:

curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash source ~/.profile 


Now install Node.js 4.3 and node-gyp

 nvm install 4.3 npm install -g node-gyp 


Requirement 4: tools for building C ++ code (supporting C ++ 11)


When you develop an addon for Node.js v4 +, you should use a compiler with C ++ 11 support. The latest versions of Visual Studio (Windows) and Xcode (Mac OS X) are suitable for development and testing, but since we need to build everything under Linux, we need g ++ 4.7 (or more recent). Here's how to install g ++ 4.9 on 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 


Create addon (locally)



We need to create two Node.js projects. One will be our C ++ addon, which generally will not contain anything related to AWS Lambda - just a classic native addon. The second project will be lambda functions in terms of AWS Lambda - that is, the Node.js module, which will import the native addon and take on the challenge of its functionality. If you want to try it on your machine - all the code here , and specifically this example is in the lambda-cpp folder.

Let's start with the addon.

 mkdir lambda-cpp mkdir lambda-cpp/addon cd lambda-cpp/addon 


To create the addon, we need three files - C ++ code, package.json to tell Node.js how to handle this addon and binding.gyp for the build process. Let's start with the simplest - binding.gyp

 { "targets": [ { "target_name": "average", "sources": [ "average.cpp" ] } ] } 


This is probably the simplest version of the binding.gyp file, which is only possible to create - we set the name of the target and the source code for compilation. If necessary, you can bloat up the most complicated things, reflecting compiler options, paths to external directories, libraries, etc. Just remember that everything that you refer to must be statically linked to the binary and compiled for x64 architecture.

Now let's create package.json, which should define the entry point of this addon:

 { "name": "average", "version": "1.0.0", "main": "./build/Release/average", "gypfile": true, "author": "Scott Frees <scott.frees@gmail.com> (http://scottfrees.com/)", "license": "ISC" } 


The key point here is the “main” property, which explains Node.js, that this particular binary is the entry point of this module and it should be loaded every time someone does require ('average').

Now the source code. Let's open average.cpp and create a simple addon with a function that returns the average of all parameters passed to it (we will not be limited to only three!).

 #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) 


If you are not familiar with using V8, please read some articles or a book about this - this post is not about that. In short, the NODE_MODULE macro at the end of the file indicates which function should be called when this module is loaded. The init function adds a new function to the list exported by this module — associating the C ++ function Average with the average function called from Javascript.

We can build all this with node-gyp configure build . If everything went well, you will see gyp info ok at the end of the output. As a simple test, let's create the test.js file and make some calls from it:

 // test.js const addon = require('./build/Release/average'); console.log(addon.average(1, 2, 3, 4)); console.log(addon.average(1, "hello", "world", 42)); 


Run this code using the node test.js command and you will see the responses 2.5 and 21.5 on the console. Please note that the lines " hello " and " world " did not affect the results of the calculations, because the addon checked the input parameters and used only numbers in the calculations.

Now we need to remove test.js - it will not be part of our addon, which we are going to fix in AWS Lambda.

Create a lambda function



And now let's create, in fact, a lambda function for AWS Lambda. As you (probably) already know, for AWS Lambda we need to create a handler that will be called each time a certain event occurs. This handler will receive the description of this event (which may be, for example, an operation of changing data in S3 or DynamoDB) as a JS object. For this test, we use a simple event described by the following JSON:

 { op1: 4, op2: 15, op3: 2 } 


We can do this right in the addon folder, but I prefer to create a separate Node.js module and pull up the local addon as an npm dependency. Let's create a new folder somewhere near lambda-cpp / addon , let it be called lambda-cpp / lambda .

 cd .. mkdir lambda cd lambda 


Now create the index.js file and write the following code in it:

 exports.averageHandler = function(event, context, callback) { const addon = require('average'); var result = addon.average(event.op1, event.op2, event.op3) callback(null, result); } 


Notice that we referred to the " average " external dependency. Let's create a package.json file, in which we will describe a link to a local addon:

 { "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" } } 


When you run the npm install command, npm will pull out your local addon and copy it to the node_modules subfolder, and also call node-gyp to build it. The structure of your folders and files after this will look like this:

 / lambda-cpp
  - / addon
     - average.cpp
     - binding.gyp
     - package.json
  - / lambda
     - index.js
     - package.json
     - node_modules /
       - average / (contains the binary addon)


Local testing



Now we have an index.js file that exports the AWS Lambda call handler and we can try loading it there. But let's first test it locally. There is a great module called lambda-local — it can help us with testing.

 npm install -g lambda-local 


After its installation, we can call our lambda function by the name of the " averageHandler " handler and pass our test event to it. Let's create the sample.js file and write to it:

 module.exports = { op1: 4, op2: 15, op3: 2 }; 


Now we can execute our lambda with the command:

 lambda-local -l index.js -h averageHandler -e sample.js Logs ------ START RequestId: 33711c24-01b6-fb59-803d-b96070ccdda5 END Message ------ 7 


As expected, the result is 7 (the average of the numbers 4, 15 and 2).

Warmth with AWS CLI



There are two ways to deploy code in AWS Lambda - through a web interface and through command line utilities (CLI). I plan to use the CLI, since this approach seems more universal to me. However, everything described below can also be done via the web interface.

If you do not have an AWS account yet, now is the time to create one. Next you need to create an Administrator. Full instructions are in the documentation of Amazon . Do not forget to add the AWSLambdaBasicExecutionRole role to the created Administrator.

Now that you have a user with Administrator privileges, you need to get a key for configuring AWS CLI. You can do this through the IAM console. How to download your key in the form of a csv-file is described here in this instruction .

Once you have the key, you can install the CLI. There are several ways to do this, and for installation we will need Python in any case. The easiest way, in my opinion, is to use the installer:

 curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip" unzip awscli-bundle.zip sudo ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws 


Next you need to configure the CLI. Run the aws configure command and enter your key and secret code. You can also select a default region and output format. You will most likely want to attach the profile to this configuration (as you will need it later) with the argument --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]: 


You can check that everything is configured correctly by running the command to view all the lambda functions:

 aws lambda list-functions { "Functions": [] } 


Since we have just begun work, there are no functions yet. But at least we have not seen any error messages - this is good.

Packaging lambda functions and addon



The most important (and often discussed on the Internet) step in the whole process is to make sure that your entire module will be packed into a zip file correctly. Here are the most important things to check:

  1. The index.js file must be in the root folder of the zip file. You should not package the / lambda-addon / lambda folder itself - only its contents. In other words - if you unzip the created zip file into the current folder - the index.js file should be in the same folder, not in a subfolder.
  2. The node_modules folder and all its contents should be packaged in a zip file.
  3. You must build the addon and package it in a zip file on the correct platform (see above requirements - Linux, x64, etc.)


In the folder where index.js is located, pack all the files that need to be packed. I will create a zip file in the parent folder.

 zip -r ../average.zip node_modules/ average.cpp index.js binding.gyp package.json 


* Pay attention to the key "-r" - we need to pack the entire contents of the node_modules folder. Check the resulting file with the command less, you should get something like this:

 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.od 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) 


If you don’t see the contents of the node_modules folder inside the zip file or if the files have an additional level of nesting in the folder hierarchy, reread everything that is written above!

Download to AWS Lambda



Now we can create a lambda function using the “lambda create-function” command.

 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 


Most of the parameters speak for themselves - but if you are not familiar with AWS Lambda, then the " role " parameter may look somewhat mysterious to you. As mentioned above, to work with AWS Lambda, you had to create a role that has AWSLambdaBasicExecutionRole permission. You can get a line starting with " arn: " for this role through the IAM web interface (by clicking on this role).

If everything goes well, you should get a JSON with a response that contains some additional information about the newly added lambda function.

Testing with AWS CLI



Now that we have secured our lambda function, let's test it using the same command line interface. Let's call our function, passing it the description of the same event as last time.

 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 


You will receive an answer in this form:

 { "LogResult": "U1RBUlQgUmVxdWVzdElkOiAxM2UxNTk4ZC02NGMxLTExZTYtODQ0Ny0wZDZjMjJjMTRhZWYgVmVyc2lvbjogJExBVEVTVApFTkQgUmVxdWVzdElkOiAxM2UxNTk4ZC02NGMxLTExZTYtODQ0Ny0wZDZjMjJjMTRhZWYKUkVQT1JUIFJlcXVlc3RJZDogMTNlMTU5OGQtNjRjMS0xMWU2LTg0NDctMGQ2YzIyYzE0YWVmCUR1cmF0aW9uOiAwLjUxIG1zCUJpbGxlZCBEdXJhdGlvbjogMTAwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMzUgTUIJCg==", "StatusCode": 200 } , { "LogResult": "U1RBUlQgUmVxdWVzdElkOiAxM2UxNTk4ZC02NGMxLTExZTYtODQ0Ny0wZDZjMjJjMTRhZWYgVmVyc2lvbjogJExBVEVTVApFTkQgUmVxdWVzdElkOiAxM2UxNTk4ZC02NGMxLTExZTYtODQ0Ny0wZDZjMjJjMTRhZWYKUkVQT1JUIFJlcXVlc3RJZDogMTNlMTU5OGQtNjRjMS0xMWU2LTg0NDctMGQ2YzIyYzE0YWVmCUR1cmF0aW9uOiAwLjUxIG1zCUJpbGxlZCBEdXJhdGlvbjogMTAwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMzUgTUIJCg==", "StatusCode": 200 } 


Not very clear yet, but it is easy to fix. The " LogResult " parameter is encoded in base64 , so that we can decode it:

 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 


It became a little more readable, but still gave us not a lot of understanding about what happened. This is because our lambda function did not write anything to the log file. If you want to see the result, you can test the function through the web interface, where it is easier to see the input and output parameters. In the meantime, you can change your index.js file, repack the zip file, redo it and call your function again:

 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); } 


After decoding the answer, you will see something like this:

 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 


Future plans



So, at the moment we have a 100% working AWS Lambda function that calls C ++ code from the addon. Of course, we have not yet done something really useful. Since our lambda function does some calculations, the next logical step is to bind it to the Gateway API so that input parameters can be taken from HTTP requests. For information on how to do this, you can read in the Getting Started section on calling lambda functions.

I hope you have now become convinced that deploying C ++ code in AWS Lambda is possible and not even too complicated - just follow the assembly requirements described at the beginning of the article and everything will be fine. The remaining steps are quite trivial and are completely analogous to the deployment of any lambda function in AWS. As I said, if your addon requires some dependencies, they will have to be statically linked to its binary.

All code from this article is available here .

Source: https://habr.com/ru/post/308330/


All Articles