Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Mastering Terraform

You're reading from   Mastering Terraform A practical guide to building and deploying infrastructure on AWS, Azure, and GCP

Arrow left icon
Product type Paperback
Published in Jul 2024
Publisher Packt
ISBN-13 9781835086018
Length 494 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Mark Tinderholt Mark Tinderholt
Author Profile Icon Mark Tinderholt
Mark Tinderholt
Arrow right icon
View More author details
Toc

Table of Contents (27) Chapters Close

Preface 1. Part 1: Foundations of Terraform
2. Chapter 1: Understanding Terraform Architecture FREE CHAPTER 3. Chapter 2: Using HashiCorp Configuration Language 4. Chapter 3: Harnessing HashiCorp Utility Providers 5. Part 2: Concepts of Cloud Architecture and Automation
6. Chapter 4: Foundations of Cloud Architecture – Virtual Machines and Infrastructure-as-a-Services 7. Chapter 5: Beyond VMs – Core Concepts of Containers and Kubernetes 8. Chapter 6: Connecting It All Together – GitFlow, GitOps, and CI/CD 9. Part 3: Building Solutions on AWS
10. Chapter 7: Getting Started on AWS – Building Solutions with AWS EC2 11. Chapter 8: Containerize with AWS – Building Solutions with AWS EKS 12. Chapter 9: Go Serverless with AWS – Building Solutions with AWS Lambda 13. Part 4: Building Solutions on Azure
14. Chapter 10: Getting Started on Azure – Building Solutions with Azure Virtual Machines 15. Chapter 11: Containerize on Azure – Building Solutions with Azure Kubernetes Service 16. Chapter 12: Go Serverless on Azure – Building Solutions with Azure Functions 17. Part 5: Building Solutions on Google Cloud
18. Chapter 13: Getting Started on Google Cloud – Building Solutions with GCE 19. Chapter 14: Containerize on Google Cloud – Building Solutions with GKE 20. Chapter 15: Go Serverless on Google Cloud – Building Solutions with Google Cloud Functions 21. Part 6: Day 2 Operations and Beyond
22. Chapter 16: Already Provisioned? Strategies for Importing Existing Environments 23. Chapter 17: Managing Production Environments with Terraform 24. Chapter 18: Looking Ahead – Certification, Emerging Trends, and Next Steps 25. Index 26. Other Books You May Enjoy

Loops and iterations

There are three different ways to iterate within HCL. The most common are two meta-arguments, for_each and count, which operate on a resource, module, or data source block. At the same time, the third option uses the for expression, which operations on any collection.

Count

The count meta-argument is Terraform’s oldest method of iterating resources: an oldie but a goodie. The count meta-argument is excellent when you want to provision the same block multiple times and have no unique identifier to key off of. In this situation, you will use the item’s index in a list to determine its uniqueness. This approach can pose challenges in the future if the items in the list need to change in such a way that would cause the indices of each item to change.

The best way to manage this is to treat your list as append-only, as this will avoid replacing related resources. Adding or removing items from the middle of the list will cause all the items below that item to shift their index, resulting in destruction and recreation.

For example, if you want to provision a five-node cluster, you wouldn’t remove a specific node from the cluster when you scale down. You would reduce the number of nodes. You don’t care which nodes get removed. You only care how many there are. In this situation, it is ideal to use count:

    resource "aws_instance" "node" {
      count = var.node_count
      # the rest of the configuration
    }

For each

An alternative to count is the for_each meta-argument, which allows you to create multiple blocks from a map collection. This approach can be a distinct improvement over the count technique because the order of the items in the collection does not matter—only the key. If you update the code to remove the key, Terraform will remove the corresponding item. If the item changes order with other items in the collection, it will not affect Terraform’s plan.

This approach is only possible with a map collection as the source of the iteration because, with a map collection type, each item must have a key that uniquely identifies it amongst its peers.

As a result, using for_each works well when deploying to multiple regions as, typically, you wouldn’t have more than one deployment in the same region; hence, the region name makes an excellent unique key for the map that drives the for_each loop. You can add or remove regions without worrying about shifting the index of the items in the collection:

    locals {
      regions = {
        westus = {
          node_count = 5
        }
        eastus = {
          node_count = 8
        }
      }
    }

Consider the preceding map configuration. Using this as the collection, we can drive any number of resources, data sources, or modules:

    module "regional_deployment" {
      for_each = local.regions
      node_count = each.value.node_count
      # the rest of the configuration
    }

