CloudFront + Lambda@Edgeを利用して、user-agentに応じて異なるwebページを表示させる

CloudFront + S3でウェブページを表示する仕組みは以前作ったことがありますが、
「**AWS上の仕組みを使ってスマートフォンとPCからのアクセスで、異なるwebページを表示させることってできるのかな？**」と思ったので、実践してみました。

## 目標
・スマートフォンからのアクセス時は「スマートフォン専用サイト」
・PCからのアクセス時は「PC専用サイト」を表示させる

## 条件
・user-agentに「iPhone」「Android」が含まれている場合にスマートフォン、含まれていない場合はPCからのアクセスと判断する
・表示させるのはS3に配置したhtmlファイル

## 構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969669/018162c8-072d-e62e-0585-2ab732d79111.png)

## 流れ
①S3に2つのhtmlファイルを配置する
②Lambda@Edgeを作成する
③CloudFrontのディストリビューションを作成する
④S3に権限を付与する
⑤Lambda実行ロールに権限を付与する
⑥CloudFrontのディストリビューションにLambda@Edgeを紐づける

## 作業
### ①S3に2つのhtmlファイルを配置する
#### 1-1. S3バケット「**example-user-agent-site**」を作成する
なお、セキュリティ観点からアクセスは「**非公開のバケットとオブジェクト**」とし、「**パブリックアクセスはすべてブロック**」で設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969669/e38b2915-f097-10e4-f121-6373a7d0973d.png)

#### 1-2. ファイルを配置する
構成は以下となる
```
-example-user-agent-site/
 ├ pc.html
 └ smartphone.html
```

なお、配置するファイルはuser-agentが表示されるようなものとしました。
※こちらの記事を参考に記載しております
[ユーザーエージェントを取得する(navigator.userAgent)](https://www.javadrive.jp/javascript/webpage/index14.html)

```pc.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PC用画面</title>
</head>
<body>

<h1>PC専用画面</h1>

<p id="msg">これはPC専用の画面です。user-agentに「iPhone」もしくは「Andrond」を含まない場合のみ表示されます</p>

<script>
let element = document.getElementById('msg');
element.insertAdjacentHTML('afterend', '<p>' + navigator.userAgent + '</p>');
</script>


</body>
</html>
```

```smartphone.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PC用画面</title>
</head>
<body>

<h1>スマートフォン専用画面</h1>

<p id="msg">これはスマートフォン専用の画面です。user-agentに「iPhone」もしくは「Andrond」を含む場合のみ表示されます</p>

<script>
let element = document.getElementById('msg');
element.insertAdjacentHTML('afterend', '<p>' + navigator.userAgent + '</p>');
</script>


</body>
</html>
```

### ②Lambda@Edgeを作成する
#### 2-1.Lambdaを作成する
まずは通常のLambdaを作成します
注意点としては、Lambdaはリージョンを「**米国東部 (バージニア北部) us-east-1**」で作成する必要があります。

|項目|設定値|
|-|-|
|関数名|check-userAgent|
|ランタイム|Node.js 20.x|
|アーキテクチャ|x86_64|
|実行ロール|新規作成|

#### 2-2.コード記載する
ユーザーエージェントにiphoneとandroidが含まれている場合はスマートフォン用、含まない場合はPC版のURLとする記載をする。
※注意点として、既存のファイル名は「index.mjs」となっているが、形式がmjsだとエラーとなる。ファイルは拡張子を変更して「index.js」とする。


```index.js
'use strict';

exports.handler = async (event) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;

    // ユーザーエージェントに基づいて異なるS3オブジェクトにリダイレクトする
    const userAgent = headers['user-agent'] ? headers['user-agent'][0].value.toLowerCase() : '';

    if (userAgent.includes('iphone') || userAgent.includes('android')) {
        request.uri = '/smartphone.html';
    } else {
        request.uri = '/pc.html';
    }

    return request;
};
```

#### 2-3.バージョンを作成する
CloudFrontで利用するためにはバージョンを作成する必要がある。今回は単純に「1」を作成する
関数の ARNは **arn:aws:lambda:us-east-1:{アカウント番号}:function:check-userAgent:1**となる
※ARNはこの後利用するので控えておく

### ③CloudFrontのディストリビューションを作成する
#### 3-1.オリジンアクセス(OAI)を作成する


|項目|設定値|
|-|-|
|名前|example-user-agent-site|
|説明|For S3 "example-user-agent-site"|
|署名動作|署名リクエスト|
|認証ヘッダーを上書きしない|チェックなし|
|オリジンタイプ|S3|


#### 3-2.ディストリビューションを作成
以下の設定をする

・オリジン
|項目|設定値|
|-|-|
|オリジンドメイン|S3 <br>(例:example-user-agent-site.s3.ap-northeast-1.amazonaws.com)|
|オリジンパス|-|
|名前|example-user-agent-site|
|オリジンアクセス|Origin access control settings|
|Origin access control|手順3-1で作成したOAI(例：example-user-agent-site)|
|カスタムヘッダーを追加|-|
|オリジンシールドを有効にする|いいえ|

・デフォルトのキャッシュビヘイビア
|項目|設定値|
|-|-|
|パスパターン|デフォルト (*)|
|オブジェクトを自動的に圧縮|Yes|
|ビューワープロトコルポリシー|HTTP and HTTPS|
|許可された HTTP メソッド|GET, HEAD|
|ビューワーのアクセスを制限する|No|
|キャッシュキーとオリジンリクエスト|Cache policy and origin request policy|
|キャッシュポリシー|CachingOptimized|
|オリジンリクエストポリシー|-|
|レスポンスヘッダーポリシー|-|

・関数の関連付け ※後で再設定する
|対象|関数タイプ|関数 ARN/名前|本文を含める|
|-|-|-|-|
|ビューワーリクエスト|関連付けなし|-|-|
|ビューワーレスポンス|関連付けなし|-|-|
|オリジンリクエスト|関連付けなし|-|-|
|オリジンレスポンス|関連付けなし|-|-|

これ以降の設定はご自身の環境に合わせて設定いただいて問題ありません。
作成後にバケットポリシーに関するポップアップが表示されるので、表示されたポリシーをコピーしておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969669/c114dd3d-d623-7ce3-4125-232768deca63.png)

