dependabot.yml の Terraform の entry は、npm や uv の entry と見た目がまったく同じだ。
- package-ecosystem: terraform
directory: /terraform/env
schedule:
interval: weekly
ところが挙動は全然違う。GCP のモノレポ(デプロイ対象 4 サービス + 共有インフラ、Terraform ディレクトリは 8 個)に Dependabot を入れたとき、Terraform まわりだけで他の全 ecosystem を合わせたより時間を食った。実際に踏んだものと、その対処を書く。
1. directory は glob が効かない。モノレポだと同じ block が N 個並ぶ
単数形の directory キーはリテラルなパスを 1 つしか取らない。うちのリポジトリは Terraform の所有権を分けていて、Cloud Run 本体と observability は各サービス配下の terraform/、SA / IAM / 共有 VPC / Cloud SQL はリポジトリ直下の terraform/ が持つ。結果、対象は 8 ディレクトリになる。
/terraform/env
/terraform/common
/app-a/terraform
/services/svc-a/terraform
/services/svc-b/terraform
/services/svc-c/terraform
/app-b/terraform
/app-b/terraform/sub
ほぼ同一の 15 行 entry が 8 個。schedule も PR 上限もラベルも group もそれぞれコピーされる。更新頻度を変えたければ 8 箇所、サービスを増やしたら 9 個目を書き忘れないようにする、という話になる。
対処は複数形の directories キー。こっちは glob が効く。公式ドキュメントにも「単数形の directory ではサポートしない」と明記されている。
- package-ecosystem: terraform
directories:
- "/terraform/*"
- "/*/terraform"
- "/services/*/terraform"
schedule:
interval: weekly
効果が一番大きいのはこれなのだが、世の中のサンプルも自動生成される starter config もだいたい単数形なので気づきにくい。
2. google と google-beta が別 PR に割れる。しかも PR 上限のせいで必ず片方だけ来る
GCP の provider は 2 パッケージに分かれていて、両方を常に同じ version に揃える必要がある。google が 8 系で google-beta が 7 系、という状態になると、両方に存在するリソースで terraform init が落ち始める。
Dependabot はこの 2 つを独立した依存として扱う。そこに Terraform では定番の open-pull-requests-limit: 1 が乗ると、ペアの片方だけを上げる PR が 1 本届く。もう片方が取り残されていることは PR のどこにも書かれていない。うちの 1 週間分がこれ。
#1206 bump hashicorp/google 7.44.0 -> 8.8.1 in /app-a/terraform
#1207 bump hashicorp/google-beta 7.45.0 -> 8.8.1 in /services/svc-c/terraform
#1203 bump hashicorp/google 7.44.0 -> 8.8.1 in /services/svc-b/terraform
3 ディレクトリに 3 本、それぞれ担当している側が違う。どれか 1 本だけマージすれば、そのディレクトリは片肺状態になる。タイトルは普通のバージョン上げにしか見えない。
対処は catch-all な group をディレクトリごとに 1 つ置いて、そのディレクトリの provider をまとめて動かすこと。
groups:
terraform-providers:
patterns:
- "*"
意図的に書いていないのが update-types だ。他の ecosystem では catch-all group を minor / patch に絞って、major は個別 PR として単独でレビューできるようにしている。Terraform provider はこれが逆になる。major こそ相方と一緒に動かないと困るので、group に major も飲ませる必要がある。
3. PR の中身は「lock file を git 管理しているか」で変わる
.terraform.lock.hcl を commit しているディレクトリと gitignore しているディレクトリが混在していると、同じ ecosystem から 3 種類の PR が飛んでくる。
| ディレクトリの状態 | PR が実際に変えるもの |
|---|---|
lock を commit、制約が狭い(~> 7.0) |
required_providers の制約 と lock file |
lock が gitignore(/terraform/env) |
制約だけ。1 行 diff |
lock を commit、制約が既に緩い(>= 5.0) |
lock file だけ。制約は触らない |
危ないのは 3 つ目。うちの svc-a の Terraform は >= 5.0 なので、制約は最初から 8 系を許容している。Dependabot は versions.tf に一切触らず、lock だけを黙って新しい major に書き換える。制約の変更を探してレビューしている人の目には何も引っかからず、diff は誰も読まない zh: ハッシュ 50 行になる。
そしてこれは後述の「ディレクトリ横断グルーピング」も静かに壊す。公式ドキュメントによると、同じ依存に対してディレクトリ間で互換性のない version 制約があると、Dependabot は PR を分けるほうにフォールバックする。うちは ~> 8.8 が 5 つ、~> 8.6 が 1 つ、>= 5.0 が 1 つなので、見事にその条件を踏んでいる。
4. CI が何も検証していないのに、緑に見える
実害が一番大きいのはこれ。Terraform provider の major こそ terraform plan を走らせたい変更なのに、plan こそが Dependabot PR では走らせられない job だ。
Dependabot 起因の workflow run には、クラウドへの OIDC 認証に必要な id-token: write が付かない。毎週 job が落ちるのを放置するわけにもいかないので、普通は job を分割して actor でゲートする。
jobs:
build:
# 依存インストール・lint・test・docker build。クラウド認証は不要
plan:
needs: build
if: github.actor != 'dependabot[bot]'
permissions:
contents: read
id-token: write
設計としては妥当だ。ただし結果として、Terraform の Dependabot PR で gh pr checks を叩くとこうなる。
add-to-project skipping
plan skipping
request-reviewer skipping
全部 skipping。/terraform/env にいたってはもっとひどくて、このパスはどの workflow の paths: にも一致しないので check が 0 件になる。provider の major 上げが、赤いものが何もない状態で置かれている。何も走っていないから赤くなりようがない。
正直なところ、Terraform に対する Dependabot は検証の仕組みではなく通知の仕組みだと割り切ったほうがいい。緑の Terraform PR は「新しい version が出た」であって「この version で動く」ではない。検証は人間が実 state に対して plan を回したときにしか起きないし、これは設定をどういじっても変わらない。
5. private module が 1 つ届かないだけで、update job 全体が落ちる
共有 Terraform は別 org の private repo を SSH source で参照している。
module "something" {
source = "git::ssh://git@github.com/other-org/terraform-modules//path?ref=v1.2.3"
}
Dependabot はここに認証できない。問題は失敗の仕方で、「その module だけスキップして残りを更新」ではなく、そのディレクトリの update job が丸ごと死ぬ(git_dependencies_not_reachable)。毎週落ちるので、同じディレクトリの provider も永遠にチェックされない。別 org かつ SSH source なので、Dependabot に渡せる credential も存在しない。
回避策は除外することだが、この構文が素直に推測できない。
ignore:
- dependency-name: "*::github::other-org/*"
*::github::owner/* というのが Terraform 固有の dependency-name の形。単一 repo ではなく org 全体に当たる pattern にしておくと、同じ org の module を後から追加で参照しても問題が再発しない。
おまけ: Dependabot PR の close は見た目より重い
Dependabot PR を close すると、こう返ってくる。
OK, I won't notify you again about this release, but will get in touch when a new version is available.
これはその version に対する恒久的な決定だ。さらに Dependabot はブランチも削除する。ブランチが消えたあとに @dependabot reopen と書いても何も起きない。コメントはそのまま無視される。まとめて close したあとに「やっぱり一部は要るな」となっても、ブランチが既に消えているものは GitHub API 経由でも復旧できず、手で作り直すしかない。
UI が軽く見えるわりに戻せないので、特に一括 close のときは慎重にやったほうがいい。
最終的にどうしたか
- Terraform のディレクトリごとに
patterns: ["*"]の group を 1 つ、update-typesは付けない。googleとgoogle-betaが必ず一緒に動く - 到達できない private module source を org 単位で
ignore -
planをbuildから分離してgithub.actor != 'dependabot[bot]'でゲート - 「Terraform の PR には検証シグナルが無い」ことを明文化して、npm / uv の PR とはレビューの仕方を変える
落ち着いてみると、Dependabot の 7 本の PR は、8 ディレクトリ 16 ファイルを触る手作りの PR 1 本に置き換わった。モノレポで GCP provider の major を上げるという作業の、正直な単位はこれくらいだと思う。
残っているのは統合のほうだ。GitHub は 2026 年 2 月にディレクトリ横断のグルーピングを出している。directories にパスを並べて group に group-by: dependency-name を付けると、ディレクトリごとに 1 本ではなく、依存ごとに全ディレクトリ分で 1 本の PR になる。
- package-ecosystem: terraform
directories:
- "/terraform/*"
- "/*/terraform"
- "/services/*/terraform"
groups:
gcp-providers:
group-by: dependency-name
patterns:
- "hashicorp/google*"
この問題が欲しがっている形はまさにこれ。ただし 3 番目の制約互換性ルールが効くので、制約がばらついているディレクトリはまた別 PR に分かれてしまう。先にやるべきは地味な片付け、つまり全ディレクトリの制約を揃えることだ。
まとめ
| 問題 | 対処 |
|---|---|
| ディレクトリごとに entry が必要 | 複数形の directories + glob |
google / google-beta が別 PR に割れる |
ディレクトリ単位の group、patterns: ["*"]、update-types なし |
| lock だけ書き換わる PR が major を隠す | 制約を揃える。.tf の diff だけでなく lock も読む |
Dependabot PR では plan が動かない |
build / plan を分離して actor でゲート。緑を「検証済み」と読まない |
| private module 1 つで job が全滅 | ignore: dependency-name: "*::github::org/*" |
| close が恒久的 | 一括 close は慎重に。ブランチが消えると reopen は効かない |
どれもバグではない。Dependabot が Terraform provider を普通のパッケージ依存として扱っている以上、必然的にこうなる。実際には provider の major はインフラの変更であって、その唯一の本当のテストは実 state に対する plan だ、というだけの話だと思う。