こんにちは! 「AI在庫管理」開発チームの小室です。
最近、Amplify Consoleで稼働中の、ReactのSPAアプリケーションの前段に、Cloudfrontを挿入する改修をおこないましたが、 Amplify Console + Cloudfront環境の情報源が限られていることもあり、特にドメイン名の移管部分で何点かハマったポイントがありました。 本記事ではその対応方法をまとめて行きたいと思います。
背景
弊開発チームでは薬局向けの在庫管理システムを開発しており、ページのホスティングにはAmplify Consoleを利用しています。選定理由としては、業務システムのためSSRなどが特に必要なく、CI/CDが容易でインフラの管理コストがないなどの理由から、立ち上げ時より利用を続けています。
しかしながら、立ち上げから3年ほどの時間が経ち、システムが大規模化してきており、エンジニアメンバーも20数名と大きなチームになってきました。このような状況でも開発のスピードを落とさずにスケールさせて行くために、最近では適切な粒度へのシステムの分解を検討しています。
そんな中、ちょうど私の所属するユニットで従来の在庫管理アプリとはコンテキストが異なる新ページの開発が検討に上がりました。いいタイミングなので将来を見据えた対応を検討した結果、Amplify Consoleの前段にCloudFrontを挿入することで、同一のURLで自由に複数に分割されたオリジンからサービスを提供できるようなアーキテクチャへのマイグレートを進めることにしました。
TOBEのイメージ
前提
- フロントエンドは
Vite + React
で構築する - 極力ダウンタイムなしで移行したい
- Amplify Consoleではカスタムドメインを利用している
大まかな手順
- 新アプリの準備
- CloudFrontをデプロイする
- カスタムドメインをAmplifyからCloudfrontへ移行する
1.新アプリの準備
まずは、新設ページをAmpilfyでホストします。
ここで考慮する点として、新設ページ側にベースパスを指定する必要があることです。 AmplifyのリダイレクトルールやCloudfrontなどインフラでパスをひねる方法もありますが、ローカルでの起動時と挙動が合うように、アプリケーション側で手当をしたほうが良いと考えました。
今回はVite + React(React Router)
でページを構築しましたので、以下の様に対応しました。
- ルーティング: React Routerの basename 設定
createBrowserRouter(routes, { basename: '/new-page' });
- アセット類: Viteの Public Base Path 設定
import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ base: '/new-page', plugins: [react()], });
ただし、アセット類の対応に付いては、ブラウザからアセットをリクエストする際のURLが書き換わりますが、vite build
で吐き出されるファイルの階層は変わらないため、Amplify側にも適当なリダイレクトルールを設定しました。
- レコード目
- 送信元アドレス:
</^[^.]+$|.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|woff2|ttf|map|json|webp)$)([^.]+$)/>
- ターゲットアドレス:
index.html
- 入力: 200
- 送信元アドレス:
- レコード目
- 送信元アドレス:
/new-page/<*>
- ターゲットアドレス:
/<*>
- 入力: 200
- 送信元アドレス:
1レコード目ではすべてのページリクエストに対してindex.html
を返却する一般的なSPA用のリダイレクトルールで、HTMLの要求だけがヒットする想定です。
2レコード目ではアセットのリクエストパスに対して、/new-page
の部分を外したパスにあるファイルを返却します。
2.CloudFrontをデプロイする
切り替えに先立って、Cloudfrontをデプロイしておきます。
ここで注意するべき点として、Amplify Consoleはマネージドサービスですが、内部的には Cloudfront + S3
を腹持ちしているためCloudfrontが2段構成になる点です。
この場合、1段目のCloudfrontから2段目のCloudfrontへ通信を転送する際に、Hostヘッダーを落さないと通信がループ判定されエラーするようです。
参考
対応として、Cloudfrontの Cache Behavior
に、デフォルトで選択可能な Managed-AllViewerExceptHostHeader
を選択することで、Host Headerのみを落とすことができます。
3.カスタムドメインをAmplifyからCloudfrontへ移行する
新規で構築するCloudfrontにカスタムドメインを移行したいのですが、Cloudfrontの代替ドメイン名は他のCloudfrontと重複して設定することができず、AssociateAlias APIなど利用して移行する必要があるようです。
参考
そこでまずは、新規作成するCloudfrontにドメインを設定可能かどうかを、 ListConflictingAliases API
で確認します。
(ドメインは、xxx.example.com
としていますが、適時みなさんが利用しているドメインに読み替えてください。)
aws cloudfront list-conflicting-aliases --alias 'xxx.example.com' --distribution-id={新規CloudfrontのID}
実行すると以下のような結果が得られます。
{ "ConflictingAliasesList": { "MaxItems": 100, "Quantity": 1, "Items": [ { "Alias": "xxx.exapmle.com", "DistriutionId": "****************", "AccountId": "{知らないAWS AccountのID}" } ] } }
この様に、競合するCloudfrontとしてAmplifyが内部的に持っていると思われるCloudfrontがリストアップされていますが、我々が管理しているのとは別のAWS Accountに存在していることが確認できます。
再度ドキュメントを確認すると、Cloudfrontのドメイン移管に利用できる AssociateAlias API
はクロスアカウントで実行する際には、移行元のCloudfrontを一度無効化する必要がある様で、当然ながら稼働中のシステムでは許容することができません。
ドキュメントには更に代案が示されており、今回はそのワイルドカードを利用した移行方法を検討しました。
ワイルドカードを利用した移行方法
まずは初期状態を示します。
Amplify Consoleにカスタムドメインを登録する際にRoute53にAmplify内部のCloudfrontへ解決するためのレコードを設定していると思います(現在は自動で設定されるようです)。
この状態で、新Cloudfrontには一つ上ドメインをワイルドカードで指定します。ここでは*.exapmle.com
としています。
次に、Route53のレコードの向き先を新規作成したCloudfrontに変更します。
この時点ではまだドメインの移行は完了しておりません。試しに現行のドメイン(xxx.example.com
)でアクセスしてDeveloperツールで確認すると、レスポンスヘッダーのVia
属性に記載されているCloudfrontは1段だけです。
最後に、Amplify Consoleからカスタムドメイン設定を削除すると、新Cloudfrontを経由した経路に変更され、Via
属性も以下の様に変化し、2段のCloudfrontを通過していることを確認できます。
ただし、この方法でも手元の観測で10秒ほど切り替え中にアクセスができなくなる時間が発生しました。
今回は他の方法を調べることができなかったため、この点は飲み込んで、夜間作業としてリリース作業を進めました。
メモ
TerraformでCloudfrontを作成する場合、restrictions.geo_restriction.locations
の指定が必須になりますが、CIなどからE2Eテストなどを実行している環境では、locations = ["JP"]
とすると通信がブロックされてしまうことがあるので、注意してください。
まとめ
本記事では比較的情報の少ない、Amplify Consoleで稼働中の既存アプリケーションの前段にCloudfrontを挿入する方法に関してまとめました。
結論として、どうしてもダウンタイムが発生してしまうことが確認されましたが、手元では10秒ほどだったので夜間作業にて対応を進めました。
本記事がなにかの参考になりましたら幸いです。