Recently I’ve been having some fun with writing a fairly complex Terraform module which of course has to make use of Conditional Logic a fair bit. The Terraform documentation covers both Conditionals, Functions and Operators very well, but practical examples are a little lacking. In this short post I’m going to look at how Conditionals work and a few helpful examples of using a few Operators and Functions to extend our functionality.
Conditional Expressions
The Terraform documentation defined the syntax for a conditional expression is as follows:
#--Standard Syntax condition ? true_val : false_val
I found that pretty unhelpful so let’s break it down with a very common, practical example.
Our “condition” is going to be the count meta-argument being set to true within the aws_s3_bucket Resource. We’re going to satisfy this by passing a Variable called provision with “type” bool as the value to be evaluated.
When a boolean is evaluated by Terraform is returned as either 0 (false) or 1 (true). As we’re using this to satisfy the count argument this means that if we set our variable to true the resource will be created and if set to false nothing will happen. Our configuration will look like:
variable "provision" { type = bool description = "Optionally Create Bucket" } resource "aws_s3_bucket" "bucket" { count = var.provision ? 1 : 0 bucket = "tinfoilbucket" acl = "private" }
If we execute an apply with this configuration we can enter a true/false at run time:
terraform apply # var.provision # Optionally Create Bucket # Enter a value: false # # Apply complete! Resources: 0 added, 0 changed, 0 destroyed. terraform apply # var.provision # Optionally Create Bucket # Enter a value: true # # aws_s3_bucket.bucket[0]: Creating... # aws_s3_bucket.bucket[0]: Creation complete after 6s [id=tinfoilbucket] # # Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Conditionally Creating Multiple Resources
A valuable option that isn’t necessarily obvious out of the gate is the need to create multiple resources conditionally. The answer seems obvious usually, we define the number of resources to create using the count meta-argument however we’re already using this to evaluate a true/false condition.
We can still use this if we simply pass a value higher than 1 to our true_val to be evaluated as a conditional when using count, passing this count to be evaluated will result in this number of resources being created. The most dynamic way to do this is by measuring the length of an existing multi-element variable using the length function.
In the below example we’ll be adding an additional variable named bucket_names, this is a list containing two names. This example in brief will provision two S3 Buckets when the variable var.provision is set to true.
variable "provision" { type = bool description = "Optionally Create Multiple Buckets" } variable "bucket_names" { type = list(string) #--Define variable as a list of strings description = ["tinfoilbucket1", "tinfoilbucket2"] #--Define the two items in the list } resource "aws_s3_bucket" "bucket" { count = var.provision ? length(var.bucket_names) : 0 #--Evaluate conditional. If var.provision is true, provision the amount of buckets defined in var.bucket_names bucket = var.bucket_names[count.index] #--Create buckets as named in var.bucket_names, iterating over list acl = "private" }
If we apply, we can see the logic in action and that our iteration works correctly:
terraform apply # var.provision # Optionally Create Multiple Buckets # Enter a value: false # # Apply complete! Resources: 0 added, 0 changed, 0 destroyed. terraform apply # var.provision # Optionally Create Bucket # Enter a value: true # # aws_s3_bucket.bucket[0]: Creating... # aws_s3_bucket.bucket[1]: Creating... # aws_s3_bucket.bucket[0]: Creation complete after 6s [id=tinfoilbucket1] # aws_s3_bucket.bucket[1]: Creation complete after 7s [id=tinfoilbucket2] # # Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Further Options
This is good, but it’s limited on it’s own. Let’s bring some Operators in to the mix and see what other options we have with some more common examples.
In the below example we’ll use && and Operator to ensure that an S3 Bucket is deployed only when BOTH the deploy_environment and deploy_storage are true:
variable "deploy_environment" { type = bool description = "Optionally Create Keys" } variable "deploy_storage" { type = bool description = "Optionally Create Keys" } resource "aws_s3_bucket" "bucket" { count = var.deploy_environment && var.deploy_storage ? 1 : 0 bucket = "tinfoilbucket" acl = "private" }
In the below example we’ll use both the && and ! Operator to ensure that an S3 Bucket is deployed when the deploy_storage variable is true but NOT when the deploy_environment is true:
variable "deploy_environment" { type = bool description = "Optionally Create Keys" } variable "deploy_storage" { type = bool description = "Optionally Create Keys" } resource "aws_s3_bucket" "bucket" { count = !var.deploy_environment && var.deploy_storage ? 1 : 0 bucket = "tinfoilbucket" acl = "private" }
In the below example we’ll use both the | | Operator to ensure that an S3 Bucket is deployed when EITHER of the deploy_environment and deploy_storage are true:
variable "deploy_environment" { type = bool description = "Optionally Create Keys" } variable "deploy_storage" { type = bool description = "Optionally Create Keys" } resource "aws_s3_bucket" "bucket" { count = var.deploy_environment || var.deploy_storage ? 1 : 0 bucket = "tinfoilbucket" acl = "private" }
Other Operators exist and innumerable combinations can be made from them.
Taking A Look At The Coalesce Function
This functionality is great but I’ve found an increasing use for both the coalesce Function and I don’t often see it coming up in conversation.
A design I like to embrace is a common naming convention throughout a given environment. Within Terraform I typically like to create this name using some string manipulations in Locals with the name based on some Random generation, however I also like to give the operator the option to provide their own names via an Input Variable.
The coalesce Function allows us to do both so let’s take a look how that works in a config:
variable "custom_bucket_name" { #--Input Variable for Custom Bucket name. Null by default type = string description = "Custom Bucket Name" default = null } resource "random_string" "bucket_id" { #--Generate random 5 letter ID for dynamic bucket name length = 5 min_lower = 5 number = false special = false } locals { bucket_dynamic_name = "tinfoilbucket-${random_string.bucket_id.id}" #--Generate Dynamic Bucket Name bucket_name = coalesce(var.custom_bucket_name, local.bucket_dynamic_name) #--Coalasece arguments } resource "aws_s3_bucket" "bucket" { bucket = local.bucket_name #--Create Bucket with name from the results of coalesce acl = "private" }
If we apply this without defining a value for custom_bucket_name we should see a randomly generated bucket name:
terraform apply # random_string.bucket_id: Creating... # random_string.bucket_id: Creation complete after 0s [id=jdhuk] # aws_s3_bucket.bucket: Creating... # aws_s3_bucket.bucket: Creation complete after 3s [id=tinfoilbucket-jdhuk] # # Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
If we set an ID for the bucket to tinfoilbucket-abcde we should see it take precedence:
terraform apply # random_string.bucket_id: Creating... # random_string.bucket_id: Creation complete after 0s [id=jdhuk] # aws_s3_bucket.bucket: Creating... # aws_s3_bucket.bucket: Creation complete after 3s [id=tinfoilbucket-abcde]] # # Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
So what’s happening here?
The behaviour of coalesce is to iterate over a given number of arguments and land on the first one that isn’t null. By setting the custom Variable to have a default value of null and making it the first option for coalesce to use, we can ensure that if a custom name has provided for our bucket, it will be used and if not then a randomly generated name will be used. Very useful.
Further Reading
There’s a lot more power to unlock and this article really only touches on the tiniest part of the surface, for further reading I strongly recommend reading the Terraform tutorial on Terraform Functions.