• Search in book...
• Toggle Font Controls

3 Functional programming

This chapter covers

• Using the full gamut of input variables, local values, and output values
• Making Terraform more expressive with functions and for expressions
• Incorporating two new providers: Random and Archive
• Templating with `templatefile``()`
• Scaling resources with `count`

Functional programming is a declarative programming paradigm that allows you to do many things in a single line of code. By composing small modular functions, you can tell a computer what you want it to do instead of how to do it. Functional programming is called that because, as the name implies, programs consist almost entirely of functions. The core principles of functional programming are as follows:

• Pure functions—Functions return the same value for the same arguments, never having any side effects.

• First-class and higher-order functions—Functions are treated like any other variables and can be saved, passed around, and used to create higher-order functions.

• Immutability—Data is never directly modified. Instead, new data structures are created each time data would change.

To give you an idea of the difference between procedural and functional programming, here is some procedural JavaScript code that multiples all even numbers in an array by 10 and adds the results together:

```const numList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let result = 0;
for (let i = 0; i < numList.length; i++) {
if (numList[i] % 2 === 0) {
result += (numList[i] * 10)
}
}```

And here is the same problem solved with functional programming (JavaScript)

```const numList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const result = numList
.filter(n => n % 2 === 0)
.map(a => a * 10)
.reduce((a, b) => a + b)```

and in Terraform:

```locals {
numList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result  = sum([for x in local.numList : 10 * x if x % 2 == 0])
}```

Although you may not consider yourself a programmer, it’s still important to grasp the basics of functional programming. Terraform does not directly support procedural programming, so any logic you want to express needs to be declarative and functional. In this chapter, we take a deep dive into functions, expressions, templates, and other dynamics features that make up the Terraform language.

The specific scenario we will look at builds a program that generates Mad Libs paragraphs from template files. Mad Libs, in case you aren’t aware, is a phrasal templating word game in which one player prompts another for words to fill in the blanks of a story. An example input is shown here:

To make a pizza, you need to take a lump of <noun> and make a thin, round, <adjective> <noun>.

For the given template string, a random noun, an adjective, and another noun will be selected to fill in the placeholders. An example output would therefore be as follows:

To make a pizza, you need to take a lump of roses and make a thin, round, colorful jewelry.

Let’s start by generating a single Mad Libs story. To do that, we need a randomized pool of words to select from, and a template file. The rendered content will then be printed to the CLI. An architecture diagram for what we’re about to do is shown in figure 3.1.

Figure 3.1 Architecture diagram of the Mad Libs template engine

3.1.1 Input variables

First, we need to create the word pool. That means we need to talk about input variables—what they are, how they are declared, and how they can be set and validated.

Input variables (or Terraform variables, or just variables) are user-supplied values that parametrize Terraform modules without altering the source code. Variables are

declared with a variable block, which is an HCL object with two labels. The first label indicates the object type, which is `variable`, and the second is the variable’s name. A variable’s name can be almost anything, as long as it is unique within a given module and not a reserved identifier. Figure 3.2 shows the syntax of a variable block.

Figure 3.2 Syntax of a variable

Variable blocks accept four input arguments:

• `default`—A preselected option to use when no alternative is available. Leaving this argument blank means a variable is mandatory and must be explicitly set.

• `description`A string value providing helpful documentation to the user.

• `type`A type constraint to set for the variable. Types can be either primitive (e.g. string, integer, bool) or complex (e.g. list, set, map, object, tuple).

• `validation`A nested block that can enforce custom validation rules.

Note Variable values can be accessed within a given module by using the expression `var.<VARIABLE_NAME>`.

For this scenario, we could define a separate variable for each particle of speech, such as nouns, adjectives, verbs, etc. If we did that, our code would look like this:

```variable "nouns" {
description = "A list of nouns"
type        = list(string)
}

description = "A list of adjectives"
type        = list(string)
}

variable "verbs" {
description = "A list of verbs"
type        = list(string)
}

description = "A list of adverbs"
type        = list(string)
}

variable "numbers" {
description = "A list of numbers"
type        = list(number)
}```

Although this code is clear, we’ll instead group the variables into a single complex variable because then later we can iterate over the words using a `for` expression.

