Terraform: Deleting an element from a list

Terraform: Deleting an element from a list

September 25, 2019

Terraform provides a great way to organise well-defined resources by way of modules, but in other scenarios where you want to quickly add one more resource of the same kind while not writing a whole new bunch of code, it is easier to make use of the list syntax and use count to determine the number of resources to create and feed the values from different variables.

Terraform Logo

Note: Using list to create resources is not considered a good practice and terraform also condemns it. Better approach would be to use the newer for_each syntax.

Creating resources based off a list variable

Let’s look at an example where we create multiple firewall rules in Google Cloud Platform using a list variable:

provider.tf
# Provider file to specify Google Project and Region for managing terraform resources
provider "google" {
  project = "someproject"
  region  = "us-central1"
}
backend.tf
# Backend file to store terraform state in GCS bucket
terraform {
  backend "gcs" {
    bucket = "terraform-state-bucket"
    prefix = "firewall"
  }
}
variables.tf
# Creating a couple of firewall rules based off a list variable
variable "firewall_rules" {
  type    = list(string)
  default = ["one", "two", "three", "four"]
}
main.tf
resource "google_compute_firewall" "test" {
  count = length(var.firewall_rules)
  name  = var.firewall_rules[count.index]

  allow {
    protocol = "tcp"
    ports    = ["80", "443"]
  }

  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["web"]
}

After running terraform plan and terraform apply, the above code will create 4 firewall rules in GCP.

Deleting or Modifying list

Let’s say we want to now delete the firewall rule named four. We can just go and remove it from the list variable. The updated variable will look something like this:

variables.tf
variable "firewall_rules" {
  type    = list(string)
  default = ["one", "two", "three"]
}

Once updated, terraform plan should now show the following output:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # google_compute_firewall.test[3] will be destroyed
  - resource "google_compute_firewall" "test" {
      - creation_timestamp      = "2019-09-25T07:33:01.441-07:00" -> null
      - destination_ranges      = [] -> null
      - direction               = "INGRESS" -> null
      - disabled                = false -> null
      - id                      = "four" -> null
      - name                    = "four" -> null
      - network                 = "https://www.googleapis.com/compute/v1/projects/someproject/global/networks/default" -> null
      - priority                = 1000 -> null
      - project                 = "someproject" -> null
      - self_link               = "https://www.googleapis.com/compute/v1/projects/someproject/global/firewalls/four" -> null
      - source_ranges           = [
          - "0.0.0.0/0",
        ] -> null
      - source_service_accounts = [] -> null
      - source_tags             = [] -> null
      - target_service_accounts = [] -> null
      - target_tags             = [
          - "web",
        ] -> null

      - allow {
          - ports    = [
              - "80",
              - "443",
            ] -> null
          - protocol = "tcp" -> null
        }
    }

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

So far so good. What if we want to delete the firewall rule named one and not four? Let’s put four back to the list and delete one.

Note: We did not apply the above plan. So, we currently have all four rules in place and our updated code will look like below after deleting one from the code:

variables.tf
variable "firewall_rules" {
  type    = list(string)
  default = ["two", "three", "four"]
}

Let’s see the terraform plan output now:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # google_compute_firewall.test[0] must be replaced
-/+ resource "google_compute_firewall" "test" {
      ~ creation_timestamp      = "2019-09-25T07:33:10.024-07:00" -> (known after apply)
      ~ destination_ranges      = [] -> (known after apply)
      ~ direction               = "INGRESS" -> (known after apply)
      - disabled                = false -> null
      ~ id                      = "one" -> (known after apply)
      ~ name                    = "one" -> "two" # forces replacement
      ~ network                 = "https://www.googleapis.com/compute/v1/projects/someproject/global/networks/default" -> "default"
        priority                = 1000
      ~ project                 = "someproject" -> (known after apply)
      ~ self_link               = "https://www.googleapis.com/compute/v1/projects/someproject/global/firewalls/one" -> (known after apply)
        source_ranges           = [
            "0.0.0.0/0",
        ]
      - source_service_accounts = [] -> null
      - source_tags             = [] -> null
      - target_service_accounts = [] -> null
        target_tags             = [
            "web",
        ]

        allow {
            ports    = [
                "80",
                "443",
            ]
            protocol = "tcp"
        }
    }

  # google_compute_firewall.test[1] must be replaced
-/+ resource "google_compute_firewall" "test" {
      ~ creation_timestamp      = "2019-09-25T07:33:01.472-07:00" -> (known after apply)
      ~ destination_ranges      = [] -> (known after apply)
      ~ direction               = "INGRESS" -> (known after apply)
      - disabled                = false -> null
      ~ id                      = "two" -> (known after apply)
      ~ name                    = "two" -> "three" # forces replacement
      ~ network                 = "https://www.googleapis.com/compute/v1/projects/someproject/global/networks/default" -> "default"
        priority                = 1000
      ~ project                 = "someproject" -> (known after apply)
      ~ self_link               = "https://www.googleapis.com/compute/v1/projects/someproject/global/firewalls/two" -> (known after apply)
        source_ranges           = [
            "0.0.0.0/0",
        ]
      - source_service_accounts = [] -> null
      - source_tags             = [] -> null
      - target_service_accounts = [] -> null
        target_tags             = [
            "web",
        ]

        allow {
            ports    = [
                "80",
                "443",
            ]
            protocol = "tcp"
        }
    }

  # google_compute_firewall.test[2] must be replaced
