はじめに
こんにちは、サービス基盤グループのfukajunです。各種サービスのインフラ全般を担当しています。 先日、弊社で運営するメディアサイトにCloudFrontを導入しました。 対象のサイトは、PC/スマホ/アプリそれぞれに表示が異なるものでした。 導入にあたっての導入前の振る舞い、CloudFrontでの実現方法、あわせて進めるにあたってのTIPSについて書きたいと思います。
デバイスごとの出し分け
PC/スマホ
まずはデバイスの種類ごとの表示の出しわけです。もともとはアプリケーション側で
PC/スマホ/アプリぞれぞれのuser-agent
ごとに表示を切り替えていました。
CloudFrontでは、デバイスを判定した結果として以下のような特定のヘッダーを設定してくれます。
このヘッダーをCloudFrontのキャッシュキーとして追加することでPC/スマホのキャッシュを
わけることができました。
参考に、デバイス判定でCloudFrontが追加してくれるヘッダーを載せておきます。
CloudFront-Is-Android-Viewer - ビューワーが Android オペレーティングシステム搭載のデバイスであると CloudFront が判断すると、true に設定します。
CloudFront-Is-Desktop-Viewer - ビューワーがデスクトップデバイスであると CloudFront が判断すると、true に設定します。
CloudFront-Is-IOS-Viewer – ビューワーが iPhone、iPod touch、その他の iPad デバイスなど、Apple モバイルオペレーティングシステム搭載のデバ イスであると CloudFront が判断すると、true に設定します。
CloudFront-Is-Mobile-Viewer - ビューワーがモバイルデバイスであると CloudFront が判断すると、true に設定します。
CloudFront-Is-SmartTV-Viewer - ビューワーがスマートテレビであると CloudFront が判断すると、true に設定します。
CloudFront-Is-Tablet-Viewer - ビューワーがタブレットであると CloudFront が判断すると、true に設定します。
アプリについて
同サイトにはアプリも存在しており、スマホ版に似たスタイルをウェブビューで表示しています。特殊な対応をせずCloudfrontのデバイス判定に任せた場合、もちろんアプリという判定はないのでスマホと同様のページが表示されます。
もともとアプリケーション側では、アプリからのリクエストでは、user-agent
ヘッダーに特定の文字列が含まれているかどうかでアプリと判定をしていました。Cloudfront側でも、user-agent
ヘッダーをキャッシュキーとして含めることで表示をだしわけることができますが、値の種類が多すぎてスマホやPCのキャッシュヒット率が下がってしまいます。
そこで、user−agent
ヘッダーに特定の文字が含まれていればx-app-name
ヘッダーという独自のヘッダーを追加し、このヘッダーをキャッシュキーとして使うことにしました。これについては、 Lambda@EdgeもしくはCloudFront Functionsを使うことでできることがわかったのですが、実際には次の理由からCloudFront Functionsを使って行うことにしました。 参考:関数を使用してエッジでカスタマイズ
- CloudFrontにリクエストがいく手前での処理であること(ビューワーリクエスト)
- 特定の文字が含まれているかチェックする処理ですぐ終わる(1ミリ秒以下)
- コード量も少ない(10KB以下)
- 簡単な処理のためメモリ使用量も少ない(2MB以下)
- Lambda@Edgeと比較してリクエストあたりの料金が安い
実装した内容について
function handler(event) { var request = event.request; var userAgent = request.headers['user-agent']; if(userAgent && userAgent.value.match(/XxxxxApp/)) { request.headers['x-app-name'] = {value: 'Xxxxx'}; } return request; }
Lambda@Edgeを利用したキャッシュ動作のデバッグ
上記の通り、CloudFrontのデバイス判定を利用してデバイス種類ごとの出し分けを行っていましたが 本番環境に導入するとスマホでアクセスしたときに、PC用の表示になってしまう現象が起きました。 なかなか状況がつかめずに困っていましたが、デバッグにLambda@Edgeを利用することで状況をつかむことできました。 デバッグ用の処理は、オリジンがレスポンスをCloudFrontに対して返却するオリジンレスポンスのタイミングに 設定します。 このタイミングであれば、CloudFrontが付与している上記のヘッダーや、アプリケーションが返しているレスポンスヘッダーやボディに対してもアクセスすることができ、内容をログに出力して確認することができます。 またこの方法であれば、アプリケーションへのデバッグ用コードの追加が不要です。
オリジンレスポンスに一時的に設定したLambda@Edgeのコードサンプル
import { createRequire } from 'module'; const require = createRequire(import.meta.url); const querystring = require('querystring'); export const handler = async (event, context, callback) => { const request = event.Records[0].cf.request; console.log('Method:', request.method, 'ClientIp:', request.clientIp, 'Uri:', request.uri, 'Headers:', JSON.stringify(request.headers)); callback(null, request); };
ちなみにデバッグした結果、たまにアクセスのあるbotのuser-agent
に対し判定結果としてCloudFrontがCloudFront-Is-Mobile-Viewer
ヘッダーをtrue
で返していることが判明しました。
通常、アプリケーションがCloudFrontが判定したヘッダーをみて表示を出し分けるように変更していれば問題になりませんが、今回はできるだけアプリケーションコードに手を入れない形で進めていたため、アプリケーション側ではCloudFrontの判定結果を参照することなく、以前のまま独自でデバイス判定を行い、このbotに関しては、CloudFront側の判定とは異なりるPCであると判定したため、結果スマホのためのキャッシュにPC版の表示がキャッシュされてしまったのでした。実際に判定内容などを目で見える形にしてみるのは大切ですね。目でみてデバッグするのは大切ですね。
しかしiOSでもAndroidでもないものがモバイルと判定されるとは思っていなかったので、予想もしていませんでした。
今回は、CloudFront-Is-Mobile-Viewer
のかわりにCloudFront-Is-IOS-Viewer
とCloudFront-Is-Android-Viewer
をキャッシュキーとして利用することでひとまず現象の発生を防ぐことができました。
Lambda@Edgeを使う上でのハマりどころ
デバッグ用のLambda@Edgeからログを出力して参照するときに、ハマったので書いておきます。
まず、Lambda@Edgeはus-east-1リージョンでのlambda関数の作成しなければならないのですが、
実際にログが出力するときは、us-east-1のリージョンではなく、CloudFrontで実際にアクセスのあったリージョン
のCloudwatch Logsにログが出力されます。
今回は、アクセスされるCloudFrontが主に日本だったのでap-norttheast-1のCloudFrontにログが出力されていました。
さらに出力されるロググループ名も一般的な/aws/lambda/<Lambda関数名>
ではなくus-east-1
を追加した/aws/lambda/us-east-1.<Lambda関数名>
出力されるため注意が必要です。
まとめ
- アプリケーションでモバイル、PCなど表示が異なってもCloudFrontは使える
- デバッグに見える化はたいせつ
- Lambda@Edge, CloudFront Functionsを使うことで実現できることが増える