Create a new project workspace for your Terraform configuration, and make a new file called madlibs.tf`.` Add in the following code.

```terraform {                                             ❶
required_version = ">= 0.15"
}

variable "words" {
description = "A word pool to use for Mad Libs"
type = object({                                       ❷
nouns      = list(string),
verbs      = list(string),
numbers    = list(number),
})
}```

Terraform settings block

Any set value must be coercible into this complex type.

Type coercion: How everything you know and love is a string

The type of object key `numbers` in `var.words` could be `list(string)` instead of `list(number)` because of type coercion. Type coercion is the ability to convert any primitive type in Terraform to its string representation. For example, boolean `true` and `false` are converted to `"true"` and `"false"`, while numbers are similarly converted (e.g. `17` to `"17"`).

Many people are not aware that type coercion exists, because it happens so seamlessly. In fact, type coercion occurs whenever you perform string interpolation without explicitly casting the value to a string with `tostring()`. It’s important to be aware of type coercion because accidently coercing a value into a string changes the result of certain calculations (for example, the expression `17=="17"` returns `false` instead of `true`).

3.1.2 Assigning values with a variable definition file

Assigning variable values with the `default` argument is not a good idea because doing so does not facilitate code reuse. A better way to set variable values is with a variables definition file, which is any file ending in either .tfvars or .tfvars.json. A variables definition file uses the same syntax as Terraform configuration code but consists exclusively of variable assignments.

Create a new file in the workspace called terraform.tfvars, and add the following code.

Listing 3.2 terraform.tfvars

```words = {
nouns      = ["army", "panther", "walnuts", "sandwich", "Zeus", "banana",
➥ "cat", "jellyfish", "jigsaw", "violin", "milk", "sun"]
adjectives = ["bitter", "sticky", "thundering", "abundant", "chubby",
➥  "grumpy"]
verbs      = ["run", "dance", "love", "respect", "kicked", "baked"]
adverbs    = ["delicately", "beautifully", "quickly", "truthfully",
➥ "wearily"]
numbers    = [42, 27, 101, 73, -5, 0]
}```

3.1.3 Validating variables

Input variables can be validated with custom rules by declaring a nested `validation` block. To validate that at least 20 nouns are passed into `var.words`, you can write a `validation` block:

```variable "words" {
description = "A word pool to use for Mad Libs"
type = object({
nouns      = list(string),
verbs      = list(string),
numbers    = list(number),
})

validation {
condition     = length(var.words["nouns"]) >= 20
error_message = "At least 20 nouns must be supplied."
}
}```

The condition argument in `validation` is an expression that determines whether a variable is valid. `true` means it’s valid, while `false` means invalid. Invalid expressions will exit with an error, and the error message `error_message` will be displayed to the user. Here is an example from the user’s perspective:

```│
│  Error: Invalid value for variable
│
│     5: variable "words" {
│
│  At least 20 nouns must be supplied.
│
│  This was checked by the validation rule at madlibs.tf:14,1-11.```

tip There is no limit to the number of `validation` blocks you can have on a variable, allowing you to be as fine-grained with validation as you like.

3.1.4 Shuffling lists

Now that we have words in our word pool, the next step is to shuffle them. If we don’t shuffle the lists, the order will be fixed, which means exactly the same Mad Libs paragraph would be generated on each execution. Nobody wants to read the same Mad Libs story over and over again, because where is the fun in that? You might expect there to be a function called `shuffle``()` that would shuffle a generic list, but there isn’t. It’s lacking because Terraform strives to be a functional programming language, which means all functions (with the exception of two) are pure functions. Pure functions return the same result for a given set of input arguments and do not cause any additional side effects. `shuffle``()` cannot be allowed because generated execution plans would be unstable, never converging on a fixed configuration.

Note `uuid()` and `timestamp()` are the only two impure Terraform functions. These are legacy functions that should be avoided whenever possible because of their potential for introducing subtle bugs and because they are likely to be deprecated at some point.

The Random provider for Terraform introduces a `random_shuffle` resource for safely shuffling lists, so that’s what we’ll use. Since we have five lists, we need five `random_shuffle`s. This is illustrated in figure 3.3.

Figure 3.3 Shuffling lists of strings from var.words

Randomness within limits