In the preceding code, we see that we are setting the for_each source to be the map stored in local.regions. We then can use the each prefix anywhere within the module block to access either the key or the value using each.key and each.value, respectively. No matter the value’s type, we can address it how we normally would, using each.value as a reference to the object.

For expressions

The for expression is a way of iterating within Terraform that does not require you to attach it to a block (i.e., resource, data source, or module). You can use the for expression to construct in-memory objects to apply object transformations to streamline block-based iteration or for output.

Iterating over a list

When iterating over a list, you must specify only one parameter to the for expression. This parameter will represent each item within your list so that you can access each item within the output block:

region_names_list = [
for s in var.regions : 
upper("${s.region}${s.country}")]

In the preceding example, we are iterating over all the objects in var.regions. As we do, during each iteration, the current value is accessible in the s parameter. We can use the output block to generate any object we desire to be created in the new list that this for expression will create.

Iterating over a map

When iterating over a map, you must change how you structure your for expression. You must specify two instead of one parameter declared immediately after the for keyword:

      region_array_from_map = [
        for k, v in var.regions :
        {
          region        = k,
          address_space = v.address_space
          node_count    = v.node_count
        }
      ]

In the preceding example, you’ll see that we specify two parameters for the for expression: k and v. We chose these names as a convention to help us remember what these variables mean within the scope of the for expression. k represents the map’s key, while v represents the value. The value can be any type, be it a primitive, collection, or complex object. If we want to access the value object, we access it based on its type. In this example, the value is a complex object with two attributes. In the for expression’s output block, we specify the structure of the object we want each item in the resulting array to have.

In this case, we are creating an array of objects with three attributes: region, address_space, and node_count, essentially flattening the original map into an array of objects. The output looks like this:

    region_array_from_map = [
      {
        "address_space" = "10.0.1.0/24"
        "node_count" = 5
        "region" = "eastus"
      },
      {
        "address_space" = "10.0.0.0/24"
        "node_count" = 8
        "region" = "westus"
      },
    ]

Outputting a list

The for expression will always output either a list or an object. You can select the output type you want by the character in which you wrap the for block. If you wrap the for expression in square brackets, then the expression will output a list:

    region_list = [for s in var.regions : "${s.region}${s.country}"]

The preceding for expression will produce the following output:

    region_list = [
      "westus",
      "eastus",
    ]

Sometimes, the names of the module or resource outputs don’t align precisely with other resources’ desired inputs. Therefore, using a for expression and outputting a list can help transform these incongruent output values into a format convenient for consumption within another part of your code.

Outputting an object

Wrapping the for expression with curly braces will output an object:

    locals {
      region_config_object = {
        for s in var.regions : "${s.region}${s.country}" =>
        {
          node_count = s.node_count
        }
      }
    }

This approach will output an object with attributes for each item in the list of regions in the regions input variable. Each attribute will take the name of the concatenation of the region and country names, and its value will be an object with a single attribute called node_count. The output will look like this:

    region_config_object = {
      "eastus" = {
        "node_count" = 8
      }
      "westus" = {
        "node_count" = 8
      }
    }

Outputting an object can be very useful in scenarios where you need to generate a JSON or YAML payload. You can reference this payload in another resource or output it so another tool can extract that value from Terraform using the terraform output command.

Converting a list to a map

One common problem is converting a list into a map. This is needed because, while a list is sometimes the most concise way of storing a simple collection, it cannot be used with the for_each iterator. Therefore, if you want to have your cake and eat it too, you need to convert that list into a map. This can be done with a simple for expression that iterates over the list in memory and outputs a map:

    locals {
      foo_list = ["A", "B", "C"]
      foo_map = {
        for idx, element in local.foo_list : element => idx
      }
    }

In the preceding code, we are invoking the for expression and outputting an object using curly braces ({}). We are taking each element within the list and setting it as the key of our map and taking the element’s index within the list and setting it as the value. It’s important to note that this will only work when the items in the list are not duplicates.

Now that we know how to loop, swoop, iterate, and cross-mojinate, we can avoid the pitfalls of copypasta by leveraging Terraform’s three extremely powerful iterators—count, for_each, and for—to build dynamic collections of resources, data sources, or anything really!

We are nearing the end of our journey into the depths of HCL. Next, we will look at a few more language expressions that help us cope when we want to use dynamic collections and conditional logic to jazz up our modules!

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image