Amplify Consoleでホスティングした静的サイトの前段にCloudFrontを建ててLambda@EdgeでカスタムOGP対応する

# Amplify Consoleでホスティングした静的サイトの前段にCloudFrontを建ててLambda@EdgeでカスタムOGP対応する

Amplify Consoleでホスティングしている静的サイトがあります。
このサイトはOGP対応をしていますが、どのページも同じ内容で表示されてしまいます。

`SPA(Single Page Application)`ではなく`SSG(Static Site Generation)`でビルドすることにより、個別ページのOGP設定ができるそうなのですが、ビルド時に決まらない、IDでGETしたページに対してカスタムOGP画像を表示したいのです。

(1) [https://yoteiasobi.w2or3w.com/](https://yoteiasobi.w2or3w.com/)
(2) [https://yoteiasobi.w2or3w.com/calendars/ic019t7gn99ckp27vtjjm2d6ro@group.calendar.google.com](https://yoteiasobi.w2or3w.com/calendars/ic019t7gn99ckp27vtjjm2d6ro@group.calendar.google.com)
どちらも以下のように同じOGP画像が表示されてしまうのですが、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/6d8fb230-179b-59f6-a91e-9a1ad0bc6e03.png)

これを、(2)の場合は以下のように独自のOGP画像が表示されるようにしたい。
(独自のOGP画像は予め作成しておくこととする。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/73a6cfa3-e0c3-4536-ef84-b3d8830c1368.png)

それを実現するための記事となります。

## 現在の設定を確認する

まず初めに、`Amplify Console`と`Route53`の設定を確認しておきます。

> 備考 : 独自ドメインのサブドメインをAmplify Consoleで割り当てる方法については以前、以下の記事に書きました。
> [お名前.comで取得した独自ドメインのサブドメインだけでなくルートドメインも割り当てる](https://qiita.com/w2or3w/items/203388cb54fbb9267af9)
> [お名前.comで取得した独自ドメインのサブドメインをAmplify Consoleで割り当てる](https://qiita.com/w2or3w/items/ddb7c4c1a602470ef96b)


### Amplify Console
Amplify Console > ドメイン管理
`mainブランチ` に `https://yoteiasobi.w2or3w.com` が割り当てられています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/4a15a207-6de7-5e62-281f-b26a74f8d7f2.png)
アクション > HTTPS証明書の表示
アクション > DNSレコードの表示
この辺りの設定も確認しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/ad3a1cc9-a6a6-83b1-80f3-32e7cc97bda3.png)
この辺の設定は、Amplify Consoleが自動的に設定してくれていたものです。後ほどドメイン割り当てを削除した上で、同じような設定をCloudFrontに対して行うことになります。

### Route53
Route53 > ホストゾーン > yoteiasobi.w2or3w.com
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/0a6e0a1e-8982-76c8-fa48-ef5ace8922dd.png)
`Aレコード`と`CNAMEレコード`はAmplify Consoleのドメイン割り当てにより自動的に設定されたものです。
こちらもAmplify Consoleのドメイン割り当てを削除した後に、設定しなおすことになります。

## Amplify Console で独自ドメイン割り当てを削除する
それでは取り掛かってゆきます。
まずは、Amplify Consoleで設定している独自ドメインの割り当てを削除します。

Amplify Console > ドメイン管理
アクション > 削除
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/5728e95f-cbdd-212c-ac4d-87ce184fddb0.png)
削除されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/8cd4ea08-bec6-2fde-e267-00af3facc106.png)
この時点で、 `https://yoteiasobi.w2or3w.com/` からはサイトへアクセスできなくなります。

Route53のホストゾーンも確認してみましょう。
Route53 > ホストゾーン > yoteiasobi.w2or3w.com
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/0849d127-264e-9903-93ae-215c5f92a539.png)
Amplify Consoleで割り当てドメインを削除すると、Aレコードも削除されることが見て取れます。
CNAMEは残ってますが、後ほど削除して新たに登録することになります。