The Random provider allows for constrained randomness within Terraform configurations and is great for generating random strings, uuids, and even pet names. It’s also helpful for preventing namespace collisions of Terraform resources and generating dynamic secrets like usernames and database passwords. A word of caution: if you do use the Random provider to generate dynamic secrets, be sure not to hardcode a seed, and be sure to secure your state and plan files. We talk more about how to do this in chapter 13.

Paste the code from the next listing into madlibs.tf to shuffle the words.

```terraform {
required_version = ">= 0.15"
required_providers {
random = {
source  = "hashicorp/random"
version = "~> 3.0"
}
}
}

variable "words" {
description = "A word pool to use for Mad Libs"
type = object({
nouns      = list(string),
verbs      = list(string),
numbers    = list(number),
})
}

resource "random_shuffle" "random_nouns" {
input = var.words["nouns"]                            ❶
}

}

resource "random_shuffle" "random_verbs" {
input = var.words["verbs"]
}

}

resource "random_shuffle" "random_numbers" {
input = var.words["numbers"]
}```

A new shuffled list is generated from the input list.

3.1.5 Functions

We’ll use the randomized list of words to replace placeholder values in a template file, rendering content for a new Mad Libs story. The built-in `templatefile()` functions allows us to do this easily. Terraform functions are expressions that transform inputs into outputs. Unlike other programming languages, Terraform does not have support for user-defined functions, nor is there a way to import functions from external libraries. Instead, you are restricted to the roughly 100 functions built in to the Terraform language. That’s a lot for a declarative programming language but almost nothing compared to traditional programming languages.

Note You extend Terraform by writing your own provider, not by writing new functions.

Returning to the problem at hand, figure 3.4 shows the `templatefile``()` syntax more closely.

Figure 3.4 Syntax of templatefile()

As you can see, `templatefile``()` accepts two arguments: a path to the template file and a map of template variables to be rendered. We’ll construct the map of template variables by aggregating together the lists of shuffled words (see figure 3.5).

Figure 3.5 Aggregating the lists of shuffled words into a map of template variables

Here’s the `templatefile``()` code:

```templatefile("\${path.module}/templates/alice.txt",
{
nouns=random_shuffle.random_nouns.result
verbs=random_shuffle.random_verbs.result
numbers=random_shuffle.random_numbers.result
})```

3.1.6 Output values

We can return the result of `templatefile``()` to the user with an output value. Output values are used to do two things:

• Pass values between modules

• Print values to the CLI

We talk more about passing values between modules in chapter 4; for now, we are interested in printing values to the CLI. The syntax for an output block is shown in figure 3.6.

Figure 3.6 Syntax of an output value

```terraform {
required_version = ">= 0.15"
required_providers {
random = {
source  = "hashicorp/random"
version = "~> 3.0"
}
}
}

variable "words" {
description = "A word pool to use for Mad Libs"
type = object({
nouns      = list(string),
verbs      = list(string),
numbers    = list(number),
})
}

resource "random_shuffle" "random_nouns" {
input = var.words["nouns"]
}

}

resource "random_shuffle" "random_verbs" {
input = var.words["verbs"]
}

}

resource "random_shuffle" "random_numbers" {
input = var.words["numbers"]
}

value = templatefile("\${path.module}/templates/alice.txt",
{
nouns      = random_shuffle.random_nouns.result
verbs      = random_shuffle.random_verbs.result
numbers    = random_shuffle.random_numbers.result
})
}```

Note `path.module` is a reference to the filesystem path of the containing module.

3.1.7 Templates

The last thing to do is create an alice.txt template file. Template syntax is the same as for interpolation values in the main Terraform language, which is anything enclosed in `\${ ... }` markers. String templates allow you to evaluate expressions and coerce the result to a string.

Any expression can be evaluated with template syntax; however, you are restricted by variable scope. Only passed-in template variables are in scope; all other variables and resources—even within the same module—are not.

Let’s create the template file now. First, create a new directory called templates to contain template files; in this directory, create an alice.txt file.

TIP Some people like to give template files a .tpl extension to indicate their purpose, but I find this unhelpful and confusing. I recommend giving template files the proper extension for what they actually are.

The next listing shows the contents of alice.txt.

Listing 3.5 alice.txt

