Amplify + Next.js + microCMSのプレビュー環境について
概要
株式会社トライビート| TRIBEAT CO., LTD. では、数年前からJamstackなWebサイトやECサイトの構築を推進(提案)しており、年々こういったご相談を受けることが増えています。 選定技術はそのときの顧客要件によって変わりますが、Amplify、Next.js、microCMSを使うことが多いです。Jamstackな構成を検討していると、SSGしてファイル生成する都合上、プレビュー機能をどうする?って課題に当たります。Headless CMSで下書きしたとき(公開前)に、Webページとしてどう見えるかを確認するユースケースですね。
アーキテクチャ的にSSGしたものをCDNでホスティングすることで、「高速化、スケーラビリティ、セキュア」を担保する一方で、リアルタイムな表示は工夫が必要になります。プレビュー用のページもSSGすれば確認可能ではありますが、ビルドには大抵数分掛かるため、UXが悪くなり実用的ではないと考えています。
Next.jsでは標準的にPreview Modeが用意されていますが、昨年の調査時はAmplify上で正しく動作しませんでした。最近、Amplify hostingのGitHub issueを確認したところ、解決されていそうだったため、改めて検証してみました。
Next.js のPreview Modeとは
上記の公式ドキュメントに実装方法が手順付きで記載されているので、詳細はこちらを確認するのが良いですが。色々端折って要約すると、Next.js には元々、APIを追加する機能が備わっており、 そのAPIの中で、 setPreviewData
関数を利用することで、プレビュー機能に必要なCookieが発行されます。
このCookieを保持しているブラウザーからアクセスされた場合、SSGで生成済みのページを返すのではなく、 getStaticProps
というビルド時に実行されるページ生成用の関数が呼び出されます。
上記機能を利用することで、以下のようなフローが実現可能となります。Headless CMSからプレビュー用URLがリクエストされたときに、Cookieを発行して、対象のページにリダイレクトすると、新たにページ生成した内容で確認が可能となります。
Cookieを保持しているかどうかは、 getStaticProps
のパラメーターにある context.preview
のbool値で判断可能となります。Cookieを発行する手前の処理で、認証処理を実装することで、Headless CMSからアクセスされたときのみ、機能が有効になるように制御します。
Amplifyでは何が課題だったのか
1つ目のissueに関連付けられている、2つ目のissueで解決したようです。Next.jsのPreview Modeは、HTTP Response Header で Cookieを発行していますが、Amplify上にデプロイすると、これがうまく動作していなかったようです。解決されたのが、2022年の12月ということです。
実運用での課題
Amplify上でNext.jsのPreview Modeが動作するということで、課題はなくなったように思えますが、ページ更新のユースケースを想定すると、若干の工夫や考慮が必要になります。当初、本番(hosting)用のAmplify環境(env)上でプレビュー機能も動作させれば、環境構成もシンプルになり最適と考えてましたが、以下のようなケースで使いにくくなると想像しています。
前提
ニュースやブログなどを扱うWebサイトを(Cookieとかはよくわからない)非エンジニアの方がコンテンツ管理している場合
ケース1
- ニュースの下書きデータを Headless CMS からプレビュー機能で確認(このときにPreview Mode の Cookie が発行)。
- その後、別な作業をしている時に、Webサイトから現在のブログ内容を確認。
- 担当者は、公開中のブログページ内容を確認するつもりだが、意図せず下書きデータが保存されていると、下書き内容でページが表示される。
ケース2
- 既に公開済みの記事に不備があり、Headless CMSからデータ編集し、プレビュー機能で確認(このときにPreview Mode の Cookie が発行)。
- 現在公開中のページと更新後のページを目視で差分チェックしようとしたところ、下書き内容のページでしか確認ができない
上記のようなケースは、Cookieを保持しているブラウザーはSSGされたものではなく、常に新しくページ生成した結果が表示されるため発生します。以下の図のAとBは、同じサイトページをリクエストしていますが、Cookieの保持により表示されるコンテンツ内容は異なる可能性があります。 CMSの操作や現状サイトの確認などを繰り返し作業しているときに、Cookieの保持について意識するのは厳しいと想像しています。
Preview ModeのCookieを発行する setPreviewData
関数は、引数なしで使用するとSession Cookieとなります(ブラウザが閉じられると、Cookieが削除されます)。引数を指定することで、Cookieの有効期間(Expired)を設定することも可能ですが、最近のPCやMacの使われ方だと、ブラウザーを閉じないでアクセス作業することも多いと想像しています。
また、Next.jsでは、Preview ModeのCookieを削除する関数も用意されていますが、何をトリガーに呼び出すべきかは悩ましいです。
上記から、Amplifyプロジェクト内で、本番用とプレビュー用に環境(env)を分けて構築しようと思いましたが、同じGitブランチを別の環境(env)に分けることは想定されていないようで、Amplify Console上で環境(env)を追加するときに、既に接続済みのGitブランチは表示されないように制御されていました。
どうすべきか悩みましたが、本番用とプレビュー用でAmplifyプロジェクトを分けてしまい、同じGitブランチを接続させるのが、いまのところ良さそうです。microCMSなどのHeadless CMSからリクエストする先を、プレビュー用のAmplifyプロジェクトへ向けることで、本番環境の表示とプレビュー環境の表示を切り分けれます。
プレビュー用のAPIについて
Next.jsの公式ドキュメントもわかりやすかったですが、microCMSのブログも丁寧な内容で助かりました。特に、 getStaticPaths
の fallback
の設定変更については、公式ドキュメントに記載がないので参考になりました。
あまり大きな問題ではないですが、以下は今後検討して作り込む予定です。
- microCMSのdraftKeyについては、Webhookのクエリストリングで渡すべきか、Amplify環境変数で設定すべきか?
- プレビュー用のAPIについては、各ページごとに作るべきか、まとめるべきか?
AmplifyのSSR対応時のビルドについて
Amplifyは内部で、Next.jsのアプリケーションがSSGまたはSSRを判断して環境構築を変えているようです。Amplify + Next.jsの構成を最初に試したときは、2〜3年前でした。その当時、SSGモードではビルドが数分で終わるものが、SSRモードに変わることで、20〜30分掛かる事象にあたりました。今回のプレビューAPIは、SSRモードでなければ動作しないこともあり、ビルド時間に懸念がありましたが、AmplifyもNext.jsに合わせて進化しており、いまのところ、SSGとSSRでビルド時間はほぼ変わらないことを確認済みです。
Flutterで環境別のGoogleMap API Keyを設定する方法(dart-define-from-file)
概要
FlutterでGoogleMapを利用するとき、API KeyはOS毎に以下のファイルで設定する必要があります。
テスト環境と本番環境にそれぞれ発行したGoogleMap API Keyを、 ビルド(ipaまたはaabファイルの生成)時に、 dart-define-from-file
オプションから設定する方法を記載します。
前提
Flutterで環境ごと設定値を分けるには、元々、Flutterビルドコマンドオプションの dart-define
を使ってましたが、 dart-define-from-file
オプションによって設定値をファイルにまとめることができるようになりました。
その方法については、以下の記事が大変参考になるので、こちらでは割愛した内容を記載します。
dart-define-from-fileからの設定
dart_defines/dev.json
や dart_defines/prod.json
は既に用意している前提で、GoogleMap API Keyをそれぞれのjsonに追加します。
"iOSGoogleMapApiKey": "hoge" "androidGoogleMapApiKey": "fuga"
iOS
<key>DART_DEFINES</key> <string>$(DART_DEFINES)</string>
import UIKit import Flutter import GoogleMaps @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // - Setup dart-define(dart-defineの値) let dartDefinesString = Bundle.main.infoDictionary!["DART_DEFINES"] as! String var dartDefinesDictionary = [String:String]() for definedValue in dartDefinesString.components(separatedBy: ",") { let decoded = String(data: Data(base64Encoded: definedValue)!, encoding: .utf8)! let values = decoded.components(separatedBy: "=") dartDefinesDictionary[values[0]] = values[1] } GMSServices.provideAPIKey(dartDefinesDictionary["iOSGoogleMapApiKey"]!) // self.window.makeSecure() GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } extension UIWindow { func makeSecure() { let field = UITextField() field.isSecureTextEntry = true self.addSubview(field) field.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true field.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true field.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true self.layer.superlayer?.addSublayer(field.layer) field.layer.sublayers?.first?.addSublayer(self.layer) } }
参考URL medium.com
Android
defaultConfig { manifestPlaceholders["googleApiKey"] = androidGoogleMapApiKey }
<meta-data android:name="com.google.android.geo.API_KEY" android:value="${googleApiKey}"/>
参考URL
Androidで正しく設定できているかどうかを確認する方法(値が正しく渡せているかを確認する)
apkファイルの拡張子をzipに変更して解凍することで、AndroidManifest.xml を見つけることはできるが、そのまま開いてもバイナリ形式にエンコードされているため確認することができない。
上記から AXMLPrinter2.jar
をダウンロードし、以下のようなコマンドで確認用のAndroidManifest.xmlを生成することが可能。
java -jar AXMLPrinter2.jarのPath AndroidManifest.xmlのPath > check_AndroidManifest.xml
PHP(ZTS)のxdebugインストール
概要
- 同じチームのエンジニアが新しいプロジェクトに参画し、既に作成済みのDocker環境にxdebugを入れようとしたところ、うまく動かないという相談を受けて調査開始
pecl
コマンドでxdebugをインストールして設定しているが、確かに動かない、、、
結論
ハマリポイント
pecl
コマンドでインストール後、以下のコマンドで確認をしていた。php -r "phpinfo();" | grep xdebug php -v
上記コマンドだと、xdebugがちゃんと入っているように見えるため、最初はリモートデバッグ接続しようとしているVSCode側の設定とかを疑って時間がかかった。
- Webブラウザから、
phpinfo();
出力したページを表示して、xdebugの設定が反映されていないことを確認できたので、ここからPHP側を疑って調査した。
解決方法
wget http://www.xdebug.org/files/xdebug-3.1.5.tgz tar xzvf xdebug-3.1.5.tgz cd xdebug-3.1.5/ zts-phpize ./configure --with-php-config=/usr/bin/zts-php-config make make install
- 上記コマンドを実行すると、
xdebug.so
ファイルが作成される - あとは、xdebug用のiniファイルを配置してあげる
zend_extension = "make installの出力先/xdebug.so" xdebug.mode=debug xdebug.start_with_request=yes xdebug.client_host=host.docker.internal xdebug.client_port=9003 xdebug.log=/tmp/xdebug.log xdebug.log_level=0
AmplifyプロジェクトでCognito送信者のLambdaトリガーを設定してみた
概要
- Amplifyプロジェクトで普通にAuthを追加してMFA対応していた
- SMS認証コードとEMail認証コードはそれぞれCognito標準機能を使っていたが、TwilioやSendGridなどの別サービスで送るための対応が必要になった
- Cognitoから認証コードを送るときに、カスタム送信できるような機構が用意されているので、それを利用
ドキュメントや参考になるブログ
対応方法
- AmplifyのCloud Formationに組み込みたかったが、現状、この機能はCloud Formationに対応されていないらしく、別途設定が必要になる
- KMS(AWSコンソール)から、「カスタマー管理型のキー」を作成する
- 特に考えずそのまま作成する
- Lambda Layerを作っておく
- Lambdaを用意する
- 公式ドキュメントにあるものを流用すれば良い
- handler関数では、Cognitoから発火されたイベント名をみて、処理をハンドリングしてあげる(TwilioやSendGridを使って認証コード送信を書く)
- LambdaにKMSの権限をアタッチしてあげる
- Lambdaの[設定]→[アクセス権限]→[実行ロール]からKMSの権限をアタッチする
Amazon Cognitoサービスプリンシパルに、Lambda関数を呼び出すための、cognito idp.amazonaws.com へのアクセス権を付与
aws lambda add-permission \ --function-name <作成したLambdaのARN> \ --statement-id "CognitoLambdaInvokeAccess" \ --action lambda:InvokeFunction \ --principal cognito-idp.amazonaws.com
Cognitoユーザープールを更新して、Lambdaトリガーを追加する
aws cognito-idp update-user-pool \ --user-pool-id <CognitoユーザープールID> \ --lambda-config "CustomSMSSender={LambdaVersion=V1_0,LambdaArn=<作成したLambdaのARN>},CustomEmailSender={LambdaVersion=V1_0,LambdaArn=<作成したLambdaのARN>},KMSKeyID=<作成したKMSのARN>" \ --profile <AWSのプロファイル>
間違ってLambdaトリガーを追加したときに解除するコマンド
aws cognito-idp update-user-pool \ --userpool-id <userpool_id> \ --lambda-config "{}"
ハマったところ
たまに認証コードが同じユーザーに2度送られる事象が発生
- SMS認証コードをTwilioのVerify APIを使って送信していたが、時たま、2度送られていた
- 設定したLambdaトリガーのログを見ていると、以下が出力されており、失敗したLambdaの約1分後に同じイベントが発火されていた。
Verify API処理が3秒超える場合がたまにあり、そのときに再度Lambdaが実行され2度送信されていることを確認
Task timed out after 3.00 seconds
対応方法
- Lambdaのタイムアウトをデフォルトの3秒から10秒に変更し、メモリも256mbに変更
- 設定値は、利用するサービス仕様に合わせる
カスタムLambdaトリガーが実行されない事象発生
- aws cognito-idp update-user-pool コマンドでLambdaカスタムトリガーを紐付けても、Lambdaが呼ばれない
- AmplifyでAuth追加した時点では、Cognitoの「どの属性を確認しますか?」の項目では、「Eメールまたは電話番号」を選択していたが、コマンド実行すると、「検証なし」に変更されていた。
対応方法
- aws cognito-idp update-user-pool コマンドのオプションに
auto-verified-attributes
を追加 - 上記オプションには、
sms-configuration
が必要になるsms-configuration
に指定には、「SMS 送信に使用する IAM ロールの ARN」と「SMS 送信に使用する IAM ロールの、信頼ポリシーに設定されている sts:ExternalId の値」が必要になる。- このIAMロールは、Amplifyで適切にAuth追加していると既に作成されているので、それを使う。
- Cognito(AWSコンソール)の「MFAそして確認」メニューの一番下に「新規ロール名」があるので、IAM(AWSコンソール)から検索して、信頼関係のタブを開くと「sts:ExternalId」の値が確認可能
aws cognito-idp update-user-pool \ --user-pool-id <CognitoユーザープールID> \ --lambda-config "CustomSMSSender={LambdaVersion=V1_0,LambdaArn=<作成したLambdaのARN>},CustomEmailSender={LambdaVersion=V1_0,LambdaArn=<作成したLambdaのARN>},KMSKeyID=<作成したKMSのARN>" \ --profile <AWSのプロファイル> \ --auto-verified-attributes {email,phone_number} \ --sms-configuration SnsCallerArn=<SMS 送信に使用する IAM ロールの ARN>,ExternalId=<SMS 送信に使用する IAM ロールの、信頼ポリシーに設定されている sts:ExternalId の値>
Next.js on Amplifyの503エラーの続き
概要
- 上記ブログの続き
- AmplifyでNext.jsをデプロイしたタイミングだと問題なく動くが、あるタイミングからCloud Frontの503エラーになっていた
- 上記事象が発生するのは、同じAmplifyプロジェクト上で同じソースコードで構築したhostingでも再現するものとしないものがあった
原因
- Next.js on Amplifyで構築される、Lambda@Edge に SQS の権限がアタッチされないことがある
- SQS の権限がアタッチ「されるとき」と「されないとき」の2パターンあるのがポイントで、ケースは以下
- ISRのキャッシュが切れて裏でページ生成されるタイミングで、Lambda@edge から SQS に SendMessage しようとしたときに権限がないと503エラーとなる
対処方法
- 解決するには、以下2つがある。(現状はどちらも試していて、どちらのケースでもうまく動いている)
- Amplify hostingを作り直す
- 事象が発生しているhostingを一度削除し、改めて作り直す
- まだサイト公開前とかであれば、こちらの方法がシンプルで良さそう
- 事象が発生している Lambda@edge にSQSの権限をアタッチする
- 雑にやるなら、AmazonSQSFullAccess ポリシーを付ける。正しくやるなら、"Resource": "arn:aws:sqs:us-east-1:<アカウント ID >:
", の Actionに "sqs:SendMessage" を指定する。
- 雑にやるなら、AmazonSQSFullAccess ポリシーを付ける。正しくやるなら、"Resource": "arn:aws:sqs:us-east-1:<アカウント ID >:
- Lambda@edge や SQS のIDについては、Amplifyコンソールのデプロイ部分のログに出力されている
- Lambda@edge に権限アタッチ後、改めてデプロイしても自分で割り当てた権限はついたままとなっている(Amplify側の挙動の保証はない?)
- Amplify hostingを作り直す
所感
- プロジェクト構築時は、シンプルな状態から始めることが多いので、途中からISR対応ページなど追加したときに気をつける
- Amplifyコンソールのデプロイフェーズのログが重要
- ここに出力されている、CloudFront や Lambda@edge のIDから詳細ログを追う力が必要
調査のときに見ていたGitHubとドキュメント
Next.js on Amplifyの503エラー
また雑多なメモを残しておきます。
概要
- Amplify上でNext.jsを動かすときに503エラーが発生
- ネットで調べていると色々と情報はでてくるが、自分がハマったケースは大きく2つ
- Next.jsでISRをしているときに発生
- Amplifyのアクセスコントロールを適用しようとして発生
Next.jsでISRをしているときに発生
- 調べ始めたとき、以下2つの日本語記事が参考になりました。
- はじめてAmplify hosting するときに、Amplify CLI と Next.js verisonのバージョンをどちらも
latest
にするのがポイント
- 上記対応はプロジェクト構築時にも見て知っていたので正しく設定していましたが、それでも503エラーが発生するケースがありました。
- ちょっとたちが悪いのが、ページ表示したタイミングで常に発生するわけではなく、何かしらの契機で発生していて、調査が難かしかったです。
- それから調査を続けてて、参考になったissueが以下です。
- 1つ目はwebpackのバージョンで、5をfalseにして4で動かすというものです。自分の場合これでは解決に至らず、結局は5に戻しています。また、このオプションは近い将来に無効になるので、可能であれば使わずに解決したいところでした。
- 2つ目は、Amplifyの環境変数にある、
_LIVE_UPDATES
の値でした。このissueにもある通り、verisonに10 と指定されていました。ビルド設定のバージョンはlatestになっているのを確認していたので、こちらはissueを見るまで気づきませんでした。 - 10になっている部分をlatestに変更し、改めてビルドし直してみましたが解決はしなかったです。ダメ元で、一度hostingを削除して改めて作り直したところ、
_LIVE_UPDATES
のNext.jsのバージョンも以下のようにすべてlatestになっており、その後は503エラーは発生していないです。
Amplifyのアクセスコントロールを適用しようとして発生
- Amplifyにはアクセスコントロールのメニューから簡単にBasic認証かけれる便利機能がありますが、Next.jsでSSR対応している場合には正しく動きませんでした。
- 以下の公式ドキュメントにも、その旨が掲載されていて読んだ記憶があるのですが、いま改めて見ると載ってません。
- このブログを書く前に試してみたところ、普通に動いているように見えます。その当時は、アクセスコントロールの設定自体は有効になり、画面を開くとID/PASSの入力を求められるのですが、正しい値を入れても認証が通らないという事象でした。
- そこからAmplifyの機能に頼らず、自分でアクセス制御しようと思い、Amplifyのデプロイ先のCloudFrontに関数を作成しました。Basic認証ではなく、以下のような、IP制限するようなプログラムを設定していました。(CloudFrontの関数ではconstが使えなくて地味にハマりました。)
function handler(event) { var request = event.request; var headers = event.request.headers; var clientIP = headers['x-forwarded-for']['value'] var IP_WHITE_LIST = []; var isPermittedIp = IP_WHITE_LIST.includes(clientIP); if (isPermittedIp) { return request; } else { var response = { statusCode: 403, statusDescription: 'Forbidden', } return response; } }
- これも最初うまく動いていたのですが、何かを契機に503が多発するようになることがあったのと。Amplifyでビルドすると、関数が外れてしまう問題を確認しているところでした。
- いまはアクセスコントロールがうまく動いていそうなので、一旦このまま経過観察していきたいと思います。
感想
- 何か困ったら大抵はGitHubでやり取りされているので、そこで解決案やアプローチ方法が見つかることが多い
- Amplifyのupdateが早いので、その時点では難しくても、最悪の逃げ道だけ用意しておけば、それを使わずとも環境側の変更で解決することが多い
その後
- この対応後、同様の事象が発生し、更に詳細を追った話
AmplifyでNext.jsをSSRしたときのビルドログ出力先
昨日、上の記事を書きましたが、Advent Calendarにまだ空きがあるので、雑多なメモを残しておきます。
概要
- Amplify環境でNext.jsのアプリケーションを構築
- Next.jsでは、ページによってSSG、ISR、SSRを使い分けている
課題
- SSRしているページの描画が遅いことが発覚しログ確認しようと思った
- SSRやISRしているときに、
getStaticProps
やgetServerSideProps
関数でconsole.log出力していたが、どこで確認できるか分からなかった
解決方法
- Amplify Console のFrontend environments から デプロイをクリック
- 以下のようにCloudFrontやLambda@Edgeの識別子を確認することができる
2021-12-01T07:37:02 [INFO]: Beginning deployment for application aaaaaaa, branch:dev, buildId aaaaaa 2021-12-01T07:37:05 [INFO]: Deploying SSR Resources. Distribution ID: bbbbbbb. This may take a few minutes... 2021-12-01T07:37:05 [INFO]: Deployed the following resources to your account: 2021-12-01T07:37:05 [INFO]: - CloudFront Domain ID: ccccccc 2021-12-01T07:37:05 [INFO]: - SSR Lambda@Edge: dddddd-dddddd 2021-12-01T07:37:05 [INFO]: - Image Optimization Lambda@Edge: eeeeee-eeeeee 2021-12-01T07:37:05 [INFO]: - ISR Lambda: ffffff 2021-12-01T07:37:05 [INFO]: - ISR SQS Queue: gggggg 2021-12-01T07:37:05 [INFO]: - S3 Bucket: hhhhhh 2021-12-01T07:37:05 [INFO]: Deployment complete
- AWSコンソールで、バージニア北部(us-east-1)リージョン からLambdaの識別子でフィルタし、そこからCloudWatchをたどることで、Next.jsのビルド時のログを確認することが可能