結論
- Terraform がリソース間の依存を張るのは、式が別のリソースや module の output を参照したときと、
depends_onを書いたときだけ。同じ値を文字列や root の変数で渡しても依存はできず、親と子は並行に作られる - 依存が無くても、親が既にある環境では通る。落ちるのはまっさらな環境への初回 apply だけで、Firestore の Field が
404になったり、API の有効化より先にリソースの作成が走ったりする depends_on = [module.x]は x の全リソースを待つ。x の中に自分の出力を使うリソースがあると、依存が一周して cycle (循環参照) になり、Error: Cycleで落ちる。x の output を書けば、その output が参照するリソースだけを待つ- 設定を持つ provider ブロックを中に書いた module には、
depends_on(とcount/for_each) を付けられない。provider は root へ上げる - module の
depends_onは、依存先に未適用の変更がある plan で、module 内の data source の読み込みを apply 時まで遅らせる。その値を ForceNew の属性に渡すと、何も変わっていなくても replace が出る
以下のコードと出力は Terraform v1.15.8 で確かめた。使ったのは terraform init -backend=false と terraform validate だけで、plan / apply は打っていない。
GCP の API を呼ぶ節と data source の節は、作業ログの記録をもとに書いた。
前提: 依存は式に書いた参照から作られる
Terraform は、どのリソースを先に作るかを依存グラフで決める。リソースが点で、「A は B を待つ」が A から B への矢印 (edge) になる。 矢印でつながっていないリソース同士は、順序を決めずに並行に作られる。この記事では、矢印ができることを「依存を張る」と書く。
矢印は、式に現れる参照から作られる。terraform_data.database.output のように別のリソースの属性を参照すると、参照した側が参照された側を待ち、参照された側が先に作られる。
depends_on は、参照の無いところに同じ依存を手で張る。
root の変数 (default、tfvars、-var で外から渡す値) は plan の時点で値が決まっているので、どのリソースとも依存を張らない。同じ値を文字列で書いた場合も依存はできない。
A が B を待ち、B も A を待つように依存が一周すると、どちらも先に作れない。これが cycle (循環参照) で、Terraform は Error: Cycle: の後ろに一周に含まれる点を並べて止まる。
依存があるかは plan を打たなくても確かめられる。親に逆向きの depends_on (親 => 子) を足して terraform validate を打つ。
子 => 親の依存が既にあれば一周するので Error: Cycle が出る。Success! なら依存は無い。以下の節はこの方法で見た。
属性を参照した子だけが親を待つ

field が database の属性を参照すると依存ができる。database に逆向きの depends_on を足すと cycle になり、validate が落ちる。
# probe-ref/main.tf
variable "db_name" {
type = string
default = "(default)"
}
resource "terraform_data" "database" {
input = var.db_name
# 逆向きの依存を足す。field => database の依存があれば cycle になる
depends_on = [terraform_data.field]
}
resource "terraform_data" "field" {
input = terraform_data.database.output # 属性を参照する
}$ terraform init -backend=false -no-color > /dev/null && terraform validate -no-color
Error: Cycle: terraform_data.field, terraform_data.databasefield と database が互いを待って一周している、という意味になる。field => database の依存が属性の参照から張られていたことが分かる。
field の input を var.db_name (probe-var) か "(default)" (probe-literal) に変えると、database と同じ値でも依存が無いので通る。
$ terraform init -backend=false -no-color > /dev/null && terraform validate -no-color
Success! The configuration is valid.module の入力変数は root の変数と違う。属性を module の入力に渡すと、module の中で var.db_name として読んでも依存は残る。
# probe-module-var/main.tf
resource "terraform_data" "database" {
input = "(default)"
# 逆向きの依存を足す。module の中の field => database の依存があれば cycle になる
depends_on = [module.child.field_id]
}
# 属性を module の入力変数に渡す
module "child" {
source = "./child"
db_name = terraform_data.database.output
}# probe-module-var/child/main.tf
variable "db_name" {
type = string
}
resource "terraform_data" "field" {
input = var.db_name # module の中では変数として読む
}
output "field_id" {
value = terraform_data.field.id
}$ terraform init -backend=false -no-color > /dev/null && terraform validate -no-color
Error: Cycle: module.child.terraform_data.field, module.child.output.field_id (expand), terraform_data.database, module.child.var.db_name (expand)依存が切れるのは、値の出どころがリソースでないとき (root の変数と文字列) だけ。
対策は、親を属性で指すか depends_on を書くこと。属性で指せば (database = google_firestore_database.database.name) 暗黙の依存になり、depends_on は要らない。
どちらでもよいので、同じ module の兄弟リソースの書き方に揃える。どの属性を参照できるかは provider の schema で引ける。
terraform providers schema -json | jq '.provider_schemas[].resource_schemas["<type>"].block.attributes'- PITFALL:
depends_onは、上流に保留中の変更があると下流の値を unknown として扱うので、闇雲に付けると plan に(known after apply)が増える。 上流が作成後に変わらないリソース (DB 本体など) なら、定常状態では保留中の変更が無いので実害は出ない。module に付けたときの害は 最後の節 に書いた。
まっさらな環境の初回 apply だけで落ちる