```ALICE'S UPSIDE-DOWN WORLD

Lewis Carroll's classic, "Alice's Adventures in Wonderland", as well
as its \${adjectives[0]} sequel, "Through the Looking \${nouns[0]}",
have enchanted both the young and old \${nouns[1]}s for the last
when she \${verbs[0]}s down a/an \${adjectives[2]} hole and lands
in a strange and topsy-turvy \${nouns[2]}. There she discovers she
can become a tall \${nouns[3]} or a small \${nouns[4]} simply by
nibbling on alternate sides of a magic \${nouns[5]}. In her travels
through Wonderland, Alice \${verbs[1]}s such remarkable
characters as the White \${nouns[6]}, the \${adjectives[3]} Hatter,
the Cheshire \${nouns[7]}, and even the Queen of \${nouns[8]}s.
end when Alice awakens from her \${nouns[8]}.```

3.1.8 Printing output

We’re finally ready to generate our first Mad Libs paragraph. Initialize Terraform by performing a `terraform init`, and then apply these changes:

```\$ terraform init && terraform apply -auto-approve
...
random_shuffle.random_adjectives: Creation complete after 0s [id=-]
random_shuffle.random_numbers: Creation complete after 0s [id=-]
random_shuffle.random_nouns: Creation complete after 0s [id=-]

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:

ALICE'S UPSIDE-DOWN WORLD

Lewis Carroll's classic, "Alice's Adventures in Wonderland", as well
as its chubby sequel, "Through the Looking sun",
have enchanted both the young and old panthers for the last
0 years, Alice's bitter adventures begin
when she kickeds down a/an thundering hole and lands
in a strange and topsy-turvy army. There she discovers she
can become a tall banana or a small jigsaw simply by
nibbling on alternate sides of a magic Zeus. In her travels
through Wonderland, Alice respects such remarkable
characters as the White walnuts, the sticky Hatter,
the Cheshire milk, and even the Queen of violins.
Unfortunately, Alice's adventures come to a/an abundant
end when Alice awakens from her violin.

EOT```

Note This would be a good place to use `terraform plan` before applying changes.

3.2 Generating many Mad Libs stories

We can generate a single Mad Libs story from a randomized pool of words and output the result to the CLI. But what if we wanted to generate more than one Mad Libs at a time? It’s easy to do using expressions and the `count` meta argument.

To accomplish this, we need to make some changes to the original architecture. Here is the list of design changes:

1. Create 100 Mad Libs paragraphs.

2. Use three template files (alice.txt, observatory.txt, and photographer.txt).

Figure 3.7 Revised architecture for the Mad Libs templating engine

1. Capitalize each word before shuffling.

2. Save the Mad Libs paragraphs as text files.

3. Zip all of them together.

Our revised architecture is shown in figure 3.7.

3.2.1 for expressions

We added a step to uppercase all strings in `var.words` prior to shuffling. This isn’t strictly necessary, but it does make it easier to see templated words. The result of the uppercase function is saved into a local value, which is then fed into `random_ shuffle`.

To uppercase all the strings in `var.words`, we need to employ a `for` expression. `for` expressions are anonymous functions that can transform one complex type into another. They use lambda-like syntax and are comparable to lambda expressions and streams in conventional programming languages. Figure 3.8 shows the syntax of a `for` expression that uppercases each element in an array of strings and outputs the result as a new list. Figure 3.9 illustrates the processed stream.

Figure 3.8 Syntax of a for expression that uppercases each word in a list

Figure 3.9 Visualization of the for expression from figure 3.8

The brackets around a `for` expression determine the output type. The previous code uses `[]`, which means the output will be a list. If instead we used `{}`, then the result would be an object. For example, if we wanted to loop through `var.words` and output a new map with the same key as the original map and a value that is the length of the original value, we could do that with the expression illustrated in figures 3.10 and 3.11.

Figure 3.10 Syntax of a for expression that iterates over var.words and outputs a map

Figure 3.11 Visualization of the for expression from figure 3.10

`for` expressions are useful because they can convert one type to another and because simple expressions can be combined to construct higher-order functions. To make a `for` expression that uppercases each word in `var.words`, we will combine two smaller `for` expressions into one mega `for` expression.

TIP Composed `for` expressions hurt readability and increases cyclomatic complexity, so try not to overuse them.

