GitHub Actions で Terraform のドリフトを検知し、Claude Code GitHub Actions に要約させて Issue 起票してみた
GitHub Actions の定期実行で terraform plan を回し、実リソースとコードの差分(ドリフト)を検知したときだけ、Claude Code GitHub Actions が差分の内容を要約して GitHub Issue に起票する仕組みを作ってみました。
以前は OpenTaco のドリフト検出機能を試しました。
検知から Issue 作成・Slack 通知まで一式やってくれます。Issue の中身は plan の出力そのままです。
今回は OpenTaco を使わず、terraform plan -detailed-exitcode と Claude Code GitHub Actions の組み合わせで「検知して通知」をやってみます。
ドリフトの有無の判定は Terraform の終了コード(exit code)に任せ、Claude が担うのは検知後の「何が変わったか」「手動変更か外部要因かの推測」「対応要否」の要約だけです。
ドリフト検知ワークフローを作る
terraform plan -detailed-exitcode の終了コードでドリフトの有無を判定します。0は差分なし、1はエラー、2は差分ありです。
用意したワークフローです。
name: terraform-drift-check
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
issues: write
id-token: write
jobs:
drift-check:
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform-drift-detection
steps:
- uses: actions/checkout@v7
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ap-northeast-1
- uses: hashicorp/setup-terraform@v4
with:
terraform_wrapper: false
- run: terraform init
- id: plan
run: |
set +e
terraform plan -detailed-exitcode -out=tfplan
code=$?
set -e
if [ "$code" = "2" ]; then
echo "drift=true" >> "$GITHUB_OUTPUT"
else
echo "drift=false" >> "$GITHUB_OUTPUT"
fi
- if: steps.plan.outputs.drift == 'true'
run: |
terraform show -json tfplan > plan.json
terraform show -no-color tfplan > plan.txt
hashicorp/setup-terraform の terraform_wrapper(既定で true)を明示的に false にしています。有効なままだとwrapper経由でterraformを呼ぶことになり、plan の終了コード 2 をwrapper自身が成功(0)として返してしまいます。
試しに terraform_wrapper: false を外した状態で、EC2 のタグを手動変更してドリフトを発生させ、workflow_dispatch で実行してみたところ、シェル側の set +e / code=$? のロジックは合っているのに drift=true にならず、後続のIssue作成ステップが全てskipされました。
これは既知の挙動で、hashicorp/setup-terraform#9や#328で報告されています。
ドリフト要約スキルを作る
.claude/skills/drift-summary/SKILL.md を作成しました。ドリフトの有無の判定は前段の終了コードで済んでいるので、このスキルは判定をやり直さず、検知後の内容整理だけをします。
---
name: drift-summary
description: terraform plan の出力(plan.json / plan.txt)からドリフト(実リソースとコードの差分)の内容を要約し、GitHub Issueで報告する。判定(ドリフトの有無)はexit codeで既に確定しており、このスキルは検知後の内容整理だけを担う。「plan.json のドリフト内容を要約」「/drift-summary」などで使う。
---
# ドリフト要約
`terraform plan -detailed-exitcode` がドリフトあり(exit code 2)と判定した後、その内容を GitHub Issue にまとめる。
ドリフトの有無の判定はワークフロー側のexit codeが既に済ませている。このスキルは判定をやり直さない。
## 対象ファイル
呼び出し元から渡されたディレクトリ(例 `terraform-drift-detection/`)にある2ファイルを読む。
- `plan.txt` — `terraform show -no-color` の生出力
- `plan.json` — `terraform show -json` の出力。`.resource_changes[]` を使う
## 変更内容の抽出
`plan.json` の `.resource_changes[]` から `actions` が `no-op` 以外の要素だけを見る。
各要素について次を確認する。
- `.address` — リソース
- `.change.actions` — `update` / `delete` / `["delete","create"]`(replace) など
- `.change.before` と `.change.after` の差分キー(値そのものは要約に転記してよいが、シークレットらしい値は伏せる)
## 原因の推測(断定しない)
変更されたキーの傾向から、次のパターンで書き分ける。断定はせず「〜の可能性がある」で止める。
- タグ・Name・説明文など運用上よく手で触る属性のみの変更 → 「コンソール等からの手動変更の可能性」
- セキュリティグループのルール、IAMポリシーなど → 「手動変更、または別の自動化ツールによる変更の可能性」
- AWS側が非同期に更新する属性(一部の `arn`, タイムスタンプ系, サービス側の既定値付与など)のみ → 「AWS側の自動更新・既定値付与による差分の可能性」
- 上記に当てはまらない、複数属性にまたがる変更 → 「原因不明。手動変更・他ツール・AWS側変更のいずれの可能性もある」と明記する
## 対応要否の目安
- タグ・Name等の軽微な属性のみで実害がない → 「放置可、または次回applyで揃える程度で良い」
- セキュリティ設定・容量・台数など実害があり得る属性 → 「要確認。コードに合わせて実態を戻すか、コード側を実態に合わせて修正するか判断が要る」
- 判断が難しい場合は「要確認」とだけ書き、機械的にimport/修正コマンドを指示しない
## Issue本文の構成
1. 冒頭に `plan.txt` の内容をそのままコードブロックで貼る
2. 続けて `## 要約` セクションに、リソースごとの変更点・原因推測・対応要否をまとめる
```
## Terraform ドリフト検知
<plan.txt の内容をそのままコードブロックで>
## 要約
### aws_instance.web が update
- 変更: tags.Name
- 原因推測: タグのみの変更のため、コンソール等からの手動変更の可能性
- 対応: 実害は小さい。次回のドリフト検知放置か、コードに合わせて実態を戻すかは運用判断
```
## 重複防止
Issue作成前に `gh issue list --state open --search "in:title Terraform ドリフト検知"` で未クローズの同種Issueがあるか確認する。
- 既存Issueがあれば、新規作成せず `gh issue comment <番号>` で今回の検知内容を追記する
- 無ければ `gh issue create` で新規作成する。タイトルは `Terraform ドリフト検知: <working-directory名>` で統一し、次回以降の検索対象にする
plan.txt をそのまま貼る設計なので、変更リソース数が多い環境では出力が長くなりそうです。今回の検証(1リソースの変更、22行)では問題になりませんでしたが、その点は未検証です。
ワークフローに Claude ステップを足す
drift検知後のステップとしてClaude Code GitHub Actionsを追加しました。
- id: claude
if: steps.plan.outputs.drift == 'true'
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 }}
/drift-summary terraform-drift-detection/plan.json のドリフト内容を要約し、GitHub Issueで報告してください。
claude_args: |
--allowedTools "Read,Bash(gh issue list:*),Bash(gh issue create:*),Bash(gh issue comment:*)"
動作確認
EC2インスタンスのNameタグを手動変更してドリフトを発生させ、workflow_dispatch で実行して確認しました。
drift=true となりIssueが新規作成されました。ドリフトを解消しないまま再度実行すると、新規Issueは作られず、作成済みのIssueにコメントとして追記されました。
作成されたIssueの本文です(インスタンスIDはマスクしています)。
## Terraform ドリフト検知
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_instance.web will be updated in-place
~ resource "aws_instance" "web" {
id = "<INSTANCE_ID>"
~ tags = {
~ "Name" = "drift-demo-manual-change" -> "drift-demo"
}
~ tags_all = {
~ "Name" = "drift-demo-manual-change" -> "drift-demo"
}
# (39 unchanged attributes hidden)
# (9 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
## 要約
### aws_instance.web が update
- 変更: `tags.Name`, `tags_all.Name` (`"drift-demo-manual-change"` -> `"drift-demo"`)
- 原因推測: タグ(Name)のみの変更のため、コンソール等からの手動変更の可能性
- 対応: 実害は小さい。次回applyでコード定義値(`drift-demo`)に揃えるか、実態(`drift-demo-manual-change`)に合わせてコードを修正するかは運用判断

plan の出力そのまま+要約という構成どおりの内容になり、原因推測も断定を避けた書き方になっています。対応要否も「importしろ」「修正しろ」のような機械的な指示ではなく、運用判断に委ねる書き方でした。
実行時間とコストも計測しました。ワークフロー全体は約1分48秒、Claude Code GitHub Actionsのステップ単体は duration_ms: 43454(約43秒)、num_turns: 13、total_cost_usd: 0.2734454(約$0.27)でした。モデルは claude-sonnet-5 です。
おわりに
判定は終了コード、要約は Claude という分担にしたことで、ワークフロー本体は短いまま「検知したら中身付きで通知」まで作れました。
plan.txt を全文貼る設計はリソース数の多い環境だと長くなるので、そこは実環境で試しながら調整したいところです。




