infralog

Revert に理由を書かないと、履歴は誤読される

git運用監視

目次
  1. 「戻された変更」は「間違っていた変更」に見える
  2. 変更の正しさと、投入タイミングの正しさは別の軸
  3. 誤読は実際に起きた
  4. 失われたのはコードではない
  5. 検討したが採らなかった選択肢
  6. まとめ
  7. 関連記事

「戻された変更」は「間違っていた変更」に見える

feat: ... の翌朝に Revert "feat: ..." が並んでいる履歴を見たとき、人は何を推測するか。ほぼ確実に「その変更には技術的な問題があった」と読む。

実際には、変更の中身には何の問題もなく、投入したブランチとタイミングだけが問題だったケースがある。そしてその理由が Revert コミットに書かれていないと、履歴を読む人は永久に誤解する。

実際に誤読が発生したので、その過程を書く。

変更の正しさと、投入タイミングの正しさは別の軸

戻された変更はこういうものだった。

あるインスタンスが、仕様上さまざまなドメインへ意図的にアクセスする。そのため GuardDuty が Trojan 系の DNS finding を High(severity 8.0)で毎日検出し、通知が Slack に流れ続けていた。所有者に確認して意図的な動作であることを確認したうえで、suppression filter を追加した。

設計はむしろ慎重な部類だった。

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つの仮説だった。

  1. 抑制の範囲が不適切だった(finding type やインスタンス指定が広すぎた/狭すぎた)
  2. ARCHIVE の挙動が実際の運用フローと噛み合わなかった
  3. もっと根本的な対処(インスタンスのアカウント分離や停止)に切り替えた
  4. IaC で suppression を持つこと自体に問題があった
  5. レビューで別の人から異論が出た
  6. 意図的な動作でも検知は残すべき、という方針判断をした

6つとも外れている。 どれも技術的に筋の通った仮説で、実際こういう理由で抑制が撤回されることはある。だからこそ質が悪い。もっともらしい誤答は、明らかな誤答より訂正されにくい。

注目すべきは、6つすべてが「変更の中身に問題があった」という前提に立っていることだ。履歴の形(feat → Revert)がその前提を強制している。「中身は正しいがタイミングが悪かった」という可能性は、選択肢としてすら挙がらなかった。

これは AI 固有の問題ではない。数ヶ月後に同じ履歴を見た自分も、新しく入った人も、同じ読み方をする。

失われたのはコードではない

この Revert で失われたのは、32行の HCL ではない。それは git show すれば復元できる。

失われたのは判断の経緯だ。

これらはコードのコメントとして書かれていたので、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行で、失われたものはそれよりはるかに大きい。

まとめ

関連記事