🔅

CloudFront + API Gateway + App Runner構成で踏んだ地雷録

に公開

はじめに

うぐいすソリューションズ Advent Calendar 2025の2日目担当ぴよです。一応会社の代表なので今年の振り返り記事とか、少なくとも開発の進め方のTips記事を書こう、と思っていたのですが、直近にシステムリリース日が控えておりめちゃくちゃ実務のログ系の記事になりました。AI使えば大抵のことはなんとかなるので「やってみた系の記事」とか「人が捉えた全体感や解釈」の記事の方が今は需要があるよな、と思いつつ、AIと共に解決したログと理解の過程を残します。アドカレ担当日にえいっと書いたせいで図もスクショもなくてごめん。

今回記事で取り扱う構成

通常のtoB向けの、社内利用のWebアプリケーションです。大まかな構成は以下です。

  • フロントエンド:Next.js、App Runnerに載せた
  • バックエンド:NestJS、App Runnerに載せた
  • 認証:Cognito
  • DB:RDS(AWS AuroraのPostgreSQL)

今回扱うAPIの通信あたりはCloudFrontでCDN&ドメイン揃えて叩き先の振り分けをする(/apiでバックエンド、それ以外はフロントエンド)、バックエンドのAPIはAPI GatewayをApp RunnerとCloudFrontの間に置いて、レート制限や監視をしやすくする構成にしました。インフラはTerraformで管理しました。

【余談】API側のCloudFront -> API Gatewayって嬉しい?

App Runnerの前にAPI Gatewayをおくと、

  • 過剰なリクエストがアプリに到達する前にブロックする
  • APIスキーマの厳格な管理をしてセキュアにしたい
  • リクエスト/レスポンス変換・バージョニングをAPIフロントで吸収できたり、監視設定を入れやすい
  • 将来的にマルチクライアントになった時に、料金設定や制限をクライアントごとに設定する

といったメリットがあります。今回は小規模でクライアントが1社、バージョニングなどもないので入れるか微妙なラインでしたが、PoCではなく実運用もするので最低限のリスクヘッジの手段として導入しました。

【余談】CloudFrontにフロントエンドもバックエンドもまとめるのは嬉しい?

CloudFrontにバックエンドもフロントエンドもまとめると、ドメインの設定箇所が1箇所で済むとか、CORSの設定が楽というメリットがあります。CloudFrontのキャッシュポリシーをAPIとフロント両方に使えるので、API側のキャッシュまでコントロールできるのも強みです。

地雷たち

ここからは実際にハマった(といってもAIがすぐ解決してくれたけど)箇所について、解説します。

地雷1.API Gatewayから429が返るのに原因はDB接続だった

事象:API Gateway で 429 Too Many Requests が返る

よしだいたいできあがったぞと思い、curlでAPI Gatewayの動作確認をしました。

$ curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/api/health
{"message":"Too Many Requests"}

「アプリケーション側で無限に叩いちゃったりしてる?」「AIがTerraformで変な設定を書いて、リクエスト数の制限がえぐい厳しい?」と思い30分くらい手動で格闘していたのですが、どの可能性も潰してClaude Codeに「他の可能性はないの?」と泣きついたところ、バックエンドのApp Runnerが503を返すせいでAPI Gatewayのスロットリングにひっかかり、429エラーが返っていました。

原因:Secrets ManagerがJSON形式だとApp Runner上で読めない

バックエンドのApp Runnerの503エラーの原因は、App RunnerからDB接続できないことでした。ECSを使う時と同様、Secrets ManagerからDBの接続情報を読んでいたのですが、{"username":"xxx","password":"xxx","engine":"postgres","host":"xxx", ...}といった形でJSON形式で値を格納していました。ECSだとARN形式を拡張したフォーマットでかけて、JSONのキーを指定して値を埋め込めるのですが、App Runnerではこの手法が使えません。
接続情報をすべてJSONではなく文字列単体で持つようにSecrets Managerを分解することで、このエラーは回避できました。

地雷2. CloudFront経由でAPIを叩けず、フロントエンドのHTMLが返ってくる

事象:/apiを叩いているのにHTMLが返ってくる

