What I've Learned Learning Terraform: Part 6
Tags: iac, terraform, devops, tutorial
Published: 2020-08-27 10:36:44

### Terraform Series

- [Part 1: Introduction](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-1-4en4)
- [Part 2: Creating the Server](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-2-jon)
- [Part 3: Provisioning the Server](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-3-2e32)
- [Part 4: Managing Terraform State](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-4-np4)
- [Part 5: Cleaner Code with Terraform Modules](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-5-j4b)
- Part 6: Loops with Terraform
- [Part 7: Conditionals with Terraform](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-7-4ae7)
- [Part 8: Testing Terraform Code](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-8-1328)

-----

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.

```tf
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.

```tf
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:

```tf
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:

```tf
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:

```tf
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.

```tf
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. :-)


[1]: https://www.terraform.io/docs/configuration/index.html

Cover photo by [Lysander Yuen](https://unsplash.com/s/photos/loop?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

-----

[Part 5](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-5-j4b)..........................................................................................................[Part 7](https://dev.to/gzg/what-i-ve-learned-learning-terraform-part-7-4ae7)