Amazon API Gatewayが非常に便利なHTTPプロキシとして進化したらしいので使ってみた

  • 31
    いいね
  • 0
    コメント

API Gatewayとは

Amazon API Gateway

今回新たに追加された機能

Amazon API Gateway に API 設定を簡素化する 3 つの新機能を追加(公式)

Amazon API Gateway が AWS Lambda と HTTP エンドポイントで API を統合しやすくする 3 つの新機能をサポートするようになりました。従来はバックエンドのエンドポイントとの統合時に API Gateway の各メソッドと統合の動作を定義する必要がありました。今後はリクエストやレスポンスのマッピングや変換を適用せずに、すべてのトラフィックを特定のバックエンドのエンドポイントにルートすることができます。

パス変数のキャッチオールをサポートするようになりました。/store/{proxy+} といったルートを定義できます。この場合の + 記号は API Gateway に /store/* パスに対するすべてのリクエストを傍受するように指示しています。ANY という新しいメソッドタイプをサポートするようになりました。ANY メソッドのキャッチオールを使用して、すべてのリクエスト (GET、POST など) に同じ統合エンドポイントを定義することができます。Lambda 関数と HTTP エンドポイントに新なプロキシ統合タイプを使用できるようになりました。Lambda 関数のプロキシ統合はデフォルトのマッピングテンプレートを適用してすべてのリクエストを関数に送信し、自動的に Lambda 出力を HTTP レスポンスにマップします。HTTP プロキシ統合はリクエスト全体を渡し、HTTP エンドポイントに直接レスポンスを送信します。

こうした新機能を合わせて使用すると、Express to Lambda のようなフレームワークを使用して作成した既存のアプリケーションで API Gateway やポートを簡単に設定することができます。詳細については、ドキュメントをご覧ください。

今までのAPI Gatewayでは、HTTPプロキシとして利用するには、通したいリクエストのすべてのリソースパスとメソッドを設定する必要がありました。これが非常に面倒でした。

しかし、今回の追加機能のおかげで、大量に設定する必要のあったものが、/{proxy+}だけで簡単にオールスルーのプロキシが作れるようになりました。ひとまず思いついた利用シーンは以下。

  • レートリミットをかける
    • API Gatewayは比較的楽にレートリミットをかけることができるので、既存APIにGatewayを挟んで使う
  • とりあえず全部Lambdaに飛ばしてゴニョゴニョする
    • リクエストパスに応じて振り分けるとか
    • 可能性は無限大な気がする、何かよさげな利用方法あれば教えてください

試してみた

→ API Gateway → Lambdaを試してみます。

Lambda

とりあえず初期設定のまま作る。Nameは今回はtestとしました。

exports.handler = (event, context, callback) => {
    // TODO implement
    callback(null, 'Hello from Lambda');
};

API Gateway

  • リソースを追加から、リソースパスに/{proxy+}と設定。
  • 振り分け先をLambda関数のtestにする。

盛大にハマったこと

テストしてみると、502になる。全然何もドキュメント読まずやるとうまくいかないですね。

Output Format of a Lambda Function for Proxy Integration

ここに書いてあるが、注意しなければならないのはレスポンス形式。以下のようにしないとAPI Gatewayがレスポンスを502にしてしまうら。

{
    "statusCode": httpStatusCode,
    "headers": { "headerName": "headerValue", ... },
    "body": "..."
}

また、こちらにLambdaの例が書いてありました。
Create an API with Lambda Proxy Integration through a Proxy Resource

ということでLambdaを以下のように修正。今回はeventの中身を見たかったのでeventをbodyとして出力するようにしました。

exports.handler = (event, context, callback) => {
    var response = {
        statusCode: 200,
        headers: {
            "x-custom-header" : "my custom header value"
        },
        body: JSON.stringify(event)
    };  
    context.succeed(response);
};

レスポンスはこんな感じ。

{
  "resource": "/{proxy+}",
  "path": "/aaa/bbb/ccc",
  "httpMethod": "GET",
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, sdch, br",
    "Accept-Language": "ja,en-US;q=0.8,en;q=0.6",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "false",
    "CloudFront-Is-Mobile-Viewer": "true",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "JP",
    "Cookie": "xxxxxxxxxxx",
    "Host": "xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36",
    "Via": "1.1 xxxxxxxxxxx.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "xxxxxxxxxxx",
    "X-Forwarded-For": "xxxxxxxxxxx",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "queryStringParameters": {
    "test": "12"
  },
  "pathParameters": {
    "proxy": "aaa/bbb/ccc"
  },
  "stageVariables": null,
  "requestContext": {
    "accountId": "xxxxxxxxxxx",
    "resourceId": "xxxxxxxxxxx",
    "stage": "dev",
    "requestId": "xxxxxxxxxxx",
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "apiKey": null,
      "sourceIp": "xxxxxxxxxxx",
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36",
      "user": null
    },
    "resourcePath": "/{proxy+}",
    "httpMethod": "GET",
    "apiId": "xxxxxxxxxxx"
  },
  "body": null
}

Lambdaでゴニョゴニョしたい場合は、queryStringParametersとかpathParameters.proxyとかhttpMethodとかを活用するのがよさそうですね。

試しに、/{proxy+}と同じレベルに/sampleを作成してみました。そして、/sampleにアクセスすると当たり前ですが/{proxy+}ではなく/sampleが呼び出されました。流石に/{proxy+}の優先度は低くされていますね。

懸念点、疑問点

レスポンスのフォーマットが決められていること

んーこれ本当になんなんでしょうかね。Lambdaを挟むのならどうとでもできますが、挟まない場合はわりと不便になりそうな気がするのですが。
LambdaからのレスポンスをAPI Gatewayがパースする際に、Lambda Proxy Integrationではパースの設定が固定されている。そのため、フォーマットが決められているが、これにしたがっておけばHTTPステータスもちゃんと返してくれるし、bodyの中身だけ返してくれるし、面倒な設定がいらずいいことばかりな気がします。詳しくはこちらの記事

レスポンスのbodyが文字列でないとダメ

今回Lambdaでbody: JSON.stringify(event)を返しています。これをbody: eventにすると502になっちゃうんですよ。後者の方も使えるようにしてほしいですねー、何か理由とかあるんでしょうか?

まとめ

ずっと欲しかった機能なのでとてもありがたいです。ユーザの声をしっかりスピーディに提供してくるところがさすがAmazon。ただ、利用にあたり個人的には色々疑問の残る感じ。もしかしたらDocumentに書いてあるかもしれませんが、ない場合は何かしらの改善や解答がほしいですね〜。