[Lambda@Edge x ウェブサイト運用] AWS S3で静的ホスティングしたウェブサイトをレスポンス400~500番台の時にエラーページへ誘導する


2021/04/21

AWS CloudFrontを介してウェブサイトを公開していると、存在しないページにブラウザからアクセスした際に、以下のような症状がみられると思います。

合同会社タコスキングダム|蛸壺の技術ブログ

通常のWebサーバーを利用したウェブサイトであれば、存在しないページにアクセスがあった場合、きちんとクライアント側にエラーページを送り返す仕組みが備わっていますが、AWS CloudFrontを利用したなんちゃってウェブサイトはCDN方式でのファイル配信を行うだけなので、当然何もしないとエラーページなんて返してくれません。

ではどうするかというと、前の回でも解説をしていた
Lambda@Edgeを再度利用します。

前回の記事のこのLambda@Edgeの使い方だと、HTTPリクエストの情報を変形してサーバー側に送りだしていたわけですが、今回は、サーバー側からのHTTPレスポンスの情報を読み取って、エラーページにリダイレクトさせる方法を紹介します。


HTTPレスポンスエラー?

ブラウザからだとクライアントの受けとるレスポンスヘッダーが分かりにくいので、Curlでウェブサイトにアクセスしてみましょう。

まずAWS S3バケットにちゃんとindex.htmlが存在していて、CloudFrontが絶賛配信中のページにアクセスしてみると、

            
            $ curl -I https://deep.tacoskingdom.com/blog/85
HTTP/2 200
content-type: text/html
content-length: 867893
date: Wed, 21 Apr 2021 03:31:02 GMT
last-modified: Wed, 21 Apr 2021 02:53:25 GMT
etag: "6d0db1bf6c10f5cb07c18c87a6f8e47e"
cache-control: no-cache, no-store
content-encoding: br
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 853dab48fd1de187261c15f5b98cd2a0.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-C4
x-amz-cf-id: 76TktFSL4CqQ5a3roVMpFyYB1npWVb8kimKz9tO7bTCLL7rzb1zAsQ==
        
というように、HTTPレスポンスステータスコードが200(リクエストが正常に終了した)ことが分かります。

対して、存在していないページにアクセスすると、

            
            $ curl -I https://deep.tacoskingdom.com/hoge
HTTP/2 403
content-type: application/xml
date: Wed, 21 Apr 2021 03:34:01 GMT
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 89d55be039a98056c94d7056281033e7.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-C4
x-amz-cf-id: dtu8tCWT0Run9IjRrQgkfRhvmTHub-F-nchXTMOqc-z4kQqsliCDdQ==
        
というように403(サーバーから適切なレスポンスの返信が拒否されている)ステータスコードが跳ね返ってきます。

このようにサーバー側で問題が起こった場合の400〜500番台のHTTPレスポンスステータスコードが返されるわけです。

参考: HTTP レスポンスステータスコード

ではどのようにしてこのエラーステータスがサーバー側から返されたときにエラーページに遷移させてクライアントに返すのかを以降で考えてみます。


困ったときのLambda@Edge

以前、301&302ステータスコードをLambda@Edgeで処置する方法を以下のブログで紹介しました。

今回もこの内容と代替同じような考え方で、
Lambda@Edgeでサーバー側からのレスポンスを盗み取って、エラーが発生していたらクライアントにエラーページのUrlにアクセスしたように仕向ける、ということをやってみます。

早速、Lambdaのhandler関数を以下のようなソースコードで新しいLambda関数を作成してみます。

なお
Lambda@Edgeのデプロイのやり方は前回と同じですので、ここので手順の説明は省略します。

            
            exports.handler = async (event, context, callback) => {
    const response = event.Records[0].cf.response;

    if (response.status >= 400 && response.status <= 599) {
        console.log('+++ Redirect To ERROR Page +++');
        response.status = 302;
        response.statusDescription = 'Found';
        response.body = '';
        response.headers['location'] = [{ key: 'Location', value: '/error/index.html' }];
        //👇①レスポンスヘッダの入れ替えは原則禁止(※後述)
        // const redirectRes = {
        //     status: '302',
        //     statusDescription: 'Found',
        //     body: '',
        //     headers: {
        //         location: [{
        //             key: 'Location',
        //             value: '/error/index.html',
        //         }],
        //     },
        // };
        // return callback(null, redirectRes);
    }

    return callback(null, response);
};
        
Lambda@Edgeをデプロイする上で、最大のつまづきポイントになるのが、Lambda関数がトリガーされるイベントの設定です。

            
            + Origin Requestタイプ:
    リクエストをオリジン(サーバー側)へ転送する直前
+ Origin Responseタイプ:
    オリジン(サーバー側)からレスポンスが到着した直後
+ Viewer Requestタイプ:
    ビュアー(クライアント側)からリクエストが到達した直後
+ Viewer Responseタイプ:
    ビュアー(クライアント側)へレスポンスを転送する直前
        
これを適切に設定していないと、503 ERROR The request could not be satisfied.でサーバー側からLambdaが実行できなかったエラーが発生します。

今回のステータスコードエラーを判定してエラーページを跳ね返すのは、
Origin Responseタイプを選択しなくてはなりません。

合同会社タコスキングダム|蛸壺の技術ブログ

さて、出来たばかりのLambda関数はまだリージョン間でリソースの参照できない状態になっている場合がありますので、メトリクスなどのエラーを監視できるようになるまでに最大24時間程度待機する必要があります。

できたての稼働状態がどうなっているかというと、CloudWatch辺りを確認して、メトリクス監視が有効になっているかどうかで判断してみてください。

合同会社タコスキングダム|蛸壺の技術ブログ

Lambda@Edgeのデプロイが完了し、リダイレクト機能が正常に働いていたら、htmlの存在しないページにブラウザからアクセスしてみますと、

合同会社タコスキングダム|蛸壺の技術ブログ

ちなみにCurlでは、

            
            $ curl -I https://deep.tacoskingdom.com/hoge
HTTP/2 302
content-length: 0
server: CloudFront
date: Wed, 21 Apr 2021 05:20:58 GMT
location: /error/index.html
x-cache: Hit from cloudfront
via: 1.1 77ffb7fa0ceed0e909a8f69baef40302.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-C4
x-amz-cf-id: S3CMArnb8i-eh213_l2W5pSt_8PV0_e7QzQbokiwnT9AXQnh7Tkvbg==
age: 14352
        
ちゃんと302リダイレクトも出来ていることが分かります。

これでCloudFrontからでも問題なくエラーページに飛ばすことが可能となりました。

よくあるエラー〜Readonlyのレスポンスヘッダは書き換えられない

Lambda@Edgeで選択したトリガーイベントタイプによっては、書き換えられないプロパティを持っていて、それをうっかり書き換えてしまうと、以下のようなエラーが起こります。このエラーはステータスコードは502で警告されることに注意しましょう。

合同会社タコスキングダム|蛸壺の技術ブログ

この原因としては、先程のソースコード内でコメントアウトして残していた①の部分のように、レスポンスのオブジェクト全体を置き換えてしまったために起こったエラーのようです。

レスポンスヘッダを書き換えるときは、Readonlyではないプロパティ値のみを置き換えるようにコーディングしましょう。


まとめ

特にエラーページはSEO対策としても薄いですし、要らないと言えば要らないのですが、こだわりのエラーページなどを実装する場合に使えるテクニックです。

覚えておくと、何かの時に役に立ちそうな気がします。


参考サイト

Lambda@EdgeでSorryページを実装してみた

origin-response トリガーを使用してエラーステータスコードを 302-Found に更新する

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。