はじめに

AWS SAMについてローカルで実行可能なテスト方法について学習したので内容を整理します。
今回は「sam init で作成したテンプレートをベースにSAMのローカルテストを実行してみる」を目標とします。

前提条件

ローカルでテストを実施するにあたって前提条件は以下の通りです。

  • AWS CLIインストールしていること
  • AWS SAM CLIインストールしていること
  • dockerコマンドの実行やコンテナ構築が可能なこと
    また、今回の説明ではPython3.13を使用します。

AWS SAMとは

AWS公式ドキュメントより抜粋

AWS Serverless Application Model (AWS SAM) は、Infrastructure as Code (IaC) を使用した、サーバーレスアプリケーション構築のためのオープンソースのフレームワークです。
AWS SAM の省略構文を使用して、デベロッパーは、デプロイ中にインフラストラクチャに変換される AWS CloudFormation リソースおよび特殊なサーバーレスリソースを宣言します。

今回の実施内容

今回は下記の3点を実施していきます。
通常であれば、テストの後にAWS環境へデプロイを行うのですが今回は省略します。
テストの対象となるプロジェクトはシンプルにAmazon API GatewayAWS Lambdaの構成として、
GETメソッドのリクエストに対して、ステータスコード、ボディにJSON形式でメッセージを返すごく単純なものとします。

  • プロジェクトを作成
    今回使用するプロジェクトを作成します。
  • ローカルでサーバーを立ち上げてAPIテストを実施してみる
    sam local start-apiコマンドでサーバーを立ち上げてテストを行います。
  • Lambda 関数を直接呼び出してテストしてみる
    sam local invokeコマンドでAWS Lambdaの関数を呼び出してテストを行います。

プロジェクトを作成

使用するプロジェクトはsam initコマンドでAWSのテンプレートを基に作成します。
プロジェクトを作成するディレクトリへ移動し、sam initコマンドを実行します。

sam init

コマンド実行後に作成するプロジェクトのテンプレートについて質問が表示されます。
作成するテンプレートは以下のように選択してください。

  • 1 – AWS Quick Start Templates
  • 1 – Hello World Example
  • python3.13
  • 1 – Zip

次に、下記の質問からオプションの有無についての質問が開始されます。
こちらはデフォルト(N)で問題ありません。

Based on your selections, the only dependency manager available is pip.
We will proceed copying the template using pip.
・・・省略・・・

作成するプロジェクト名は任意で構いません。
デフォルトのsam-appとしておきます。

Project name [sam-app]:

作成されたテンプレートのtemplate.yamlからは使用しているAWSサービスの構成内容を確認し、
hello_world/app.pyからはLambdaのレスポンス内容を確認してみます。
なお、本稿ではコメントアウト部分を除外していますが、内容はテンプレートで作成されるものと同じです。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.13
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Outputs:
  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/app.py

import json


def lambda_handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
        }),
    }

プロジェクトの準備が整えば、
sam validateコマンドで実装ミスがないかチェックをした後、sam buildでビルドを行います。
sam buildの実行後に.aws-samが作成されることを確認してください。

sam validate
sam build

これ以降のテストで実施するコードはsam buildで生成されたコードを使用します。
もしも、挙動がおかしいと感じたらsam buildを実行してみてください。

ローカルでサーバーを立ち上げてAPIテストを実施してみる

sam local start-apiコマンドを使用することで、
API GatewayとLambdaの構成をローカル環境で再現します。
この再現されたローカル環境を使用して、APIリクエストを通じてLambda関数の挙動を確認できるため、
ある程度、本番環境に近い形でのデバッグや開発が可能になります。

プロジェクト直下ディレクトリでローカル環境を立ち上げます。

sam local start-api

ローカル環境が立ち上がると下記のようにホストURLが確認でき、
そのままログを確認できるようになります。

* Running on http://127.0.0.1:3000
2025-03-14 00:00:00 Press CTRL+C to quit

ブラウザで次のURLを開くか、ターミナルで curl コマンドを実行することで、APIリクエストを送信し、レスポンスを確認できます。
http://127.0.0.1:3000/hello

以下では、curlコマンドに-iオプションをつけてステータスコードを確認できるようにします。
このレスポンスでステータスコード 200 OK と JSONレスポンス が返ってくれば、正常に動作していることが確認できます。

curl -i http://localhost:3000/hello
HTTP/1.1 200 OK
Server: Werkzeug/3.0.6 Python/3.11.10
Date: Fri, 14 Mar 2025 00:00:00 GMT
Content-Type: application/json
Content-Length: 26
Connection: close
{"message": "hello world"}

この時、立ち上げたローカル環境のログから次のようなログを合わせて確認することもできます。

[14/Mar/2025 00:00:00] "GET /hello HTTP/1.1" 200 -

Lambda 関数を直接呼び出してテストしてみる

sam local invokeコマンドを使用することでLambda関数をローカル環境で直接実行し、その動作を確認できます。
コマンドを実行する際に-eまたは--eventでJSONファイルパスを指定することでLambdaに渡すイベントデータを設定することが可能です。
今回作成したプロジェクトテンプレート中のevents/event.jsonを使用してみます。

