javascriptaws-lambda

Load and execute a javascript file from S3 in a Lambda


There is a hello.js in S3 that contains

function greet() {
    console.log(" From Greetings: ");
}

A nodeJS lambda that is trying to load this file and execute the script. The permissions to access s3 is working, the script is able to load. But what happens when it calls exec is unclear. There are no errors, but there is also no "From Greetings" printed in the logs.

const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const { exec } = require('child_process');

exports.handler = async (event, context) => {
    const s3Bucket = 'monitor-state';
    const s3Key = 'hello.js';

    

    const params = { Bucket: s3Bucket, Key: s3Key };

    try {
        const data = await s3.getObject(params).promise();
        const script = data.Body.toString();

        // Execute the script
        exec(`node -e "${script}"`, (error, stdout, stderr) => {
            if (error) {
                console.error(`Script execution error: ${error}`);
                return context.fail('Error executing the script.');
            } else {
                console.log('Script execution output:');
                console.log(stdout);
                return context.succeed('Script executed successfully.');
            }
        });
    } catch (error) {
        console.error('Error fetching the script from S3:', error);
        return context.fail('Error executing the script.');
    }
};

I have tried a few alternatives in how I call exec, none of them seem to work.


Solution

  • Your lambda handler runs exec but doesn't wait for the child process to complete.

    exec returns right away, and since your handler is async, the handler returns a completed promise right after it. This is enough for Lambda to stop the processing.

    Turn your exec callback into a promise and await on it:

    const AWS = require("aws-sdk");
    const s3 = new AWS.S3();
    const { exec } = require("child_process");
    
    exports.handler = async () => {
      const s3Bucket = "monitor-state";
      const s3Key = "hello.js";
    
      const params = { Bucket: s3Bucket, Key: s3Key };
    
      const data = await s3.getObject(params).promise();
      const script = data.Body.toString();
    
      // Execute the script
      const [stdout, stderr] = await new Promise((resolve, reject) =>
        exec(`node -e "${script}"`, (error, stdout, stderr) => {
          if (error) {
            reject(error);
          }
          resolve([stdout, stderr]);
        }),
      );
      console.log('Script execution output:');
      console.log(stdout);
    };
    

    You don't need to use context if you are using promises, all rejected promises will be treated as errors and logged to CloudWatch as such.

    Also, what is the use case for loading code from S3 on each Lambda invocation? Depending on your task, it might be better to put it on the Lambda layer or use Step functions.