apacheからS3に静的ページを移行してパス補完で詰まった話


何を書いた記事か

AWS S3を用いて静的ページをホスティングする際のTipsです。
特に、apacheなどで配信されていた静的ページをS3に移行する際に使えると思います。

apacheなどのMWがよしなに解析してくれていたURI PathをS3を用いたホスティングでどのように実現するかについて記載します。

ストーリー

とあるWebサイトのクラウド移行案件を進めることになりました。
そのWebサイトには、静的コンテンツ(HTML/CSS/JavaScript)のみで構成されたLP(静的ページ)が存在することがわかり、せっかくクラウドに移行するので、信頼性の高いS3から配信するようにしようと考えました。
また、対象のLPは接続できるIPに制限をかける必要があり、その実現についても考える必要がありました。

As-Is

  • apacheで静的ページを配信
  • LoadBalancerでSSLを終端し、FirewallでIP制限を実施
  • アクセスログはapache→fluentdで取得

To-Be

  • 静的コンテンツはS3上に配置
  • SSL通信を実現するため、CloudFrontにACM管理の証明書を持たせ、オリジンにS3を指定
  • IP制限を実現するため、CloudFrontの手前にAWS WAFを設定し、IP Restrictionルールを設定
  • アクセスログはCloudFrontとS3側で取得設定

S3静的コンテンツのSSL化参考
https://dev.classmethod.jp/cloud/aws/tls-for-s3-web-hosting-with-cloudfront/

AWS WAFを用いたIP制限参考
https://blog.mmmcorp.co.jp/blog/2018/12/14/s3-cloudfront-awswaf/

発生した問題

コンテンツをS3に配置し、CloudFrontとAWS WAFを構築して、さぁこれで完璧と思ってテストをすると、いくつかのリンクが切れていることが判明しました。
原因は、apacheがよしなに解析・補完してくれていたパスが今回の構成では補完できなくなったことにあります。

例えば、下記のような補完です。

https://hoge.com/hoge/ -> https://hoge.com/hoge/index.html
https://hoge.com/hoge -> https://hoge.com/hoge/index.html

補完が必要なのは、2パターンでした。

  1. URIが / で終わっていて、インデックスドキュメントが省略されているパターン
  2. URIがディレクトリ名で終わっていて、ディレクトリを指定する / とインデックスドキュメントが省略されているパターン

逆に、下記のようなパターンは補完せずにそのままレスポンスを返す必要があります。

  1. URIにディレクトリ・ドキュメントパスまで明確に指定されているパターン。 https://hoge.com/hoge/hoge.htmlなど

今回の構成でこのようなパス補完についてどのように対応すべきか考えました。

対処

結果として、Lambda@Edgeで補完処理を実行してあげると、想定通りの動きになることがわかりました。

Lambda@Edgeはエッジロケーション、つまりCloudFrontと同じロケーションで実行されます。
CloudFrontが受け取ったリクエストをインターセプトして、所定の処理を実施し、CloudFrontに流すことが可能です。

今回上で補完対応が必要だとわかったパターンについて、requestエンティティを補完してあげるLambdaを作成し、@Edgeにデプロイしました。

lambda_function
'use strict';
exports.handler = (event, context, callback) => {
    //
    // CloudFrontが受け取ったリクエストを取得
    var request = event.Records[0].cf.request;
    //
    // URIを補完前後で変数に格納
    // 変換が不要なパターンもあるので、初期化の段階でnewuriにはolduriの値を代入
    var olduri = request.uri;
    var newuri = olduri;
    // 
    // URIが / で終わってる場合(パターン1)
    if (olduri.slice(-1) === '/') {
        // 末尾にindex.htmlを詰める
        newuri = olduri.replace(/\/$/, '\/index.html');
    } else {
        // URIを / でsplitし、最後の要素を取得。
        // そこにピリオドが含まれていなければパターン2としてインデックスドキュメントを詰める
        var last_string = olduri.split('/').pop()
        if (!last_string.includes('.')) {
            newuri = olduri + '/index.html';
        }
    }
    console.log(newuri)
    //
    // requestオブジェクトのuriプロパティを、置換後の値に変更
    request.uri = newuri;
    //
    // CloudFrontに返す
    return callback(null, request);
    //
};

これで、必要な補完は実行してくれつつ、他が不要なURI構成についてはそのままCloudFrontz→オリジンS3にアクセスが行くこととなります。

注意点

今回の対応を実施したLPを運用していて気づいた注意点があります。

  • Lambda@Edgeは同時実行数制限をかけることができない(コンソール上は指定できるが、効かない)
  • Lambda@Edgeはエッジロケーションで実行されるので、どこのリージョンのログに現れるかがわからない(国内サービスであればほぼap-noetheast-1ですが)

他サービスで多重にlambdaを起動する必要がある場合などはedgeで同時実行数が消費されて他サービスに影響が出る可能性があるので、上限を引き上げるかAWSアカウントを分離するか検討した方がいいと思います。

まとめ

なんだかんだ言ってapacheは便利で賢いな、と思いました。
まだまだ要件次第では静的Webサイトホスティングをサーバレスで実現するのは難しいケースがありそうです。

また今回の問題はそもそもHTMLのリンクパスの記法が統一されていないことが原因で発生しました。
HTML書く時はlink属性に詰めるパスパラメータの書き方は統一し、なるべく補完に頼らずにフルの相対パスを記載するのがいいと思います。