Lambda オーソライザーのレスポンスコンテキストを、バックエンド統合した Minimal API へカスタムヘッダーとして送信する

2022.07.23

いわさです。

.NET の Minimal API を API Gateway + Lambda でホストしているサーバーレスアプリケーションで、Lambdaオーソライザーで任意のコンテキスト情報を追加し、それを統合された Lambda へカスタムヘッダーとして引き渡す方法を紹介します。

バックエンド Lambda を用意

ベースとなるサーバーレスアプリケーションは以下を利用して dotnet tool で用意します。

今回は Lambda 関数へカスタムヘッダーが引き渡されているのかを確認したいので、ヘッダー情報をそのままレスポンスに使うようにコードを実装します。

Program.cs

using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

// Add AWS Lambda support. When application is run in Lambda Kestrel is swapped out as the web server with Amazon.Lambda.AspNetCoreServer. This
// package will act as the webserver translating request and responses between the Lambda event source and ASP.NET Core.
builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);

var app = builder.Build();


app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.MapGet("/", (HttpRequest request) => 
{
    return JsonSerializer.Serialize(request.Headers);
});

app.Run();

そして、こちらをデプロイしまずはカスタムヘッダー云々なしで動作確認してみましょう。

% aws s3 mb s3://hoge0723dotnet --profile hoge
make_bucket: hoge0723dotnet

% dotnet lambda deploy-serverless --profile hoge
Amazon Lambda Tools for .NET Core applications (5.4.4)
Project Home: https://github.com/aws/aws-extensions-for-dotnet-cli, https://github.com/aws/aws-lambda-dotnet

:

Stack finished updating with status: CREATE_COMPLETE

Output Name                    Value                                             
------------------------------ --------------------------------------------------
ApiURL                         https://7b09sm85j8.execute-api.ap-northeast-1.amazonaws.com/Prod/

% curl https://7b09sm85j8.execute-api.ap-northeast-1.amazonaws.com/Prod/ | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   721  100   721    0     0   2867      0 --:--:-- --:--:-- --:--:--  2930
{
  "Accept": [
    "*/*"
  ],
  "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-ASN": [
    "13335"
  ],
  "CloudFront-Viewer-Country": [
    "JP"
  ],
  "Host": [
    "7b09sm85j8.execute-api.ap-northeast-1.amazonaws.com"
  ],
  "User-Agent": [
    "curl/7.79.1"
  ],
  "Via": [
    "2.0 1b17bb9a33fa05d7c4f60c4d8d96b95a.cloudfront.net (CloudFront)"
  ],
  "X-Amz-Cf-Id": [
    "qdTgHEe_365UaRfNExqR2dLuCWvfdGhW-fovf0zvojoJm3OoHyg2OQ=="
  ],
  "X-Amzn-Trace-Id": [
    "Root=1-62db1283-71c3b60f3cb6c0e95d9c7c18"
  ],
  "X-Forwarded-For": [
    "203.0.113.1, 203.0.113.2"
  ],
  "X-Forwarded-Port": [
    "443"
  ],
  "X-Forwarded-Proto": [
    "https"
  ],
  "Content-Length": [
    "0"
  ]
}

これで、リクエストヘッダー内容をそのままレスポンスする .NET Minimal API が API Gateway + Lambda で実行出来る状態になりました。

Lambda オーソライザーを用意

次に、要件のひとつである、Lambda オーソライザーを準備します。
Lambda オーソライザーのコンテキストについては以前以下の記事にて少し調べたことがあります。
今回は、関数もほぼこのまま流用する形で、本来は Lambda オーソライザーにて動的に認証結果をポリシーとして返却するべきですが、ここではそのあたりは重要ではないので、静的に実装しています。

lambda_function.py

import json

def lambda_handler(event, context):
    print(json.dumps(event))
    return {
        'principalId': 'abc123',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                'Action': 'execute-api:Invoke',
                'Effect': 'Allow',
                'Resource': event['methodArn']
            }]
        },
        'context': {
            'stringKey': 'stringval',
            'numberKey': 123,
            'booleanKey': True
        }
    }