## CloudFrontでディストリビューションを作成する
続いて、CloudFrontでディストリビューションを作成します。
CloudFrontにアクセスし、CloudFront Distributions > Create Distribution ボタンを押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/1619547d-9f3f-c025-8d59-4a65f91b2a0c.png)
Select a delivery method for your content. で、Get Started ボタンを押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/9aa2fd35-300c-9889-3b13-c525acad9866.png)
Create Distributionで、以下を設定。

* Origin Domain Name : Amplify Consoleの目的のブランチのドメインを設定
* Minimum Origin SSL Protocol : 必要に応じて設定を変更
* Allowed HTTP Methods : 必要に応じて設定を変更
* Alternate Domain Names (CNAMEs) : 今は空でいいです (後で設定します)
* SSL Certificate : 今は Default CloudFront Certificate (*.cloudfront.net) のままでいいです (後で設定します)

Create Distributionボタンを押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/fb495b82-865b-5b78-fc58-b55a0c9eb827.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/8176b170-b4d8-510e-1821-d7bc198c6fe6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/2ed17991-dbce-a766-c859-67f53640c2bf.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/fe3628a4-8c04-b317-e1c9-13414dd9a5c6.png)
ディストリビューションが作成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/f28ac929-5e2f-1266-2713-4c44a10198f6.png)
CloudFrontが発行したアドレスでサイトが表示されること、また、証明書の内容を確認しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/5a3b28e1-4b9b-4670-7e5d-a09116ad5475.png)

## CloudFrontのディストリビューションに独自ドメインと証明書を割り当てる
独自ドメインでサイトにアクセスできるようにします。また証明書も発行して割り当てます。

CloudFront Distributionsで目的のディストリビューションを選択、GeneralタブのEditボタンを押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/6407f767-59a1-6d5d-5d2b-7ba208af173a.png)
Edit Distribution画面で以下を設定します。
Alternate Domain Names(CNAMEs) : 目的のドメイン
証明書が無いので `Custom SSL Certificate (example.com) ラジオボタン`が無効状態です。
新たに証明書をリクエストするため、`Request or Import a Certificate with ACM ボタン`を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/926de57e-249b-eadd-83a3-4747a501eab8.png)
証明書のリクエスト画面で、以下のように設定を進めてゆきます。

* ドメイン名の追加 : 目的のドメインを追加する。
* 検証方法の選択 : DNSの検証 を選択する。
* タグを追加 : 必要に応じてタグを設定する。
* 確認 : 内容を確認して「確認とリクエストボタン」を押下する。
* 検証 : 内容を確認して「続行」ボタンを押下する。(CNAMEの値は後で使います)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/2ab1711f-12da-21cb-cba5-28c2445c0d6f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/9a89b1f3-0c0e-cc5a-ae72-5dbfde2def28.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/0220f7b0-7c2e-4ac4-5444-f2e2c04c6588.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/3a84aa69-3615-a4ed-6a05-330a9b6d2885.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/17a064d3-884a-c48c-7e17-97073bb2293e.png)
証明書が発行されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/972d3c60-4997-f2c0-6d83-f143261a71e9.png)

Route53の目的のホストゾーンへレコードを作成します。
Route53 > ホストゾーン > yoteiasobi.w2or3w.com
レコードを作成ボタンを押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/902cde26-b4ac-d3d5-db22-03661e3316a6.png)
レコードを作成画面にて、AレコードとCNAMEレコードを作成します。
**Aレコードの作成**

* レコード名 : 空
* レコードタイプ : A
* エイリアス : ON
* トラフィックのルーティング先 : CloudFront ディストリビューションのエイリアス
* ディストリビューションを選択 : CloudFront の目的のディストリビューションのドメイン名

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/aa012a4e-ae63-7201-e933-6957ba47f3f4.png)

**CNAMEレコードの作成**

