Remove from middle of block list

Hi. I have my terraform provider and I have an issue with removing an element from block list. I have a resource and inside that resource there is a block list of hosts:

resource "yandex_mdb_postgresql_cluster" "default" {
  name        = "hosts"
  environment = "PRODUCTION"
  network_id  = yandex_vpc_network.mdb-pg-test-net.id

  config {
    version = "13"
    resources {
      resource_preset_id = "s2.micro"
      disk_type_id       = "network-ssd"
      disk_size          = 10
    }
  }

  host_master_name = "a3"

  host {
    name = "a1"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-a.id
    zone = "ru-central1-a"
  }
  host {
    name = "a2"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
    zone = "ru-central1-b"
  }
  host {
    name = "a3"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-c.id
    zone = "ru-central1-c"
  }
}

If I remove the first one, it won’t be just removed, terraform will change the second host to the first, third to second and the third will be deleted. This is not what I want, because it forces api to make unnecessary modifications. Terraform plan output:

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:

  # yandex_mdb_postgresql_cluster.default will be updated in-place
  ~ resource "yandex_mdb_postgresql_cluster" "default" {
        id                  = "c9qq8b9ngc1doq46getb"
        name                = "hosts"
        # (11 unchanged attributes hidden)

      ~ host {
          ~ fqdn             = "***HIDDEN***" -> "***HIDDEN***"
          ~ name             = "a1" -> "a2"
          ~ zone             = "ru-central1-a" -> "ru-central1-b" 
        }
      ~ host {
          ~ assign_public_ip = false
          ~ fqdn             = "***HIDDEN***"
          ~ name             = "a2" -> "a3"
          ~ zone             = "ru-central1-b" -> "ru-central1-c" 
        }
      - host {
          - assign_public_ip = false -> null
          - fqdn             = "***HIDDEN***" -> null
          - name             = "a3" -> null
          - priority         = 0 -> null
          - role             = "MASTER" -> null
          - subnet_id        = "e9bphpro3ceue1k78irh" -> null
          - zone             = "ru-central1-c" -> null
        }

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 2 to change, 1 to destroy.

I’ve seen this issue #14275, but it was about resources, but I have an attribute.

What I tried:
I tried making it set instead of list, but terraform didn’t see the changes of any block until I remove one element from the list. Maybe I did something wrong, if so, let me know.

Hi @diPhantxm,

Since the only solution to not have ordered indexes in a collection is to use an unordered data type, perhaps showing an example of how you used a set type and what happened would be of more help. You can use the Plugin Development - HashiCorp Discuss topic too, which has more users familiar with the legacy SDK as well as the new plugin framework.

Thank you for your reply.
Using sets instead of lists looked the same in .tf file. I used name as a key (hash).
Having this:

  host {
    name = "a1"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-a.id
    zone = "ru-central1-a"
  }
  host {
    name = "a2"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
    zone = "ru-central1-b"
  }
  host {
    name = "a3"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-c.id
    zone = "ru-central1-c"
  }

and changing to something else:

  host {
    name = "a1"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
    zone = "ru-central1-b"
  }
  host {
    name = "a2"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
    zone = "ru-central1-b"
  }
  host {
    name = "a3"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
    zone = "ru-central1-b"
  }

didn’t show any changes. But if I remove the first host terraform plan shows something like this:

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:

  # yandex_mdb_postgresql_cluster.default will be updated in-place
  ~ resource "yandex_mdb_postgresql_cluster" "default" {
        id                  = "c9qq8b9ngc1doq46getb"
        name                = "hosts"
        # (11 unchanged attributes hidden)

    -  host {
         - name = "a1"
         - subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
         - zone = "ru-central1-b"
      }

    ~  host {
         ~ subnet_id = "e9bphpro3ceue1k78irh" -> "..."
         ~ zone = "ru-central1-b" -> "ru-central1-c"
      }



        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 1 to destroy.

So removal works fine, but I cannot change anything.
The question is - is it my mistake I cannot change or it’s supposed to work that way?

The Plugin Development topic is probably better suited for this type of question. It sounds like you are using the legacy SDK, which is making some assumptions about the set data, combined with specifying only one attribute as the source of the set element identity. You probably don’t want to be overriding the set behavior in this case (or really in almost any case for that matter), since that will conflict with what Terraform sees as changes in the configuration.

I should add I’m using Terraform SDK 2.14.0

Hi @diPhantxm :wave:

Sorry you ran into trouble here.

Would you be able to share the schema code from your resource, including the schema, any code which might be modifying the plan (e.g., CustomizeDiff() and DiffSuppressFunc() functions) and the CRUD functions?

Yeah, sure.
This is schema code: schema
This a very tricky code, that is why I’m trynna fix it. There is no custom diff functions.
Create
Read
Update
Delete

Were you using a set and defining the Set field?

For instance:

			"host": {
				Type:     schema.TypeSet,
				MinItems: 1,
				Required: true,
				Elem:     resourceYandexMDBPostgreSQLClusterHost(),
				Set:      ....,
			},

If so, do you still find that using a set but not specifying the Set field still results in no changes when you alter zone?

For instance:

			"host": {
				Type:     schema.TypeSet,
				MinItems: 1,
				Required: true,
				Elem:     resourceYandexMDBPostgreSQLClusterHost(),
			},

I were using a set and defining the Set field. This is just current code with a TypeList. And I still find that using a set results in no changes when altering zone. The code with TypeSet was almost the same, just changed slices to *TypeSet. I used name as hash.
Can it be because of the version of SDK?

Can you paste a copy of your schema code when you are using schema.TypeSet for host?

Does it look the same as the following?

			"host": {
				Type:     schema.TypeSet,
				MinItems: 1,
				Required: true,
				Elem:     resourceYandexMDBPostgreSQLClusterHost(),
			},

My schema also has a hash

"host": {
 	Type:     schema.TypeSet,
	MinItems: 1,
	Required: true,
	Elem:     resourceYandexMDBPostgreSQLClusterHost(),
	Set:      pgHostHash,
},

And pgHostHash returns hashcode of name (host.Name)

If you remove the hash, does Terraform not report any changes when zone is altered?

For example, rather than using:

"host": {
 	Type:     schema.TypeSet,
	MinItems: 1,
	Required: true,
	Elem:     resourceYandexMDBPostgreSQLClusterHost(),
	Set:      pgHostHash,
},

Use the following instead:

"host": {
 	Type:     schema.TypeSet,
	MinItems: 1,
	Required: true,
	Elem:     resourceYandexMDBPostgreSQLClusterHost(),
},

The reason for suggesting this is that if you define the Set field you are overriding the default SDK hash algorithm.

Now when I alter some attributes, terraform removes the host and creates a new one with that attribute

Are you expecting/wanting an in-place update?

Can you share the Terraform configuration that you are using, before and after the changes when you are altering the attributes. Can you also include the output from Terraform when you run terraform plan?

Yes, I want in-place update.
This is Terraform configuration before update:

resource "yandex_mdb_postgresql_cluster" "default" {
  name        = "hosts"
  environment = "PRODUCTION"
  network_id  = yandex_vpc_network.mdb-pg-test-net.id

  config {
    version = "13"
    resources {
      resource_preset_id = "s2.micro"
      disk_type_id       = "network-ssd"
      disk_size          = 10
    }
  }

  host_master_name = "a3"

  host {
    name = "a1"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-a.id
    zone = "ru-central1-a"
  }
  host {
    name = "a3"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-a.id
    zone = "ru-central1-a"
  }
  host {
    name = "a2"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
    zone = "ru-central1-b"
  }
}

This is after:

resource "yandex_mdb_postgresql_cluster" "default" {
  name        = "hosts"
  environment = "PRODUCTION"
  network_id  = yandex_vpc_network.mdb-pg-test-net.id

  config {
    version = "13"
    resources {
      resource_preset_id = "s2.micro"
      disk_type_id       = "network-ssd"
      disk_size          = 10
    }
  }

  host_master_name = "a3"

  host {
    name = "a1"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-a.id
    zone = "ru-central1-a"
    assign_public_ip = true
  }
  host {
    name = "a3"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-a.id
    zone = "ru-central1-a"
  }
  host {
    name = "a2"
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
    zone = "ru-central1-b"
  }
}

This is output terraform plan:

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:

  # yandex_mdb_postgresql_cluster.default will be updated in-place
  ~ resource "yandex_mdb_postgresql_cluster" "default" {
        id                  = "c9qq8b9ngc1doq46getb"
        name                = "hosts"
        # (11 unchanged attributes hidden)

      - host {
          - assign_public_ip = false -> null
          - fqdn             = "***HIDDEN***" -> null
          - name             = "a1" -> null
          - priority         = 0 -> null
          - role             = "REPLICA" -> null
          - subnet_id        = "e9bphpro3ceue1k78irh" -> null
          - zone             = "ru-central1-a" -> null
        }
      + host {
          + assign_public_ip        = true
          + fqdn                    = (known after apply)
          + name                    = "a1"
          + priority                = (known after apply)
          + replication_source      = (known after apply)
          + replication_source_name = (known after apply)
          + role                    = (known after apply)
          + subnet_id               = "e9bphpro3ceue1k78irh"
          + zone                    = "ru-central1-a"
        }

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

I believe that this is performing an in-place update in that the resource is not destroyed and recreated.

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

However, if you’re wanting/expecting the host itself to be updated in-place then I’m not sure that’s possible as the default hash algorithm used by SDKv2 for schema.TypeSet uses the underlying Elem field when generating the hash, so altering the values of the fields within Elem will alter the hash and result in the old host being removed and the new one added.

You might be able to use the GetChange() function within the resource Update function to obtain the behaviour you’re looking for, but this will require writing bespoke code.

Alternatively, migrating to terraform-plugin-framework would allow you to use a map which would remove the issues around ordering associated with using a list, and remove the issues associated with modifying elements resulting in changes to the hash of a set.

What about using name as hash? I don’t change name, so hash is the same, but it doesn’t show changes.

Interestingly, Terraform itself has the concept of a map-type nested block which could be written like:

  host "a1" {
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-a.id
    zone = "ru-central1-a"
  }
  host "a2" {
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-b.id
    zone = "ru-central1-b"
  }
  host "a3" {
    subnet_id = yandex_vpc_subnet.mdb-pg-test-subnet-c.id
    zone = "ru-central1-c"
  }

Done this way, Terraform knows which block is which and can render diffs appropriately.

There’s a slight sticking point here … Terraform Plugin Framework has declined to implement support for this mode of operation. I managed to get it working locally, but didn’t have an immediate use-case with which to convince upstream to reconsider the decision not to support, at the time I was last looking into this.

If the hash doesn’t change, then no changes will be detected and no updates will take place.

Reading through this thread again, I realise there’s a detail that hasn’t been discussed explicitly - it’s a bit complicated, so bear with me as I set the scene:

A Terraform plan is a description of what the intended target state is.

But, when the plan is shown to the human user, it’s rendered into the form of differences, because that’s what humans want to see.

When you’re dealing with resources themselves, there’s a necessary correspondance between the the rendered diff, and what actually happens, because Terraform is calling the resource Create, Update, Delete logic based on its interpretation of the plan.

But when you’re dealing with nested blocks, this is no longer the case!

With nested blocks, for rendering a plan diff for humans, Terraform is going to apply some rules based on whether the block is defined to have list / set / map nature… but it’s still going to be part of one single Update call to the provider telling the provider to do everything needed to update the resource. At that point the provider is going to have to figure out what combination of deletes/add/updates apply to whatever real world infrastructure relates to the changed nested block.

For the sanity of the human user, it’s very desirable the provider behaves in the same way Terraform’s rendered of the diff does - but unlike with changing whole resources, there’s nothing baked into the product that ensures that must be the case!

In summary, all of that was to explain that you should not assume that the additions/deletions displayed in terraform plan output, actually match what the provider is going to do in its updatePGClusterHosts function - terraform-provider-yandex/resource_yandex_mdb_postgresql_cluster.go at 7ff2bf4655d0b8ae60d1c806bdb569d1cb41ef01 · yandex-cloud/terraform-provider-yandex · GitHub - in which it will make its own decisions on how it intends to achieve the target state, independently from how Terraform core chose to render the textual diff.