はじめに
フロントエンドはReactなどのフレームワークを使い、バックエンドはAPI化するというアーキテクチャはよく見ると思います。基本的にフロントエンドとバックエンドを別のドメインで構成するように設計されるかもしれませんが、やむを得ず同一のドメインで構成することもあると思います。同一ドメインでCloudFrontを使ったときにカスタムエラーページ設定でバックエンドのエラー応答をCloudFrontで上書きしてちょっとハマったので気をつけたいポイントとして残しておきます。
構成
今回は簡単に以下の構成で説明してみます。
検証目的なのでwww.hoge.com
部分はCloudFrontがネイティブに提供するFQDNを使います。
検証してみた
こちらのコードで検証していきます。
今回は以下の順番で進めます。
- 検証環境のデプロイ
- バックエンドAPIの応答を200から404に書き換え
- CloudFrontのカスタムエラーページの追加
- CloudFront Functionsによるエラーレスポンスのカスタマイズ
- Lambda@Edgeによるエラーレスポンスのカスタマイズ
- 【おまけ】 CloudFront Functionsによるメンテナンスページの表示
1. 検証環境のデプロイ
対象のリポジトリをCloneした後、フロントエンドアプリのビルドを行います。※この手順ではDocker上でビルドします。
cd cloudfront-custom-error/files/frontend-app
docker run -it --rm -v $PWD:/app -w /app node:alpine3.21 npm run build
続いてterraformを実行します。※AWSのクレデンシャルの設定は省略します。
cd ../..
terraform init
terraform apply
ルートパスにアクセスすると以下のようなフロントエンドの画面が返されるようにしています。
Submitボタンを押すとバックエンドに通信が走り、レスポンスBody内のMessageが以下のように応答結果が表示されます。
2. バックエンドAPIの応答を200から404に書き換え
バックエンドから404を返すように設定した場合、以下のような表示になります。尚、今回のサンプルではAPI GatewayのMockを使って固定で返すようにしています。terraformのvariable backend_status_code
を404に設定してterraform apply
することで返却するステータスコードを変更します。
変更後アクセスすると404応答が返され、レスポンスBody内のMessageが表示されました。
処理イメージ
ここまでの処理イメージは以下の通りです。
3. CloudFrontのカスタムエラーページの追加
よく使われるであろうCloudFrontの機能のカスタムエラーページで404エラーの設定をしてみます。terraformのmain.pyに記載している以下のコメントアウトを解除して再度terraform apply
します。※CloudFrontの更新なので少し時間がかかります。
custom_error_response = [
{
error_code = 404
response_code = 404
response_page_path = "/404.html"
},
{
error_code = 503
response_code = 503
response_page_path = "/error500.html"
}
]
更新完了後再度Submitボタンを押下すると以下のような画面表示になりました。フロントエンドがAPIからJSONで返されることを期待した作りとしているため、HTMLが返ってきたことによって例外が発生してしまっています。フロントエンドとバックエンドAPIが同一のドメインであるが故にCloudFrontのカスタムページの仕組みで応答が変わってしまってしまいました。
フロントエンド上に存在しないリソース({cloudfrontのFQDN}/sample
)にアクセスしてみると以下のような画面が表示されます。
処理イメージ
ここまでの処理イメージは以下の通りです。404のレスポンス部分でAPIGWから返ってくるjsonをCloudFrontがhtmlに変えてBrowserに返してしまいます。
4. CloudFront Functionsによるエラーレスポンスのカスタマイズ
APIからの応答はJSONのまま、フロントエンドのエラーはhtmlで返したいので他の方法を考えてみます。
まずはCloudFrontのリクエスト・レスポンスをカスタムする方法として簡単に実装できるCloudFront Functionsを使ってみます。
またこちらをコメントアウトして
#custom_error_response = [
# {
# error_code = 404
# response_code = 404
# response_page_path = "/error404.html"
# },
# {
# error_code = 503
# response_code = 503
# response_page_path = "/error500.html"
# }
#]
以下のコメントアウト・インを行い、terraform apply
を実行します。
before
function_association = {
# 通常用
"viewer-request" = {
function_arn = aws_cloudfront_function.main.arn
}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.viewer_response.arn
#}
# エラーページ用
#"viewer-request" = {
# function_arn = aws_cloudfront_function.maintenance_request.arn
#}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.maintenance_response.arn
#}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.error_custom_response.arn
#}
}
after
function_association = {
# 通常用
"viewer-request" = {
function_arn = aws_cloudfront_function.main.arn
}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.viewer_response.arn
#}
# エラーページ用
#"viewer-request" = {
# function_arn = aws_cloudfront_function.maintenance_request.arn
#}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.maintenance_response.arn
#}
"viewer-response" = {
function_arn = aws_cloudfront_function.error_custom_response.arn
}
}
viewer-response
に定義したfunctionではレスポンスコードを判定して固定のhtmlを返すスクリプトを組んでいます。これが上手く動けば「ページがみつかりません。」というメッセージが表示されます。設定し、改めて先ほど404エラーになったページへアクセスすると以下のような味気ない画面が返されました。
こちらの記事の通り残念ながらCloudFront Functionsはステータスコードが400以上の時発動しないようでレスポンスボディの書き換えはできないので、フロントエンドの404等標準的なエラーページ表示には使えなさそうです。
処理イメージ
今回やろうとしたことは以下の通りです。※フロントエンド部分のみ抜粋。
5. Lambda@Edgeによるエラーレスポンスのカスタマイズ
次にCloudFront Functionsよりも少し設定が面倒な分、凝ったことができるLambda@Edgeを試してみます。
まず、先程の変更を元に戻しつつ、Lambda@Edgeを紐づける設定も併せて行います。変更後、terraform apply
を実行してください。
before
function_association = {
# 通常用
"viewer-request" = {
function_arn = aws_cloudfront_function.main.arn
}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.viewer_response.arn
#}
# エラーページ用
#"viewer-request" = {
# function_arn = aws_cloudfront_function.maintenance_request.arn
#}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.maintenance_response.arn
#}
"viewer-response" = {
function_arn = aws_cloudfront_function.error_custom_response.arn
}
}
#lambda_function_association = {
# "origin-response" = {
# lambda_arn = module.lambda_at_edge.lambda_function_qualified_arn
# }
#}
after
function_association = {
# 通常用
"viewer-request" = {
function_arn = aws_cloudfront_function.main.arn
}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.viewer_response.arn
#}
# エラーページ用
#"viewer-request" = {
# function_arn = aws_cloudfront_function.maintenance_request.arn
#}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.maintenance_response.arn
#}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.error_custom_response.arn
#}
}
lambda_function_association = {
"origin-response" = {
lambda_arn = module.lambda_at_edge.lambda_function_qualified_arn
}
}
反映後、改めて{cloudfrontのFQDN}/sample
にアクセスしてみると想定どおり「ページがみつかりません。」というページが表示されました。(味気ない表示であることはスルーでお願いします。)Lambda@Edgeであればレスポンスをカスタマイズできることが確認できました。これを使えばCloudFrontのカスタムエラーページの代わりにフロントエンドのエラーレスポンスを設定しつつ、バックエンドAPIのエラーを上書きしてしまうのを回避できそうです。
処理イメージ
今回行ったことは以下の通りです。※フロントエンド部分のみ抜粋。
6. 【おまけ】 CloudFront Functionsによるメンテナンスページの表示
フロントエンドにパスルーティングされるところでリクエスト・レスポンスを書き換えてエラーページを出すように変えてみます。想定されるケースとしてはフロントエンドを閉鎖するメンテナンスページを表示したい時などです。
以下のコメントアウト・インを行います。
before
function_association = {
# 通常用
"viewer-request" = {
function_arn = aws_cloudfront_function.main.arn
}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.viewer_response.arn
#}
# エラーページ用
#"viewer-request" = {
# function_arn = aws_cloudfront_function.maintenance_request.arn
#}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.maintenance_response.arn
#}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.error_custom_response.arn
#}
}
lambda_function_association = {
"origin-response" = {
lambda_arn = module.lambda_at_edge.lambda_function_qualified_arn
}
}
after
function_association = {
# 通常用
"viewer-request" = {
function_arn = aws_cloudfront_function.main.arn
}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.viewer_response.arn
#}
# エラーページ用
"viewer-request" = {
function_arn = aws_cloudfront_function.maintenance_request.arn
}
"viewer-response" = {
function_arn = aws_cloudfront_function.maintenance_response.arn
}
#"viewer-response" = {
# function_arn = aws_cloudfront_function.error_custom_response.arn
#}
}
#lambda_function_association = {
# "origin-response" = {
# lambda_arn = module.lambda_at_edge.lambda_function_qualified_arn
# }
#}
リロードすると以下の画面になりました。CloudFront Functionsでviewer-requestとviewer-responseをカスタムすることでメンテナンス画面表示は実現できそうです。
処理イメージ
一通り検証が完了しました。料金節約の為、最後に今回作った環境をterraform destroy
で削除して完了です。
所感
今回のようにフロントエンドをCSR、バックエンドをAPIで構成するケースは多いかと思いますが、エラーページの制御がツラいので、できればCORSなどの設定等面倒はあれど、別のFQDNで構成するのが良さそうだと思いました。
最後まで読んでいただきありがとうございました。