The general logic is as follows:

1. Loop through each key-value pair in `var.words`.

2. Uppercase each word in the value list.

3. Save the result to a local value.

Looping through each key-value pair in `var.words` and outputting a new map can be done with the following expression:

`{for k,v in var.words : k => v }`

The next expression uppercases each word in a list and outputs to a new list:

`[for s in v : upper(s)]`

By combining these two expressions, we get

`{for k,v in var.words : k => [for s in v : upper(s)]}`

Optionally, if you want to filter out a particular key, you can do so with the `if` clause. For example, to skip any key that matches `"numbers"`, you could do so with the following expression:

`{for k,v in var.words : k => [for s in v : upper(s)] if k != "numbers"}`

note We do not need to skip the `"numbers"` key (even if it makes sense to do so) because `uppercase("1")` is equal to `"1"`, so it’s effectively an identity function.

3.2.2 Local values

We can save the result of an expression by assigning to a local value. Local values assign a name to an expression, allowing it to be used multiple times without repetition. In making the comparison with traditional programing languages, if input variables are analogous to a function’s arguments and output values are analogous to a function’s return values, then local values are analogous to a function’s local temporary symbols.

Local values are declared by creating a code block with the label `locals`. The syntax for a `locals` block is shown in figure 3.12.

Figure 3.12 Syntax of a local value

Add the new local value to madlibs.tf, and update the reference of all `random_shuffle` resources to point to `local .uppercase_words` instead of `var.words`. The next listing shows how your code should now look.

```terraform {
required_version = ">= 0.15"
required_providers {
random = {
source  = "hashicorp/random"
version = "~> 3.0"
}
}
}

variable "words" {
description = "A word pool to use for Mad Libs"
type = object({
nouns      = list(string),
verbs      = list(string),
numbers    = list(number),
})
}

locals {                                                                   ❶
uppercase_words = {for k, v in var.words : k => [for s in v : upper(s)]}
}

resource "random_shuffle" "random_nouns" {
input = local.uppercase_words["nouns"]
}

}

resource "random_shuffle" "random_verbs" {
input = local.uppercase_words["verbs"]
}

}

resource "random_shuffle" "random_numbers" {
input = local.uppercase_words["numbers"]
}```

for expression to uppercase strings and save to a local value

3.2.3 Implicit dependencies

At this point, it’s important to point out that because we’re using an interpolated value to set the input attribute of `random_shuffle`, an implicit dependency is created between the two resources. An expression or resource with an implicit dependency won’t be evaluated until after the dependency is resolved. In the current workspace, the dependency diagram looks like figure 3.13.

Figure 3.13 Visualizing the dependency graph and execution order

Nodes toward the bottom of the dependency graph have fewer dependencies, while nodes toward the top have more dependencies. At the very top is the root node, which is dependent on all other nodes.

You need to know the following about dependency graphs:

• Cyclical dependencies are not allowed.

• Nodes with zero dependencies are created first and destroyed last.

• You cannot guarantee any ordering between nodes at the same dependency level.

Note dependency graphs quickly become confusing when developing non-trivial projects. I do not find them useful except in the academic sense.

3.2.4 count parameter

To make 100 Mad Libs stories, the brute-force way would be to copy our existing code 100 times and call it a day. I wouldn’t recommend doing this because it’s messy and doesn’t scale well. Fortunately, we have better options. For this particular scenario, we’ll use the `count` meta argument to dynamically provision resources.

Note In chapter 7, we cover `for_each`, which is an alternative to `count`.

Count is a meta argument, which means all resources intrinsically support it by virtue of being a Terraform resource. The address of a managed resource uses the format `<RESOURCE` `TYPE>.<NAME>`. If `count` is set, the value of this expression becomes a list of objects representing all possible resource instances. Therefore, we could access the Nth instance in the list with bracket notation: `<RESOURCE` `TYPE>.<NAME>[N]` (see figure 3.14).

Figure 3.14 Count creates a list of resources that can be referenced using bracket notation.

Let’s update our code to support producing an arbitrary number of Mad Libs stories. First, add a new variable named `var.num_files` having type `number` and a default value of `100`. Next, reference this variable to dynamically set the `count` meta argument on each of the `shuffle_resource`s. Your code will look like the next listing.

