Simple minds think alike

より多くの可能性を

【Golang】AWS LambdaをSAMでデバッグする方法

Go言語で実装したAWS Lambda関数をVSCodeデバッグ実行できるようになるまでの手順を書いてみようと思います。

Lambda関数をローカル環境でデバッグ実行できるようにすることで、AWS上でのデプロイやテストをしなくても、開発を円滑に進めることができます。

また、AWS Lambdaをローカル環境で実行するにはSAM(Serverless Application Model)を使用します。実行時にデバッグポートを割り当てらることができ、VSCodeからデバッガをアタッチしてデバッグ実行を実現します。

SAMとは

SAM(Serverless Application Model)は、AWSが提供するサーバレスアプリケーションを開発、デプロイ、管理するためのフレームワークです。SAMを使用することで、AWS LambdaやAmazon API Gatewayなどのサーバレスサービスを使用したアプリケーションを簡単に構築したり、管理したりすることができます。

また、SAMはAWS CloudFormationのサブセットであり、CloudFormationのテンプレートを使用してサーバレスアプリケーションを定義します。

前提

  • 検証環境: macOS Monterey 12.6.2
  • Homebrewインストール済み
  • AWS CLIインストール、設定済み

※ 本記事で紹介している情報は2022年12月時点のものであり、閲覧時には古くなっている場合があります。最新の情報に関してはAWSの公式ドキュメントにしてください。

AWS SAM CLIインストール

ローカル環境でLambda関数を実行できるようにするために、公式ドキュメントを参考にAWS SAM CLIをインストールします。

$ brew tap aws/tap
$ brew install aws-sam-cli

正常にSAM CLIがインストールされていることを確認します。

$ sam --version
SAM CLI, version 1.66.0

実行するGoプロジェクトを作成する

まずディレクトリを作り、その中にデバッグ実行するLamba関数である main.go、SAMテンプレートファイル、go.modを作成します。

$ mkdir lambda-debug-sample
$ cd lambda-debug-sample
$ touch main.go
$ touch template.yml
$ go mod init lambda-debug-sample

Lambda関数(Go言語)の作成

main.goAWS公式のサンプルコードをそのまま書いてみます。
MyEvent構造体の値のNameを出力するだけのシンプルなコードです。

package main

import (
        "fmt"
        "context"
        "github.com/aws/aws-lambda-go/lambda"
)

type MyEvent struct {
        Name string `json:"name"`
}

func HandleRequest(ctx context.Context, name MyEvent) (string, error) {
        return fmt.Sprintf("Hello %s!", name.Name ), nil
}

func main() {
        lambda.Start(HandleRequest)
}

テンプレートファイルの作成

作成した main.go をLambda関数として動かすためのシンプルなテンプレートファイルを記載してみます。

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

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

Resources:
  HelloGopher: # リソース名を指定
    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: ./
      Handler: hello-world
      Runtime: go1.x
      Architectures:
        - x86_64

Resourcesの部分で、HelloGopher というリソース名を指定しています。他の箇所から参照する時に、このリソース名を使用できます。
Typeに AWS::Serverless::Function を指定し、CloudFormationがLambdaを生成できるようにします。

また、Propertiesでは以下の4つを指定しています。

※その他のオプションに関しては公式ドキュメントをご確認ください。

試しにSAM CLIを使って実行してみる

今の状態で作ったLambda関数を実行してみます。