* レコード名 : 目的のドメインに発行した証明書のCNAMEレコード名
* レコードタイプ : CNAME
* 値 : 目的のドメインに発行した証明書のCNAMEレコード値

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/522ea8ed-9f6a-88fb-4464-5fec481ae3ca.png)
レコード名や値は`Certificate Manager`で目的のドメインを選択することで確認できます。
https://console.aws.amazon.com/acm/home
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/3b61fbf4-6094-a934-44cf-3bd6642ea20e.png)

もともとあったCNAMEレコードは削除してください。

それでは、CloudFront Distributionsの、目的のディストリビューション編集画面へ戻りましょう。
目的のディストリビューションに作成した証明書を割り当てます。
`Custom SSL Certificate (example.com) ラジオボタン`を選択し、先ほど発行したドメインの証明書を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/5e16bf1d-461d-e1f4-cb9e-dfb1178463a9.png)
編集を完了したら、ドメインでアクセスできること、そして証明書の内容を確認してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/8f709aaa-131e-ddca-72a5-3c6d67ed1023.png)

## LambdaをCloudFrontのEdgeに割り当てる
いよいよ Lambda@Edge の設定です。

私てっきり`Lambda@Edge`というサービスがあると思っていたのですが、そうではないんですね。
『LambdaをCloudFrontのディストリビューションへエッジ機能として割り当てる』ことをそう呼んでいるみたいです。

なお、**CloudFrontから呼び出すLambdaは、リージョンを「バージニア北部」にする必要**があります。
また、CloudFrontから呼び出すLambdaがサポートしているランタイムは以下のみです。(2021年5月現在)
(※最新の情報は[公式のページ](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-lambda-function-configuration)を参照ください。)
> Python 3.8, 3.7
> Node.js 12, 10

という事で、バージニア北部にLambdaを作成しましょう。私は Python 3.8 で作成しました。名前は`yoteiasobi-ogp`です。
リージョン以外にも、以下を選択した上で作成します。

* デフォルトの実行ロールの変更 : `AWSポリシーテンプレートから新しいロールを作成`を選択。
* ポリシーテンプレート : `基本的な Lambda@Edge のアクセス権限 (CloudFront トリガーの場合)`を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/92b1db52-bf19-f9af-c0ca-04b3fbded53c.png)

Lambdaのコードを書いたらデプロイし、さらに、新しいバージョンを発行する必要があります。
CloudFrontのディストリビューションへLambdaのARNを設定するのですが、特定のバージョンを参照する必要があります。
(そうしないとエラーとなって設定できません。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/0c28e54c-d350-6163-48b2-c1c90946d306.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/d2af5045-a501-9188-6b8d-866389147ca3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/a8cc85c6-eb5b-699f-8673-7df8103e3699.png)

Lambdaをデプロイしてバージョンを発行したら、ARNをコピーして、それをCloudFrontのディストリビューションへエッジ機能として割り当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/70626fe0-fead-b769-45c8-d01264360911.png)
CloudFront Distributions の目的のディストリビューションを選択。
BehaviorsタブでDefault(*)を選択しEditボタンを押下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/1d09a33a-b1c8-c2ef-5607-19ed3cd7d963.png)
Edit Behavior画面の`Edge Function Associations`で以下の設定をします。

* Edge Function : `Lambda@Edge`を選択
* CloudFront Event : `Viewer Request`を選択
* Function ARN/Name : LambdaのARN(バージョン発行したもの)を設定

さてこれで、サイトにアクセスしたらLambdaが呼ばれるようになりました。Lambda@Edgeの完成です！

### Lambda@Edgeのログ参照時の注意点
Lambdaが実行されたことを確認するためにCloudWatch Logsを参照することになるわけですが、知っておくべきことがあります。
CloudWatchのロググループは`/aws/lambda/function-name`ではなく`/aws/lambda/us-east-1.function-name`へ出力されます。
LambdaのモニタリングからCloudWatchのログを表示すると`/aws/lambda/function-name`へ飛ぶので注意が必要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/2fefcf23-4218-ddd5-497d-2fe91c3eabd4.png)
また、ログが出力されるリージョンはクライアントから最も近いロケーションとなります。
日本からWebサイトへアクセスした場合は「東京」、twitterbotからの場合は「北カルフォルニア」でした。

