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でデバッグできるようになりました。

Node.jsのコマンドラインオプションをNODE_OPTIONS環境変数で指定する

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

Node.jsのコマンドラインオプションを環境変数で指定できないのかな?と思って調べてみたのですが、NODE_OPTIONS環境変数
これに該当しそうなので、試してみます。

NODE_OPTIONS環境変数

NODE_OPTIONS環境変数は、こちらに記載があります。

Command-line API / Environment variables / NODE_OPTIONS=options...

NODE_OPTIONS環境変数には、スペース区切りでオプションを指定するようです。

A space-separated list of command-line options.

指定したオプションはコマンドラインオプションの前に解釈されるので、同時に使うとコマンドラインオプションで指定した方が優先される
(オーバーライドする)挙動になるようですね。

options... are interpreted before command-line options, so command-line options will override or compound after anything in options....

それぞれ、例が書かれています。

オプションの値にスペースが含まれる場合は、引用符で囲います。

$ NODE_OPTIONS='--require "./my path/file.js"'

NODE_OPTIONS環境変数コマンドラインオプションで同じオプションを指定した場合で、かつフラグのように1度だけ指定する
オプションは、コマンドラインオプションの方が優先(オーバーライド)されます。

# The inspector will be available on port 5555
$ NODE_OPTIONS='--inspect=localhost:4444' node --inspect=localhost:5555

複数回指定できるオプションの場合は、両方を合わせた結果になるようですね。

$ NODE_OPTIONS='--require "./a.js"' node --require "./b.js"
# is equivalent to:
node --require "./a.js" --require "./b.js"

また、指定可能なコマンドラインオプションおよびV8オプションは規定されているようです。

指定可能なコマンドラインオプションはこちら。

  • --conditions, -C
  • --diagnostic-dir
  • --disable-proto
  • --dns-result-order
  • --enable-fips
  • --enable-source-maps
  • --experimental-abortcontroller
  • --experimental-fetch
  • --experimental-global-webcrypto
  • --experimental-import-meta-resolve
  • --experimental-json-modules
  • --experimental-loader
  • --experimental-modules
  • --experimental-network-imports
  • --experimental-policy
  • --experimental-specifier-resolution
  • --experimental-top-level-await
  • --experimental-vm-modules
  • --experimental-wasi-unstable-preview1
  • --experimental-wasm-modules
  • --force-context-aware
  • --force-fips
  • --frozen-intrinsics
  • --heapsnapshot-near-heap-limit
  • --heapsnapshot-signal
  • --http-parser
  • --icu-data-dir
  • --input-type
  • --insecure-http-parser
  • --inspect-brk
  • --inspect-port</code>, <code>--debug-port
  • --inspect-publish-uid
  • --inspect
  • --max-http-header-size
  • --napi-modules
  • --no-addons
  • --no-deprecation
  • --no-experimental-repl-await
  • --no-force-async-hooks-checks
  • --no-global-search-paths
  • --no-warnings
  • --node-memory-debug
  • --openssl-config
  • --pending-deprecation
  • --policy-integrity
  • --preserve-symlinks-main
  • --preserve-symlinks
  • --prof-process
  • --redirect-warnings
  • --report-compact
  • --report-dir</code>, <code>--report-directory
  • --report-filename
  • --report-on-fatalerror
  • --report-on-signal
  • --report-signal
  • --report-uncaught-exception
  • --require</code>, <code>-r
  • --secure-heap-min
  • --secure-heap
  • --throw-deprecation
  • --title
  • --tls-cipher-list
  • --tls-keylog
  • --tls-max-v1.2
  • --tls-max-v1.3
  • --tls-min-v1.0
  • --tls-min-v1.1
  • --tls-min-v1.2
  • --tls-min-v1.3
  • --trace-atomics-wait
  • --trace-deprecation
  • --trace-event-categories
  • --trace-event-file-pattern
  • --trace-events-enabled
  • --trace-exit
  • --trace-sigint
  • --trace-sync-io
  • --trace-tls
  • --trace-uncaught
  • --trace-warnings
  • --track-heap-objects
  • --unhandled-rejections
  • --use-bundled-ca
  • --use-largepages
  • --use-openssl-ca
  • --v8-pool-size
  • --zero-fill-buffers

指定可能なV8オプションは、以下になります。

  • --abort-on-uncaught-exception
  • --disallow-code-generation-from-strings
  • --huge-max-old-generation-size
  • --interpreted-frames-native-stack
  • --jitless
  • --max-old-space-size
  • --perf-basic-prof-only-functions
  • --perf-basic-prof
  • --perf-prof-unwinding-info
  • --perf-prof
  • --stack-trace-limit

また、以下のオプションはLinux環境のみで有効です。

  • --perf-basic-prof-only-functions
  • --perf-basic-prof
  • --perf-prof-unwinding-info
  • --perf-prof

確認はこれくらいにして、自分でも試してみましょう。

環境

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

$ node --version
v16.16.0

確認してみる

--max-old-space-sizeを使って確認してみましょう。

Command-line API / Useful V8 options / --max-old-space-size=SIZE (in megabytes)

サンプルプログラム。

app.js

const v8 = require('v8');

console.log(`heap total available size = ${Math.floor(v8.getHeapStatistics().total_available_size / 1024 / 1024)} MB`);

まずは、ふつうに実行してみます。

$ node app.js
heap total available size = 4141 MB

NODE_OPTIONS環境変数を使って、--max-old-space-sizeを指定してみます。

$ NODE_OPTIONS='--max-old-space-size=1536' node app.js
heap total available size = 1581 MB

反映されたようです。

コマンドラインオプションと両方指定してみましょう。

$ NODE_OPTIONS='--max-old-space-size=1536' node --max-old-space-size=2048 app.js
heap total available size = 2093 MB

コマンドラインオプションで指定した方が優先されましたね。

最後に、複数指定してみましょう。--max-old-space-sizeに加えて、--inspect-brkも指定してみます。

$ NODE_OPTIONS='--max-old-space-size=1536 --inspect-brk' node app.js 
Debugger listening on ws://127.0.0.1:9229/6643fd31-bf72-4a94-8144-982fcf779991
For help, see: https://nodejs.org/en/docs/inspector

起動すると--inspect-brkが効いてすぐに待ちになるので、chrome://inspectにアクセスしてDevToolsで対象のプロセスを開きます。

アタッチされて停止しているプログラムを進めていくと、--max-old-space-sizeも効いていることが確認できます。

heap total available size = 1580 MB

こんな感じでしょうか。覚えておこうと思います。