監視/Datadog
PR

Datadog の ALB 4xx アラートがノイズ化したので ALB アクセスログを Athena で調べて正体を突き止めた

saratogax
記事内に商品プロモーションを含む場合があります

ある日から、Datadog に設定していた ALB の 4xx エラー率アラートが継続的に発火するようになりました。

ステージング環境の話ではあるのですが、通知が鳴り続けるとノイズになり、本当に見たいアラートが埋もれてしまいます。

結論から言うと、正体は外部デバイスが存在しないエンドポイントを叩き続けていたことによる 404で、しかもそれは Datadog のモニタ画面からは特定できず、ALB アクセスログを Athena で調べて初めて分かりました

本記事では、なぜメトリクスモニタでは正体が分からないのか、そしてどうやって ALB アクセスログから正体にたどり着いたのか、という調査の流れを残しておきます。

前提:見ていたモニタの構成

今回のモニタは、ALB の 4xx 発生率をしきい値監視するものでした。

Datadog のメトリクス式としては、おおよそ次のような比率で組んでいました。

  • 分子: aws.applicationelb.httpcode_elb_4xx(ELB が返した 4xx の件数)
  • 分母: aws.applicationelb.request_count(総リクエスト数)
  • 式: 分子 / 分母 * 100(4xx の比率 %)

アラート通知のタイトルには kubernetes.io/ingress-name が含まれていたので、特定の Ingress で 4xx 率が上がっている、というところまでは分かっていました。

最初のつまずき:メトリクスに URL の次元が無い

やりたかったのはシンプルで、「ノイズになっている特定 URL だけをモニタから除外する」ことでした。

ところが、ここで最初の壁にぶつかります。

ALB のメトリクス(aws.applicationelb.*)は、URL やパスという次元(タグ)を持っていません。

これらは CloudWatch 由来のメトリクスで、次元は基本的に LoadBalancer / TargetGroup / AvailabilityZone 程度です。

つまり「この URL だけ」をタグ条件でモニタから除外する、ということがそもそも仕組み上できないのです。

では、サービス単位(Kubernetes の service-name)でならどうか、と考えてタグを探しました。

ここでもう一つ重要な事実に気付きます。

elb_4xx と target_4xx は別物である

ALB の 4xx メトリクスには、実は 2 種類あります。

メトリクス意味service-name タグ
httpcode_elb_4xxALB 自身が生成した 4xx(ターゲットに届く前)無い
httpcode_target_4xxターゲット(バックエンド)が返した 4xxある
※表は横スクロールできます

私が監視していたモニタの分子は httpcode_elb_4xx、つまり ALB 自身が生成した 4xx でした。

ところが「ノイズ源だろう」と当たりを付けていたのは、バックエンドのアプリが返す 404 でした。

アプリが返す 404 は本来 httpcode_target_4xx 側に出るものであり、分子の httpcode_elb_4xx とは別のメトリクスです。

この httpcode_elb_4xxhttpcode_target_4xx の違いは、アラートを「インフラ側の問題か、アプリ側の問題か」に切り分けるうえでの土台になります。別記事で詳しく整理しているので、あわせて読むと今回の調査の意味がより腑に落ちるはずです。

あわせて読みたい
DatadogでALBを監視するなら知っておきたい!httpcode_elb_4xx と httpcode_target_4xx の決定的な違い
DatadogでALBを監視するなら知っておきたい!httpcode_elb_4xx と httpcode_target_4xx の決定的な違い

実際に Metrics Explorer で httpcode_elb_4xx のタグ候補を確認すると、service-name のような細かい次元は存在せず、namespaceavailability-zone といった粗い次元しかありませんでした。

この時点で、現行のメトリクスモニタの設定をいじるだけでは、URL もサービスも除外できないことが確定しました。

メトリクスからログへ降りる

ここが今回の調査の一番のポイントです。

CloudWatch のメトリクスは「4xx を何回返したか」という件数のカウンタでしかなく、どの URL に・どのクライアントから・なぜ 4xx になったか、という情報を一切持ちません。

そうした個別リクエストの中身が記録されているのは、ALB のアクセスログ(S3)だけです。

ALB アクセスログを S3 に出力していれば、それを Amazon Athena でクエリして中身を調べられます。

アクセスログの各行には、調査に必要な情報がひととおり揃っています。

  • elb_status_code / target_status_code(ELB とターゲット、それぞれのステータスコード)
  • request_verb / request_url(HTTP メソッドと URL)
  • client_ip / user_agent(送信元とクライアントの種別)
  • time(リクエスト時刻、UTC)