```variable "words" {
description = "A word pool to use for Mad Libs"
type = object({
nouns      = list(string),
verbs      = list(string),
numbers    = list(number),
})
}

variable "num_files" {❶
default = 100
type    = number
}

locals {
uppercase_words = {for k,v in var.words : k => [for s in v : upper(s)]}
}

resource "random_shuffle" "random_nouns" {
count = var.num_files                                                    ❷
input = local.uppercase_words["nouns"]
}

count = var.num_files                                                    ❷
}

resource "random_shuffle" "random_verbs" {
count = var.num_files                                                  ❷
input = local.uppercase_words["verbs"]
}

count = var.num_files                                                    ❷
}

resource "random_shuffle" "random_numbers" {
count = var.num_files                                                    ❷
input = local.uppercase_words["numbers"]
}```

Declares an input variable for setting count on the random_shuffle resources

References the num_files variable to dynamically set the count meta argument

3.2.5 Conditional expressions

Conditional expressions are ternary operators that alter control flow based on the results of a boolean condition. They can be used to selectively evaluate one of two expressions: the first for when the condition is true and the second for when it’s false. Before variables had validation blocks, conditional expressions were used to validate input variables. Nowadays, they serve a niche role. The syntax of a conditional expression is shown in figure 3.15.

Figure 3.15 Syntax of a conditional expression

The following conditional expression validates that at least one noun is supplied to the `nouns` word list. If the condition fails, then an error will be thrown (because it is preferable to throw an error than proceed with invalid input):

```locals {
v = length(var.words["nouns"])>=1 ? var.words["nouns"] : [][0]        ❶
}```

var.words["nouns"] must contain at least one word.

If `var.words["nouns"]` contains at least one word, then application flow continues as normal. Otherwise, an error is thrown:

```Error: Invalid index

on main.tf line 8, in locals:
8:     v = length(var.words["nouns"])>=1 ? var.words["nouns"] : [][0]```

Lazy evaluation is why this validation trick works. Only the expression that needs to be evaluated is evaluated—the other control path is ignored. The expression `[][0]` always throws an error if it’s evaluated (since it attempts to access the first element of an empty list), but it’s not evaluated unless the boolean condition is false.

Conditional expressions are most commonly used to toggle whether a resource will be created. For example, if you had a boolean input variable called `shuffle_enabled`, you could conditionally create a resource with the following expression:

` count = var.shuffle_enabled ? 1 : 0`

WARNING Conditional expressions hurt readability a lot, so avoid using them if you can.

3.2.6 More templates

Let’s add two more template files to spice things up a bit. We’ll cycle between them so we have equal number of Mad Libs stories using each template. Make a new template file called observatory.txt in the templates directory, and set the contents as follows.

Listing 3.8 observatory.txt

```THE OBSERVATORY

Out class when on a field trip to a \${adjectives[0]} observatory. It
was located on top of a \${nouns[0]}, and it looked like a giant
\${nouns[1]} with a slit down its \${nouns[2]}. We went inside and
looked through a \${nouns[3]} and were able to see \${nouns[4]}s in
the sky that were millions of \${nouns[5]}s away. The men and
women who \${verbs[0]} in the observatory are called
\${nouns[6]}s, and they are always watching for comets, eclipses,
and shooting \${nouns[7]}s. An eclipse occurs when a \${nouns[8]}
comes between the earth and the \${nouns[9]} and everything
gets \${adjectives[1]}. Next week, we place to \${verbs[1]} the
Museum of Modern \${nouns[10]}.```

Next, make another template file called photographer.txt and set the contents as follows.

Listing 3.9 photographer.txt

```HOW TO BE A PHOTOGRAPHER

Many \${adjectives[0]} photographers make big money
photographing \${nouns[0]}s and beautiful \${nouns[1]}s. They sell
the prints to \${adjectives[1]} magazines or to agencies who use
have to have a \${nouns[3]} camera. You also need an
\${adjectives[2]} meter and filters and a special close-up
\${nouns[4]}. Then you either hire professional \${nouns[1]}s or go
out and snap candid pictures of ordinary \${nouns[5]}s. But if you
want to have a career, you must study very \${adverbs[0]} for at
least \${numbers[0]} years.```