$ sam local invoke
Invoking hello-world (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.66.0-x86_64.

START RequestId: b35b26b2-bfa1-4973-9c55-188775457e2d Version: $LATEST
fork/exec /var/task/hello-world: no such file or directory: PathError
null
END RequestId: b35b26b2-bfa1-4973-9c55-188775457e2d

すると、template.yml ファイルのHandlerで指定したバイナリファイル hello-world が存在しないので、PathError が発生することを確認できました。

ビルドする

ビルドすると.aws-samにビルド結果が出力されます。

$ go mod tidy
$ sam build

Running GoModulesBuilder:Build

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

再度SAM CLIで実行してみる

実行すると Hello ! と出力され、正常に実行できることを確認できます。

$ sam local invoke
Invoking hello-world (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.66.0-x86_64.

START RequestId: 16b172ed-6023-4ee8-999b-89d907784099 Version: $LATEST
END RequestId: 16b172ed-6023-4ee8-999b-89d907784099
REPORT RequestId: 16b172ed-6023-4ee8-999b-89d907784099  Init Duration: 0.20 ms    Duration: 191.46 msBilled Duration: 192 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
"Hello !"

イベントを渡して実行してみる

ローカル環境でLambda関数が動いたので、次は色々なやり方でLambda関数を起動してみようと思います。

まずはイベントを作成します。イベントは本来S3やSQSで作成・更新のイベントが発生した際に、Lambda関数が呼ばれる際に渡されるものですが、今回は仮のシンプルなデータを作成します。

$ touch event.json
{
    "name": "HogeHoge"
}

作成したイベント event.json を指定して関数を実行すると、Hello HogeHoge! と表示され、event.jsonからの値を確認できます。

$  sam local invoke -e event.json
Invoking hello-world (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.66.0-x86_64.

START RequestId: a54238e1-41d4-4c15-b6aa-c3b36f20c426 Version: $LATEST
END RequestId: a54238e1-41d4-4c15-b6aa-c3b36f20c426
REPORT RequestId: a54238e1-41d4-4c15-b6aa-c3b36f20c426  Init Duration: 0.24 ms    Duration: 201.85 msBilled Duration: 202 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
"Hello HogeHoge!"

リソース名を指定して実行してみる

リソース名を指定して起動することもできます。実行内容は同じです。

$ sam local invoke HelloGopher -e event.json

AWS Lambda をエミュレートするローカルエンドポイントを起動して実行してみる

次にsam local start-lambda で、ローカルエンドポイントを起動して実行してみます。

まずは、ローカルエンドポイントの起動からです。デフォルトURLは http://127.0.0.1:3001です。

$ sam local start-lambda
Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2022-12-31 17:28:26  * Running on http://127.0.0.1:3001/ (Press CTRL+C to quit)

AWS CLIを使って起動したエンドポイント叩きます。

$ aws lambda invoke --function-name HelloGopher --endpoint "http://127.0.0.1:3001" --payload '{"name": "HogeHoge"}' --cli-binary-format raw-in-base64-out response.json
{
    "StatusCode": 200
}

response.json にLambdaの実行結果が出力されます。

"Hello HogeHoge!"

VSCodeデバッグ実行できるようにする

delveのインストール

Goのデバッグツールである delve をインストールします。

$ GOARCH=amd64 GOOS=linux go install github.com/go-delve/delve/cmd/dlv@latest

Lambda関数及び delve が実行されるのはLambdaコンテナ内(Amazon Linux)なので、GOOSに linux を指定し、インストールします。(参考github issueコメント).

デバッガーパスを指定してLambda関数を実行

delve$GOPATH/bin/linux_amd64 されるので、このパスをデバッガーパスに指定、デバッグポートに 8099 を指定して、Lambda関数を実行します。

API server (8099 ポートリッスン)で起動することが確認できます。

$  sam local invoke HelloGopher -e event.json  -d 8099 --debugger-path=$GOPATH/bin/linux_amd64 --debug-args="-delveAPI=2"
Invoking hello-world (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.66.0-x86_64.

START RequestId: 909cfcd8-e0cc-448c-8b55-e8afe022b5b8 Version: $LATEST
API server listening at: [::]:8099
2022-12-31T08:47:59Z warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
2022-12-31T08:47:59Z info layer=debugger launching process with args: [/var/task/hello-world]

Visual Studio Codeからアタッチする

最後にVSCodeからアタッチするための情報を設定します。

プロジェクト( main.go と同じところ)に以下の .vscode/launch.json を作成します。

{
    "configurations": [

        {
            "name": "SAMでデバッグ",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "port": 8099,
            "host": "localhost",
            "env": {},
            "args": []
        }
    ]
}

main.goVSCodeで開き、ブレイクポイントを設定したら、実行とデバッグを押します。 SAMでデバッグする際のVSCodeデバッグ画面

ブレイクポイントに止まることを確認できます。 SAMでデバッグ中のVSCodeデバッグ画面

1ステップでデバッグ実行できるようにする

以下の2ステップでデバッグ実行を行いましたが、launch.jsonの書き方を変えると1ステップでできるようになり、デバッグポートを指定する必要がなくなります。

  • デバッガーパスを指定してLambda関数を実行
  • Visual Studio Codeからアタッチする

先ほどの launch.json を以下のように変更し、実行とデバッグから2つ目の方を指定すると同様にブレイクポイントに止まることが確認できます。

{
    "configurations": [
        {
            "name": "SAMでデバッグ",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "port": 8099,
            "host": "localhost",
            "env": {},
            "args": []
        },
        // ↓↓↓ここの部分を追記↓↓↓
        {
            "name": "SAMでデバッグ2",
            "type": "aws-sam",
            "request": "direct-invoke",
            "invokeTarget": {
                "target": "template",
                "templatePath": "${workspaceFolder}/template.yml",
                "logicalId": "HelloGopher"
            },
            "lambda": {
                "payload": {
                    "path": "event.json"
                    // "json": { "name": "HogeHoge" },
                },
                "environmentVariables": {}
            }
        },
        // ↑↑↑ここの部分を追記↑↑↑
    ]
}

サンプルプロジェクト

github.com

参考記事