[アップデート]AWS SAM CLIのsam local start-apiコマンドがLambdaオーソライザーに対応しました

2023.04.18

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

初めに

本日AWS SAM CLIのv1.80.0がリリースされました。

以前のアップデートまではSAMテンプレートでLambdaオーソライザーを定義している場合、実際のデプロイにはLambdaオーソライザーが含まれますが、sam local start-apiによるローカルでの実行時にはLambdaオーソライザーが利用されずに無視される仕様となっておりました。

今回のアップデートではsam local start-apiで実行しアクセスした場合にもLambdaオーソライザーが適用されるようになり、SAM環境でもそれを含めた動作確認が可能となります。

なお要望自体については2017年末近くからあったようです。

実行サンプル

Authorizationヘッダの値がSuccessの場合のみ許可される処理を実装します。

コード

template.yml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Timeout: 60
    MemorySize: 128

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      MethodSettings:
        - ResourcePath: /
          HttpMethod: GET
      Auth:
        DefaultAuthorizer: LambdaAuthorizer
        Authorizers:
          LambdaAuthorizer:
            FunctionArn: !GetAtt AuthFunction.Arn
            FunctionPayloadType: TOKEN
            Identity:
              Headers:
              - Authorization
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - arm64
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref Api
  AuthFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: auth/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - arm64

hello_world/app.py

import json

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

auth/app.py

def lambda_handler(event, context):
    auth_result = "Allow" if 'authorizationToken' in event and "Success" == event['authorizationToken'] else "Deny"
    return {
        'principalId': 'sam-local',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': 'execute-api:Invoke',
                    'Effect': auth_result,
                    'Resource': [
                        event['methodArn']
                    ]
                }
            ]
        }
    }

以前

以前まではテンプレート上指定があるにもかかわらずLambdaオーソライザが処理がされないためAuthorizationヘッダの指定有無に関わらずhello_worldのLambda関数の実行が確認できました。

% sam local start-api
...
#別ウィンドウで実行
% curl http://127.0.0.1:3000/hello
{"message": "hello world"}%

アップデート後

Lambdaオーソライザーが利用されるようになりました。

% sam local start-api
...
# 別ウィンドウで実行
# 成功時
% curl http://127.0.0.1:3000/hello -H "Authorization: Success"
{"message": "hello world"}%
# Authorizationヘッダ未指定時
% curl http://127.0.0.1:3000/hello                           
{"message":"Unauthorized"}                                                                                                                   
# Authorizationヘッダの指定誤りで"Deny"が返却される場合
% curl http://127.0.0.1:3000/hello -H "Authorization: Succes" 
{"message":"User is not authorized to access this resource"}

実際にデプロイしないと確認できない範囲ということが減った一方で、(見落としがなければ)オプションで有無効の切り替えはできなさそうなのでアプリによっては環境準備に追加の手順が必要な手間が発生するかもしれません。

ハマりどころ

この記事についてはサクッと試してサクッと書こうと思っていたのですが2時間近くはまりました。

v1.80.0環境上ですので後続バージョンでは異なる可能性がある点はご注意ください。

FunctionPayloadType未指定時の挙動

現状FunctionPayloadTypeの値が未設定の場合sam local start-api環境上ではLambdaオーソライザーが起動しません。

https://github.com/aws/aws-sam-cli/compare/v1.79.0...v1.80.0#diff-db5e396f58196b0400b0f30a4107754dd5fd922f97384dbd6c84f678edc79d7aR282

アップデート後キャッシュ削除等を行なってもLambdaオーソライザーが起動すらせず原因特定のために--debugオプションでログを追っていたところ上記に記載のソースコード部分にあたりました。

情報が足りなかったためaws-sam-cliのソースコード自体に手を入れてデバッグしたところ、どうもSAMテンプレートでFunctionPayloadTypeの値を指定しない場合、本来はTOKENの値が設定されるようですが、現状tokenの値が指定されていました。

未指定の場合小文字であるtokenに対してupper()で大文字にキャストした値で比較をかけるので実質的に規定外の値指定扱いとなりLambdaオーソライザーが利用されません。

おそらく同様の理由でToken等の場合も規定外の値扱いになるように見えます。
(定数っぽい値側にキャストをかけてるので仕様ではなくバグのような気がします)

Lambdaオーソライザーの返却値

ポリシーステートメントのResourceの値は単一の場合は配列ではなく文字列で指定することも多いですが、今回試していたところSAM環境ではResourceの値は配列である必要がありそうです。

仕様上明確に書かれているというよりは文字列で指定した場合Authorizer 'LambdaAuthorizer' policy document contains an invalid 'Resource'が発生してしまいどうにも解決できなかったというところですのでもしかしたら自分の方で誤りがあるかもしれません。

このエラー時のLambdaオーソライザーの値の返却値は以下のように指定していました。

    return {
        'principalId': 'sam-local',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': 'execute-api:Invoke',
                    'Effect': auth_result,
                    'Resource': event['methodArn']
                }
            ]
        }
    }

https://github.com/aws/aws-sam-cli/compare/v1.79.0...v1.80.0#diff-f4591db380fb8bc50e8b19ffb6e55f17777bccdc3ca758fe637bf5844ce6ee1dR183

今回実装されたテストコード部分を見てみるとResourceの値が配列前提になっていたのでそれに合わせ配列値で返却したところ解決に辿り着きました。

実はLambdaオーソライザーは今回初めて触ったのですが、Amazon API Gateway Lambda オーソライザーからの出力を見る限りResourceの値は文字列で指定されており、文字列で指定しても実際のAWS上では正常に動くためもしかしたらSAM側のバグかもしれません。

なお文字列で指定した場合はInternal server error扱いとなり関数自体が確認できなくなるので心当たりのある環境の方はバージョンアップの前に使い捨ての環境等で確認した方が良いかもしれません。

% curl http://127.0.0.1:3000/hello -H "Authorization: Success"
{"message":"Internal server error"}

結果的にですがFunctionPayloadTypeを未指定にすることでLambdaオーソライザーを使わせずこの現象を回避することができます。

終わりに

今回のアップデートで実際のAWS環境を使わずともSAMの環境内でエミュレートできる範囲がまた1つ広がりました。

ただ少し動作が怪しい部分があるためアップデートの際はまずは戻しのできる環境で一度試してみてください。