CLOVER🍀

That was when it all began.

LocalStackにデプロイしたAWS Lambda関数(Node.js)をGoogle Chrome DevToolsでデバッグする

これは、なにをしたくて書いたもの?

LocalStackのドキュメントを見ていて、AWS Lambdaのリモートデバッグについて記載があるのに気づきまして。

Remote Debugging | Docs

このドキュメントに書かれているのはPythonJavaなのですが、読んでいてNode.jsでもできるのでは?と思って試してみました。

結論

LocalStackのLAMBDA_DOCKER_FLAGS環境変数を使い、AWS Lambda関数のDockerコンテナ実行時にオプションを指定します。

Configuration / Local AWS Services / Lambda

指定するのは-p [port]:[port]、それからNODE_OPTIONS環境変数です。NODE_OPTIONS環境変数には--inspect--inspect-brk
--inspect-portオプションを指定します。

Command-line API | Node.js v16.16.0 Documentation

Google Chrome DevTools等のツールで、デバッグできるようになるオプションですね。

--inspect--inspect-brk--inspect-portを指定する際には、0.0.0.0にバインドするところがポイントです。また、-pオプションで
指定するポートも--inspect--inspect-brk--inspect-portに合わせる必要があります。

つまり、こうなります。

LAMBDA_DOCKER_FLAGS='-p 9229:9229 -e NODE_OPTIONS="--inspect=0.0.0.0:9229"'

この発想に至ったのは、こちらのドキュメントに書かれているリモートデバッグの方法を見て、同じようにリモートデバッグできる言語なら
環境変数経由でオプションを指定すればできるのでは?と思ったからですね。

Remote Debugging | Docs

では、実際に試してみましょう。

環境

今回の環境は、こちらです。

$ localstack --version
1.0.0

AWS Lambda関数の作成には、AWS SAMとLocalStack提供のCLIを使うことにします。

$ samlocal --version
SAM CLI, version 1.36.0

AWS Lambda関数を作成する

今回のAWS Lambda関数は、AWS SAM CLIで生成できるデフォルトのものを使うことにします。

$ samlocal init --name localstack-nodejs-lambda-debug --runtime nodejs14.x --app-template hello-world  --package-type Zip

プロジェクト内へ移動。

$ cd localstack-nodejs-lambda-debug

主要な生成されたファイルを確認。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  localstack-nodejs-lambda-debug

  Sample SAM Template for localstack-nodejs-lambda-debug

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

hello-world/package.json

{
  "name": "hello_world",
  "version": "1.0.0",
  "description": "hello world sample for NodeJS",
  "main": "app.js",
  "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs",
  "author": "SAM CLI",
  "license": "MIT",
  "dependencies": {
    "axios": "^0.21.1"
  },
  "scripts": {
    "test": "mocha tests/unit/"
  },
  "devDependencies": {
    "chai": "^4.2.0",
    "mocha": "^9.1.4"
  }
}

hello-world/app.js

// const axios = require('axios')
// const url = 'http://checkip.amazonaws.com/';
let response;

/**
 *
 * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
 * @param {Object} event - API Gateway Lambda Proxy Input Format
 *
 * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
 * @param {Object} context
 *
 * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 * @returns {Object} object - API Gateway Lambda Proxy Output Format
 *
 */
exports.lambdaHandler = async (event, context) => {
    try {
        // const ret = await axios(url);
        response = {
            'statusCode': 200,
            'body': JSON.stringify({
                message: 'hello world',
                // location: ret.data.trim()
            })
        }
    } catch (err) {
        console.log(err);
        return err;
    }

    return response
};

app.jsソースコード本体ですね。

これで、AWS Lambda関数のソースコードの準備は完了です。

LocalStackを起動して、AWS Lambda関数をデプロイ、デバッグする

では、LocalStackを起動します。

$ LAMBDA_EXECUTOR=docker-reuse LAMBDA_DOCKER_FLAGS='-p 9229:9229 -e NODE_OPTIONS="--inspect=0.0.0.0:9229"' localstack start


## もしくは、こちら
$ LAMBDA_DOCKER_FLAGS='-p 9229:9229 -e NODE_OPTIONS="--inspect-brk=0.0.0.0:9229"' localstack start

何回もデプロイする場合は、--inspect-brkを使った方がよいかもしれません。

なお、デフォルトのタイムアウト設定ではデバッグしている間にタイムアウトすると思うので、こちらの値は調整しておいた方が良いでしょう。

Globals:
  Function:
    Timeout: 3

AWS Lambda関数をデプロイ。

$ yes | samlocal sync --stack-name $(uuidgen) --region us-east-1

デプロイが完了しました。

CloudFormation outputs from deployed stack
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldApi
Description         API Gateway endpoint URL for Prod stage for Hello World function
Value               https://he20jha0yf.execute-api.amazonaws.com:4566/Prod/hello/

Key                 HelloWorldFunction
Description         Hello World Lambda Function ARN
Value               arn:aws:lambda:us-east-1:000000000000:function:19c4bea0-0970-404d-908c-ac1bd01f520-HelloWorldFunction-fc689a38

Key                 HelloWorldFunctionIamRole
Description         Implicit IAM Role created for Hello World function
Value               arn:aws:iam::000000000000:role/19c4bea0-0970-404d-908c-ac1bd01-HelloWorldFunctionRole-2f771769
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Stack creation succeeded. Sync infra completed.

{'StackId': 'arn:aws:cloudformation:us-east-1:000000000000:stack/19c4bea0-0970-404d-908c-ac1bd01f5206/c5b866a6', 'ResponseMetadata': {'RequestId': '2WT2XE0560OEW4WLQTTLXB8DLX77TYAXAQR714J9P02OIVWT0HRD', 'HTTPStatusCode': 200, 'HTTPHeaders': {'content-type': 'text/xml', 'content-length': '409', 'access-control-allow-origin': '*', 'access-control-allow-methods': 'HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH', 'access-control-allow-headers': 'authorization,cache-control,content-length,content-md5,content-type,etag,location,x-amz-acl,x-amz-content-sha256,x-amz-date,x-amz-request-id,x-amz-security-token,x-amz-tagging,x-amz-target,x-amz-user-agent,x-amz-version-id,x-amzn-requestid,x-localstack-target,amz-sdk-invocation-id,amz-sdk-request', 'access-control-expose-headers': 'etag,x-amz-version-id', 'date': 'Fri, 15 Jul 2022 16:08:41 GMT', 'server': 'hypercorn-h11'}, 'RetryAttempts': 0}}

AWS Lambda関数にアクセスしてみます。

$ REST_API_ID=$(awslocal apigateway get-rest-apis --query 'reverse(sort_by(items[], &createdDate))[0].id' --output text)
$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello
{"message":"hello world"}

動いていますね。

この状態でchrome://inspectにアクセスすると、「Remote Target」でAWS Lambda関数を認識しているのが確認できます。

「inspect」を選択してみます。

この時点ではファイルはわからないので

「ファイルを開く」から

目的のファイルを探します。

あとは、開いたファイルに対してブレークポイントを貼って、アクセスすればOKです。

これで、LocalStackにデプロイしたAWS Lambda関数をGoogle Chrome DevToolsでデバッグできるようになりました。