LoginSignup
12
7

More than 5 years have passed since last update.

CloudFrontとLambda@Edgeを使ってWordpressをサーバーレス化した話

Last updated at Posted at 2017-12-19

先日AWS re:Inventに参加したところ、サーバーレスの講演が異常に人気でした。(re:Inventの感想はこちら
そんな波にのっかろうと思い、弊社の休眠しているサービスをどうにかサーバーレスに移行できないか?トライしてみました。

要件と構成

Wordpressで作られている YADORU を cloudfront, s3, lambda@edge を用いてサーバーレスにすることを目指しました。
ただし、検索部分だけは、まだサーバーで処理させたいと考えました。

構成図

Web App Reference Architecture (1).png

流れ

  1. 各ページをhtmlファイルとして保存し、s3にuploadする
  2. cloudfrontのオリジンとしてs3およびec2を設定する
  3. cloudfrontのbehaviorを設定する
  4. 以下3つの振り分けを行うlamdaを作成する
    • querystringに?s= のクエリが含む => ec2
    • UserAgentがスマートフォン => s3の /sp フォルダ
    • a,b以外 => s3の pc フォルダ
  5. cloudfrontにlambdaを設定する
  6. route53にてdomainの向き先をcloudfrontに変更する

実装1: 各ページをhtmlファイルとして保存し、s3にuploadする

まずは、既存サイトのページをhtmlファイルにしてs3にuploadします。これは以下の手順で行いました
1. sitemapからurl一覧を抽出
2. urlを一つずつ、スマートフォン、PCのUserAgentを使ってcurlで叩き、urlのpathのディレクトリ構造を保持した状態でディレクトリ名、ファイル名を指定して保存
3. スマートフォンのUserAgentで得られたhtmlを /sp、PCのUserAgentで得られたhtmlを /pc にそれぞれアップロード

問題点: ディレクトリでありファイルでもあるpathが存在する

このサイトでは、カテゴリ一覧のurlが階層構造になっていました。
例えば、以下のようなurlが存在します
https://yadoru.jp/pregnancy ... 妊娠に関する記事一覧
https://yadoru.jp/pregnancy/pregnancy-sign ... 妊娠兆候に関する記事一覧
この場合、 /pregnancy は「ディレクトリでありファイルでもあるpath」となります。
通常webサーバーでは、 /pregnancy のファイルは /pregnancy/index.html におけばよいので、今回もそのようにしました

実装2: cloudfrontのオリジンとしてs3およびec2を設定する

create originから、ec2のurlおよびs3を設定します
s3オリジンにおいては、s3のwebhostingのドメイン
ec2オリジンにおいてはそのドメイン(ここではelbのドメイン)
を指定します。

[ s3オリジン ]
origin.png

[ ec2オリジン ]
origin for elb.png

問題点: index.htmlが表示されない

s3はwebsite hostingにすることで、index.htmlを表示できます
しかし、originとしてs3のbucketを設定してもその設定は有効にはなりません。
cloudfrontのオリジンを指定する際、フォームにフォーカスするとs3バケットの一覧が表示されますが、それを選択してしまうとindex.htmlが表示されません。また、cloudfront-is-mobile-viewer などのcloudfrontが付与してくれるヘッダーも渡ってきません。
website hostingのドメイン名をしていすることで、ディレクトリが指定された際に、index.htmlを表示してくれるようになります。

[bucket名].s3-website-[region名].amazonaws.com
# 例) test-bucket.s3-website-ap-northeast-1.amazonaws.com

実装3: cloudfrontのbehaviorを設定する

s3オリジンをデフォルトに指定します。
pathが search を含んでいれば ec2オリジンに流すようにします
ヘッダーのwhitelistにモバイル判定に用いるヘッダーを追加すること、クエリのwhitelistに s を追加することを忘れないようにします。

[ behavior ]
cloudfrontのbehavior.png

[ s3 behavior 詳細 ]
s3 behavior.png

[ ec2 behavior 詳細 ]
ec2 behavior.png

実装4: 振り分けを行うlamdaを作成する

以下のようなlambda関数を作成します。この時、lambda関数はus-east-1(N. Virginia)に作成しなければいけません。
以下の関数は
- querystringに s を含んでいれば、 /search を付与したパスにリダイレクト => ec2オリジンに
- mobileと判定すれば、パスに /sp を付与する => s3オリジンの /sp
- それ以外なら、パスに /pc を付与する => s3オリジンの /pc
を実行しています。

const querystring = require('querystring');
exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;
    const params = querystring.parse(request.querystring);;

    const desktopPath = '/pc';
    const mobilePath = '/sp';

    const userAgent = request.headers['user-agent'][0]['value'];
    const path = request.uri;

    if(params.s !== null) {
        const response = {
            status: '302',
            statusDescription: 'Found',
            headers: {
                location: [{
                    key: 'Location',
                    value: '/search/'+ params.s,
                }],
            },
        };
        callback(null, response);
        return
    } else if(headers['cloudfront-is-mobile-viewer']
               && headers['cloudfront-is-mobile-viewer'][0].value === 'true') {
        request.uri = mobilePath + request.uri;
    } else if (headers['cloudfront-is-tablet-viewer']
               && headers['cloudfront-is-tablet-viewer'][0].value === 'true') {
        request.uri = mobilePath + request.uri;
    } else {
        request.uri = desktopPath + request.uri;
    }

    callback(null, request);
};

