GitHub Actions で terraform plan の Conftest(OPA)違反理由を Claude Code に解説させてみた

GitHub Actions で terraform plan の Conftest(OPA)違反理由を Claude Code に解説させてみた

2026.07.05

GitHub Actions で Conftest(OPA)のポリシーチェックに失敗したとき、Claude Code GitHub Actions に違反の理由を解説させる仕組みを作ってみました。

GitHub の suggestion で修正案も出してもらいます。

GitHub Actions と Conftest によるポリシーチェックの仕組みは以前書きました。

https://dev.classmethod.jp/articles/github-actions-terraform-plan-conftest-opa/

plan の結果を Claude にレビューさせる記事も書いており、そちらは危険度の判断も Claude が担う設計にしています。

https://dev.classmethod.jp/articles/github-actions-terraform-plan-claude-code-review/

今回はこの2つと違い、pass/fail の判定は Conftest の Rego ルールのみで行います。Claude は判定に関与しません。

Claude に担当させるのは、fail の理由解説と、機械的に直せる部分の修正提案(GitHub の suggestion)だけです。

検証は、SG の 0.0.0.0/0 禁止・必須タグ・EBS 暗号化の3ルールで Conftest が動作している状態から始めています。ワークフローと Rego ルールは前回記事のものをそのまま使います。

Conftest 結果解説スキル作成

.claude/skills/conftest-explain/SKILL.md というスキルを作りました。引数に conftest の結果 JSON のパスを取り、/conftest-explain <path> の形で呼びます。

