アプリケーション(LightSail)のURLのルートだけをSSR(Nuxt3)で動かす(AWS)
前提状況
- 稼働しているexample.comのサービスを引き継いで受け持つことになった。
- Laravel + LightSailで稼働している。
- 現在サービスTOPのサイトはLaravel内にBladeテンプレートで構築されている
- サービスTOPだけをリニューアルすることになった
- 動的に描画する部分があり、またSEOも気にするのでSSRにしたい
要件
- example.comの/のルートで来た時は、サービスサイトをSSRで動かす
- システムは/user/loginや/company/loginなどがLaravelで動いている
- AWSの内部ネットワークもHTTPSで接続する。
本文
自分はLaraveのバックエンドで動いている既存のアプリケーションを、上記のような状況でフロントページだけ改めてSSRで動かさなければならなくなったことが2回ほど案件であった。
いくつか試行錯誤の末、CloudFrontでの構成を行うことにした。
事前準備
- Route53でexample.comを登録しておく
- ACMでalb.example.comとcloudfront.example.comを取得しておく。サーバーがあるリージョンとus-east-1両方で取得しておく必要がある。
手順①LightSailでVPCピアリングを行う
VPCピアリングを行うことでALBに接続することが可能になる。
ALBに接続できれば、ネットワークの冗長性を確保でき、WAFの連携や環境の切り替えなども楽になる。
デフォルトのVPCが無い場合は別途VPCを作成する
以下公式より抜粋
- Lightsail コンソール で、上部のナビゲーションメニューでユーザー名を選択します。
ドロップダウンから [アカウント] を選択します。
[Advanced] (アドバンス) タブを選択します。
VPC ピアリングを有効にする AWS リージョン の横にあるステータスを切り替えます。
手順②ALBの設定
ターゲットグループの登録画面にて「その他のIPアドレス」を選択。
LightsailのPrivateIPアドレスを入力する
「Application Load Balancer」を作成。
ターゲットグループとして上記設定を追加
手順③Route53のALBのドメイン追加
※443で内部で通信する場合、これをやっておかないとCloudFrontからSSHで接続できずハマります。80番しか使わないなら問題無いハズです。
Route53にalb.example.comというサブドメインを作成する。
ここまででバックエンドは完了。
手順④Lambdaで関数を作る
設定注意点
-
メモリ
512MBか1024MBにする -
タイムアウト
10秒などにする(デフォルトでは3秒なので画像が多いサイトやAPIを使うサイトなどは注意) -
権限
CloudFrontから打てるように。適宜権限を追加 -
同時実行数
デフォルトはリージョンによって異なるが数百程度のこともあるので注意。上限引き上げ申請が必要なことがある。
手順⑤S3でBucketを作る
設定注意点
-
パブリックアクセス
「パブリックアクセスをすべて ブロック」でOK。直感に反するが大丈夫。 -
権限
CloudFrontからの権限を追加する。
下記のような感じ。ちなみに出来てない場合、CloudFrontの設定画面からS3の権限の推奨設定をコピーすることも可能。
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::[S3バケット名]/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::1xxxxxxxxxxx:distribution/[Cloud FrontのID]"
}
}
}
]
}
手順⑥Nuxt3のSSRをLambda+S3でビルド&デプロイする
Nuxt3のnuxt.config.tsに
nitro: {
preset: "aws-lambda",
serveStatic: false,
},
を追加する。(ちなみにnitroはナイトロと読むらしい。ニトロはドイツ語読み。これ豆な)
これでビルドコマンドを打つと.nuxt/.outputのserverにindex.mjsファイルが、publicに静的ファイルが配置される。
AWS CLIをインストールしてプロファイルを設定した上で、
Makefileに下記のようにコマンドを書いておくと便利。
(そのうちgitワークフローでCI/CDとして作りなおす予定。)
include .env
build:
npm run build
build-deploy:
@make build
@make zip
@make s3-upload
@make lambda-upload
s3-upload:
aws s3 sync .output/public s3://[S3バケット名] --delete --profile $(AWS_CLI_PROFILE)
zip:
cd .output/server && zip -r ../../function.zip . && cd ../..
lambda-upload:
aws lambda update-function-code --function-name [Lambda関数名] --zip-file fileb://function.zip --profile $(AWS_CLI_PROFILE) --output text
これで実行ファイルがlambdaに、静的ファイルがS3にアップロードされる
手順⑦CloudFrontを作成
CloudFrontを作成する。
「オリジン」の登録
オリジンとは、ALBで言えばターゲットグループみたいなもので、どこにネットワークを流すかの向き先のこと。
ここで先ほど作った
- Lambda
- S3
- ALB
3つのオリジンをそれぞれ登録する。
特別な事情がなければTLSv1.2、プロトコルはHTTPSのみかマッチビューワー(HTTP/HTTPS両方)で良い。
ちなみに次に設定するビヘイビアでHTTPSにリダイレクトを行うかを聞かれる。
この辺は環境に適宜合わせる。
ALBのオリジンをまず最初に作る
最初に作った「オリジン」が次に設定する「ビヘイビア」のデフォルトとして自動選択される。
(パスが自動で「*」となりすべてのフォールバック先になる)
注意点としてはALBに対してHTTPSで接続したい場合、プルダウンの選択では選ばないこと。
上記「手順③Route53のALBのドメイン追加」でやったようにALBのデフォルトドメイン(例:xxxxx-loadbalancer-111111111.ap-northeast-1.elb.amazonaws.com )ではなく、CNAMEで登録したalb.example.comのようなサブドメインを直接指定しなければならないことに注意。
また証明書(ACM)は、alb.example.comはALBのリージョンでも、また米国東部 (バージニア北部)us-east-1でも同じサブドメインの証明書を取得しておかないといけないので注意。一つでも違うとネゴシエーションが失敗してCloudFrontからALBへ接続できない。
ALBのビヘイビアの設定では「キャッシュキーとオリジンリクエスト」を「Legacy cache settings」にしてヘッダーとクエリ文字、Cookieを「すべて」通す設定にする
(ちなみに今回の環境ではALB側でWAFを噛ませていてセキュリティはそちら任せだったのと、システム側で静的キャッシュは不要のためこの設定で大丈夫と判断したが、環境によってはもう少し細かい調査と調整をした方がいいかもしれない。)
その他「ビヘイビア」の登録と作成
-
Lambdaに/で来たときにネットワークを流す
「パスパターン」で/を登録
上記で設定したLambdaのオリジンを選択
プロトコルポリシーやHTTPメソッドは環境に合わせる。
「キャッシュポリシーとオリジンリクエスト」はLambdaFunctionの場合の推奨設定が出てくるのでその通りにする。
-
S3に/_nuxt/*で来たときにネットワークを流す
「パスパターン」で/_nuxt/*を登録
上記で設定したS3のオリジンを選択
プロトコルポリシーやHTTPメソッドは環境に合わせる。
「キャッシュポリシーとオリジンリクエスト」はS3の場合の推奨設定が出てくるのでその通りにする。
よくあるトラブルシューティング(おまけ)
上記説明でも書いてるが、下記がハマりポイント。
こんなエラーが起きやすいので解決法と共にまとめておく。
⇒原因:ACMのリージョンが違う
Route53でCloudFrontをエイリアスとして登録する際、代替ドメイン名をCloudFront側に登録しておき、そのドメイン/サブドメインが合致していなければ、エイリアスとして選択できない。
またCloudFrontではSSHの証明書は、ACMのリージョンus-east-1の証明書しか選択出来ない。
なので、稼働しているリージョンと同じACMだけではなく、us-east-1も取る必要がある。
⇒原因:ACMのサブドメインが異なっている
またそのACMリージョン間で、登録されたサブドメインが異なっているとALBとCloudFrontとのSSHのネゴシエーションが失敗する。そのため、ドメイン、サブドメインを合わせる必要がある。
⇒原因:ALBがデフォルトドメインになっている
CloudFrontで「オリジン」としてALBを登録する際、ALBをプルダウンで選択できるようになっているが、SSHでの通信を望む場合、ここで気軽にプルダウンを選択してはいけない。
「手順③Route53のALBのドメイン追加」で行ったalb.example.comを単に手入力する必要がある。
⇒原因:CloudFrontからALBに対してhostヘッダーがない(詳細未検証)
ALBに対する「ビヘイビア」においてプロキシした時のCloudFrontからのヘッダーに問題があるようだ。
ビヘイビアで「キャッシュキーとオリジンリクエスト」を「Legacy cache settings」にしてヘッダーとクエリ文字、Cookieを「すべて」通す設定にすると解決する。(環境によってはもう少し細かい調整が必要)
⇒原因:ALBから80番のみでLightsailにつないでいて、かつLightsail上のapacheが80番で来たときに443にリダイレクトする設定になっている場合、リダイレクトループが起きてしまう。
apacheのHTTPSリダイレクトを切るか、ALB側で内部443番でもつなぐ。
当初Lightsailだけで運用している環境だと起きがち。
ルーティングでうまく行かなった方法(おまけ)
結論Amplify+Nginxはうまく出来ませんでした。
Amplifyで当初開発用でSSRで動かしていたのもあり、そのままNginxでリバースプロキシを行う事でフロントとバックエンドシステムのパスによる切り替えが出来ないかと考えたが、リバースプロキシを通した際「403 ERROR The request could not be satisfied.」というエラーを解決できずに見送った。
Amplifyでは入口にCloudFrontを使っているらしく、2重でリバースプロキシを噛ませるのが難しいようだった。またこの辺がうまく隠蔽されてるため、エラーの細かい調査や対策も難しかったため見送る事にした。
Discussion