先週、社内のフォーム送信基盤(/contact のLambda)で送信が数件こぼれていたのに、誰も気づいていなかった、ということがありました。
アラームは仕込んであったんです。むしろ仕込みすぎていた。Slackのアラートチャンネルは日に何十件も鳴っていて、その結果、全員が通知をミュートしていた。鳴っているのに誰も見ていない状態です。正直なところ、これは監視が「ある」のに「機能していない」典型で、運用を考えると一番まずいパターンでした。
このあたりを腰を据えて作り直したので、何をどう変えたかを残しておきます。使ったのはCloudWatchだけで、派手なSaaSは入れていません。
「Errorsにアラーム」だけでは、拾えない失敗があった
最初にやったのは、Lambdaが自動でCloudWatchに出してくれる標準メトリクス(Errors / Throttles / Duration)にアラームを付けることでした。これ自体は正しい第一歩です。
ただ、運用してみて分かったのは、標準メトリクスの Errors が拾うのは「関数がthrowして異常終了した」ケースだけ、ということでした。うちのフォーム基盤は、送信先のスプレッドシートへの書き込みが一時的に失敗してもリトライで吸収し、最終的に握りつぶして exit 0 で返すことがある。つまり関数としては成功扱いなのに、業務的にはリードを1件取りこぼしている。この手の「静かな失敗」は Errors には一切出てきません。
なので、アプリ側でその状況になったら明示的に ERROR 行をログに吐き、それをCloudWatch Logsのメトリクスフィルタで数値メトリクスに変換することにしました。
# メトリクスフィルタのパターン(ロググループに対して設定)
# アプリが吐く構造化ログの level=ERROR だけを1件としてカウントする
{ $.level = "ERROR" && $.component = "lead-writer" }
# → メトリクス namespace: XECIN/Forms
# metric name: LeadWriteFailures
# metric value: 1
もう一つ運用の勘所として、Throttles は「1件でも出たら要対応」の扱いにしました。同時実行の上限に当たっている合図なので、閾値を上げるか予約済み同時実行を見直すかの判断が要る。ここは平均やパーセンタイルで丸めず、発生ゼロを前提にしています。
最初にやらかした「鳴りっぱなし」と「四六時中データ不足」
ここからが失敗談です。
メトリクスフィルタで作った LeadWriteFailures に、素朴に「1分間で1件以上なら発報」というアラームを付けました。閾値1、評価1回。これで漏れなく拾えるだろう、と。
結果は鳴りっぱなしでした。ネットワークの瞬断で単発のリトライ失敗が出るたびに発報し、しかも数十秒後には自動回復してOKに戻る。オオカミ少年状態で、これがミュートの直接の原因でした。
さらに厄介だったのが、このアラームが日に何度も INSUFFICIENT_DATA(データ不足)に落ちること。最初は設定ミスを疑ったんですが、原因はメトリクスフィルタの挙動そのものでした。
メトリクスフィルタは、ログイベントが発生した分しかメトリクス値を発行しません。深夜帯にフォーム送信が1件も来なければ、その時間はログイベント自体がゼロなので、メトリクスには何も報告されない。値が「0」なのではなく「無い」んです。ドキュメントにはデフォルト値を設定できると書いてあるんですが、あれは「ログイベントは発生したがパターンに一致しなかったとき」に出るもので、イベントがそもそも無い時間帯は埋めてくれない。
対処は2つです。フィルタ側にデフォルト値0を設定しつつ、アラーム側で「欠落データを正常(notBreaching)として扱う」を明示する。これで無トラフィックの時間帯が障害扱いされなくなりました。
aws cloudwatch put-metric-alarm \
--alarm-name "forms-lead-write-failures" \
--namespace "XECIN/Forms" \
--metric-name "LeadWriteFailures" \
--statistic Sum \
--period 300 \
--evaluation-periods 3 \
--datapoints-to-alarm 2 \
--threshold 3 \
--comparison-operator GreaterThanOrEqualToThreshold \
--treat-missing-data notBreaching \
--alarm-actions "arn:aws:sns:ap-northeast-1:xxxx:forms-alerts"
このアラーム作成はまだCLIを手で叩いていて、環境ごとに引数を書き換えている状態です。改善の余地があるのはここで、本来はSAMなりCDKなりでアラーム定義をコード管理し、preview環境と本番で同じ定義が展開されるようにすべきだと思っています。今は「作った人しか全体像を把握していない」ので、属人化の芽になっている。
しきい値はSLOから逆算し、M out of N で誤検知を減らす
鳴りっぱなしを直すうえで一番効いたのは、閾値の決め方を変えたことでした。
それまでは「エラー1件で鳴らす」のような、根拠のないマジックナンバーで設定していました。これをやめて、まず自分たちのSLO(フォーム送信の成功率をどれくらいに保ちたいか)を先に決め、そこから許容できる失敗数を逆算して閾値にする、という順番に変えた。数字の出どころが「なんとなく」から「この失敗率を割ったら困る」に変わっただけで、閾値をいじる時の議論が一気に楽になりました。
評価ロジックも、瞬間的なスパイクと継続的な異常を区別できるように M out of N にしました。上のCLIの --evaluation-periods 3 と --datapoints-to-alarm 2 がそれで、「直近3回の評価のうち2回が閾値超え」で初めて発報する。単発のリトライ失敗では鳴らず、失敗が続いている時だけ鳴る。
それと、成功率のような「分母が小さいと極端に暴れる」指標には、Metric Mathで下駄を履かせました。送信数が少ない時間帯は率が意味を持たないので、そもそも評価しない、という書き方です。
# m1 = 送信失敗数, m2 = 送信総数
# 送信総数が10件に満たない時間帯は率を0扱いにして誤検知を防ぐ
IF(m2 >= 10, 100 * (m1 / m2), 0)
treat-missing-data はアラームの性格ごとに使い分けが必要で、ここは最初に一覧化して迷わないようにしました。うちで実際に使っている整理はこんな感じです。
| 欠落データの扱い | 向いている対象 | 考え方 |
|---|---|---|
| notBreaching(正常扱い) | 低頻度なメトリクスフィルタ由来のエラー数 | 無トラフィック=障害ではない。沈黙を異常にしない |
| breaching(異常扱い) | 外形監視・ヘルスチェック | 応答が返らない=落ちている、とみなす |
| missing(そのまま) | バッチなど発生が不規則なメトリクス | データが無い区間は判定材料にしない |
ここは好みが分かれるところですが、フォーム失敗のような「静かな失敗」は notBreaching、外形監視は breaching、という切り分けに落ち着いています。
見落としていたコスト:ログ取り込みは、消しても戻らない
監視を厚くしていく過程で、もう一つ想定外だったのがコストでした。
CloudWatchは、ログの保存料金よりも取り込み料金のほうがずっと高い。しかも取り込みは課金が確定した時点で不可逆で、あとからログを消しても取り込み分は戻ってきません。うちは初期にLambdaがINFOやDEBUGを何でもかんでも吐いていて、監視のためにログを増やしたつもりが、じわじわ効いてくるコストを積み上げていました。
対策としてやったのは3つです。
まず、アプリ側で定常運転時のINFO/DEBUG出力を絞り、ERROR と主要な処理境界のログだけ残すようにした。次に、ロググループの保持期間を無期限から必要な日数に切り替えた。そして地味に効いたのが、毎朝Logs Insightsでエラーを手クエリしていたのをやめたことです。Logs Insightsはスキャンしたデータ量で課金されるので、複数のLambdaが同じロググループに出していると、1回の検索で想像以上の量をなめてしまう。この「定常的な確認」こそメトリクスフィルタとダッシュボードで自動化すべきもので、手クエリを置き換えたら検索課金がほぼゼロになりました。
マネージドサービスを使うかどうかの判断でいつも言っていることですが、CloudWatchは「入れれば安心」ではなく「出す量と見る量を設計するもの」なんですよね。ここを設計せずにログを増やすと、監視を厚くしたつもりでコストだけ厚くなる。
振り返って、これからやりたいこと
作り直して一番変わったのは、アラートチャンネルのミュートが外れたことでした。鳴る回数が減って、鳴ったら必ず対応する、という状態に戻せた。可観測性という言葉を使うと大げさですが、入口はこの「鳴ったら信じられるアラート」を保つことだと、改めて思っています。
まだ足りていないところも正直あります。アラーム定義をCLIで手管理しているのはさっき書いた通りで、ここはコード化したい。それと、重要度の違うアラームが同じチャンネルに混ざっているので、Composite Alarmで「即対応」と「翌営業日でいい」を分けて通知経路を変えていきたい。
派手な監視基盤を組む前に、まず手元のCloudWatchで「鳴りっぱなし」と「データ不足」を潰すだけでも、運用の体感はかなり変わります。同じように通知がミュートされている現場があれば、閾値の根拠を問い直すところから始めてみてほしいです。