Using Terraform’s Try() ,Can(), and Input Validation
HahsiCorp has added two new tools in Terraform. As of Terraform v.12.20
there are two new functions available for consumers try()
and can()
. Along with these two functions there is an experimental feature available, variable_validation
. In this article we’re going to look into how these new functions are used and how they works.
This article was originally published on Medium. Link to the Medium article can be found here.
All code snippets can be found at https://github.com/karl-cardenas-coding/terraform-functions
Note: Variable validation is an experimental feature as of v12.20 use with caution as it is not recommended for production usage at this time.
The can and try()function can only catch and handle dynamic errors resulting from access to data that isn’t known until runtime. It will not catch >errors relating to expressions that can be proven to be invalid for any input, such as a malformed resource reference.
Can()
The can()
function attempts to execute the following code provided inside of it and returns a boolean value. The main purpose behind can()
function is input validation according to the official documentation. Let’s put it to test.
To enable input_validation
add the following code block to your Terraform configuration.
|
|
|
|
Shout out to @d-henn for the regex example
In the example above we have a variable named “os”, short for “operating system”. This variable is also leveraging the new validation functionality. So let’s break things down here. The validation block has two components:
- condition (required)
- error_message (required) (does NOT support interpolation)
The syntax for the can()
is can(logic for test, value or variable to test)
. In the example above, the variable had the value “linux” hard-coded as a default value. Let’s change that to another value, say “z/OS” and see how it behaves on a terraform plan
or a terraform apply
|
|
Pretty neat! The error message is pretty descriptive due to our ability to author it. Terraform also returns the file name and location in the file for where the incorrect variable value is can.tf:15,3–13
.
Fun fact, variable validation is opinionated as it expects proper English grammar 👵🏻.
|
|
Input validation can also be used without the can()
. In the code snippet below you can see that the length of the variable “word” is evaluated to see if it is greater than 1. The variable is tied to the random pet provider and will dictate how many pets are in the word string generated.
|
|
HashiCorp does call out in their documentation that can()
should not be used for error handling, or any context outside of input validation (though technically possible). For those other scenarios try()
is recommended. So on that note, let’s move on to the try()
Try()
The try() evaluates all [arguments …]
expression passed into it, and it will return the value of the 1st one that does not return an error. At this time, try()
is recommended for use in local values variables. The main reason behind this recommendation is for reduced code complexity and to only use it for normalization. Perhaps future use will allow consumers to wrap resources blocks inside a try()
?
Before we dive the into example code below, let’s talk through it at a high level.
Purpose: To query an endpoint. This query retrieves all public IPs of the AWS services. https://ip-ranges.amazonaws.com/ip-ranges.json
How: The Terraform data resource http
will query Amazon’s endpoint that return a JSON response.
Locals: This is where our try()
logic will come into action, for our three variables, syncToken
, services
, and regions
.
Reason: The reasoning behind the try()
blocks is in case Amazon changes the JSON schema or perhaps the service(s) are no longer exposing public IPs. If a breaking change were to occur then we can take that into account.
|
|
So with that explanation out of the way, let’s dive into the local
block. The first variable syncToken
, attempts to use the jsondecode
on the response from data.http.primary-server.body
.
|
|
The jsondecode
functions converts a JSON object into HCL, which then allows us to access the the syncToken value by using dot notation at the end. If the server were to not return a syncToken then we have a fallback value of “NO TOKEN AVAILABLE”. The fallback value can be set to anything the consumer desires. Without the try()
function, this would had resulted in an ugly error.
Without the try()
block this stopped our Terraform run. Observe the results below with the try()
block in our code.
|
|
Although we didn’t receive the syncToken from the endpoint, our fallback value kicked in and our Terraform run was able to complete. Neat!
For()
*Skip to conclusion if you are familiar with the for loop
To retrieve the filtered list of all AWS S3 IP addresses, we can simply leverage the Terraform for
loop expression. If you haven’t played with the for loop don’t let the code below intimidate you. Allow me to break it down line by line:
-
Wrap it around a
try()
#line 3
-
Retrieve all unique values through the
distinct()
#line 3
-
We want the results to be of the type list, so let’s use
[]
and wrap our for loop inside of it#line 3
-
Declare the loop using the keyword
for
. Let’s use a temporary variable named items to hold each unique value as we iterate through the provided list. The provided list in this loop will be the resource attribute reference fromdata.http.primary-server.body
. However, because we don’t want to work with a raw JSON object, let’s convert it to HCL type so we can use dot notation. Therefore we wrap the list insidejsondecode()
. Lastly, because we can use expect dot notation, let’s go after the prefixes attribute in our object.#line 4
-
As we iterate trough our loop, let’s add each IP address, but only if its service is S3
#line 5
-
Let’s close out our loop, and pass the fallback value should the list we passed to the loop not exist. In this case, we return a string explaining the issue.
It helps to look at the JSON object from https://ip-ranges.amazonaws.com/ip-ranges.json to better understand the dot notation
|
|
Conclusion
The can()
and try()
functions are pretty neat and will certainly add a lot of value to consumers’ configuration templates. Just remember the following rules of thumb before starting to use these new functions.
can()
is intended for the experimental featureinput_validation
and the expression must return a boolean.try()
should be used for operations related to normalization, preferably in thelocals{}
code block.try()
will return the first non-error resulting expression if multiple arguments are provided[arguments…]
.- Neither function can handle dynamic errors resulting from access to data that isn’t known until runtime.
Hopefully you learned something new! Get out there and automate your life away from ClickOps!