For_each with list of objects still cares about resource ordering

I have code that I’ve refactored from count to for_each, however since I’m using list(object) as the main variable I’m iterating on, whenever I try to add resources in the middle of the list the order still matters and TF wants to replace the objects.

My code:

variables.tf:

variable "dbs" {
  type = list(object({
    db_id   = string
    db_name = string
    labels  = map(string)
    iam_members = list(object({
      role   = string
      member = string
    }))
    tables = list(object({
      id          = string
      partitioned = string
      field       = string
      type        = string
      clustering  = optional(list(string))
    }))
    materialized_views = optional(list(object({
      id                  = string
      enable_refresh      = optional(string)
      refresh_interval_ms = optional(string)
    })))
    views = optional(list(object({
      id             = string
      use_legacy_sql = optional(string)
    })))

  }))
  description = "BigQuery Dataset to be created"
}

main.tf:


resource "google_bigquery_dataset" "database" {
  for_each                   = { for k, v in var.dbs : k => v }
  dataset_id                 = each.value.db_id
  friendly_name              = each.value.db_name
  description                = "Dataset created by Terraform"
  location                   = "US"
  delete_contents_on_destroy = var.delete_contents_on_destroy
  labels                     = each.value.labels
}

module "tables" {
  source   = "./tables"
  for_each = { for k, v in var.dbs : k => v }
  db_id    = google_bigquery_dataset.database[each.key].dataset_id
  labels   = each.value.labels
  tables   = each.value.tables
}

module "materialized_views" {
  source             = "./materialized_views"
  for_each           = { for k, v in var.dbs : k => v if v.materialized_views != null }
  db_id              = google_bigquery_dataset.database[each.key].dataset_id
  materialized_views = each.value.materialized_views
  labels             = each.value.labels
  depends_on         = [module.tables]
}

module "views" {
  source     = "./views"
  for_each   = { for k, v in var.dbs : k => v if v.views != null }
  db_id      = google_bigquery_dataset.database[each.key].dataset_id
  views      = each.value.views
  labels     = each.value.labels
  depends_on = [module.tables]
}

module "iam" {
  source      = "./iam"
  for_each    = { for k, v in var.dbs : k => v }
  db_id       = google_bigquery_dataset.database[each.key].dataset_id
  iam_members = each.value.iam_members
}

dev.tfvars:

dbs = [
  {
    db_id   = "some_dataset1"
    db_name = "some_dataset1"
    labels = {
      environment = "dev"
    }
    iam_members = [
      {
        role   = "roles/bigquery.dataEditor"
        member = "serviceAccount:1@project.iam.gserviceaccount.com"
      },
      {
        role   = "roles/bigquery.dataOwner"
        member = "group:1@thebest.com"
      }
    ]
    tables = [
      {
        id          = "results"
        partitioned = "true"
        field       = "date_time_of_run"
        type        = "DAY"
      },
      {
        id          = "results_extended"
        partitioned = "true"
        field       = "date"
        type        = "DAY"
      }
    ]

    views = [
      {
        id             = "results_view"
        use_legacy_sql = "false"
      }
    ]
  },
  {
    db_id   = "some_dataset3"
    db_name = "some_dataset3"
    labels = {
      environment = "dev"
    }
    iam_members = [
      {
        role   = "roles/bigquery.dataEditor"
        member = "serviceAccount:1@project.iam.gserviceaccount.com",
        member = "group:2@thebest.com",
        member = "group:3@thebest.com"

      },
    ]
    tables = [
      {
        id          = "daily_inventory"
        partitioned = "true"
        field       = "inventory_timestamp"
        type        = "DAY"
      }
    ]
    materialized_views = [
      {
        id                  = "inventory_materialized_view"
        enable_refresh      = "true"
        refresh_interval_ms = "1800000"
      }
    ]
  },
  {
    db_id   = "some_dataset2"
    db_name = "some_dataset2"
    labels = {
      environment = "dev"
    }
    iam_members = [
      {
        role   = "roles/bigquery.dataEditor"
        member = "serviceAccount:1@project.iam.gserviceaccount.com",
        member = "group:1@thebest.com",
        member = "group:1@thebest.com"
      }
    ]
    tables = []
  }
]

As you can see, some_dataset3 was inserted between some_dataset1 and some_dataset2.

Getting:

...
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
-/+ destroy and then create replacement

  # google_bigquery_dataset.database["1"] must be replaced
-/+ resource "google_bigquery_dataset" "database" {
~ dataset_id  = "some_dataset2" -> "some_dataset3" # forces replacement
....

On my second try, I changed my code to be:


resource "google_bigquery_dataset" "database" {
  for_each                   = { for k in var.dbs : k.db_id => k }
  dataset_id                 = each.value.db_id
  friendly_name              = each.value.db_name
  description                = "Dataset created by Terraform"
  location                   = "US"
  delete_contents_on_destroy = var.delete_contents_on_destroy
  labels                     = each.value.labels
}

module "tables" {
  source   = "./tables"
  for_each = { for k in var.dbs : k.db_id => k }
  db_id    = google_bigquery_dataset.database[each.key].dataset_id
  labels   = each.value.labels
  tables   = each.value.tables
}

module "materialized_views" {
  source             = "./materialized_views"
  for_each           = { for k in var.dbs : k.db_id => k if k.materialized_views != null }
  db_id              = google_bigquery_dataset.database[each.key].dataset_id
  materialized_views = each.value.materialized_views
  labels             = each.value.labels
  depends_on         = [module.tables]
}

module "views" {
  source     = "./views"
  for_each   = { for k in var.dbs : k.db_id => k if k.views != null }
  db_id      = google_bigquery_dataset.database[each.key].dataset_id
  views      = each.value.views
  labels     = each.value.labels
  depends_on = [module.tables]
}

module "iam" {
  source      = "./iam"
  for_each    = { for k in var.dbs : k.db_id => k }
  db_id       = google_bigquery_dataset.database[each.key].dataset_id
  iam_members = each.value.iam_members
}

Now, since I’m constructing the keys based on “db_id”, terraform wants to destroy all resources and recreate them with non-index named resources in state. Since of course it’s undesirable to delete datasets, is there a way to avoid recreating the resources except using moved blocks?

Thanks in advance!

No, you will need to use moved blocks to get all the existing resources migrated from numbered to named for_each keys.