Terraform: Deleting an element from a list
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.
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 file to specify Google Project and Region for managing terraform resources
provider "google" {
project = "someproject"
region = "us-central1"
}
# Backend file to store terraform state in GCS bucket
terraform {
backend "gcs" {
bucket = "terraform-state-bucket"
prefix = "firewall"
}
}
# Creating a couple of firewall rules based off a list variable
variable "firewall_rules" {
type = list(string)
default = ["one", "two", "three", "four"]
}
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:
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:
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
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.
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
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.