「ALB が自分で返した 4xx」を切り分ける

今回のモニタが拾っていたのは httpcode_elb_4xx、つまり ALB 自身が生成した 4xx でした。

この「ALB がターゲットに渡す前に自分で返した 4xx」は、アクセスログ上では target_status_code-(ハイフン)になります。

ターゲットに届いていないのでターゲットのステータスコードが存在しない、というわけです。

この特徴を使うと、Athena で「ALB 由来の 4xx」だけを次のように絞り込めます。

WHERE elb_status_code LIKE '4%' AND target_status_code = '-'

Athena で正体を集計する

まずは「どの URL に、どんな User-Agent・メソッドで、どのステータスが来ているか」を件数で集計しました。

テーブル名やカラム名はアクセスログ用テーブルの定義に合わせてください(以下はサンプルの alb_access_logs です)。

ALB アクセスログのテーブルは日付でパーティション分割していることが多いので、必ずパーティション(date など)で絞るのがポイントです。フルスキャンになるとスキャン量が無駄に増えます。

SELECT
  elb_status_code,
  request_verb,
  request_url,
  user_agent,
  count(*) AS cnt
FROM alb_access_logs
WHERE date BETWEEN '2026/01/09' AND '2026/01/10'
  AND elb_status_code LIKE '4%'
  AND target_status_code = '-'   -- ALB 自身が生成した 4xx に絞る
GROUP BY 1, 2, 3, 4
ORDER BY cnt DESC
LIMIT 100;

結果は一目瞭然でした。

ある 1 つの URL への GET リクエストが、2 位以下を大きく引き離して圧倒的多数を占めていたのです。

しかもその User-Agent は、Web ブラウザでも一般的な HTTP クライアントでもなく、IoT マイコン系の組み込みデバイスを示すものでした。

さらに興味深いことに、リクエストの Host自分たちが管理していないドメインになっていました。

なぜ 404 が返っていたのか

ALB のリスナールールを確認すると、すべてのルールが「特定の Host ヘッダー(自分たちのドメイン)」を前提に組まれていました。

そして、どのルールにもマッチしなかった場合のデフォルトアクションが固定レスポンスの 404になっていました。

つまり、外部のデバイスが自分たちの管理外の Host ヘッダーを付けて ALB の IP を叩いてきた結果、どのリスナールールにもマッチせず、デフォルトルールの固定レスポンス 404 に落ちていたわけです。

これは設計どおりの正常な挙動で、異常でも設定漏れでもありません。

ここまで来て、最初に感じていた事象がすべてつながりました。

  • target_status_code- だったのは、ターゲットに転送されずデフォルトルールで返していたから。
  • アプリのログに何も出ていなかったのは、リクエストがバックエンドに一切届いていなかったから。
  • httpcode_elb_4xx に計上されていたのは、ALB 自身が返した 4xx だったから。

発生源と開始時刻も特定する

ノイズがいつから始まったのか、送信元がどれくらいの規模なのかも、同じアクセスログから調べられます。

日別の件数と、送信元 IP のユニーク数を一緒に集計しました。

SELECT
  date,
  count(DISTINCT client_ip) AS uniq_ips,
  count(*) AS cnt
FROM alb_access_logs
WHERE date BETWEEN '2026/01/05' AND '2026/01/10'
  AND request_url LIKE '%example.invalid%'
GROUP BY date
ORDER BY date;

結果、ある日を境に件数が急増しており、体感していた「昨日くらいから」という感覚と一致しました。

そして送信元 IP はごく少数(同一サブネット内の数個)に固定されていました。

送信元 IP が少数に固定されているなら、GROUP BY client_ipmin(time) / max(time) も取れば、発生源をさらに追い込めます。

あとはその IP の素性(自組織のものか、外部か)を確認すれば、対処方針が決まります。

要注意:ALB アクセスログの時刻は UTC

ここで一つ、ハマりやすい落とし穴を共有しておきます。

ALB アクセスログの time は UTC で記録されます。

実際に min(time) / max(time) を取ると、末尾が Z(Zulu time = UTC)の形式で返ってきます。

たとえば 2026-01-07T23:16:28Z という値は、日本時間(JST は UTC+9)に直すと 2026-01-08 の朝 8 時台です。

私は最初、日別集計で「1 日目(UTC)から少しだけ発生している」と読んでいたのですが、その少量のアクセスはすべて UTC の深夜帯に集中していました。