やることは次の4つです。

  1. failuresmsg を読み、リソースアドレス(aws_security_group.web など)から該当の .tf ファイルと行を Grep で特定する
  2. policy/*.rego の該当ルールを読み、そのルールが何を防ごうとしているかを把握する
  3. 違反ごとに該当行にインラインコメントを投稿する(見出しは「ルールの意図」「修正方法」で統一)
  4. 修正後の値が機械的に一意に決まる場合だけ suggestion を付ける

最後の基準が今回の設計で最も検討を要した点です。CIDR の許可範囲や Owner タグの値は人の判断が要ります。ここまで suggestion にすると、ワンクリックで誤った値がコミットされる可能性があります。そのため、スキルには「機械的に確定できる修正だけ suggestion を付け、判断が要る修正は選択肢を解説するだけにする」という基準を明記しました。

今回利用した SKILL.md 全文です。

.claude/skills/conftest-explain/SKILL.md
---
name: conftest-explain
description: Conftest(OPA)のポリシーチェックで検出された違反を解説し、PR の該当行にインラインコメントと修正提案(suggestion)を投稿する。引数に conftest の JSON 出力ファイルのパスを取る。「/conftest-explain <path>」で使う。
---

# Conftest 違反の解説と修正提案

引数で渡された conftest の結果 JSON(例: `terraform-conftest-opa/conftest-result.json`)を読み、違反ごとに「なぜ落ちたか」と「どう直すか」を PR のインラインコメントで解説する。

## 役割分担(このスキルの前提)

- 違反の判定は OPA/Rego で完了している。判定をやり直さない・上書きしない
- 結果 JSON の `failures` に載っている違反だけを扱う。それ以外の指摘や粗探しは行わない
- 修正を適用するかどうかは人間が決める。suggestion は提案であって自動修正ではない

## 手順

1. 引数の JSON を Read し、`failures``msg` を列挙する
2.`msg` に含まれるリソースアドレス(例: `aws_security_group.web`)から、Grep で該当の .tf ファイルと行番号を特定する
3. 違反に対応する Rego ルールを `policy/*.rego` から読み、ルールの意図(何を防ぎたいルールか)を把握する
4. `gh pr diff` で PR の差分を確認し、違反行が差分に含まれるかを見る
5. `gh pr view <PR番号> --comments` で既存コメントを確認する(重複回避。次節を参照)
6. 違反ごとに該当行にインラインコメントを投稿する
7. 最後に `gh pr comment` で全体サマリ(違反件数と各違反の1行要約)を1件投稿する

## 再実行時の重複回避

このワークフローは PR が更新されるたびに再実行される。前回の実行が投稿したコメントが残っているので、同じ違反に同じ指摘を重ねて投稿しない。

- 既存コメントに同じリソース・同じ違反内容への指摘がすでにあれば、その違反へのインラインコメント投稿はスキップする
- 全違反が既存コメントで指摘済みなら、サマリコメントも投稿しない(何も投稿せず終了してよい)
- 新しく増えた違反、または行が変わって既存コメントが outdated になった違反だけ投稿する

## コメントの型

各インラインコメントは次の構成で書く。見出しはこの表記をそのまま使う。日本語で書く。

1. **ルールの意図**: `msg` の直訳ではなく、Rego ルールがこの設定を禁止することで何を防いでいるかまで書く。2〜3文
2. **修正方法**: 1〜2文
3. **suggestion ブロック**: 付けられる場合のみ(次の基準を参照)

## suggestion を付ける基準

- 修正後の値が機械的に一意に決まる場合だけ suggestion を付ける(例: `encrypted = true`
- 修正値に人の判断が要る場合は付けない(例: 許可すべき CIDR、Owner タグに入れる値)。代わりに、考えられる選択肢とその決め方を解説に書く
- 理由: 判断が要る値を suggestion に入れると、ワンクリックで誤った値がコミットされる

## 投稿方法

- CI(GitHub Actions)では `mcp__github_inline_comment__create_inline_comment` を使う。`path``line`(複数行なら `startLine``line`)・`body` を指定し、`confirmed: true` で投稿する
- suggestion は body 内の ```suggestion フェンスで書く。suggestion は指定した行範囲全体を置き換えるため、置換後の内容は行として構文が完結していること(インデントも元ファイルに合わせる)
- suggestion は PR の差分に含まれる行にしか付けられない。差分外の行を直す必要がある場合は、コメント本文に通常のコードブロックで修正例を示す
- インラインコメントのツールが使えない環境(ローカル実行など)では、投稿予定の内容を「ファイルパス:行番号」付きで標準出力に出力する

「再実行時の重複回避」は最初から入れていたわけではありません。

検証の途中で、SG だけ直して push したところ、まだ違反が残っている EBS に対して前回と同じインラインコメントとサマリがもう1セット投稿される問題がありました。

ワークフローは PR 更新のたびに再実行され、Claude は前回自分が何を投稿したかを知らないためです。

gh pr view <PR番号> --comments で既存コメントを確認し、指摘済みの違反はスキップする、という手順をスキルに追記して解消しました。

ワークフローに Claude ステップを足す

以前作成した Conftest ワークフローに、Claude を動かすステップを1つ足します。(冒頭の記事を参照ください)

.github/workflows/terraform-opa-check.yml
       - uses: hashicorp/setup-terraform@v4
       - run: terraform init
       - run: terraform plan -out=tfplan
       - run: terraform show -json tfplan > plan.json
-      - shell: bash
+      - id: conftest
+        shell: bash
         run: |
           docker run --rm -v "$PWD":/project \
             openpolicyagent/conftest:v0.68.2 \
             test plan.json --policy policy --output json | tee conftest-result.json
       - if: always()
         uses: actions/upload-artifact@v7
         with:
           name: conftest-result
           path: terraform-conftest-opa/conftest-result.json
+      # conftest が違反で落ちたときだけ Claude を動かす。
+      # failure() が無いと暗黙の success() が AND され、直前の失敗後は評価されない。
+      # steps.conftest.outcome で「conftest の失敗」に限定する(terraform plan 自体の失敗では動かさない)。
+      - id: claude
+        if: failure() && steps.conftest.outcome == 'failure'
+        uses: anthropics/claude-code-action@v1
+        with:
+          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          prompt: |
+            REPO: ${{ github.repository }}
+            PR NUMBER: ${{ github.event.pull_request.number }}
+
+            /conftest-explain terraform-conftest-opa/conftest-result.json の違反を解説してください。
+            各違反は該当行にインラインコメント(日本語解説 + 修正提案)で投稿してください。
+          claude_args: |
+            --allowedTools "Read,Grep,Glob,mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"

id: conftest を足したのは、後続のステップから steps.conftest.outcome を参照するためです。

if の条件に failure() を書かないと、暗黙に success() が AND されます。GitHub のドキュメントにも「これらの関数(success() 等)のどれかを含めない限り、デフォルトで success() のステータスチェックが適用される」と明記されています。

A default status check of success() is applied unless you include one of these functions.

https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#status-check-functions

同じページの「failure with conditions」の例でも、failure() && steps.demo.conclusion == 'failure' という形で failure()steps.<id>.conclusion を組み合わせています。

そのため steps.conftest.outcome == 'failure' だけでは動きません。

steps.conftest.outcome を AND しているのは、terraform initterraform plan 自体の失敗時に Claude を動かさないためです。

もう1点、defaults.run.working-directoryuses: ステップには効きません。プロンプト内のファイルパスは、サブディレクトリ名を含めて terraform-conftest-opa/conftest-result.json のように書いています。

Claude のステップで allowedTools の明示が要る点は以前書いたコードレビュー記事と同じです。

https://dev.classmethod.jp/articles/terraform-review-skill-claude-code-github-actions/

動作確認

検証用リポジトリに、違反を2件仕込んだ PR を作りました。1つは SG の ingress を 0.0.0.0/0 に開放する変更です。

もう1つは EBS ボリュームを encrypted = true から false にし、サイズも拡張する変更です。Conftest はこの2件を検出します。

conftest-result.json(違反2件)
[
	{
		"filename": "plan.json",
		"namespace": "main",
		"successes": 1,
		"failures": [
			{
				"msg": "aws_ebs_volume.data: EBS volume not encrypted",
				"metadata": { "query": "data.main.deny" }
			},
			{
				"msg": "aws_security_group.web: ingress open to 0.0.0.0/0 (port 80)",
				"metadata": { "query": "data.main.deny" }
			}
		]
	}
]

Conftest のステップが失敗すると、if 条件により Claude のステップが動きます。実際に PR へ投稿されたインラインコメントは次のとおりです。

EBS の違反には suggestion が付きました。

ルールの意図: このルールは、aws_ebs_volume リソースが create/update される際に encrypted が有効化されていないと deny するものです。EBSボリュームが暗号化されていないと、ディスクスナップショットの共有や紛失・不正アクセス時に、保存されているデータ(この例では data ボリューム)が平文で読み取られてしまいます。保存データの機密性を確保するための基本的なガードレールです。

修正方法: encryptedtrue に戻してください。今回の差分で truefalse に変更されており、これが違反の直接の原因です。

  encrypted         = true

pr-summary-comment-and-checks-failing.png

SG の違反には suggestion を付けず、選択肢だけ示しました。

ルールの意図: policy/network.rego は、セキュリティグループの ingress ルールが cidr_blocks0.0.0.0/0(全世界の IPv4 アドレス)を含む変更を拒否します。今回のポートは 80(HTTP)ですが、送信元を無制限にすると、意図しない第三者からのアクセスやスキャン・攻撃の踏み台化を許してしまうため、公開範囲を明示的に絞ることを強制しています。

修正方法: 本当に全世界公開が必要なサービスでなければ、cidr_blocks を必要な送信元だけに限定してください。例えば元の値である X.X.X.X/X(オフィス IP)に戻す、あるいは ALB/CloudFront など前段の実際のアクセス元 CIDR や VPN の範囲に絞る、といった選択肢があります。

許可すべき CIDR はシステムの利用形態に依存するため、機械的な修正案(suggestion)は付けていません。

inline-comment-ebs-suggestion.png

inline-comment-sg-no-suggestion.png

インラインコメントのアンカー行は、1回目の実行で意図どおり main.tf の該当行(EBS は42行目、SG は22行目)に付きました。

ここから修正します。EBS の指摘は GitHub UI の「Apply suggestion」をクリックし、そのままコミットしました。

SG の指摘は選択肢の中から元の CIDR に戻す修正を push しました。

再実行後は Conftest のステップが success になり、ジョブ全体も green になりました。

このとき Claude のステップは if 条件により skipped です。

違反のない PR では、Claude を動作させない設定にしました。

checks-green-after-fix.png

CI ジョブ全体は約1分50秒でした。Conftest までのステップが約40秒、残りが Claude のステップです。

モデルはアクションのデフォルトである claude-sonnet-5 で、実行時間47秒で料金は$0.33でした。

違反のない PR では if 条件でステップごとスキップされるため、Claudeのコストはゼロです。

おわりに

判定は Conftest で行い、Claude には解説と機械的に決まる修正提案をやってもらいました。

判断が要る値を suggestion に含めると、レビュー前に自動適用される可能性があります。

実装する際は、suggestion を付ける基準を明確にしておくと良さそうです。


Claudeならクラスメソッドにお任せください

クラスメソッドは、Anthropic社とリセラー契約を締結しています。各種製品ガイドから、業種別の活用法、フェーズごとのお悩み解決などサービス支援ページにまとめております。まずはご覧いただき、お気軽にご相談ください。

サービス詳細を見る

この記事をシェアする

関連記事