3.2.7 Local file

Instead of outputting to the CLI, we’ll save the results to disk with a `local_file` resource. First, though, we need to read all the text files from the templates folder into a list. This is possible with the built-in `fileset()` function:

```locals {
templates = tolist(fileset(path.module, "templates/*.txt"))
}```

Note Sets and lists look the same but are treated as different types, so an explicit cast must be made to convert from one type to another.

Once we have the list of template files in place, we can feed the result into `local_ file`. This resource generates `var.num_files` (i.e. 100) text files:

```resource "local_file" "mad_libs" {
count    = var.num_files
content  = templatefile(element(local.templates, count.index),
{
nouns      = random_shuffle.random_nouns[count.index].result
verbs      = random_shuffle.random_verbs[count.index].result
numbers    = random_shuffle.random_numbers[count.index].result
})
}```

Two things worth pointing out are `element``()` and `count.index`. The `element()` function operates on a list as if it were circular, retrieving elements at a given index without throwing an out-of-bounds exception. This means `element``()` will evenly divide the 100 Mad Libs stories between the two template files.

The `count.index` expression references the current index of a resource (see figure 3.16). We use it to parameterize filenames and ensure that `templatefile``()` receives template variables from corresponding `random_shuffle` resources.

Figure 3.16 random_nouns and mad_libs are lists of resources and must be kept in sync.

3.2.8 Zipping files

We can create arbitrary numbers of Mad Libs stories and output them in a madlibs directory, but wouldn’t it be great to zip the files together as well? The `archive_file` data source can do just this. It outputs all the files in a source directory to a new zip file. Add the following code to madlibs.tf:

```data "archive_file" "mad_libs" {
type        = "zip"
}```

The `depends_on` meta argument specifies explicit dependencies between resources. Explicit dependencies describe relationships between resources that are not visible to Terraform. `depends_on` is included here because `archive_file` must be evaluated after all the Mad Libs paragraphs have been created; otherwise, it would zip up files in an empty directory. Normally we would express this relationship through an implicit dependency by using an interpolated input argument, but `archive_file` does not accept any input arguments that it would make sense to set from the output of `local_file`, so we are forced to use an explicit dependency, instead.

TIP Prefer implicit dependencies over explicit dependencies because they are clearer to someone reading your code. If you must use an explicit dependency, at least document the reason you are using it and what the hidden dependency is.

For reference, the complete code for madlibs.tf is shown in the following listing.

```terraform {
required_version = ">= 0.15"
required_providers {
random = {
source  = "hashicorp/random"
version = "~> 3.0"
}
local = {
source  = "hashicorp/local"
version = "~> 2.0"
}
archive = {
source  = "hashicorp/archive"
version = "~> 2.0"
}
}
}
variable "words" {
description = "A word pool to use for Mad Libs"
type = object({
nouns      = list(string),
verbs      = list(string),
numbers    = list(number),
})
}

variable "num_files" {
default = 100
type    = number
}

locals {
uppercase_words = { for k, v in var.words : k => [for s in v : upper(s)] }
}

resource "random_shuffle" "random_nouns" {
count = var.num_files
input = local.uppercase_words["nouns"]
}

count = var.num_files
}

resource "random_shuffle" "random_verbs" {
count = var.num_files
input = local.uppercase_words["verbs"]
}

count = var.num_files
}

resource "random_shuffle" "random_numbers" {
count = var.num_files
input = local.uppercase_words["numbers"]
}

locals {
templates = tolist(fileset(path.module, "templates/*.txt"))
}

count    = var.num_files
content  = templatefile(element(local.templates, count.index),
{
nouns      = random_shuffle.random_nouns[count.index].result
verbs      = random_shuffle.random_verbs[count.index].result
numbers    = random_shuffle.random_numbers[count.index].result
})
}

type        = "zip"
}```

3.2.9 Applying changes

We’re ready to apply changes. Run `terraform init` to download the new providers, and follow it with `terraform apply`:

```\$ terraform init && terraform apply -auto-approve
...
local_file.mad_libs[71]: Creation complete after 0s [id=382048cc1c505b6f7c2ecd8d430fa2bcd787cec0]
[id=8b6d5cc53faf1d20f913ee715bf73dda8b635b5d]
[id=4a151807e60200bff2c01fdcabeab072901d2b81]

Apply complete! Resources: 600 added, 0 changed, 0 destroyed.```