-/+ resource "google_compute_firewall" "test" {
      ~ creation_timestamp      = "2019-09-25T07:33:01.437-07:00" -> (known after apply)
      ~ destination_ranges      = [] -> (known after apply)
      ~ direction               = "INGRESS" -> (known after apply)
      - disabled                = false -> null
      ~ id                      = "three" -> (known after apply)
      ~ name                    = "three" -> "four" # forces replacement
      ~ network                 = "https://www.googleapis.com/compute/v1/projects/someproject/global/networks/default" -> "default"
        priority                = 1000
      ~ project                 = "someproject" -> (known after apply)
      ~ self_link               = "https://www.googleapis.com/compute/v1/projects/someproject/global/firewalls/three" -> (known after apply)
        source_ranges           = [
            "0.0.0.0/0",
        ]
      - source_service_accounts = [] -> null
      - source_tags             = [] -> null
      - target_service_accounts = [] -> null
        target_tags             = [
            "web",
        ]

        allow {
            ports    = [
                "80",
                "443",
            ]
            protocol = "tcp"
        }
    }

  # google_compute_firewall.test[3] will be destroyed
  - resource "google_compute_firewall" "test" {
      - creation_timestamp      = "2019-09-25T07:33:01.441-07:00" -> null
      - destination_ranges      = [] -> null
      - direction               = "INGRESS" -> null
      - disabled                = false -> null
      - id                      = "four" -> null
      - name                    = "four" -> null
      - network                 = "https://www.googleapis.com/compute/v1/projects/someproject/global/networks/default" -> null
      - priority                = 1000 -> null
      - project                 = "someproject" -> null
      - self_link               = "https://www.googleapis.com/compute/v1/projects/someproject/global/firewalls/four" -> null
      - source_ranges           = [
          - "0.0.0.0/0",
        ] -> null
      - source_service_accounts = [] -> null
      - source_tags             = [] -> null
      - target_service_accounts = [] -> null
      - target_tags             = [
          - "web",
        ] -> null

      - allow {
          - ports    = [
              - "80",
              - "443",
            ] -> null
          - protocol = "tcp" -> null
        }
    }

Plan: 3 to add, 0 to change, 4 to destroy

As you can see, terraform thinks that we are trying to rename the resources as their indices have been changed. Position of one is being taken by two and the position of two is being taken by three and so on. Thus, recreating all the resources in the list after the point or index that we are deleting. This happens because terraform state uses index binding with name of the resource blocks (Read: Hard-coding the indices with resource names that we give).

Solution: Terraform state commands

We can use the terraform state manipulation commands that terraform provides to safely move the ith element to delete to the last position and then delete that from our code as we did above.

Steps

1. Remove the item to be deleted from the state file

terraform state rm google_compute_firewall.test[0]

This will remove/untrack firewall rule at position 0 (rule one) from terraform state. terraform plan at this point would try to add the rule back since it does not exist in the state file but it is there in the code. terraform apply would fail with error code 409 since the resource already exists in GCP.

2. Remove the element from code

variable "firewall_rules" {
  type    = list(string)
  default = ["two", "three", "four"] # one is removed from the list
}

3. Move last element to the ith position to delete

We now have a blank spot at index 0 and hence need to fill it with something. Last item in the list will be the ideal candidate.

terraform state mv google_compute_firewall.test[3] google_compute_firewall.test[0]

4. Update code to match the step 3 operation

variables.tf
variable "firewall_rules" {
  type    = list(string)
  default = ["four", "two", "three"] # four is moved at 0th index
}

terraform plan at this point should show no changes. We can now go ahead and delete the resource from GUI, but that would not be the Infrastructure as Code way of doing things.

5. Import the resource to be deleted back to the last position and update the code

We want to delete the resource via terraform and hence need to start tracking it again.

terraform import google_compute_firewall.test[3] one

The above command will start tracking firewall named one via terraform. Hence we need to update the code to reflect the same.

variables.tf
variable "firewall_rules" {
  type    = list(string)
  default = ["four", "two", "three", "one"]
}

We now have the firewall rule one at the desired/last position so that we can safely delete it from the list, plan and apply to delete the rule from GCP.

6. Finally, delete the element from the list

variables.tf
variable "firewall_rules" {
  type    = list(string)
  default = ["four", "two", "three"] # one is removed safely
}

Now when you run terraform plan, you should see only the deletion of the one firewall rule without affecting the other resources.


Originally published on Medium on September 25, 2019.

Last updated on