30

I'd like to use the npm package "request" in an AWS lambda function.

I'm trying to follow the procedure outlined in this article here: https://medium.com/@anjanava.biswas/nodejs-runtime-environment-with-aws-lambda-layers-f3914613e20e

I've created a directory structure like this:

nodejs
│   package-lock.json
│   package.json
└───node_modules

My package.json looks like this:

{
  "name": "my-package-name",
  "version": "1.0.0",
  "description": "whatever",
  "author": "My Name",
  "license": "MIT",
  "dependencies": {
    "request": "^2.88.0"
  }
}

As far as I can tell from the article, all I should have to do with the above is run npm i, zip up the directory, upload it as a layer, and add the layer to my lambda function.

screenshot

I've done all of that, but all that I get when I try to test my function is this:

{
  "errorType": "Runtime.ImportModuleError",
  "errorMessage": "Error: Cannot find module 'request'\nRequire stack:\n- /var/task/index.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js",
  "trace": [
    "Runtime.ImportModuleError: Error: Cannot find module 'request'",
    "Require stack:",
    ...

...as if the layer had never been added. The error is exactly the same whether the layer is added or not. If there's some sort of permissions issue that needs to be resolved, there's nothing in the article that indicates that.

I've tried a few different things, like whether or not my .zip file contains the top-level directory "nodejs" or just its contents. I've tried adding "main": "index.js", to my package.json, with an index.js file like this:

export.modules.request = require('request');

...all to no avail.

What am I missing?

kshetline
  • 12,547
  • 4
  • 37
  • 73

7 Answers7

38

Oh, I can't believe it's just this!

The top-level directory for the .zip file must LITERALLY be named "nodejs"! I was using a different name, and only changed it back to "nodejs" in the text of this post to be more generic, but the directory name was the real problem all along.

Sigh.

kshetline
  • 12,547
  • 4
  • 37
  • 73
  • 2
    Same as you, been struggling with this for hours and that worked 100%. Feel such a fool :) Thank you! – JasonMHirst Mar 18 '20 at 21:56
  • Wait, what exactly did you do? – joeCarpenter May 13 '20 at 22:20
  • @joeCarpenter, I just changed the the name of the top-level directory inside my .zip file to literally be `nodejs`, just like I'd seen in sample code, rather than the other name I'd choose. In my original post here, it says `nodejs` not because that's what I had really been using, but because I was trying to make my personal code look more generic... not realizing that I was stumbling onto the real fix needed. – kshetline May 13 '20 at 23:10
  • @joeCarpenter, Is it possible to elaborate more or show me the sample code. I am still struggling with it. – Ghyath Serhal Mar 17 '22 at 06:49
  • 1
    @GhyathSerhal renaming my zip file to nodejs and then uploading it to AWS via the lambda console seemed to work for me, I think this is what this answer is talking about. – bongoSLAP Mar 18 '22 at 15:12
  • No you need to have the folder `nodejs` inside your zip, not the `node_modules` directly. So you should end up with `nodejs/node_modules/...`. I was having same issue and now fixed (with python you also need `python/...` so that makes sense) – Guillaume May 31 '22 at 12:08
  • This is still unclear to me, what folder is this supposed to end up in on the lambda image? The root? /tmp? LAMBDA_TASK_ROOT? – fullStackChris Dec 15 '22 at 13:22
  • What does this mean you need to change in the case of a CDK project where you don't manage the ZIP file yourself? – Dan Wuensch Mar 20 '23 at 20:12
  • This is probably because Lambdas set `NODE_PATH` to `/opt/nodejs/node12/node_modules/:/opt/nodejs/node_modules:$LAMBDA_RUNTIME_DIR/node_modules` https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime. Layers are extracted under `/opt/`. So your approach puts it under `/opt/nodejs/node_modules` (which appears in the `NODE_PATH`.) – Carl G Jul 03 '23 at 07:27
1

Usually, it's got to do with the name of the folder/files inside. And if those files are referred elsewhere, it's gonna percolate and complain there as well. Just check the folder structure thoroughly, you will be able to catch the thief. I struggled for a day to figure out, it was a silly typo.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Arjun Kalidas
  • 919
  • 9
  • 14
1

I got this error also. The src.zip file should have the source code directly without any parent folder.

For example, if you want to zip src folder, you need to do this.

cd src/ && zip -r ../src.zip .
Linden X. Quan
  • 584
  • 5
  • 18
0

For me, what was causing these issues was having a version of the package.json still inside an older version of the .build folder which had also been deployed. Once I removed that, packages were installed as expected.

LeaveTheCapital
  • 2,530
  • 1
  • 7
  • 11
0

Ok so I found my issue. I was zipping a file containing my lambda instead of just my lambdas root. This was causing the lambda to look for my handler at ./index, but not finding it as it was located at ./nodejs/index.js.

Here is the command i used to properly zip my files from the root:

cd nodejs/
ls # should look like this: node_modules  index.js  package-lock.json  package.json
zip -r ../nodejs.zip ./*

This zips everything properly so that the lambda finds your files at the root of the lambda like in the default configuration for creating a lambda through the aws UI.

0

I was stuck for a while running into this issue in a new project that uses CDK with aws-cdk-lib/aws-lambda-nodejs/NodejsFunction.

The problem for me was that the node module I was using, JSONPath, loaded its own dependencies in a few unusual ways, for example

fs.readFileSync(require.resolve("../include/module.js"))

and

var file = require.resolve('esprima');

This was resulting in runtime errors executing my Lambda due to not being able to find these modules. It also appeared in warnings when synthesizing, for example:

"../include/module.js" should be marked as external for use with "require.resolve" [require-resolve-not-external]

"esprima" should be marked as external for use with "require.resolve" [require-resolve-not-external]

The errors would go away if I manually removed these includes from the bundled index.js file which included the jsonpath library and its dependencies.

To get around this in a less hacky way, you can use the nodeModules property in NodejsFunctionProps.bundling to request that esbuild include certain dependencies in a node_modules folder with the Lambda instead of bundling it in the main file. This can help get around some weird file system quirks like this. For example:

new NodejsFunction(this, 'myLambdaFunction', {
   bundling: {
     nodeModules: ['jsonpath', 'esprima']
   },
   entry: join(__dirname, 'lambdas', 'myLambda.ts'),
   ...nodeJsFunctionProps,
});

For more info, see the AWS @aws-cdk/aws-lambda-nodejs module docs.

Dan Wuensch
  • 104
  • 1
  • 4
-14

Accessing table data from RDS using lambda function with encrypted key (KMS) and Environment variable

Step 1 :- First Enable key in KMS(Key Management Service (KMS)) enter image description here enter image description here enter image description here enter image description here

Review your key Policy and Done! with KMS creation

{
"Id": "key-consolepolicy-3",
"Version": "2012-10-17",
"Statement": [
    {
        "Sid": "Enable IAM User Permissions",
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::163806924483:root"
        },
        "Action": "kms:*",
        "Resource": "*"
    },
    {
        "Sid": "Allow access for Key Administrators",
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::163806924483:user/User1@gmail.com"
        },
        "Action": [
            "kms:Create*",
            "kms:Describe*",
            "kms:Enable*",
            "kms:List*",
            "kms:Put*",
            "kms:Update*",
            "kms:Revoke*",
            "kms:Disable*",
            "kms:Get*",
            "kms:Delete*",
            "kms:TagResource",
            "kms:UntagResource",
            "kms:ScheduleKeyDeletion",
            "kms:CancelKeyDeletion"
        ],
        "Resource": "*"
    },
    {
        "Sid": "Allow use of the key",
        "Effect": "Allow",
        "Principal": {
            "AWS": [
                "arn:aws:iam::163806924483:user/User1@gmail.com",
                "arn:aws:iam::163806924483:user/User2@gmail.com",
                "arn:aws:iam::163806924483:user/User3@gmail.com"
            ]
        },
        "Action": [
            "kms:Encrypt",
            "kms:Decrypt",
            "kms:ReEncrypt*",
            "kms:GenerateDataKey*",
            "kms:DescribeKey"
        ],
        "Resource": "*"
    },
    {
        "Sid": "Allow attachment of persistent resources",
        "Effect": "Allow",
        "Principal": {
            "AWS": [
                "arn:aws:iam::163806924483:user/User1.dilip@gmail.com",
                "arn:aws:iam::163806924483:user/User2@gmail.com",
                "arn:aws:iam::163806924483:user/User3@gmail.com"
            ]
        },
        "Action": [
            "kms:CreateGrant",
            "kms:ListGrants",
            "kms:RevokeGrant"
        ],
        "Resource": "*",
        "Condition": {
            "Bool": {
                "kms:GrantIsForAWSResource": "true"
            }
        }
    }
]
}

enter image description here

Step:- 2 Create a policy in IAM for KMS assign to ur each lambda function

"StringEquals": {
            "kms:EncryptionContext:LambdaFunctionName": [
                "LambdaFunction-1",
                "LambdaFunction-2",
                "LambdaFunction-3"
            ]
        }

enter image description here

Step 3:- Assign a Policy created in Step-2 to ur default lambda Role(1st Lambda need to be created to get default lambda role) enter image description here

Step 4:- Create lambda Function

Node.js Code for lambda Function

const mysql = require('mysql');
const aws = require("aws-sdk");


const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
let res;
let response={};
exports.handler = async(event) => {
reset_globals();

// load env variables
const rds_user = await kms_decrypt(process.env.RDS_USERNAME);
const rds_pwd = await kms_decrypt(process.env.RDS_PASSWORD)

// setup rds connection
var db_connection = await mysql.createConnection({
    host: process.env.RDS_HOSTNAME,
    user: rds_user,
    password: rds_pwd,
    port: process.env.RDS_PORT,
    database: process.env.RDS_DATABASE
});

var sqlQuery = `SELECT doc_id from documents`;
await getValues(db_connection,sqlQuery);

}

async function getValues(db_connection,sql) {
await new Promise((resolve, reject) => {
    db_connection.query(sql, function (err, result) {
        if (err) {
            response = {statusCode: 500, body:{message:"Database Connection Failed", 
             error: err}};
            console.log(response);
            resolve();
        }
        else {
           
            console.log("Number of records retrieved: " + JSON.stringify(result));
            res = result;
           
            resolve();
        }
    });
    });
}

async function kms_decrypt(encrypted) {
const kms = new aws.KMS();
const req = { CiphertextBlob: Buffer.from(encrypted, 'base64'), EncryptionContext: { 
LambdaFunctionName: functionName } };
const decrypted = await kms.decrypt(req).promise();
let cred = decrypted.Plaintext.toString('ascii');
return cred;
}


function reset_globals() {
res = (function () { return; })();
response = {};
}

Now u should see KMS in Lambda. enter image description here

Step 5:- Set Environment Variable and encrypt it.

Lambda ->Functions -> Configuration -> Environment Variable -> Edit

RDS_DATABASE docrds

RDS_HOSTNAME docrds-library.c1k3kcldebmp.us-east-1.rds.amazonaws.com

RDS_PASSWORD root123

RDS_PORT 3306

RDS_USERNAME admin

enter image description here

In Lambda Function to decrypt the encrypted environment variabled use below code

function kms_decrypt(encrypted) {
const kms = new aws.KMS();
const req = { CiphertextBlob: Buffer.from(encrypted, 'base64'), EncryptionContext: { 
LambdaFunctionName: functionName } };
const decrypted = await kms.decrypt(req).promise();
let cred = decrypted.Plaintext.toString('ascii');
return cred;
}

My RDS document table looks like:-

enter image description here

I am accessing column doc_id using sqlQuery in lambda function

var sqlQuery = `SELECT doc_id from documents`;

After testing the lambda function, I get below output.

enter image description here

If u gets SQL import Error, then can must add a layer.

enter image description here

errorType": "Runtime.ImportModuleError",
"errorMessage": "Error: Cannot find module 'mysql'\nRequire stack:\n- 
/var/task/index.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js",
 "trace": [
"Runtime.ImportModuleError: Error: Cannot find module 'mysql'",

You can configure your Lambda function to use additional code and content in the form of layers. A layer is a ZIP archive that contains libraries, a custom runtime, or other dependencies. With layers, you can use libraries in your function without needing to include them in your deployment package.

To include libraries in a layer, place them in the directory structure that corresponds to your programming language.

Node.js – nodejs/node_modules

Python – python

Ruby – ruby/gems/2.5.0

Java – java/lib

First create a zip archieve that contain mysql archieve.

First create a react-project

Then in terminal $project-path > npm init

Then $project-path > npm install mysql

You should see node_modules folder created.

Zip node_modules that folder and upload on layer as shown below.

Then, Goto Lambda--> Layer-->Create layer.

enter image description here enter image description here

enter image description here enter image description here

Ajay
  • 176
  • 6