### ④S3に権限を付与する
#### 4-1. S3にバケットポリシーを追記する
手順3-2で控えておいたポリシーをS3のバケットポリシーに記載する

```
{
        "Version": "2008-10-17",
        "Id": "PolicyForCloudFrontPrivateContent",
        "Statement": [
            {
                "Sid": "AllowCloudFrontServicePrincipal",
                "Effect": "Allow",
                "Principal": {
                    "Service": "cloudfront.amazonaws.com"
                },
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::example-user-agent-site/*",
                "Condition": {
                    "StringEquals": {
                      "AWS:SourceArn": "arn:aws:cloudfront::{アカウント番号}:distribution/{ディストリビューション番号}"
                    }
                }
            }
        ]
      }
```




### ⑤Lambda実行ロールに権限を付与する
#### 5-1. Lambda実行ロールに信頼関係を記載する

手順2で作成したLambdaの実行ロール　(例:check-userAgent-role-aqs8vip)に、信頼関係を追記する
※これを入れることでLamnda@EdgeとしてCloudFrontからの呼び出しが可能になる

```信頼されたエンティティ
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "lambda.amazonaws.com",
                    "edgelambda.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

#### 5-2. Lambda実行ロールにインラインポリシーを追加する
例：check-userAgent-policy
```
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"iam:CreateServiceLinkedRole"
			],
			"Resource": "*"
		},
		{
			"Effect": "Allow",
			"Action": [
				"lambda:GetFunction",
				"lambda:EnableReplication"
			],
			"Resource": "arn:aws:lambda:us-east-1:{アカウント番号}:function:check-userAgent:1"
		},
		{
			"Effect": "Allow",
			"Action": [
				"cloudfront:UpdateDistribution"
			],
			"Resource": "arn:aws:cloudfront::{アカウント番号}:distribution/{ディストリビューション番号}"
		}
	]
}
```

ちなみにこの段階で直接ファイルパスを指定するとアクセスが可能である
例：https://{ディストリビューションドメイン名}/pc.html
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969669/730338e3-5aca-084d-a760-e713d42a51fb.png)

例：https://{ディストリビューションドメイン名}/smartphone.html
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969669/3480d1d0-a413-d5a5-de81-bf9ef95e36df.png)

→この時点だとLambda@Edgeを紐づけていないので特に振り分けはされない


### ⑥CloudFrontのディストリビューションにLambda@Edgeを紐づける
#### 6-1. 手順③で作成したCloudFrontディストリビューションのビヘイビアにLambda@Edgeを紐づける
デフォルトのビヘイビアの編集をクリックし、関数の関連付けを実施する
・関数の関連付け
|対象|関数タイプ|関数 ARN/名前|本文を含める|
|-|-|-|-|
|ビューワーリクエスト|関連付けなし|Lambda@Edge|関数ARN:バージョン<br>(例：arn:aws:lambda:us-east-1:{アカウント番号}:function:check-userAgent:1)|
|ビューワーレスポンス|関連付けなし|-|-|
|オリジンリクエスト|関連付けなし|-|-|
|オリジンレスポンス|関連付けなし|-|-|

#### 6-2. アクセス確認する
CloudFrontのディストリビューションドメイン名(https://xxxxx..cloudfront.net)に対して、PCとスマートフォンからアクセスして挙動を確認する

★PCからアクセスする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969669/730338e3-5aca-084d-a760-e713d42a51fb.png)

★iPhoneからアクセスする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969669/80fd1949-f111-1d4a-2972-4941e0b09023.png)


きちんと振り分けられていることがわかりますね！

## まとめ
CloudFront と Lambda@Edgeを組み合わせることでいろんなことができそう。
またLambda@Edge利用時にいろいろと引っかかった部分もあったので、別途記事にする。

## 参考文献
- [ユーザーエージェントを取得する(navigator.userAgent)](https://www.javadrive.jp/javascript/webpage/index14.html)