IntelliJを使って、SAM CLIから起動したPythonのコードをリモートデバッグする
サーバーレス開発部@大阪の岩田です。
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
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
無事にpydevd.settrace
した直後で止まりました!
ここからステップ実行したり、変数の中身をみたりもバッチリです!
まとめ
いかがだったでしょうか? ユニットテストのライブラリを活用することで、ある程度効率的にLambdaのコードをデバッグすることは可能ですが、やはりローカルでcurlやpostman等のツールで実際にAPIを叩きながらデバッグしたい状況もあると思います。 今回紹介したような方法でリモートデバッグを行うことで、より効率的に開発が進められるのではないでしょうか?