Note If you previously ran an `apply` before adding `archive_file`, it will say that zero resources were added, changed, and destroyed. This is somewhat surprising, but it happens because data sources are not considered resources for the purposes of an `apply`.

The files in the current directory are now as follows:

```.
...
├──   templates
│   ├── alice.txt
│   ├── observatory.txt
│   └── photographer.txt
├── terraform.tfstate
├── terraform.tfstate.backup
└── terraform.tfvars```

Here is an example of a generated Mad Libs story for your amusement:

```\$ cat madlibs/madlibs-2.txt
HOW TO BE A PHOTOGRAPHER

Many CHUBBY photographers make big money
photographing BANANAs and beautiful JELLYFISHs. They sell
the prints to BITTER magazines or to agencies who use
have to have a CAT camera. You also need an
ABUNDANT meter and filters and a special close-up
WALNUTS. Then you either hire professional JELLYFISHs or go
out and snap candid pictures of ordinary PANTHERs. But if you
want to have a career, you must study very DELICATELY for at
least 27 years.```

This is an improvement because the capitalized words stand out from the surrounding text and, of course, because we have a lot more Mad Libs. To clean up, perform `terraform` `destroy`.

Note `terraform` `destroy` will not delete madlibs.zip because this file isn’t a managed resource. Recall that madlibs.zip was created with a data source, and data sources do not implement `Delete()`.

3.3 Fireside chat

Terraform is a highly expressive programming language. Anything you want to do is possible, and the language itself is rarely an impediment. Complex logic that takes dozens of lines of procedural code can be easily expressed in one or two functional lines of Terraform code.

The focus of this chapter was on functions, expressions, and templates. We started by comparing input variables, local values, and output values to the arguments, temporary symbols, and return values of a function. We then saw how we can template files using `templatefile``()`.

Next, we saw how to scale up to an arbitrary number of Mad Libs stories by using `for` expressions and `count`. `for` expressions allow you to create higher-order functions with lambda-like syntax. This is especially useful for transforming complex data before configuring resource attributes.

The final thing we did was zip up all the Mad Libs paragraphs with an `archive_ file` data source. We ensured that the zipping was done at the right time by putting in an explicit `depends_on`.

Terraform includes many kinds of expressions, some of which we have not had the opportunity to cover. Table 3.1 is a reference of all expressions that currently exist in Terraform.

Table 3.1 Expression reference

Name

Description

Example

Conditional expression

Uses the value of a boolean expression to select one of two values

condition ? true_value : false_value

Function call

Transforms and combines values

<FUNCTION NAME>(<ARG 1>, <ARG2>)

for expression

Transforms one complex type to another

[for s in var.list : upper(s)]

Splat expression

Shorthand for some common use cases that could otherwise be handled by for expressions

var.list[*].id

Following is the equivalent `for` expression:

[for s in var.list : s.id]

Dynamic block

Constructs repeatable nested blocks within resources

dynamic "ingress" {

for_each = var.service_ports

content {

from_port = ingress.value

to_port = ingress.value

protocol = "tcp"

}

}

String template interpolation

Embeds expressions in a string literal

"Hello, \${var.name}!"

String template directives

Uses conditional results and iterates over a collection within a string literal

%{ for ip in var.list.*.ip }

server \${ip}

%{ endfor }

Summary

• Input variables parameterize Terraform configurations. Local values save the results of an expression. Output values pass data around, either back to the user or to other modules.

• `for` expressions allow you to transform one complex type into another. They can be combined with other `for` expressions to create higher-order functions.

• Randomness must be constrained. Avoid using legacy functions such as `uuid``()` and `timestamp``()`, as these will introduce subtle bugs in Terraform due to a non-convergent state.

• Zip files with the Archive provider. You may need to specify an explicit dependency to ensure that the data source runs at the right time.

• `templatefile``()` can template files with the same syntax used by interpolation variables. Only variables passed to this function are in scope for templating.

• The `count` meta argument can dynamically provision multiple instances of a resource. To access an instance of a resource created with `count`, use bracket notation `[]`.

• No Comment
..................Content has been hidden....................