上記のデプロイした Lambda 関数を、先程デプロイした API Gateway のオーソライザーとして登録し、今回検証に使いたい任意のリソースで認可に使用されるよう選択します。

この時点で一度 API をステージへデプロイして外部から動作確認しておくと良いです。
API Gateway の API テスト機能ではオーソライザーはスキップされるので、最後の外部動作確認時に問題が起きると、オーソライザーが原因なのかその他が原因なのか切り分けしなきゃいけなくなっちゃうので。

マッピングテンプレートを構成

さて、ここからがメインの部分になります。
今回バックエンドの Lambda 関数は変更したくないので、API Gateway でどうにかする形になります。
そして、API Gateway でどうにかする時は、ほぼマッピングテンプレートをいじってどうにか頑張る形になります。

まず、先程デプロイされた API は、Lambdaプロキシ統合が使用されています。
API Gateway は様々なバックエンドと統合出来るのですが、Lambda プロキシ統合を有効化すると Lambda 用に良い感じのデフォルトテンプレートで API Gateway と Lambda の間のリクエストとレスポンスをマッピングしてくれます。
この機能は非常に便利なのですが、今回はマッピングテンプレートをカスタマイズしたいので無効化します。

ここで、API Gateway から Lambda へ引き渡す部分と、Lambda の返却値を API Gateway に引き渡す部分の2つのマッピング設定をテンプレートで行います。

リクエストマッピング

リクエストマッピングテンプレートでは以下を参考に、デフォルトテンプレートを簡略化したものを改造してみます。

そして、マッピングテンプレートでは以下で案内されているいくつかの変数を使うことで、コンテキスト値などを取得することが出来ます。

ここでは、$context.authorizer.propertyを使うことで Lambda オーソライザーの関数から返されたcontextのキー値を取得出来ますのでこちらを静的に使いたいと思います。
マッピングテンプレートは、Velocity Template Language (VTL) で実装され、分岐や繰り返しを組み込むことが出来ますが、それはどうにでもなりそうなのでここでは割愛して、静的にキー値を指定しています。

{
  "resource": "$context.resourceId",
  "path": "$context.resourcePath",
  "httpMethod": "$context.httpMethod",
  "headers": {
    "x-hoge-string": "$context.authorizer.stringKey",
    "x-hoge-num": "$context.authorizer.numberKey",
    "x-hoge-bool": "$context.authorizer.booleanKey"
  },
  "requestContext": {
    "hoge": "hoge"
  },
  "body": "hoge body",
  "isBase64Encoded": false
}

上記ハイライト部分がヘッダーを設定している部分になります。
その他の部分は便宜上静的に仮の値を設定しています。このままでも動作はしますが、実際に利用する際は様々なプロパティを動的に設定する必要が出てきますのでその点だけ覚えておいてください。

レスポンスマッピング

レスポンスに関しては、今回はbodyをそのまま出力するだけにしますので以下のように$input.path('$.body')を使います。

実行してみる

さて、最後に実行してみましょう。

% curl https://7b09sm85j8.execute-api.ap-northeast-1.amazonaws.com/Prod/ | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   116  100   116    0     0    525      0 --:--:-- --:--:-- --:--:--   544
{
  "x-hoge-string": [
    "stringval"
  ],
  "x-hoge-num": [
    "123"
  ],
  "x-hoge-bool": [
    "true"
  ],
  "Host": [
    "apigateway--"
  ],
  "Content-Length": [
    "9"
  ]
}

良いですね。
Lambda オーソライザーで設定した値を、カスタムヘッダーとしてLambda 関数で実行している ASP.NET アプリケーションに引き渡すことが出来ました。

まとめ

本日は、Lambda オーソライザーのレスポンスコンテキストを、バックエンド統合した Minimal API へカスタムヘッダーとして送信しました。
Minimal API だとか、.NET だとかは然程重要ではないので、色々なシーンでマッピングテンプレートでどうにか頑張るというのは応用出来るかと思います。
ちょっと最初のテンプレート準備だけ大変ですが、試してみてください。