cloud frontとlambda@edgeでUA判定をしてPCページとSPページを出し分けてみた

# cloud frontとlambda@edgeでUA判定をしてPCページとSPページを出し分けてみた
Webサービスを構築する仕事がきたのですが、ビジネスロジックが不要なサービスだったので
できるだけカンタンに作ろうと思いサーバーレス構築に挑戦してみた際にやってみたレポートになります。

## 使ったAWSのサービス
* cloud front
* lambda（ **＠Edgeはバージニア北部リージョンで関数作成する必要があります** ）
* S3
* Route53（ドメイン設定の話なので今回は特に登場させません）
* ACM（SSL証明書の話なので今回は特に登場させません）
* Cloud Watch（lambdaを利用したら自動でついてきた）

## やったこと
1. S3にHTMLを配置
2. cloud frontとS3を紐付け
3. ドメインにアクセスした際にどのHTMLを出すか設定（トップページの設定）
4. lambda@edgeにUA判定処理を実装
5. lambda@edgeとcloud frontの紐付け（トリガー設定）今回はViewerリクエストをトリガーに設定

ざっくり動くまででだいたい２〜３時間くらいでできました。

## lambda側に設定する必要のあるポリシーの信頼関係※記載わすれてたので追記しました（2019/07/10）

アクセス権限はS3とCloudFrontによしなに設定してあげてください。
お手軽にやるならフル権限つけてしまってよいかと思います。

~~~
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "edgelambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
~~~

1〜３はいろいろなところに記事が落ちているのでlambdaのソースと引っかかったポイントを紹介します。
## lambdaに実装したソース
~~~javascript
'use strict';

const whitelist = [
  'android',
  'iphone',
  'ipad'
];

const isSpBrowser = (uas) => {
  if (uas && Array.isArray(uas) && uas.length > 0) {
    return uas.some(ua => whitelist.some(w => ua.value.toLowerCase().indexOf(w) !== -1));
  }
  return false;
};

// User-AgentからPC・SP判定を行ない描画ページを切り替える
exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;
    
    // console.log('headers.user-agent. = ' + headers['user-agent']);
    if (headers['user-agent']) {
        // URI整理
        // cloud frontにトップページとして設定したindex.htmlがURIに入ってくるので除去
        var uri = request.uri.slice(-10) === ('index.html') ? request.uri.slice(0, -10) : request.uri;
        uri = uri.slice(-1) === ('\/') ? uri : uri + '\/';
        console.log('headers[user-agent] = ' + headers['user-agent'][0].value);
        // URI判定
        // ファイル読み込みで呼び出された場合中断
        if(uri.match(/(　\.png|\.css|\.js|\.jpeg|\.jpg|\.bmp|\.gif|\.pdf|\.svg|\.zip)/)) {
            callback(null, request);
            return;
        }
        // UA判定
        if (isSpBrowser(headers['user-agent'])) {
            uri = uri + 'SP用描画ファイル名'
        } else {
            uri = uri + 'PC用描画ファイル名'
        }
        console.log('uri = ' + uri);
        request.uri = uri
        callback(null, request);
        return;
    }
    callback(null, request);
};
~~~

PC/SPの判定においてはこういう書き方もできます。（というか今はこっちのほうが推奨されてます）
※下記ソースで行う場合トリガーをオリジンリクエストイベントに設定する必要があります。
（ビューワーリクエストイベントの後に CloudFront-Is-*-Viewer ヘッダーを追加されるため）
__※Whitelist HeadersにHostsを混ぜるとS3へのリクエストはCloudFrontが署名を行う形になり想定外の挙動になる可能性があるので要注意__

~~~javascript
    // UA判定　前述コードのUA判定部分と置き換える
    if (headers['cloudfront-is-desktop-viewer']
        && headers['cloudfront-is-desktop-viewer'][0].value === 'true') {
        uri = uri + 'PC用描画ファイル名'
    } else if (headers['cloudfront-is-mobile-viewer']
               && headers['cloudfront-is-mobile-viewer'][0].value === 'true') {
        uri = uri + 'SP用描画ファイル名'
    } else if (headers['cloudfront-is-tablet-viewer']
               && headers['cloudfront-is-tablet-viewer'][0].value === 'true') {
        uri = uri + 'SP用描画ファイル名' // タブレットでSP画面で見せる場合
    } else if (headers['cloudfront-is-smarttv-viewer']
               && headers['cloudfront-is-smarttv-viewer'][0].value === 'true') {
        uri = uri + 'PC用描画ファイル名' // SmartTVVでPC画面で見せる
    }
~~~

参考：
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-device

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html#lambda-examples-redirecting-examples

## ポイント
1. ログはCloud Watchに出力されるがアクセスされたcloud frontのリージョンに出力されたのでlambdaを配置しているリージョンではない
2. ドキュメントは基本英語版が正
3. トリガー設定はlambda側からやるほうがらく
4. 困ったら迷わず有料サポートを使う（結果そのほうが早いのでトータルでの費用が下がる）

## 最後に
イベントページなどの後ろにビジネスロジックがいらないサービスであればこの構成は結構おすすめできると思います。
インフラ作業部分（エンジニアが必要な作業分）は大体５人日程度で収まったのでその分デザインやプロモーションにコストがまわせるのも強みかと
※初回なのでこれくらいですが慣れればもっと短くなる予感