🎊

[AWS] Lambda@Edgeでレスポンスボディの一部分を書き換える(付け足す)

2024/11/15に公開

Lambda,CloudFrontの細かい説明は省きます。

Lambda@Edgeとは

Lambda@Edgeについてまず簡単に。
公式↓
https://aws.amazon.com/jp/lambda/edge/

CloudFrontのエッジロケーションからコードを実行するLambda関数のことで、
ユーザに近い場所でコードが実行されるので高速なコンテンツ配信が可能になる仕組み。
コードをLambdaにアップロードするだけで自動的にコードの実行やスケーリングが行われる。

最大のメリットはコンテンツがオリジンから返されたときにキャッシュされる可能性を高めたり、既にキャッシュされたコンテンツの利便性を高め、キャッシュヒット率を向上させること。
らしい。

トリガーとレスポンスボディ

本記事ではこのLambda@Edgeで受け取ったレスポンスボディの一部を書き換えてクライアントに返すことをやる。

まず、Lambda@Edgeは上述した通り、CloudFrontとセットで使用する。具体的には、CloudFrontのイベントをトリガーとして

  1. ビューワーリクエスト
  2. ビューワーレスポンス
  3. オリジンリクエスト
  4. オリジンレスポンス

の4つをトリガーとして設定できる。



実際にマネジメントコンソールから指定する場合は、指定したCloudFrontディストリビューションのビヘイビアの一番下にある「関数の関連付け」から設定できる。

スクリーンショット 2023-12-28 17.38.30.png

図に表すとこんな感じ
スクリーンショット 2023-12-28 18.10.42.png
公式↓
https://aws.amazon.com/jp/blogs/news/lambdaedge-design-best-practices/

この4つのうちどれをトリガーにするかは要件によって変わってくる。
具体的には

  • キャッシュミスの時に関数を実行したいか? → オリジンのトリガーを選択
  • 全てのリクエストに対して関数を実行したいか?→ ビューワーのトリガーを選択
  • キャッシュキー(URL 、Cookie、ヘッダー、クエリ文字列) を変更したいか?→ ビューワーのリクエストをトリガーに選択
  • 結果をキャッシュせずにレスポンスを変更したいか?→ ビューワーのレスポンスをトリガーに選択
  • 動的にオリジンを選択したいか?→ オリジンのリクエストをトリガーに選択
  • オリジンの URL を書き換えたいか?→ ビューワーのリクエストをトリガーに選択
  • キャッシュされないレスポンスを生成したいか?→ ビューワーのリクエストをトリガーに選択
  • キャッシュされる前にレスポンスを変更したいか?→ オリジンのレスポンスをトリガーに選択
  • キャッシュされるレスポンスを生成したいか?→ オリジンのリクエストをトリガーに選択

といった感じ。
ざっくりと、
ビューワー〇〇 → CloudFrontのキャッシュ関係なく、Lambdaの処理を実行したい
オリジン〇〇 → CloudFrontにキャッシュがあるときは実行したくない
ぐらいでまずは考えてよさそう。


そして、もう一つミソなのが、レスポンスボディの書き換えはビューワーリクエスト・オリジンリクエストでしかできない
ということ。(↓参照)
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-updating-http-responses.html


最初の図にある、「本文を含める」の本文がレスポンスボディのことである。
ただ、レスポンスボディを丸々置き換える(特定のレスポンスを受け取った時にエラーページを表示するHTMLを返すとか)は可能。
あくまでCloudFrontが受け取ったレスポンスボディの一部を書き換えるということができないということになる。


それを考慮した上で、Lambda@Edgeでレスポンスボディの一部分を書き換えるためには、Lambda@Edgeからオリジンに対してHTTPリクエストを送り、Lamda@Edgeで受けとったレスポンスのボディを書き換えてクライアントに返す必要がある。

実装

具体的には以下の感じ。

const axios = require('axios');

exports.handler = async (event, context, callback) => {
  const request = event.Records[0].cf.request;
  
  const headersFromEvent = event.Records[0].cf.request.headers;
  const headers = {};
  const uri = request.uri;
  const url = host + uri;

  Object.entries(headersFromEvent).forEach(([key, value]) => { headers[key] = value[0].value; });
  
  let response;
  if (request.method === 'GET') {
    console.log("Axios GET");
    response = await axios.get(url, { headers }).catch(err => {console.log(err)});
  } else if(/*条件*/){
      //...
  } else {
    console.log("例外です");
  }

  if(response) {
    const updatedHeaders = {};
    Object.entries(response.headers).forEach(([key, value]) => { updatedHeaders[key] = [{ key: key, value: value }]; });

    const addElement = "<p>Lambda@Edgeで追加したDOM</p> "
    let responseBodyRewrite = "";
    if(response.data) {
      responseBodyRewrite = response.data.replace(/(<body>)/, '$1' + addElement); 
    }
    
    //最終的なレスポンス
    let BodyRewriteResponse = {
      status: response.status,
      statusDescription: response.statusText,
      headers: updatedHeaders,
      body: responseBodyRewrite
    };
    callback(null, BodyRewriteResponse); 
  }
};

上記はオリジンリクエストをトリガーにしたLambda@Edge。
細かいところは一旦置いといて、ポイントはaxios.getしている部分と、それをresponseに代入してresponse.dataaddElementを追加して最後のcallbackに渡している。
(HTTPリクエストはaxiosでなくても大丈夫です。今回はaxiosを使用したのでCloudFrontのリクエストヘッダの形式を書き換えてます)

Lamndaから直接エンドユーザーにレスポンスを返すことで実現できる。

この関数のARNを一番最初の画像の「関数 ARN/名前」に設定して。「本文を含める」にチェックを入れると実際に確認できます。

GitHubで編集を提案

Discussion