CloudFront上で、apiオリジンをAPI Gatewayに向け、frontendオリジンをフロントエンドのApp Runnerに向けました。 ビヘイビアで/api/*パスパターンはapiオリジンに向け、デフォルトのパスパターンはfrontendオリジンに向け、優先順位は/api/*パスパターンを上にして、「よし、これで/apiでリクエストしたらAPI側に通信するぞ!」と思ったのですが.....何度試しても、Chromeの開発者ツールで見るとAPI通信の変わりにHTMLが返ってきます。API Gatewayを直接叩く、App Runnerを直接叩く...と切り分けましたが、CloudFront上でだけHTMLが返ります。なんとなくエラーをフォールバックしているような気がするのに、API GatewayやAPP Runner経由でエラーが出ない、おかしい....と時間を溶かしました。

原因:ヘッダーの転送設定がおかしい

今回のアプリケーションでは、CognitoのAuthorizationヘッダーを使ってAPI Gatewayに認証をする構成でした。CloudFrontはオリジンにリクエストする時、デフォルトだと一部だけヘッダーを送るので、細かいコントロールにはOrigin Request Policyの設定が必要です。
今回の場合

  • 「All viewer headers(ブラウザからのヘッダー全部) を転送する」ようなOrigin Request Policyを使う
  • Cache Policy の cache key に Authorization を含める

のどちらかの対応が必要です。私はAIに過度な信頼を寄せていたので正しい情報で作っているだろうと思い、ぱっと見も正しかったのですが、どうやらHostヘッダーもCloudFrontのものを送っていたようです。

$ curl -H "Host: xxx.cloudfront.net" \
    https://xxx.execute-api.ap-northeast-1.amazonaws.com/api/health
{"message":"Forbidden"}

API GatewayやApp Runnerは自身のドメインをHostとして期待するので、上書きされていると403が返ります。CloudFrontからHostヘッダーを転送されると困るので、Origin Request PolicyにAllViewerExceptHostHeaderをつけることで解決しました。HTMLが返るところからの問題の切り分けを手動でしたので、AI時代にしては骨の折れるエラーでした。

地雷3. TerraformでCloudFront のカスタムポリシー削除ができない

事象:不要になったカスタムOrigin Request Policyを削除しようとするとエラーになる

地雷2を直す過程で、CloudFrontにカスタムポリシーを設定したり管理ポリシーを設定したりといろいろ試していたのですが、不要になったカスタムOrigin Request Policyを削除しようとコードを書き換えてterraform applyをするとTerraformでエラーが起きました。

Error: deleting CloudFront Cache Policy: CachePolicyInUse:
The specified cache policy is currently associated with a cache behavior.

「手動で管理してるリソースないんだからterraformで全部消えるはずだよね....」と何度かapplyを繰り返しましたが結果は変わらず。

原因:削除のライフサイクルがDistributionとポリシーで違う

CloudFrontではポリシーがどこかのDistributionに紐づいていると削除できない、という挙動があります。terraform applyでDistributionの設定を変更し、新しいポリシーを参照した結果、古いポリシーは不要としDelete APIが呼ばれますが、CloudFrontの変更は非同期で反映されるため、Delete APIを呼んだ瞬間はまだポリシーが参照中とみなされます。

こういった場合、

  1. state rmでリソースをTerraformの管理から外す
  2. 手動で削除する

というオペレーションが必要です。(Terraformの依存関係の定義でなんとかできることもありますが)。terraform.tfstate上にどのリソースがどの実リソースと対応しているかの紐付けがあるので、ライフサイクルの乖離があって削除エラーが出るようなものはどんどんstate管理をやめてとりあえずterraform apply→手動で消す、を私はしています。脳筋戦略ですが。

まとめ

以上、CloudFront + API Gateway + App Runner構成でClaude Codeに任せてTerraformを書いて踏んだ地雷集でした。同じような構成で設計をしているエンジニアや、AWSを勉強しているエンジニアの参考になればと思います。

【余談】本件でのAIとの付き合い方

ここまであげてきた地雷の問題解析や修正は全部AIがやってくれたのですが、(Claude Codeと、AWS上のリソース解析はGUIからAmazon Qを使いました)、生成されたコードや修正を読むのはもちろん、なぜそうなったかは人間も理解しておいた方が少なくとも現時点では良いな、と思っています。知らないと人間がレビューできず、AI任せて振る舞いのみで判断して開発することになりますが、2025年11月時点のAIだと人間側のAI力をカバーしたり、絶対に沼にハマらず適切に対処してくれる、みたいなことはないので。このへんの深掘りをしておいて、会話ログから記事のネタを出してもらうと記事執筆が捗りそうですね。

Zenn、記事のレビューしてくれるじゃん

「図もスクショもなくてごめんじゃねえ、作って貼れよ!」とレビューをもらいましたが遅筆ですでにだいぶ長いこと記事を書いてるので許してくれえ....😌

AIに怒られてるスクショ。

さて、ここまで読んでくださりありがとうございました!次回は弊社の大喜利得意エンジニアの@KoukiNakae さんが何か書いてくれるはずです。

うぐいすソリューションズTechBlog

Discussion