Lambda@Edge でデバイス判定をする

こんにちは、インフラストラクチャー部の沼沢です。

今回は、2016年の re:Invent で発表された Lambda@Edge を使って、リクエスト元のデバイス判定を実装してみます。

Lambda@Edge といえば、CloudFront の Edge ロケーション上で Lambda が実行できる 画期的なサービスです。
現在は Limited Preview 中で、General Availability を待ち望んでいるサービスの1つです。

Lambda@Edge についてはこちら → AWS Lambda@Edge (プレビュー)

本投稿時点ではまだ Preview ですので、この記事の内容を試したい場合はこちらから事前に利用申請をしておきましょう。

re:Invent 2016 で発表された各サービスについては以下をご参考まで。
AWS re:Invent 2016 新サービスまとめ 1 | mediba Creator × Engineer Blog
AWS re:Invent 2016 新サービスまとめ 2 | mediba Creator × Engineer Blog

デバイス判定は既に簡単にできる機能がある

デバイスタイプに基づいてオブジェクトをキャッシュするように CloudFront を設定する

上記に記載の通り、以下の4タイプの判定で事足りる場合は、このヘッダをそれぞれオリジンサーバーに転送し、オリジンサーバー側でこのヘッダを読み取ってレスポンスを分けたりする方法が良いと思います。

  • CloudFront-Is-Desktop-Viewer
  • CloudFront-Is-Mobile-Viewer
  • CloudFront-Is-SmartTV-Viewer
  • CloudFront-Is-Tablet-Viewer

しかし、この4タイプでは足りない場合(例えば iOS, Android でデザインを分けたい等)には、この方法は使えません。
その場合、User-Agent をオリジンサーバーに転送してアプリケーション側で判定する方法を取ることで解決できますが、そうすると CloudFront のキャッシュ効率が悪くなってしまうというデメリットがあります。

そこで、Lambda@Edge を使ってこの課題を解決してみたいと思います。

概要

今回は、以下の判定をできるようにします。

  • iPhone
  • iPad
  • Android
  • 上記以外(Other)

CloudFront にアクセスに来た際に、Lambda@Edge を Viewer Request で起動するように設定し、Request Header にカスタムヘッダを付けて nginx でログに出力するところまでを実装します。
Viewer Request とする理由は、CloudFront のエッジ上の動作が行われる前に、デバイス判定→ヘッダ追加を行いたいためです。

やってみる

EC2(nginx) を用意

詳細な手順は割愛しますが、以下の手順で nginx がインストールされた EC2 インスタンスを用意します。

  • EC2 インスタンスをローンチ
  • ローンチした EC2 インスタンスに EIP を付与(Public IP でも可)
  • ローンチした EC2 インスタンスに ssh ログインし、nginx をインストール

nginx のアクセスログをカスタマイズ

/etc/nginx/nginx.conf の log_format を以下のように修正し、nginx を再起動します。

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for" '
                  '$http_custom_device ';   # 追加

Lambda ファンクションを用意

  • Lambda 関数の作成
  • 設計図の選択: ブランク関数
  • トリガーの設定: (何も変更せず) 次へ
  • 関数の設定
    • 名前: device_judge
    • 説明: device judge
    • ランタイム: Edge Node.js 4.3
  • Lambda 関数のコード
    • コード エントリ タイプ: コードをインラインで編集
    • 以下のコードを入力
'use strict';

# User-Agent からデバイスを判定し、"Custom-Device" ヘッダを追加
exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;
    if (headers['User-Agent']) {
        headers['Custom-Device'] = headers['User-Agent'][0].match(/(Android|iPhone|iPad)/)? RegExp.$1: 'Other';
    }
    callback(null, request);
};
  • Lambda 関数ハンドラおよびロール
    • ハンドラ: index.handler
    • ロール: “カスタムロールの作成” で作成される “lambda_basic_execution” を指定

CloudFront Web Distribution を用意

以下の通り設定していきます。

作成後、Status が Deployed になるまで待ち、CloudFront の Domain Name (xxxx.cloudfront.net) にアクセスした際に nginx のデフォルトページが表示されれば準備は OK です。

動作検証

ブラウザで、各 User-Agent でアクセスし、nginx のアクセスログを確認します。

“その他” 判定

“その他” を判定させるため、Google Chrome で User-Agent を変更せずアクセスしてみます。

***.***.***.*** - - [17/Apr/2017:17:29:08 +0900] "GET / HTTP/1.1" 200 3770 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36" "***.***.***.***" Other

無事に末尾に “Other” が記録されました。

“iPhone” 判定

“iPhone” を判定させるため、Google Chrome で User-Agent を “iPhone 6 Plus” に変更し、アクセスしてみます。

***.***.***.*** - - [17/Apr/2017:17:47:41 +0900] "GET / HTTP/1.1" 200 3770 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1" "***.***.***.***" iPhone

無事に末尾に “iPhone” が記録されました。
念のため、"iPhone 5" にしてアクセスしてみても、同じように末尾に “iPhone” が記録されました。

“iPad” 判定

“iPad” を判定させるため、Google Chrome で User-Agent を “iPad” に変更し、アクセスしてみます。

***.***.***.*** - - [17/Apr/2017:17:55:54 +0900] "GET / HTTP/1.1" 200 3770 "-" "Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1" "***.***.***.***" iPad

無事に末尾に “iPad” が記録されました。
念のため、"iPad Pro" にしてアクセスしてみても、同じように末尾に “iPad” が記録されました。

“Android” 判定

最後に、"Android" を判定させるため、Google Chrome で User-Agent を “Galaxy S5” に変更し、アクセスしてみます。

***.***.***.*** - - [17/Apr/2017:17:59:28 +0900] "GET / HTTP/1.1" 200 3770 "-" "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Mobile Safari/537.36" "***.***.***.***" Android

無事に末尾に “Android” が記録されました。
念のため、"Nexus 6P" にしてアクセスしてみても、同じように末尾に “Android” が記録されました。

まとめ

今回は検証のため Header を全てオリジンサーバーへ渡しましたが、実際には CloudFront の設定でオリジンサーバーへ渡すヘッダを指定(“Forward Headers” の Whitelist で “Custom-Device” を指定) することで、指定した Header の値ごとにキャッシュを持たせることができるようになります。

また、今回は Lambda@Edge をデバイス判定に利用しましたが、他にもいろんな用途で利用できると思います。

まだ Preview なのが残念ですが、今後のシステム設計の参考になれば幸いです。