依存の無い子は、既存の環境では親が既にあるので落ちない。まっさらなプロジェクトへの初回 apply だけで、親と並行に作られて落ちる。
Firestore の TTL 用の google_firestore_field が database = "(default)" のように文字列で Database を指していると、新しいプロジェクトでは Database と並行に作られる。
同じ module の composite index には depends_on があり、Field には無かった。
Error creating Field: googleapi: Error 404: Requested database was not found.resource "google_firestore_field" "expire_at_ttl" {
database = google_firestore_database.database.name # 参照にすれば暗黙の依存になる
collection = "webhooks"
field = "expireAt"
ttl_config {}
# もしくは depends_on = [google_firestore_database.database]
}API の有効化も同じ形で落ちる。google_project_service で API を有効にしても、それを参照しない module や resource は有効化を待たずに作成が走る。
「この project の API が有効なこと」はリソース定義に参照として現れないので、グラフに依存ができない。API 有効化のリソースへ、全 module から depends_on を張る。
resource "google_project_service" "service" {
for_each = toset(var.services)
service = each.value
}
module "artifact_registry" {
# source などは省略
depends_on = [google_project_service.service]
}同じ形の競合は IAM でも起きる。サービスアカウントへの権限付与とワークロードの作成に順序が無いと、権限が付く前に起動したリビジョンが Secret を読めずに失敗する
(Permission denied on secret: ... The service account must be granted ...)。文面が「権限が無い」なので、実際には付いている権限の方を疑ってしまう。
- PITFALL: 失敗した apply でも、Database と index は state に入っている。直した後は、
terraform plan -target=<field>が1 to addだけになることを確かめてから apply し直す。
この節は GCP の API を呼ぶ apply が要るので、手元では再現していない。仕組みは前の節と同じ。
module 全体を待つと cycle (循環参照) になる

root の resource や module に、別の module の中の API 有効化だけを待たせたいとき、depends_on = [module.commons] と module 全体を書くと Cycle になることがある。
commons の中に、待たせる側 (psk) の出力を使うリソースがあるとき。
# modules/commons/main.tf
variable "psk_value" {
type = string
}
# API の有効化の代わり
resource "terraform_data" "services" {
input = "enabled"
}
# psk module の出力を使うリソース
resource "terraform_data" "vpn" {
input = var.psk_value
}
output "enabled_services" {
value = terraform_data.services.id
}# modules/psk/main.tf
resource "terraform_data" "key" {
input = "secret"
}
output "value" {
value = terraform_data.key.id
}# whole/main.tf
module "commons" {
source = "../modules/commons"
psk_value = module.psk.value
}
module "psk" {
source = "../modules/psk"
# module 全体を待つ
depends_on = [module.commons]
}$ terraform init -backend=false -no-color > /dev/null && terraform validate -no-color
Error: Cycle: module.psk.terraform_data.key, module.commons.var.psk_value (expand), module.commons.terraform_data.vpn, module.commons (close), module.psk (expand), module.psk.output.value (expand)cycle に module.commons (close) が入っている。module 全体への depends_on は、commons の全リソースが終わる点 (close) を待つ。
psk の key は module.psk (expand) を通して module.commons (close) を待ち、close は vpn を待ち、vpn は var.psk_value を通して psk の value、つまり key を待つ。
depends_on に commons の output を書くと、その output が参照する services だけを待つので cycle にならない。
# output/main.tf
module "commons" {
source = "../modules/commons"
psk_value = module.psk.value
}
module "psk" {
source = "../modules/psk"
# API の有効化を返す output だけを待つ
depends_on = [module.commons.enabled_services]
}$ terraform init -backend=false -no-color > /dev/null && terraform validate -no-color
Success! The configuration is valid.provider を持つ module には depends_on を付けられない

