きっかけ
2026年9月18日の夜、Amplifyのコンソールを何気なく開いたら、リクエスト数が「468,231」になっていました。1時間あたりの数字です。
普段は1時間で1,000〜2,700件くらい。多い日でも6,000件ちょっとです。桁が2つ違います。
やばい、未だかつてない強烈なリクエストが来てる、と思いました。
結局2時間で約85万リクエスト来ました。この記事は、その正体を調べて、前段にCloudflareを入れるまでの記録です。
9/16から9/22までのグラフで、9/18のところだけ針が振り切れています。左がリクエスト数、右が転送量です。
このサイトの構成
個人開発で、論文の検索サイトを作っています。構成はこんな感じです。
- フロントはNext.js。AWS Amplify HostingのSSRで配信
- その前段にAmplify管理のCloudFrontがいる。キャッシュ設定はこちらからいじれない
- 論文ページは初回アクセスのときに生成するon-demand ISR
- バックエンドはAPI Gateway + Lambda。月100万件・10rpsの上限をかけている
- DNSはお名前.comで取って、権威DNSはRoute53
防御らしきものも一応ありました。アプリ側でBytespider・Lightpanda・GPTBotをUser-Agentで弾いていて、robots.txtでもSEO系のボットをいくつか拒否しています。ただ今回の相手には、どちらもまったく効きませんでした。
起こったこと
リクエスト概要
| 時刻(JST) | リクエスト数 | 転送量 | 5xx |
|---|---|---|---|
| 平常時 | 1,000〜2,700 | 12〜45MB | 0 |
| 18:00 | 1,485 | 14MB | 0 |
| 19:00 | 55,598 | 442MB | 34 |
| 20:00 | 468,231 | 3.67GB | 161 |
| 21:00 | 328,090 | 2.42GB | 23,461 |
| 22:00 | 2,447 | 24MB | 0 |
19時51分頃から21時58分頃までの約2時間で、合計約85万リクエスト、約6.5GB。いちばん多かった20時台の468,231件は、平常のよく出るほうの数字(2,700件)と比べても170倍です。1分あたりの最大は11,015件だったので、だいたい180 req/sでした。
22時台にストンと戻っているとおり、こちらが何かして止めたわけではありません。相手が勝手にやめました。
では、この85万リクエストは何をしていたのか。
CDNのキャッシュをほぼ完全に回避されていた
アクセスログを引っ張ってきて、まず目についたのがURLでした。いちばん感心したのもここです。
相手は/、/legal、/japan、/wiki、/about、/trends、/figuresの7ページに、?_rsc=のあとに16文字の乱数を付けてアクセスしていました。しかも毎回ちがう値です。/legalだけで117,556件、うちクエリ付きが66,419件で、その_rscの値は66,165種類ありました。ほぼ1リクエストごとに別のURLです。
_rscは、Next.jsがRSC(React Server Components)のデータを取りに行くときに使うパラメータです。そしてCloudFrontのキャッシュキーにはクエリ文字列が入るので、/legal?_rsc=AAAAと/legal?_rsc=BBBBは別物として扱われます。毎回ちがう値が付いていれば、キャッシュがあっても使われません。
念のため素の/legalを自分で叩いてみると、2回目からはちゃんとCloudFrontでHitします。キャッシュ設定が壊れていたわけではなく、今回のアクセスだけが結果としてキャッシュを回避していた、ということになります。
アプリ側のログで見ると、94%がRSCリクエストの印を持っていました。ページのHTMLではなく、その裏にあるデータの方を取りに来ていたようです。
つまり、こういう連鎖でした。
RSCのデータを大量に取りに来る →
_rscの値が毎回変わる → CloudFrontのキャッシュキーも毎回変わる → キャッシュが効かない → SSRまで毎回届く
85万リクエストのほとんどがcomputeまで到達していたのは、これが理由です。CDNがほとんど肩代わりしていません。
送信元は3,126個のIPに分散していた
送信元は3,126 IP。そのうち98%が43.172.0.0/16と43.173.0.0/16に収まっていました。whoisを引くとACEVILLE PTE.LTD.、つまりTencent Cloudのシンガポール(AS132203)です。受けたCloudFrontのエッジもシンガポールでした。
1 IPあたりにならすと約2時間で270件くらいで、1台1台は驚くほど大人しい。「1 IPあたり何件を超えたらブロック」という素直なレート制限では、まず引っかかりません。
User-AgentはWindows版のChromeを名乗っていました。ただしバージョンが106、116、124、131、133とローテーションしていて、どれも古い。2026年9月時点の本物は153前後です。Cookieも持っていないし、HTTP/1.1が99.97%でした。本物のChromeなら、まずHTTP/2以上で来ます。この癖は、あとでWAFのルールを決めるときに使います。
「攻撃」というよりスクレイピングに見える
ここまでで分かった事実を並べると、こうなります。
- 3,000以上のIPに分散している
- 特定の7ページを巡回している
- 大半がRSCリクエスト
-
_rscの値をほぼ毎回変えている - 結果としてCDNのキャッシュを回避している
- 約2時間で突然終わっている
ここから先は推測です。この挙動だと、うちを狙って落としにきたというより、Next.jsのサイトからRSCのデータを集めるために作られた自動スクレイピング基盤なんだろうなと思っています。こちらに出た85万リクエストとSSRの負荷は、その副作用です。たぶん相手はこちらのことを認識すらしていません。
少なくとも、ログを見る前に想像していた「誰かがうちを狙ってDDoSしている」という姿とは、だいぶ違うものでした。
実害
実害は2つです。読者にエラーを返してしまったことと、キャッシュを回避された分がそのままAmplifyの従量課金に乗ったこと。
読者にエラーを返していた
同じ時間帯には、論文ページ/papers/*への巡回も約6万件ありました。こちらは乱数付きではありませんが、まだ生成していないページに初回アクセスが集中した形です。
論文ページを生成するときはバックエンドAPIを呼ぶので、そこで10rpsの流量制限に当たりました。
- API Gatewayの4XX: 約25,000件
- フロント
/papers/*の500: 約23,000件
バックエンドは流量制限のおかげで持ちこたえましたが、その代わりフロントが500を返しています。つまりその時間帯に来た読者やGooglebotにも、エラーページが返っていたはずです。お金だけの話ではなく、普通に迷惑をかけていました。
Amplifyの請求は8倍になった
Amplifyの請求で効いてくるのは「0.5GB × オリジンに到達したリクエストの処理時間(秒)」の部分です。普段は1日0.26〜0.35ドルくらいで、そのうち4分の3がこのcompute分でした。
大事なのは「オリジンに到達した」という条件で、CloudFrontがキャッシュで返した分には課金されません。今回はそこをほとんどすり抜けられたので、85万件の大部分が請求に乗りました。
Cost Explorerで確定した9月18日のAmplify代は2.45ドルでした。普段の8倍ですが、クラウド破産はしなさそうで、ほっとしました。
怖かったのは止まらなかった場合のほうだった
今回はたまたま約2時間で止まりました。ピーク時の468,231件/時が1か月続いたとすると、720時間で約3.37億件。今回の実績単価(100万件あたり約2.9ドル)で引き直すと、こうなります。
| 構成 | 1か月続いたら |
|---|---|
| 当時のまま | 約970ドル |
| アプリ側で遮断する | 約140ドル |
| AWS WAFで遮断する | 約200ドル + 月21ドルの固定費 |
| Cloudflare無料プランで遮断する | 0ドル |
料金体系や負荷のかかり方で単純比例しない部分もあるので、あくまで概算です。それでも「何もしなければ、来ている間ずっと課金され続ける」という構造は変わりません。そのとき自分の手元には、入口で止める仕組みが何もありませんでした。
守られたのは、絞る場所を置いていたところだけだった
バックエンドには最初から制限を入れていました。
- API Gatewayに10rpsの流量制限
- 月100万件の上限
今回バックエンドが落ちなかったのは、これが効いたからです。一方でフロント側には、同じ考え方の仕組みを置いていませんでした。絞る場所を作っておいたところだけが守られて、置き忘れたところが素通りした、という分かりやすい結果になっています。
従量課金は落ちないぶん、財布が払う
固定容量のサーバーなら、負荷が限界を超えるとサイトが落ちます。従量課金だと処理能力のほうが勝手に増えるので、サイトは動き続けて、代わりに処理した分だけ料金が増える。固定容量ならサーバーが先に限界を迎えて、従量課金なら財布が先に限界を迎える、という違いです。
こういう形で課金を増やされるのはDenial of Walletと呼ぶらしいです。しかもAmplifyには上限を設定する手段がありません。
攻撃を受けた瞬間に自分の財布が痛む構造って、健全ではない気がするんですよね。
対策
必要だったのは、アプリの中ではなく、その手前で大量アクセスを止める仕組みでした。アプリ側で弾いてもAmplifyのcomputeは動くので、負荷は軽くできても「受けた分だけ課金される」という構造は残ります。
1. 応急処置
1つめは、アプリ側に「送信元が43.172.0.0/15なら弾く」という判定を足したこと。弾いてもcomputeは4msくらい走るので、ページ生成の100msに比べればずっと安い、という程度の効果です。
2つめはCloudWatchアラーム。リクエストが5分で3,000件を超えたらメールが飛ぶようにしました。平常の5分あたり最大が1,833件なので余裕があります。今回の洪水なら、開始1分後には鳴っていた計算です。
ただしどちらも、安くする・気づくだけで、止められるわけではありません。
2. 恒久対応としてCloudflareの導入
AWS WAF(Amplify Firewall)も検討しましたが、Amplify本体が月10ドル弱なのに月21〜22ドルの固定費がかかるので、コストに見合わず見送りました。無料のShield StandardはL3/L4が対象なので、今回のようなHTTPリクエストの洪水には効きません。
それで、Cloudflareの無料プランを前段に置こう、となりました。HTTP層の洪水を0ドルのまま入口で止められる手段が、自分にはこれくらいしか見つかりませんでした。
入れること自体はずっと前から考えていたのですが、AmplifyとRoute53とACMの組み合わせに罠があって、手が止まっていました。NSをCloudflareに移すと、Route53側にしかないACMの検証用CNAMEが公開DNSから消えて、証明書の自動更新が静かに失敗します。Cloudflare側にもミラーしてから切り替えました。ここを9月12日に調べてあったので、洪水の夜に「じゃあ今日やるか」と動けています。
何を止めるかはログから決めた
無料プランのカスタムルールは5本まで使えます。直近7日分のAmplifyのアクセスログ(洪水の85万件を除いた約22.6万件)を集計して、何を止めるか決めました。
| # | 対策 | アクション | 目的 |
|---|---|---|---|
| 1 | クラウド・ホスティング事業者のASN(検証済みボットを除く) | Challenge | データセンター発の自動アクセスを絞る |
| 2 | 名乗らずに巡回しているクローラー | Block | 正体を偽っている巡回を遮断 |
| 3 | robots.txtを無視するUAと、設定ファイル・WordPressの探索 | Block | 拒否済みbotと自動スキャンを遮断 |
| 4 | ブラウザとして不自然な通信(検証済みボットを除く) | Challenge | botらしいアクセスだけ追加検証 |
意外だったのが、MetaのASNから、Metaを名乗らずに普通のChromeとしてやって来るアクセスでした。7日で約8万件あって、オリジン到達の4割弱を占めていました。中の人が誰かまでは分かりませんが、名乗る設定になっていない何かが動いているようです。
robots.txtを無視するクローラーを止めるルールでは、/robots.txt自体へのアクセスだけは通しています。拒否した相手にもrobots.txtは読ませる、という方針です。行儀を直したクローラーが「あ、ここはダメなのか」と自分から来なくなる道を残しておきたいので。
ブラウザとして不自然なアクセスを検出するルールと、クラウド事業者のASNを対象にするルールは、BlockではなくManaged Challengeにしました。どちらも怪しいアクセスによく当たるのですが、会社のプロキシ経由で同じ形になってしまう人や、データセンター経由で普通に読んでいる人まで巻き込みます。チャレンジなら通れます。Cloudflareが検証済みと判定したボットも、両方から外しました。
設定後は、User-AgentやHTTPバージョンを変えながら、通したいアクセスは200、止めたいアクセスは403かチャレンジになるか確認しました。ただしcurlでGooglebotのUser-Agentを名乗っても、本物のGooglebotとして検証されるわけではありません。Cloudflareは送信元IPなども見ているので、これで巻き込みゼロを確認できたわけではないです。実際の読者や正規のクローラーを弾いていないかは、運用しながら見ていくことになります。
この4本で、洪水を除いた平常時のオリジン到達が週10万件くらい減る見込みです。4割強になります。
導入後の結果
Cloudflareへの切り替えから10分後には、もうTencentからのアクセスがブロックされ始めました。最初の24時間で350件、306 IP。ほとんどが/papers/*へのアクセスでした。クエリパラメータは付いていませんでしたが、IP帯やUser-Agentの傾向は洪水のときとよく似ています。今回の2時間だけではなく、普段から同じ系統のアクセスが来ていたようです。
Cloudflare側のカスタムルールの画面です。
ルール別に見ると、多かったのはクラウド事業者からのアクセスと、ブラウザとして不自然な通信を検出するルールでした。.envやWordPressを探すスキャンは、目立つわりに件数としてはそれほどでもありません。
Managed Challengeを出したアクセスのうち、突破した割合は0%と0.02%。Challengeを最後まで通過したアクセスは、ほとんどありませんでした。ただしChallengeを見て離脱した人がいても同じ数字になるので、正常な利用者をまったく巻き込んでいないとまでは言えません。ここは引き続きログを見ていきます。
コストにも変化が出ました。
| 日 | Amplify | 1日の合計 |
|---|---|---|
| 9/17(洪水前) | $0.32 | $0.46 |
| 9/18(洪水) | $2.45 | $2.85 |
| 9/19(Cloudflare導入後) | $0.16 | $0.29 |
Amplifyの料金は洪水前の約半分になりました。まだ1日分なので断定はできませんが、翌20日も同じくらいの水準です。普段からオリジンまで到達していた不要なアクセスを、Cloudflare側でかなり止められているようです。
DNSの切り替え中には、まだRoute53を参照してCloudflareを迂回してくるアクセスも少し残っていました。ここは先に入れていたアプリ側のIP判定が受け止めています。37件、7件、0件と減って、DNSの浸透とともに消えました。応急処置のつもりで入れた判定でしたが、切り替え期間中はちょうど予備の防御として働いてくれました。
レイテンシへの影響もほとんどありません。3回ずつ測った範囲で、Cloudflare経由が0.15〜0.22秒、直結が0.13〜0.23秒でした。1段挟んだことで体感できるほど遅くなる、ということはなさそうです。
思ったこと
Cloudflareは最初から入れておくべきだった
重い腰が上がってから入れるものではなかったな、というのが正直なところです。
データはもう出ていました。8月に調べた時点でcomputeの55〜60%がボットで、Bytespiderは3日で16.6万件来ていました。Metaの名乗らない巡回も、たぶん公開以来ずっと来ていたはずです。分かっていて放置していました。
一方で、実際に移せる状態になったのは、ACMとRoute53の罠の正体が分かった9月12日以降でもあります。その下調べがあったから、洪水の夜に一晩で、しかも無停止で移せました。
次に何か公開するときは、DNSを最初からCloudflareに置いて、WAFを入れてから出すつもりです。
インターネットは治安悪い
一連の調査を終えたあとの率直な感想です。インターネットに公開したら色々やられるらしい、という話は聞いていました。それを今回、身をもって実感した形です。
ログを漁っていると、Tencentの洪水以外にもいろいろ出てきます。多いのは/.envや/.git/config、/wp-json/*といったURLを片っ端から叩いていくアクセスです。.envはAPIキーやDBのパスワードを書いておくファイルで、.git/configはリポジトリの設定ファイル。どちらも置き場所が決まっているので、うっかり公開されたままになっているサイトを機械的に探して回っているわけです。うちはNext.jsなので全部404ですが、404を返すためだけに1件ずつcomputeが走っていました。
RSCの脆弱性(CVE-2025-55182)を狙うスキャンも来ていました。うちは修正済みのバージョンでしたが、来ていたという事実のほうが印象に残りました。
GA4のDirect流入の74%がデータセンター発だったのも同じ話です。こちらに見る仕組みがなくて、気づいていなかっただけでした。
個人開発のサイトにも、容赦なく来ます。相手はこちらの規模を見て手加減してくれるわけではないので、たぶん誰が何を公開しても同じです。これから何か出す人は、頭の片隅に置いておいた方がいいです。