先日AWS re:Inventに参加したところ、サーバーレスの講演が異常に人気でした。(re:Inventの感想はこちら)
そんな波にのっかろうと思い、弊社の休眠しているサービスをどうにかサーバーレスに移行できないか?トライしてみました。
要件と構成
Wordpressで作られている YADORU を cloudfront, s3, lambda@edge を用いてサーバーレスにすることを目指しました。
ただし、検索部分だけは、まだサーバーで処理させたいと考えました。
構成図
流れ
- 各ページをhtmlファイルとして保存し、s3にuploadする
- cloudfrontのオリジンとしてs3およびec2を設定する
- cloudfrontのbehaviorを設定する
- 以下3つの振り分けを行うlamdaを作成する
- querystringに
?s=
のクエリが含む => ec2 - UserAgentがスマートフォン => s3の
/sp
フォルダ - a,b以外 => s3の
pc
フォルダ
- cloudfrontにlambdaを設定する
- route53にてdomainの向き先をcloudfrontに変更する
実装1: 各ページをhtmlファイルとして保存し、s3にuploadする
まずは、既存サイトのページをhtmlファイルにしてs3にuploadします。これは以下の手順で行いました
- sitemapからurl一覧を抽出
- urlを一つずつ、スマートフォン、PCのUserAgentを使ってcurlで叩き、urlのpathのディレクトリ構造を保持した状態でディレクトリ名、ファイル名を指定して保存
- スマートフォンの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のドメイン)
を指定します。
問題点: 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
を追加することを忘れないようにします。
実装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関数が実行されてしまい無駄が生じてしまいます)
問題点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をテンプレートエンジンとして使って、サーバーレスにできないか?と思い挑戦してみました。
完全にはできなかったものの、かなり近しい状態まで持っていけました。
サーバーレスの参考にしていただければ幸いです。