DEV Community

Cover image for Terraform - Fun with Functions
Marcel.L
Marcel.L

Posted on

Terraform - Fun with Functions

Overview

In todays tutorial we will take a look at Terraform functions and how we can use them in a few real world examples, and boy are there many functions to get creative and have fun with.

But what are they?

When writing Infrastructure as Code, you may come across certain complexities or maybe you want to improve or simplify your code by using Terraform functions. You can even use functions to guardrail and safeguard your code from platform related limitations. (For example character limitations or case sensitivity when building certain resources in a cloud provider like Azure).

Functions are expressions to transform and combine values in order to manipulate these values to be used in other ways. Functions can also be nested within each other.

Most Terraform functions follow a common syntax, for example:

<FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)
Enter fullscreen mode Exit fullscreen mode

Example

NOTE: You can use terraform console in a command prompt to run any of the function examples shown later, or to test your own function logic.

Say for example you want to provision an Azure storage account using Terraform. As you may know, storage account names in Azure have certain name rules and character limitations.

The length of a storage account name must be between 3-24 characters, and can only be lowercase letters and numbers.

Take this example of provisioning a storage account in Azure:

variable "storage_account_name" {
  type        = string
  description = "Specifies Storage account name"
  default     = "MySuperCoolStorageAccountName9000"
}

resource "azurerm_storage_account" "example" {
  name                     = var.storage_account_name
  resource_group_name      = "MyRgName9000"
  location                 = "uksouth"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}
Enter fullscreen mode Exit fullscreen mode

As you can see from the above example the storage account name provided by the default value in the variable 'storage_account_name' is: 'MySuperCoolStorageAccountName9000'

Because of the provider limitations, if the default value is used in the deployment, this resource creation would fail.

So how can we safeguard that the default value, or any value that is provided will always work?

You guessed it, we can use Terraform functions.

So we will use two functions namely, 'substr' and 'lower':

Lets look at each function:

  • substr extracts a substring from a given string by offset and (maximum) length. Usage: substr(string, offset, length)
  • lower converts all cased letters in the given string to lowercase. Usage: lower(string)

So lets test this using terraform console:

$ substr("MySuperCoolStorageAccountName9000", 0, 24)
"MySuperCoolStorageAccoun"
Enter fullscreen mode Exit fullscreen mode

The result 'MySuperCoolStorageAccoun' has now been truncated to only 24 characters, but this would still fail because there are still uppercase characters present. Let's nest this inside the 'lower' function:

$ lower(substr("MySuperCoolStorageAccountName9000", 0, 24))
"mysupercoolstorageaccoun"
Enter fullscreen mode Exit fullscreen mode

This is much better! The storage account can now be provisioned by simply amending our original terraform code as follow:

variable "storage_account_name" {
  type        = string
  description = "Specifies Storage account name"
  default     = "MySuperCoolStorageAccountName9000"
}

resource "azurerm_storage_account" "example" {
  name                     = lower(substr(var.storage_account_name, 0, 24))
  resource_group_name      = "MyRgName9000"
  location                 = "uksouth"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}
Enter fullscreen mode Exit fullscreen mode

But what if we want to improve this even more, by making the value always work, but always be unique as well?

Maybe we can shorten the name a bit more using 'subsr' and then add a unique random string?

As you thought, yes we can!!!

Let's look at another special function called 'uuid':

  • uuid generates a unique identifier string. Usage: uuid()

So lets test this using terraform console again:

$ uuid()
"908b8d83-f33e-aa2e-7318-8232077dfe10"
Enter fullscreen mode Exit fullscreen mode

First, let's shorten our storage account name down to 18 characters:

$ lower(substr("MySuperCoolStorageAccountName9000", 0, 18))
"mysupercoolstorage"
Enter fullscreen mode Exit fullscreen mode

Now that our storage account name is only 18 characters long, we are left with 6 characters to play around with, that we can generate a random identifier string that would act as a suffix, using the 'uuid' function with the 'substr' to get the following result:

$ substr(uuid(), 0, 6)
"e807be"
Enter fullscreen mode Exit fullscreen mode

You may be wondering?... How can we combine the function: 'lower(substr("MySuperCoolStorageAccountName9000", 0, 18))' with the function that creates the unique suffix: 'substr(uuid(), 0, 6)'?

You guessed it, there is a function we can use!!!

Let's look at the function called 'join':

  • join produces a string by concatenating together all elements of a given list of strings with the given delimiter. Usage: join(separator, list)

So as a basic example join can combine two strings in the following way:

$ join("", ["StringA", "StringB"])
"StringAStringB"
Enter fullscreen mode Exit fullscreen mode

Let's apply 'join' to our storage account name function and unique suffix function:

$ join("", [lower(substr("MySuperCoolStorageAccountName9000", 0, 18)), substr(uuid(), 0, 6)])
"mysupercoolstoraged29fc5"
Enter fullscreen mode Exit fullscreen mode

Voila! We now have a randomly generated storage account name, that will always be unique and not be limited to character and case limitations for our storage account/s.

Let's run this function a few times to see the results:

$ join("", [lower(substr("MySuperCoolStorageAccountName9000", 0, 18)), substr(uuid(), 0, 6)])
"mysupercoolstoraged29fc5"

$ join("", [lower(substr("MySuperCoolStorageAccountName9000", 0, 18)), substr(uuid(), 0, 6)])
"mysupercoolstorage3b39e2"

$ join("", [lower(substr("MySuperCoolStorageAccountName9000", 0, 18)), substr(uuid(), 0, 6)])
"mysupercoolstoragefe9d25"

$ join("", [lower(substr("MySuperCoolStorageAccountName9000", 0, 18)), substr(uuid(), 0, 6)])
"mysupercoolstorage716b99"
Enter fullscreen mode Exit fullscreen mode

Lastly, let's apply this to our original code:

variable "storage_account_name" {
  type        = string
  description = "Specifies Storage account name"
  default     = "MySuperCoolStorageAccountName9000"
}

resource "azurerm_storage_account" "example" {
  name                     = join("", [lower(substr(var.storage_account_name, 0, 18)), substr(uuid(), 0, 6)])
  resource_group_name      = "MyRgName9000"
  location                 = "uksouth"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}
Enter fullscreen mode Exit fullscreen mode

Bonus example

If you are familiar with provisioning resources in the cloud on Azure you'll know that each resource has a resource ID.

Here is a fun little function that I have used in the past to get the last element of any resource ID (usually the name of the resource), without fail:

#Basic Example
$ element(split("/", "/x/y/z"), length(split("/", "/x/y/z"))-1)
"z"

#Resource Group name based of resource ID
$ element(split("/", "/subscriptions/829efd7e-aa80-4c0d-9c1c-7aa2557f8e07/resourceGroups/MSDO-Lab-ADO"), length(split("/", "/subscriptions/829efd7e-aa80-4c0d-9c1c-7aa2557f8e07/resourceGroups/MSDO-Lab-ADO"))-1)
"MSDO-Lab-ADO"

#VNET name based of resource ID
$ element(split("/", "/subscriptions/829efd7e-aa80-4c0d-9c1c-7aa2557f8e07/resourceGroups/Pwd9000-EB-Network/providers/Microsoft.Network/virtualNetworks/UKS-EB-VNET"), length(split("/", "/subscriptions/829efd7e-aa80-4c0d-9c1c-7aa2557f8e07/resourceGroups/Pwd9000-EB-Network/providers/Microsoft.Network/virtualNetworks/UKS-EB-VNET"))-1)
"UKS-EB-VNET"

#Key Vault name based on Resource ID
$ element(split("/", "/subscriptions/829efd7e-aa80-4c0d-9c1c-7aa2557f8e07/resourceGroups/Pwd9000-EB-Core/providers/Microsoft.KeyVault/vaults/pwd9000-core-kv"), length(split("/", "/subscriptions/829efd7e-aa80-4c0d-9c1c-7aa2557f8e07/resourceGroups/Pwd9000-EB-Core/providers/Microsoft.KeyVault/vaults/pwd9000-core-kv"))-1)
"pwd9000-core-kv"
Enter fullscreen mode Exit fullscreen mode

Take a closer look at the functions in use above and how they are combined and nested together.

  • element retrieves a single element from a list. Usage: element(list, index)
  • split produces a list by dividing a given string at all occurrences of a given separator. Usage: split(separator, string)
  • length determines the length of a given list, map, or string. Usage: length(["a", "b"])

NOTES on 'element':

$ element(["a", "b", "c"], 1)
"b"
Enter fullscreen mode Exit fullscreen mode

If the given index is greater than the length of the list then the index is "wrapped around" by taking the index modulo the length of the list:

$ element(["a", "b", "c"], 3)
"a"
Enter fullscreen mode Exit fullscreen mode

To get the last element from the list use 'length' to find the size of the list (minus 1 as the list is zero-based) and then pick the last element:

$ element(["a", "b", "c"], length(["a", "b", "c"])-1)
"c"
Enter fullscreen mode Exit fullscreen mode

Conclusion

There are so many more cool Terraform functions out there to make your code even better and more robust!

Go check out the official documentation for more details.

I hope you have enjoyed this post and have learned something new. ❤️

Author

Like, share, follow me on: 🐙 GitHub | 🐧 Twitter | 👾 LinkedIn

Top comments (1)

Collapse
 
hinodeya69 profile image
Hinodeya69

Great Thanks @pwd9000