プロジェクト直下ディレクトリへ移動し、Lambda関数を実行します。

sam local invoke HelloWorldFunction --event events/event.json

この実行結果でも先ほどと同じようにステータスコード 200 OK と JSONレスポンスが返ってくることが確認できます。

{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

先ほどテンプレートにあらかじめ作成されたファイルを使用しましたが、
sam local generate-eventを使用すると、新しいJSONパラメータを生成できます。
このコマンドを使用すると、S3 や SQS などのさまざまなサービスのイベントを生成でき、
ローカル環境で各種サービスのイベントをLambda関数に渡し、その動作を再現できます。
なお、生成された JSON パラメータはターミナル上に標準出力されるため、
ファイルとして保存する場合は > 出力先ファイルパス を指定します。

sam local generate-event > 出力先ファイルパス

今回のAmazon API GatewayとAWS Lambdaでの構成なので、
serviceはapigateway、eventはaws-proxyを指定しています。

sam local generate-event apigateway aws-proxy --method GET --path /hello --body '{}' > events/api-event.json

この生成結果は次のとおりとなります。
events/api-event.json

{
    "body": "e30=",
    "resource": "/{proxy+}",
    "path": "//hello",
    "httpMethod": "GET",
    "isBase64Encoded": true,
    "queryStringParameters": {
    "foo": "bar"
    },
    "multiValueQueryStringParameters": {
    "foo": [
        "bar"
    ]
    },
    "pathParameters": {
        "proxy": "//hello"
    },
    "stageVariables": {
        "baz": "qux"
    },
    "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Encoding": "gzip, deflate, sdch",
        "Accept-Language": "en-US,en;q=0.8",
        "Cache-Control": "max-age=0",
        "CloudFront-Forwarded-Proto": "https",
        "CloudFront-Is-Desktop-Viewer": "true",
        "CloudFront-Is-Mobile-Viewer": "false",
        "CloudFront-Is-SmartTV-Viewer": "false",
        "CloudFront-Is-Tablet-Viewer": "false",
        "CloudFront-Viewer-Country": "US",
        "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Custom User Agent String",
        "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
        "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
        "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
        "X-Forwarded-Port": "443",
        "X-Forwarded-Proto": "https"
    },
    "multiValueHeaders": {
        "Accept": [
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
        ],
        "Accept-Encoding": [
            "gzip, deflate, sdch"
        ],
        "Accept-Language": [
            "en-US,en;q=0.8"
        ],
        "Cache-Control": [
            "max-age=0"
        ],
        "CloudFront-Forwarded-Proto": [
            "https"
        ],
        "CloudFront-Is-Desktop-Viewer": [
            "true"
        ],
        "CloudFront-Is-Mobile-Viewer": [
            "false"
        ],
        "CloudFront-Is-SmartTV-Viewer": [
            "false"
        ],
        "CloudFront-Is-Tablet-Viewer": [
            "false"
        ],
        "CloudFront-Viewer-Country": [
            "US"
        ],
        "Host": [
            "0123456789.execute-api.us-east-1.amazonaws.com"
        ],
        "Upgrade-Insecure-Requests": [
            "1"
        ],
        "User-Agent": [
            "Custom User Agent String"
        ],
        "Via": [
            "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
        ],
        "X-Amz-Cf-Id": [
            "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
        ],
        "X-Forwarded-For": [
            "127.0.0.1, 127.0.0.2"
        ],
        "X-Forwarded-Port": [
            "443"
        ],
        "X-Forwarded-Proto": [
            "https"
        ]
    },
    "requestContext": {
        "accountId": "123456789012",
        "resourceId": "123456",
        "stage": "prod",
        "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
        "requestTime": "09/Apr/2015:12:34:56 +0000",
        "requestTimeEpoch": 1428582896000,
        "identity": {
            "cognitoIdentityPoolId": null,
            "accountId": null,
            "cognitoIdentityId": null,
            "caller": null,
            "accessKey": null,
            "sourceIp": "127.0.0.1",
            "cognitoAuthenticationType": null,
            "cognitoAuthenticationProvider": null,
            "userArn": null,
            "userAgent": "Custom User Agent String",
            "user": null
        },
        "path": "/prod//hello",
        "resourcePath": "/{proxy+}",
        "httpMethod": "GET",
        "apiId": "1234567890",
        "protocol": "HTTP/1.1"
    }
}

生成したファイルは先ほどと同様に

sam local invoke HelloWorldFunction --event events/api-event.json

で実行でき、

{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

で結果を確認することができます。

まとめ

今回、AWS SAMのテスト機能を使用してみて、以下のような点が分かりました。

  • Lambdaをコンソール上でデプロイ&テストする場合と比べて、手順を簡略化できる
  • コストを抑えながらテストが可能
  • 再現したいイベントの実行が簡単にできる
  • ローカル環境は 「ある程度の再現」 にとどまるため、実際のAWS環境との違いを考慮してテストの範囲を明確にする必要がある

今回の内容を試しただけでも、AWS SAMにはさまざまな機能があることが分かりました。
さらに深掘りすることで、より多様な構成でのテストが可能になりそうです。