UTC の深夜帯は JST では翌日の午前なので、日本時間で見ると「実質的に 2 日目の朝から始まった」ということになります。体感していた発生タイミングとも、こう解釈して初めて一致しました。

さらに見落としがちなのが、パーティションキー(date など)も UTC 基準である点です。

JST の 0 時〜9 時のデータは UTC では前日の date パーティションに入るため、JST の 1 日をまるごと見たいときは、UTC で前日のパーティションも含める必要があります。

もし JST 基準の日付で集計し直したいなら、time を 9 時間ずらして日付を取り出せます。

SELECT
  date_format(
    parse_datetime(time, 'yyyy-MM-dd''T''HH:mm:ss.SSSSSS''Z') + interval '9' hour,
    '%Y/%m/%d'
  ) AS date_jst,
  count(*) AS cnt
FROM alb_access_logs
WHERE date BETWEEN '2026/01/04' AND '2026/01/10'  -- UTC パーティション。JST の端を拾うため広めに取る
  AND request_url LIKE '%example.invalid%'
GROUP BY 1
ORDER BY 1;

そして意外と見落としやすいのが、Datadog などの監視ツールのグラフは、表示タイムゾーンの設定(日本だと JST にしている人が多い)で見えているという点です。

Datadog のグラフ(JST 表示)と Athena の結果(UTC)を突き合わせるときは、9 時間のズレを常に意識すると、時刻の食い違いで混乱せずに済みます。

対処の選択肢

正体が「外部デバイスが管理外の Host で叩いてきている 404」だと分かった上での対処は、大きく 3 つに整理できます。

方針内容向いているケース
発生源を止める送信元デバイスの向き先を修正してもらう送信元が自組織の機器と特定できた場合(完全解決)
ALB 側で受け流す未知 Host を固定レスポンスや WAF で弾く外部由来で発生源を止められない場合
モニタを作り替えるアクセスログを Log-based メトリクス化し Host/URL で除外URL 単位で監視・除外したい場合(唯一の手段)
※表は横スクロールできます

ここで押さえておきたいのは、CloudWatch/ALB のメトリクスモニタのままでは、URL や Host 単位でノイズを除外することはできないという点です。

URL 粒度で切りたいのであれば、アクセスログをログとして取り込み(Datadog Logs 連携など)、そこから Log-based メトリクスを作り直すしかありません。

この調査から得た教訓

今回の一番の学びは、「メトリクスの次元の限界を知り、必要ならログに降りる」という判断です。

  • ALB のメトリクスは件数の集計であり、URL・Host・クライアント IP といった個別の次元を持たない。
  • httpcode_elb_4xx(ALB 由来)と httpcode_target_4xx(ターゲット由来)は別物で、混同すると調査が空回りする。
  • 「ALB が自分で返した 4xx」はアクセスログ上 target_status_code = '-' で切り分けられる。
  • 個別リクエストの正体は ALB アクセスログ(S3)にしかなく、Athena で集計すれば URL・UA・送信元・開始時刻まで一気に分かる。

アラートがノイズ化したとき、つい「モニタの設定で除外できないか」とメトリクス側だけで解決しようとしがちです。

ですが、メトリクスに欲しい次元が無いと分かった時点で、素早くアクセスログに降りると、遠回りせずに正体へたどり着けます。

ALB アクセスログと Athena の組み合わせは、こうした「メトリクスでは見えない事象」の調査で強力な引き出しになります。

あわせて読みたい

今回のようにモニタが拾う 4xx の正体を切り分けるには、httpcode_elb_4xxhttpcode_target_4xx の違いを先に押さえておくのが近道です。

あわせて読みたい
DatadogでALBを監視するなら知っておきたい!httpcode_elb_4xx と httpcode_target_4xx の決定的な違い
DatadogでALBを監視するなら知っておきたい!httpcode_elb_4xx と httpcode_target_4xx の決定的な違い

そもそも Datadog が ALB のメトリクスを取得できているかは AWS インテグレーションに依存します。権限不足の警告が出ているときは、こちらもあわせて確認してみてください。

あわせて読みたい
【Datadog】AWS Integrationで「警告: GetAccountInformation」が出た時の対処法(Terraform対応)
【Datadog】AWS Integrationで「警告: GetAccountInformation」が出た時の対処法(Terraform対応)
ABOUT ME
saratoga
saratoga
フリーランスエンジニア
仕事にも趣味にも IT を駆使するフリーランスエンジニア。技術的な TIPS や日々の生活の中で深堀りしてみたくなったことを備忘録として残していきます。
記事URLをコピーしました