DEV Community

Cover image for What I've Learned Learning Terraform: Part 6
Ekin Öcalan
Ekin Öcalan

Posted on • Updated on

What I've Learned Learning Terraform: Part 6

Terraform Series


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"
}
Enter fullscreen mode Exit fullscreen mode

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}"
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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",
]
Enter fullscreen mode Exit fullscreen mode

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)