実装5. lambda関数をcloudfrontに設定する

s3オリジンのbehaviorのorigin requestに先ほど作成したlambda関数を設定します。
(viewer requestでも可能ですが、その場合は cloudfront-is-mobile-viewerヘッダー ではなく user-agent でモバイル判定を行うことになります。しかしこの場合、キャッシュが存在していたとしてもlambda関数が実行されてしまい無駄が生じてしまいます)
lambdaの設定.png

問題点1: cloudfrontのbehaviorにおけるpath patternの方が先に評価される

cloudfrontのbehaviorにおけるpath patternの方が先に評価されてしまうため、以下のようにparamsにsを含むものは /search に流すように以下の設定をしようとしましたが結果s3の /search に流されてしまいました。
これは、origin requestを viewer requestに変えても同じでした。

if(params.s !== null) {
   request.uri = "/search" + request.uri;
}

問題点2: エンドスラッシュの有無に関するリダイレクトがうまく動かない

フォルダのindex.htmlを表示したい時、
[s3 webhostingドメイン]/pregnancy でアクセスすると [s3 webhostingドメイン]/pregnancy/ にリダイレクトされるのですが、 [cloudfrontドメイン]/pc/pregnancy/ にリダイレクトされるようになってしまいました。
そこで、以下のlambda関数を origin response に指定することで回避しました。
リダイレクトのLocationが /pc/sp ではじまるパスだった場合、それらを取り除くようにしてます。

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

    if(status === "302" && location !== null){
        const match = location[0]['value'].match(/^\/(pc|sp)(\/.*)/);
        if(match !== null){
            response.headers.location[0]['value'] = match[2];
        }
    }

    callback(null, response);
};

実装6: route53にてdomainの向き先をcloudfrontに変更する

route53のAレコードをcloudfrontに向けます

問題点: ssl certificateもus-east-1で生成しなければならない

cloudfrontで割り当てられるドメインではなく、カスタムのドメインを用いる場合、ssl certificateもus-east-1で再度生成しなければなりません。
「certificateあるはずなのに、出てこない」とはまりました。

結果

以下のurlで検索以外サーバーレスを実現できました。
https://d1nb4cler3jaeb.cloudfront.net
現状ec2においてmobile判定を user-agent で行なっているのでmobileで検索すると、pcの検索結果が表示されてしまいます。 これは、cloudfront-is-mobile-viewerで判定をするように今後修正予定です。

レスポンスタイム

「既存のec2」、「viewer requestにlambdaを設定したケース」「origin requestにlambdaを設定したケース」でレスポンスタイムを計測してみました

[既存のec2]
初回: 約200ms
2回目以降: 約40ms

[viewer requestにlambdaを設定したケース]
初回: 約370ms
2回目以降cloudfront Refresh: 約140ms
2回目以降cloudfront Hit: 約60ms

[origin requestにlambdaを設定したケース]
初回: 約320ms
2回目以降cloudfront Refresh: 約170ms
2回目以降cloudfront Hit: 約20ms

最後に

今回はwordpressをテンプレートエンジンとして使って、サーバーレスにできないか?と思い挑戦してみました。
完全にはできなかったものの、かなり近しい状態まで持っていけました。
サーバーレスの参考にしていただければ幸いです。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7