Terraform Series
- Part 1: Introduction
- Part 2: Creating the Server
- Part 3: Provisioning the Server
- Part 4: Managing Terraform State
- Part 5: Cleaner Code with Terraform Modules
- Part 6: Loops with Terraform
- Part 7: Conditionals with Terraform
- Part 8: Testing Terraform Code
We're going to continue on the same path as we left off. I'm going to do a deep dive into Terraform with the help of some DigitalOcean resources. Instead of focusing on resources, though, I'll explain what we can do with those resources using some of the basic Terraform features.
Loops
The Terraform language is declarative, describing an intended goal rather than the steps to reach that goal. 1
Since we need to declare the end goal in Terraform, it shouldn't come as a surprise when I say that Terraform does not have loops. But there are three different features that we can emulate loops.
count
parameter to create multiple resources
Let's imagine you want to create multiple instances from the same resource type. How do you achieve that? By copying the resource declaration. What if you need a lot of them? It's inefficient to repeat the same logic on a single resource over and over and over.
Terraform's count
parameter is the oldest way of encapsulating repeating the resources. It's got supercharged by Terraform's 0.13 version and is now also available in module blocks as well.
Let's imagine you want to create tags on DigitalOcean, later to be used on droplets. We'd like to tag droplets by the amount of RAM it's provided with.
resource "digitalocean_tag" "ram_8" {
name = "ram_8"
}
Now we can use this resource for tagging droplets with 8 GB of RAM. But RAM amounts are variable. So we need to create a variety of tags starting from 1 GB and going up until maybe 64 GB. Let's use the count
parameter to make all of them.
resource "digitalocean_tag" "ram" {
count = 64
name = "ram_${count.index + 1}"
}
When you run terraform plan
, you'll see a list of tags that Terraform will create:
Terraform will perform the following actions:
# digitalocean_tag.ram[0] will be created
+ resource "digitalocean_tag" "ram" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "ram_1"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
# digitalocean_tag.ram[1] will be created
+ resource "digitalocean_tag" "ram" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "ram_2"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
# digitalocean_tag.ram[2] will be created
+ resource "digitalocean_tag" "ram" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "ram_3"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
...
...
...
# digitalocean_tag.ram[63] will be created
+ resource "digitalocean_tag" "ram" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "ram_64"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
Plan: 64 to add, 0 to change, 0 to destroy.
The *_count
parameters inside the tag resources are not related to our own count
parameter to let us create multiple resources. And as you can see from our Terraform code above, we'll be able to access the index of the count
parameter and convert it to a 1-based index before naming our tag.
One other thing to note here is the internal name of the resource. When we did not use the count
parameter, our tag was named ram_8
, so we could access it with digitalocean_tag.ram_8
. However, now that we have a list of digitalocean_tag
resources, we'll access our resources like they're inside an array: digitalocean_tag.ram[0]
.
for_each
parameter to iterate over a data structure
count
parameter was helpful to create numeric resources. But what if you need to iterate over a data structure like a list, a set, or a map? Let's continue from our earlier example, but this time create tags for operating systems.
Let's first create a variable to list our available operating systems:
variable "operating_systems" {
description = "Operating systems available on droplets"
type = list(string)
default = ["ubuntu", "centos", "debian", "fedora", "freebsd"]
}
Notice how we defined our type as the list of strings. We'll come back to type usages at a later point. Now let's create our tags by using the for_each
parameter:
resource "digitalocean_tag" "os" {
for_each = toset(var.operating_systems)
name = each.value
}
The reason we are using toset
on the operating_systems
variable is that for_each
supports a set or a map of strings. Otherwise, it would complain:
The given "for_each" argument value is unsuitable: the "for_each" argument must be a map or set of strings, and you have provided a value of type list of string.
Also, note how we used each.value
to get the value from each iteration. Now when we run terraform plan
, we'll see our tags in the execution plan:
Terraform will perform the following actions:
# digitalocean_tag.os["centos"] will be created
+ resource "digitalocean_tag" "os" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "centos"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
# digitalocean_tag.os["debian"] will be created
+ resource "digitalocean_tag" "os" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "debian"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
# digitalocean_tag.os["fedora"] will be created
+ resource "digitalocean_tag" "os" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "fedora"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
# digitalocean_tag.os["freebsd"] will be created
+ resource "digitalocean_tag" "os" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "freebsd"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
# digitalocean_tag.os["ubuntu"] will be created
+ resource "digitalocean_tag" "os" {
+ databases_count = (known after apply)
+ droplets_count = (known after apply)
+ id = (known after apply)
+ images_count = (known after apply)
+ name = "ubuntu"
+ total_resource_count = (known after apply)
+ volume_snapshots_count = (known after apply)
+ volumes_count = (known after apply)
}
Plan: 5 to add, 0 to change, 0 to destroy.
for
expression to map & filter
Another good loop-like behavior Terraform provides us with is the for
expression. It resembles Python's comprehensions.
Let's define a variable for RAM requirements of operating systems:
variable "os_requirements" {
description = "Which operating system requires how much RAM"
type = map(number)
default = {
ubuntu = 4
centos = 2
debian = 3
fedora = 2
freebsd = 1
}
}
The numbers are entirely arbitrary, of course. :-) But note how we defined the type and assigned the RAM amounts to different operating systems here. Each of the OS described in the variable is the key in the map. RAM amounts are the values in the map.
Now we are going to filter & map within the same for expression.
output "usable_operating_systems" {
value = [for os, ram in var.os_requirements : upper(os) if ram <= 2]
}
I always liked this type of one-liner comprehension. Let's see what we have achieved with this one-liner:
- First, we extract both keys (
os
) and values (ram
) from the map (var.os_requirements
). - Then, we filter all map elements by their RAM amount. It should be smaller than or equal to 2.
- For every filtered operating system, we map its name to uppercase by
upper(os)
/ - Finally, we map the whole result into a list with the surrounding brackets
[]
.
That's one type of filtering and two types of mapping in a one-liner.
Now we need to use terraform apply
instead of terraform plan
here because our Terraform code will not create any resources. To see the output, let's run it:
$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
Terraform will perform the following actions:
Plan: 0 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
usable_operating_systems = [
"CENTOS",
"FEDORA",
"FREEBSD",
]
Perfect, now we know which operating systems we can use with the amount of RAM we have. :-)
Cover photo by Lysander Yuen
Part 5..........................................................................................................Part 7
Top comments (0)