module の中に設定を持つ provider ブロックがあると、その module の呼び出しに depends_on を書けない。count と for_each も同じ。
depends_on は module 全体の評価を遅らせるが、provider の設定が遅れると、その provider を使う他のリソースの計画が立てられない。
google-beta や aws.secondary のような追加の provider を module の中に書いているときに当たる。
# main.tf
resource "terraform_data" "service" {
input = "api enabled"
}
module "child" {
source = "./child"
depends_on = [terraform_data.service]
}# child/main.tf
terraform {
required_providers {
google-beta = {
source = "hashicorp/google-beta"
}
}
}
# module の中に、設定を持つ provider ブロックを書く
provider "google-beta" {
project = "example-project"
}
resource "terraform_data" "app" {
input = "app"
}init の Initializing modules... の段で止まり、provider の取得までは進まない。
$ terraform init -backend=false -no-color
Initializing modules...
Error: Module is incompatible with count, for_each, and depends_on
on main.tf line 7, in module "child":
7: depends_on = [terraform_data.service]
The module at module.child is a legacy module which contains its own local
provider configurations, and so calls to it may not use the count, for_each,
or depends_on arguments.
If you also control the module "./child", consider updating this module to
instead expect provider configurations to be passed by its caller.provider ブロックを root へ上げ、module には required_providers だけを残す。module には root の provider が暗黙に渡る。
# fixed/main.tf
terraform {
required_providers {
google-beta = {
source = "hashicorp/google-beta"
}
}
}
# provider の設定は root に置く
provider "google-beta" {
project = "example-project"
}
resource "terraform_data" "service" {
input = "api enabled"
}
module "child" {
source = "./child"
depends_on = [terraform_data.service]
}# fixed/child/main.tf
terraform {
required_providers {
google-beta = {
source = "hashicorp/google-beta"
}
}
}
# provider ブロックは書かない。root の google-beta が暗黙に渡る
resource "terraform_data" "app" {
input = "app"
}$ terraform init -backend=false -no-color
Initializing modules...
- child in child
Initializing provider plugins...
- terraform.io/builtin/terraform is built in to Terraform
- Finding latest version of hashicorp/google-beta...
...module の段を抜けて provider の取得へ進む。設定を持たない provider "terraform" {} を module に書いた場合は、Redundant empty provider block の警告が出るだけで、depends_on を付けても init と validate は通った。
- PITFALL: エラーの文面が「provider の設定は呼び出し元から渡す形に直せ」と案内している。そこを読み飛ばすと原因の分からない失敗に見える。 provider を移した後は、module の中で使われなくなった変数 (labels など) が残るので消す。
module の depends_on は data source の読み込みを apply まで遅らせる

module に depends_on を付けると、依存先に未適用の変更がある plan では、module の中の data source の読み込みが apply 時まで遅れる。data source が依存先の変更を待つ必要があるので、plan の時点では読めない。
読み込みが遅れた値は (known after apply) になり、それを ForceNew の属性に渡すと、何も変わっていなくても plan に replace が出る。
API の有効化を module の depends_on で待たせつつ、module の中で既存のリソースを data source で参照しているときに当たる。
API を 1 つ足しただけの plan で、App Check の登録に replace が出た。組み込みの provider だけの構成でも同じ plan になる。
値が既に分かっているなら、data source を通さず変数で渡す。
# NG: data "google_firebase_web_app" から読んだ app_id を渡す
# OK: 変数から直接渡す
resource "google_firebase_app_check_recaptcha_enterprise_config" "default" {
for_each = var.app_check_web_apps
app_id = each.value.app_id
# ...
}- PITFALL: その replace は、Delete が state から外すだけで実害が無い場合もある。それでも「destroy の件数を見る」確認が効かなくなる。
この節は plan が要るので、手元では再現していない。
まとめ
| 書き方 | 起きること | 対策 |
|---|---|---|
| 子が親を文字列か root の変数で指す | 依存が無く、親と並行に作られる。初回 apply だけで落ちる | 親の属性を参照するか、depends_on を書く |
| 子が親の属性を参照する / module の入力に属性を渡す | 依存ができ、親を待つ | なし |
| API 有効化を参照しない module | 有効化より先に作成が走る | 全 module に depends_on = [google_project_service.service] |
| 権限付与とワークロード作成に順序が無い | 権限が付く前のリビジョンが Secret を読めない | 権限付与への依存を張る |
depends_on = [module.x] で、x が自分の出力を使う | Cycle | x の output を depends_on に書く |
設定を持つ provider ブロックがある module に depends_on / count / for_each | init で Module is incompatible with count, for_each, and depends_on | provider を root へ上げる |
module に depends_on を付け、中で data source を読む | 依存先に変更がある plan で (known after apply)。ForceNew なら replace | 値が分かっているなら変数で渡す |
闇雲な depends_on | plan に (known after apply) が増える | 上流が作成後に変わらないリソースなら実害は無い |