### Lambda@Edgeで例外が発生した場合
Lambdaで例外が発生した場合、503エラーが返ってしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/87899b8d-8e3b-46ea-881d-df8ccdd972b4.png)
> 503 ERROR
> The request could not be satisfied.
> The Lambda function associated with the CloudFront distribution is invalid or doesn't have the required permissions. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
> If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.

## カスタムOGP対応するLambda(Python)
CloudFrontのViewer Requestで呼ばれるLambdaでヘッダの内容を書き換えてOGPの画像を変更します。
サンプルコードを載せておきます。

```lambda_function.py
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
BOTS = [
    "twitterbot",
    "facebookexternalhit"
]
def lambda_handler(event, context):
    logger.info("=== START ===")
    logger.info(json.dumps(event, ensure_ascii=False, indent=2))
    
    request = event["Records"][0]["cf"]["request"]
    headers = request["headers"]
    uri = request["uri"];
    
    if "user-agent" not in headers:
        return request
    
    userAgent = headers["user-agent"][0]["value"].lower()
    logger.info("request uri = {0}".format(uri))
    logger.info("userAgent = {0}".format(userAgent))
    
    list = [item for item in BOTS if item in userAgent]
    logger.info(list)
    
    isBot = len(list) > 0
    logger.info("isBot = {0}".format(isBot))

    if not isBot:
        return request
    else:
		customOgpImageAddress = "https://w2or3w.s3-ap-northeast-1.amazonaws.com/yoteiasobi/images/ic019t7gn99ckp27vtjjm2d6ro%40group.calendar.google.com.png"
		customOgpDescription = "YOTEIASOBIのサンプルカレンダー by w2or3w"
        return {
            "status": 200,
            "statusDescription": "OK",
            "headers": {
                "content-type": [{
                    "key": "Content-Type",
                    "value": "text/html"
                }]
            },
            "body": getContent(customOgpImageAddress, customOgpDescription)
        }

def getContent(customOgpImageAddress, customOgpDescription):
    return (''
        '<!doctype html>'
        '<html>'
          '<head>'
            '<title>YOTEIASOBI</title>'
        	'<meta data-n-head="1" charset="utf-8">'
        	'<meta data-n-head="1" data-hid="description" name="description" content="よてい で あそぼ！YOTEIASOBI は Googleカレンダーを ゆるっと共有 できる サーバーレスWebアプリです。">'
        	'<meta data-n-head="1" data-hid="og:site_name" property="og:site_name" content="YOTEIASOBI">'
        	'<meta data-n-head="1" data-hid="og:type" property="og:type" content="website">'

        	'<meta name="twitter:card" content="summary_large_image">'

        	'<meta property="og:url" property="og:url" content="https://yoteiasobi.w2or3w.com/calendars/ic019t7gn99ckp27vtjjm2d6ro@group.calendar.google.com">'
        	'<meta property="og:title" property="og:title" content="YOTEIASOBI">'
        	'<meta property="og:image" property="og:image" content="{0}">'.format(customOgpImageAddress) + 
        	'<meta property="og:description" property="og:description" content="{0}">'.format(customOgpDescription) + 
          '</head>'
          '<body>'
          '</body>'
          '</html>'
        )
```

### OGP画像生成をテストするツール
上記コードは、TwitterやFacebookからシェアしようとした際にカスタムOGP画像に差し替える実装をしています。
動作確認する際に毎回実際に投稿するのだと大変ですが、その代わりに以下のサイトを利用するとはかどります。
https://cards-dev.twitter.com/validator
https://developers.facebook.com/tools/debug/

## あとがき
5/1の「浜松ITもくもく会」でコレをやろうと思ってたのですが、諸事情で出来ませんでした。

https://twitter.com/w2or3w/status/1388390953556213761

それから1週間かけて、なんとかQiitaに記事書くところまでできたから良かったです。
(厳密にいうと、アプリがOGP画像を生成するところはまだ未着手なのですが、まぁこれはそのうちやります！)