依赖项管理最佳实践

本文档提供了有关表达 Terraform 配置中资源之间依赖关系的建议。

优先使用隐式依赖关系,而不是显式依赖关系

当一种资源依赖于其他资源的存在时,就会出现资源依赖性。Terraform 必须能够理解这些依赖关系,以确保资源按正确的顺序创建。例如,如果资源 A 对资源 B 有依赖性,则资源 B 会在资源 A 之前创建。

可以通过隐式和显式依赖关系声明来建立 Terraform 配置依赖关系。隐式依赖关系通过表达式引用进行声明,而显式依赖关系则通过使用 depends_on 元参数进行指定。depends_on 参数指定 Terraform 必须先完成资源或模块所依赖的对象的所有操作,然后才能继续处理依赖对象。

虽然这两种方法都能确保操作顺序正确,但隐式依赖关系通常能更高效地规划资源更新和替换。这是因为 Terraform 可以智能地跟踪隐式依赖关系中涉及的特定字段,如果这些特定字段在依赖关系中保持不变,则可能会避免更改依赖的资源。

与隐式依赖关系相比,显式依赖关系传递的信息不太具体。这意味着,在不知道构成依赖关系的特定属性的情况下,Terraform 只能制定更保守的资源创建、更新和替换计划。在实践中,这会影响 Terraform 创建资源的顺序,以及 Terraform 如何确定资源是否需要更新或替换。

我们建议您仅在万不得已时才使用带有 depends_on 元参数的显式依赖关系,即当两个资源之间的依赖关系处于隐藏状态且无法通过隐式依赖关系来表达时。

在以下示例中,必须先启用所需的项目服务,然后才能创建 BigQuery 数据集。此依赖关系是明确声明的:

不推荐:

module "project_services" {
  source  = "terraform-google-modules/project-factory/google//modules/project_services"
  version = "~> 14.4"

  project_id = var.project_id
  activate_apis = [
    "bigquery.googleapis.com",
    "bigquerystorage.googleapis.com",
  ]
}

module "bigquery" {
  source       = "terraform-google-modules/bigquery/google"
  version      = "~> 5.4"

  dataset_id   = "demo_dataset"
  dataset_name = "demo_dataset"
  project_id   = var.project_id
  depends_on = [module.project_services] # <- explicit dependency
}

以下示例通过将 project_id 参数引用为 project_services 资源的 project_id 输出属性,将显式依赖关系替换为隐式依赖关系:

推荐:

module "bigquery" {
  source       = "terraform-google-modules/bigquery/google"
  version      = "~> 5.4"

  dataset_id   = "demo_dataset"
  dataset_name = "demo_dataset"
  project_id   = module.project_services.project_id # <- implicit dependency
}

使用隐式依赖关系可以精确声明依赖关系,例如指定需要从上游对象收集的确切信息。这样还可以减少在多个位置进行更改的需求,进而降低出错风险。

引用依赖资源的输出属性

通过引用上游资源中的值来创建隐式依赖关系时,请务必仅引用输出属性,尤其是尚不确定的值。这样可确保 Terraform 在预配当前资源之前等待上游资源创建完成。

在以下示例中,google_storage_bucket_object 资源引用了 google_storage_bucket 资源的 name 参数。在 Terraform 规划阶段,参数具有已知值。这意味着当 Terraform 创建 google_storage_bucket_object 资源时,它不会等待 google_storage_bucket 资源创建,因为引用已知参数(存储桶名称)不会在 google_storage_bucket_objectgoogle_storage_bucket 之间创建隐式依赖关系。这会违背两个资源之间隐式依赖关系声明的初衷。

不推荐:

# Cloud Storage bucket
resource "google_storage_bucket" "bucket" {
  name = "demo-bucket"
  location = "US"
}

resource "google_storage_bucket_object" "bucket_object" {
  name   = "demo-object"
  source = "./test.txt"
  bucket = google_storage_bucket.bucket.name # name is an input argument
}

相反,google_storage_bucket_object 资源必须引用 google_storage_bucket_object 资源的 id 输出属性。由于 id 字段是输出属性,因此只有在执行了相应资源的创建操作后,才会设置其值。因此,Terraform 会等待 google_storage_bucket_object 资源的创建完成,然后再开始创建 google_storage_bucket_object 资源。

推荐:

resource "google_storage_bucket_object" "bucket_object" {
  name   = "demo-object"
  source = "./test.txt"
  bucket = google_storage_bucket.bucket.id # id is an output attribute
}

有时,没有明显的输出属性可供引用。例如,假设存在以下示例,其中 module_a 将生成文件的名称作为输入。在 module_a 中,文件名用于读取文件。如果按原样运行此代码,您将收到 no such file or directory 异常,这是由于 Terraform 在其规划阶段尝试读取该文件而导致的,此时该文件尚未创建。在这种情况下,检查 local_file 资源的输出属性会发现,没有明显的字段可用于代替文件名输入参数。

不推荐:

resource "local_file" "generated_file" {
 filename = "./generated_file.text"
 content = templatefile("./template.tftpl", {
   project_id = var.project_id
 })
}

module "module_a" {
 source = "./modules/module-a"
 root_config_file_path = local_file.generated_file.filename
}

您可以通过引入显式依赖关系来解决此问题。作为最佳实践,请务必添加注释,说明为何需要显式依赖关系:

推荐:

module "module_a" {
 source = "./modules/module-a"
 root_config_file_path = local_file.generated_file.filename
 depends_on = [local_file.generated_file] # waiting for generated_file to be created
}

后续步骤