IntelliJを使って、SAM CLIから起動したPythonのコードをリモートデバッグする

SAM CLIを使ってローカルでAPI Gatewayもどきを起動しながら、Pythonで書かれたLamdaのコードをリモートデバッグしてみました。
2018.05.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

サーバーレス開発部@大阪の岩田です。

Pythonで開発しているLambdaをデバッグするためにSAM CLIを使ったリモートデバッグを試したので、その時の手順をまとめます。

雛形作成

まずはアプリの雛形を作成します。 SAMテンプレートは下記のように記述しました。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
    python remote debug
Globals:
    Function:
        Timeout: 300
Resources:
    HelloWorldFunction:
        Type: AWS::Serverless::Function
        Properties:
            Handler: app.lambda_handler
            Runtime: python3.6
            Events:
                HelloWorld:
                    Type: Api
                    Properties:
                        Path: /
                        Method: get

注意点として、タイムアウト時間をがっつり長くしています。 ここが短いと、ステップ実行したり、変数の中身を確認したりしている間にLambdaがタイムアウトしてしまうからです。

次にPythonのコードです。

def lambda_handler(event, context):

    status_code = 200
    body = 'hello'
    return {
        "statusCode": status_code,
        "body": body
    }

ここまでできたら、まずは動作確認してみます。

sam local start-api -t template.yml

でAPIをローカルで起動し、curlでテストしてみます。

curl http://127.0.0.1:3000/
hello

OKです。

リモートデバッグのための設定

ここからが本題です。 curlから発火した先ほどのコードをリモートデバッグできるように設定していきます。

pydevdのインストール

まずリモートデバッグで使用するために、pydevdをインストールします。

pip install pydevd -t lib

注意点として、Lambdaを実行するDockerコンテナにpydevdをデプロイしてやる必要があるので、アプリケーションのルートディレクトリ配下にlibというディレクトリを指定してインストールしています。 今回のSAMテンプレートではcodeUriを特に指定していないので、Dockerコンテナにはtemplate.ymlが置いてあるディレクトリの中身が丸々マウントされます。 結果、Dockerコンテナ内の/var/task/libというディレクトリにpydevdが配置されます。

Dockerコンテナの環境変数を設定

SAMテンプレートに下記の記述を追加します。

            Environment:
                Variables:
                    PYTHONPATH: "/var/runtime:/var/task/lib"

Lambda(Python)用Dockerコンテナのデフォルトでは環境変数PYTHONPATHに/var/runtimeが設定されているのですが、追加で/var/task/libを設定することで、Dockerコンテナ内で起動するLambdaが先ほどインストールしたpydevdを読み込むことが可能になります。 ※このやり方は中山に教えてもらいました。

InteliJの設定

Run → Edit Configurations からPythonをリモートデバッグするための設定を追加します。 適当にホスト名とポート番号を設定した後、ローカルの開発環境とDockerコンテナ間のパスのマッピングを追加します。 ローカル側はアプリケーションのルートディレクトリをフルパスを指定、Dockerコンテナ側は/var/taskにコードがデプロイされるので、決め打ちで/var/taskを指定します。

Lambdaの修正

次にpythonのコードにリモートデバッグ用のロジックを埋め込みます。 将来的に色々なLambda関数をデバッグすることを考え、decorator.pyというファイルを新しく作成し、その中にリモートデバッグ用のロジックを実装しました。

import os


def remote_debugable(func):

    def remote_debug_wrapper(*args, **kwargs):
        if not os.getenv("AWS_SAM_LOCAL", False):
            return func(*args, **kwargs)

        parameters = args[0]["queryStringParameters"]
        if parameters is None:
            return func(*args, **kwargs)
        debug = "DEBUG" in parameters.keys()
        if debug:
            import pydevd
            host = parameters.get("DEBUG_HOST", ["host.docker.internal"])[0]
            port = int(parameters.get("DEBUG_PORT", [54321])[0])
            pydevd.settrace(host, port=port, stdoutToServer=True, stderrToServer=True)

        return func(*args, **kwargs)
    return remote_debug_wrapper
※2018.07.07追記 執筆当時はSAM CLIの0.3.0を使用していました。 0.4.0からは修正されていますが、0.3.0だと実際のAPI Gatewayの挙動と異なり、クエリストリングの中身がリスト形式で渡されてきます。そのため上記のようなコードになっています。 https://github.com/awslabs/aws-sam-cli/pull/405

SAM CLIからLambdaが起動された場合は、環境変数AWS_SAM_LOCALが入っているので、まずは7行目で環境変数のチェックを行っています。 次に、SAM CLIから起動した場合でもリモートデバッグしたくない場合もあるので、クエリストリングにDEBUGが設定された場合だけリモートデバッグを実行するようにしています。ソースコードの13,14行目の部分になります。 また、pydevdの接続先ホストとして、デフォルトでhost.docker.internalを指定しています。 Dockerのドキュメントに記載されているのですが、Docker for Macではブリッジインターフェースのdocker0が作成されないため、ホストのIPをそのまま指定しても通信できないためです。

次にLambda本体にリモートデバッグ用のコードを注入します。

from decorator import remote_debugable


@remote_debugable
def lambda_handler(event, context):

    status_code = 200
    body = 'hello'
    return {
        "statusCode": status_code,
        "body": body
    }

リモートデバッグを開始!

これで準備OKです。実際にリモートデバッグを試してみます。 まずIntelliJ側で、先ほど作成した構成でデバッグを開始します。

IntelliJ側で待ち受けの準備ができたら、ターミナルからcurlでAPIを呼び出します。

curl http://127.0.0.1:3000/?DEBUG=1

IntelliJ Debug Stop

無事にpydevd.settraceした直後で止まりました! ここからステップ実行したり、変数の中身をみたりもバッチリです!

まとめ

いかがだったでしょうか? ユニットテストのライブラリを活用することで、ある程度効率的にLambdaのコードをデバッグすることは可能ですが、やはりローカルでcurlやpostman等のツールで実際にAPIを叩きながらデバッグしたい状況もあると思います。 今回紹介したような方法でリモートデバッグを行うことで、より効率的に開発が進められるのではないでしょうか?