Debugging NodeJS Lambda functions locally using LocalStack

Debugging NodeJS Lambda functions locally using LocalStack

Unlock the power of local development & debugging for NodeJS Lambda functions with LocalStack & VS Code integration

AWS Lambda is a compute service that enables you to build and deploy serverless, event-driven applications. While developing these serverless functions, debugging is often seen as tedious and time-consuming, with the developers needing to upload and trigger their code every time they deploy. This development setup is often cumbersome, requiring you to redeploy and re-trigger your Lambda function until you get to the desired behavior.

LocalStack is a cloud emulator enabling you to build and deploy your Lambda functions locally. Unlike SAM CLI, LocalStack allows you to build and integrate your Lambda functions with other AWS services, like DynamoDB, API Gateway, and more! It allows you to debug your Lambda functions on your local machine without having to configure anything in your real AWS account and emulate a native behavior that significantly improves your Lambda development & testing experience.

In this blog, we will walk through how we can set up a remote debugging configuration with LocalStack & VS Code to debug your NodeJS Lambda functions. With LocalStack’s local Lambda debugging, you can quickly debug your NodeJS Lambdas by setting breakpoints, while local code mounting will allow you to validate your changes instantly.

Prerequisites

For this tutorial, you will need the following:

  • LocalStack to create a local Lambda function

  • awslocal command-line interface to run local AWS commands against LocalStack

  • VS Code to run a debug configuration

  • Docker Compose to launch LocalStack with additional configuration

Setting up LocalStack

To get started with remote debugging your Lambda functions, we need to configure LocalStack before using it. For local code mounting our Lambda functions, we need to disable the flag that determines whether Lambda code is copied or mounted into containers.

We would also need to pass additional flags to Lambda Docker to set our Node options and the port to be exposed. We need to configure specific environmental variables available via LocalStack configuration options to do that.

Here is an illustration that depicts how LocalStack makes it easy for you to mount your source code to a Lambda directly, and allows you to interact with local Cloud APIs while configuring a specialized debug port:

For this blog, we will start LocalStack with a Docker-Compose setup. Create a new file named docker-compose.yml and add the following configuration:

version: "3.8"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack:latest
    ports:
      - "127.0.0.1:4566:4566"            # LocalStack Gateway
      - "127.0.0.1:4510-4559:4510-4559"  # external services port range
    environment:
      - DEBUG=1
      - LAMBDA_REMOTE_DOCKER=0
      - LAMBDA_DOCKER_FLAGS=-e NODE_OPTIONS=--inspect-brk=0.0.0.0:9229 -p 9229:9229
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

The LAMBDA_REMOTE_DOCKER configuration is set to 0 (deactivated) to ensure that the Lambda volume mounts work while we mount a temporary folder on the host. It allows you to mount your source code directly into a Lambda and run it, rather than zipping your code and deploying it repeatedly!

The LAMBDA_DOCKER_FLAGS configures the Node options and defines a Docker flag that exposes port 9229 for debugging the Lambda handler code that will run inside the container. Now with our Docker-Compose setup configured, let us start LocalStack:

$ docker compose up -d

Your LocalStack container will now be ready to build & test against! Optionally, if you don’t prefer using Docker-Compose, you can start LocalStack using the localstack CLI:

$ LAMBDA_DOCKER_FLAGS='-e NODE_OPTIONS=--inspect-brk=0.0.0.0:9229 -p 9229:9229' \
LAMBDA_REMOTE_DOCKER=0 \
localstack start -d

Setting up remote debugging

After setting up LocalStack and launching it in detached mode, we can now configure our remote debugging. To get started, create a new directory named .vscode on the root directory and create a new file named tasks.json inside it.

We will use Tasks inside VS Code to run scripts and start processes without providing any external configuration. Let us add a new task by adding the following code to our tasks.json file:

{
    "version": "2.0.0",
    "tasks": [
        {
          "label": "Wait Remote Debugger Server",
          "type": "shell",
          "command": "while [[ -z $(docker ps | grep :9229) ]]; do sleep 1; done; sleep 1;"
        }
    ]
}

