Revert に理由を書かないと、履歴は誤読される
「戻された変更」は「間違っていた変更」に見える
feat: ... の翌朝に Revert "feat: ..." が並んでいる履歴を見たとき、人は何を推測するか。ほぼ確実に「その変更には技術的な問題があった」と読む。
実際には、変更の中身には何の問題もなく、投入したブランチとタイミングだけが問題だったケースがある。そしてその理由が Revert コミットに書かれていないと、履歴を読む人は永久に誤解する。
実際に誤読が発生したので、その過程を書く。
変更の正しさと、投入タイミングの正しさは別の軸
戻された変更はこういうものだった。
あるインスタンスが、仕様上さまざまなドメインへ意図的にアクセスする。そのため GuardDuty が Trojan 系の DNS finding を High(severity 8.0)で毎日検出し、通知が Slack に流れ続けていた。所有者に確認して意図的な動作であることを確認したうえで、suppression filter を追加した。
設計はむしろ慎重な部類だった。
- アクションは削除ではなく
ARCHIVE。検知自体は止めず、コンソール上には archived として残る - 対象を finding type 2種に限定。そのインスタンスの他の finding は素通りする
- 対象インスタンスも1台に限定
- なぜこの抑制が妥当かを、コードのすぐ上に長いコメントとして残した
resource "aws_guardduty_filter" "suppress_dns_trojan" {
detector_id = module.guardduty.detector_id
action = "ARCHIVE"
rank = 1
finding_criteria {
criterion {
field = "type"
equals = [
"Trojan:EC2/PhishingDomainRequest!DNS",
"Trojan:EC2/DriveBySourceTraffic!DNS",
]
}
criterion {
field = "resource.instanceDetails.instanceId"
equals = ["i-xxxxxxxx"]
}
}
}
この変更が、投入の約17時間後、翌朝に Revert された。
理由は、リリース作業中のブランチに入れてはいけない変更だったから。それだけだ。中身の妥当性は一切関係ない。
そして Revert コミットのメッセージは、git revert が生成する既定文のままだった。
Revert "feat(guardduty): ... の finding を抑制"
This reverts commit 1a2b3c4...
なぜ戻したかは、どこにも残っていない。
誤読は実際に起きた
この履歴を、経緯を知らない第三者に読ませたらどうなるか。試す機会があった。AI に git ログを渡して、Revert の理由を推測させた。
返ってきたのは、次の6つの仮説だった。
- 抑制の範囲が不適切だった(finding type やインスタンス指定が広すぎた/狭すぎた)
ARCHIVEの挙動が実際の運用フローと噛み合わなかった- もっと根本的な対処(インスタンスのアカウント分離や停止)に切り替えた
- IaC で suppression を持つこと自体に問題があった
- レビューで別の人から異論が出た
- 意図的な動作でも検知は残すべき、という方針判断をした
6つとも外れている。 どれも技術的に筋の通った仮説で、実際こういう理由で抑制が撤回されることはある。だからこそ質が悪い。もっともらしい誤答は、明らかな誤答より訂正されにくい。
注目すべきは、6つすべてが「変更の中身に問題があった」という前提に立っていることだ。履歴の形(feat → Revert)がその前提を強制している。「中身は正しいがタイミングが悪かった」という可能性は、選択肢としてすら挙がらなかった。
これは AI 固有の問題ではない。数ヶ月後に同じ履歴を見た自分も、新しく入った人も、同じ読み方をする。
失われたのはコードではない
この Revert で失われたのは、32行の HCL ではない。それは git show すれば復元できる。
失われたのは判断の経緯だ。
- 所有者に確認を取ったという事実
- 削除ではなく
ARCHIVEを選んだ理由 - 対象を2つの finding type に絞った根拠
これらはコードのコメントとして書かれていたので、Revert と同時に履歴の奥へ引っ込んだ。そして「戻された変更」というラベルが付いたことで、復元する価値があるものとは見なされにくくなった。
そして実際に起きたのは、もっと厄介なことだった。
通知が流れ続ける状況は現実に困るので、抑制はコンソールと CLI から手動で適用された。 運用上の必要は満たされた。問題は解決した。
その結果がこれだ。
AWS 上 : suppression filter が存在する
Terraform コード : 存在しない(Revert されたまま)
Terraform state : 存在しない(手動作成なので管理外)
典型的な drift だが、質の悪い種類のものだ。 Terraform は state に無いリソースを削除しない。つまり terraform plan は何も検出しないし、apply で消えることもない。警告すら出ない。
この状態でリポジトリを読んだ人は、次のように理解する。抑制を入れようとした形跡がある、しかし戻されている、したがって現在は抑制されていない。三つ目が間違っている。
そして「なぜこの finding が上がってこないのか」を調べる人は、コードのどこにも答えを見つけられない。所有者に確認を取ったことも、ARCHIVE を選んだ理由も、対象を絞った根拠も、コードから消えたままだ。判断の経緯だけが失われ、判断の結果だけが AWS の中に残っている。
Revert のコミットメッセージに理由と後続作業が書かれていれば、この分岐は起きなかった。リリース後に IaC へ戻す、という道筋が履歴から自明だったからだ。
検討したが採らなかった選択肢
Revert せず、そのまま残す。 リリース作業中のブランチに予定外の変更が乗っている状態を許容する案。リリースの検証範囲が変わってしまうので採れない。監視設定の変更はリリース対象の挙動に直接影響しないように見えるが、「影響しないはずだ」という判断こそリリース中に持ち込みたくないものだ。
cherry-pick で別ブランチに退避してから Revert する。 変更を物理的に残す方法。これは有効で、やっておけば再投入は git cherry-pick 一回で済んだ。ただし退避先ブランチの存在自体を覚えておく必要があり、結局は記憶に依存する。 忘れられたブランチは忘れられたコミットと大差ない。
Revert のコミットメッセージに理由を書く。 これが最も安い。git revert は -e なしでもエディタが開くので、既定文の下に3行足すだけでよかった。
Revert "feat(guardduty): ... の finding を抑制"
This reverts commit 1a2b3c4...
理由: リリース作業中の develop に入れるべき変更ではなかったため。
抑制の内容自体に問題はない。リリース完了後に再投入すること。
これがあれば、履歴を読んだ誰も誤解しないし、再投入も忘れられなかった。コストは3行で、失われたものはそれよりはるかに大きい。
まとめ
feat→Revertという履歴の形は、読む人に「中身が間違っていた」と強制的に推測させる- Revert の理由が「中身」ではなく「タイミングやブランチ」にある場合、それを書かないと確実に誤読される
- Revert コミットには、戻した理由と、再投入の要否を書く。 既定文の下に3行で足りる
- 理由が残らないと、運用上の必要は別経路(手動適用)で満たされ、IaC とのずれだけが残る