Through this task, we will wait for the remote debugger server we have launched alongside LocalStack and wait for it on port 9229. Let us now go ahead and create a new file named launch.json inside the .vscode directory.

A launch.json file will support our debugging scenario, and we can define attributes to support our configuration. Let us add the following code in our launch.json file:

{
    "version": "0.2.0",
    "configurations": [
        {
            "address": "127.0.0.1",
            "localRoot": "${workspaceFolder}",
            "name": "Attach to Remote Node.js",
            "port": 9229,
            "remoteRoot": "/var/task/",
            "request": "attach",
            "type": "node",
            "preLaunchTask": "Wait Remote Debugger Server"
        },
    ]
}

The preLaunchTask, will check if the port is available to connect for debugging while remoteRoot is configured to point towards the directory where the Docker container will mount the Lambda. We are now ready with our remote debugging configuration! Let us create a new NodeJS Lambda function and set breakpoints while we inspect the execution flow.

Creating a NodeJS Lambda locally

We will create a simple NodeJS Lambda function to send a static response! Create a new file named function.js and add the following code:

exports.handler = async (event) => {
    console.log(event);
    const response = {
        statusCode: 200,
        body: "ok",
    };
    return response;
};

The Lambda function handles an incoming event and returns a simple response to the client. To start debugging, we first need to create the Lambda function on LocalStack. We will deploy the Lambda using a unique S3 bucket indicated by hot-reload as the bucket name.

The S3 key path should point to the directory of your Lambda function code. The handler is referenced by the filename of your Lambda function, where the code inside is invoked. Run the following command to create a Lambda function locally:

$ awslocal lambda create-function \
    --function-name localstack-nodejs-lambda-function \
    --code S3Bucket="hot-reload",S3Key="$(pwd)/" \
    --handler function.handler \
    --runtime nodejs14.x \
    --timeout 120 \
    --role arn:aws:iam::000000000000:role/lambda-role

We have referenced the function name as localstack-nodejs-lambda-function and set the timeout to 120 seconds. We are using a mock IAM role here, and you can further use LocalStack's IAM to enforce actual policies while working with local cloud APIs in LocalStack. The handler is configured function.handler, and we have configured the Lambda runtime.

After the Lambda function is created, you can list down all Lambda functions using the following command:

$ awslocal lambda list-functions
{
    "Functions": [
        {
            "FunctionName": "localstack-nodejs-lambda-function",
            "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:localstack-nodejs-lambda-function",
            "Runtime": "nodejs14.x",
            "Role": "cool-stacklifter",
            "Handler": "function.handler",
         …
        }
}

Let us now set up breakpoints and check out our execution flow.

Debugging Lambda functions locally

Bring up the Run and Debug view on VS Code by selecting the Run and Debug icon in the Activity Bar on the side of VS Code. We have already configured our running and debugging; hence you might be able to notice the Attach to Remote Node.js configuration in the Configuration dropdown in the Run and Debug view. Start debugging by clicking the Start Debugging button (or by clicking F5).

It will start a debugging session and execute the script defined as a VS Code task. Let us now set up breakpoints — Navigate to your function.js script, and set up breakpoints as you desire. After the breakpoints are configured, run your Lambda function using the following command:

$ awslocal lambda invoke \
    --function-name localstack-nodejs-lambda-function test.lambda.log \
    --cli-binary-format raw-in-base64-out \
    --payload '{"hello":"world"}'

You will automatically see our Lambda debugger in action:

LocalStack's NodeJS Lambda debugger in action

You can now see the Variables and Call Stack and continue to analyze your Lambda function. You can additionally continue making changes to your code and invoke the function without having to re-deploying your Lambda over and over again!

Conclusion

Debugging your Lambda function with LocalStack in your IDE is a great way to enable quick feedback cycles and to test and validate your code logic before pushing it to production. This allows for a rapid development & debug loop without the need to redeploy the Lambda after the handler is changed! However, due to the ports used by the debugger, you can currently only debug one Lambda at a time, due to which multiple concurrent invocations will not work.

Check out the above code on our repository and our remote debugging documentation to see how you can set up similar configurations for Python and Java!