Silly Things with Terraform v0.11

❤.tf

Terraform has been a best friend for years now. Through thick and thin provisioning, VM ups and downs, and terrible puns, terraform has been a best friend.

With terraform v0.12 just around the corner, it’s worth taking a peek back to how we’ve used terrafor v0.11. If you (ab)used any of the items below it would probably be best to revisit that code in preparation for the newer version.

Lightning Fast Introduction to Terraform and IaC

If you aren’t familiar, terraform is a tool for infrastructure that allows us to build our infrastructure as code (IaC). Code, in the traditional sense, will probably summon thoughts of functions and loops in the minds of individuals experienced with general purpose programming languages. Not so fast, however, as terraform (and many other IaC tools) don’t quite have the features of a general purpose programming language. Instead, we rely and depend on them to allow us to code what we want our infrastructure to loo like in a declarative manner.

As an over simplification, a declarative way to request someone give you a peanut butter and jelly sandwich would be “Give me a peanut and jelly sandwich”. The opposite, imperative, would be listing to that individual every step it takes to create a peanut butter and jelly sandwich.

resource "sandwich" "mysandwich" {
name = "My Sandwich"
type = "PB&J"
}

Here we ask terraform to create our sandwich resource. Code within terraform and its sandwich provider will locate the butter knife, find a spoon, scoop out the jelly, etc, etc, etc. All we need to know and declare is our expectation after the operation. At the end, we expect running the code to be idempotent — it should not trigger a change if the current state already matches the declared state. If a sandwich already exists it should not create a new one (unless we specify another resource for another sandwich, of course).

This makes terraform great for not only managing infrastructure but also as a living document describing your infrastructure. That’s our goal — a living, breathing, way to manage infrastructure with an easy reference. There is no guessing whether or not someone ssh’d into a machine for a last minute change or updated the RAM in the Cloud Provider Console since terraform maintains all of this for us while serving as living documentation.

Silly Things You Should Not Do #1: FizzBuzz

Sometimes people see the code portion of Infrastructure-as-Code and expect a traditional general purpose programming language (think JavaScript, Python, Ruby, etc, etc). Being unfamiliar with tools like terraform can lead to scenarios where an individual correlates the code portion to what they know.

While I haven’t (yet) had someone ask me to draft out Towers of Hanoi, a popular programming challenge for interviews, I have actually had someone ask me to create Fizz Buzz in terraform. I should not need to tell you that this is not a very sane request. :)

Just in case, here’s one solution:

provider "template" {
version = "~> 2.1"
}
variable "numbers" {
default = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
}
data "template_file" "fizzbuzz" {
template = <<EOF
%{ for number in split(",", numbers) ~}
$${number % 3 == 0 ? "Fizz" : "" ~}
$${number % 5 == 0 ? "Buzz" : "" ~}
$${number % 5 == 0 || number % 3 == 0 ? "" : number}
%{ endfor ~}
EOF
vars = {
numbers = "${join(",", compact(var.numbers))}"
}
}
output "result" {
value = "${data.template_file.fizzbuzz.rendered}"
}

Above, you can see that we have to first serialize our array of numbers provided by user input. Values used in templates with terraform must be primitives.

“Note that variables must all be primitives. Direct references to lists or maps will cause a validation error.”Terraform Template File Resource Reference

We then have to split() the data passed in order for us to turn it back into an array for use with our for loop. We will print “fizz” for numbers divisible by 3, “buzz” for numbers divisible by 5, and only the number if the number is not divisible by either 3 or 5.

This is not what you should be doing with terraform. Ever.

Terraform, and Infrastructure-as-Code in general, should be readable and dependable. “Clever” usage like serializing and de-serializing to bypass a language restriction is not clever and particularly not clear to the next person maintaining infrastructure. Our use-case of fizzbuzz (instead of, say, managing infrastructure) is more than likely far outside of the scope of the goals of the terraform team. We can’t be certain this “cleverness” will continue to work.

A Quick Explanation of `count`

Terraform features a count parameter that can be used with resources to instantiate 0 or more of the resource. This is great for creating numbers of identical resources.

A common confusion that you might encounter (or even entertain yourself) is that count can be used with other items like modules.

It can’t. count is for resources.

Specifying a resource with a count of 0 can be used, and often is, as a way to conditionally included pieces of infrastructure. A resource with count = 0 is in a very peculiar state — it exists as a resource definition and you cannot create a resource with the same terraform resource name, but you cannot ever reference or use it.

variable "vegetarian" {
default = true
}
resource "sandwich" "ham" {
count = "${var.vegetarian == true ? 0 : 1}"
}
resource "sandwich" "tofurkey" {
count = "${var.vegetarian == true ? 1 : 0}"
}
resource "meal" "lunch" {
sandwich = "${sandwich.tofurkey.id}"
}

The code above is intended to create a sandwich for our user. Both sandwiches, ham or tofurkey, exist as resources when terraform validates it. At runtime, only one will be created as controlled by the var.vegetarian value.

As it stands above, our pseudocode example will create a vegetarian sandwich for our vegetarian user. What happens if you flip the vegetarian flag?

The tofurkey sandwich referenced for lunch no longer exists and terraform will error. You cannot use an inline if like sandwich.tofurkey.id || sandwich.ham.id and terraform will error. Mostly any obvious thing you might try will result in a terraform error.

The objects terraform uses to manage resources are simply not meant for this. They are validated and sent to the provider with its own validation. You will receive a reference error for a resource that does not exist.

Silly Things You Should Not Do #2: Abuse `count`

If you have decided you absolutely must abuse count for dynamic resources there is a terrible solution: element(concat(a, b, 0)).

concat operates on lists and will receive an undefined or null list without error for some reason within the implementation.

This should probably not work, but does:

variable "vegetarian" {
default = true
}
resource "sandwich" "ham" {
count = "${var.vegetarian == true ? 0 : 1}"
}
resource "sandwich" "tofurkey" {
count = "${var.vegetarian == true ? 1 : 0}"
}
resource "meal" "lunch" {
sandwich = "${element(concat(sandwich.tofurkey.*.id, sandwich.ham.*.id), 0)}"
}

An undefined resource will not happen and the reference will “fall through” without adding any elements to the concatenated list. The element() function will select the first element.

This is a terribly unclear thing to do that outright abuses language features and implementations of built-ins. It is unlikely that terraform developers consider this a valid use of terraform and you will risk this no longer working in future versions.

Silly Things You Should Not Do #3: Abuse features to create Dynamic Nested Blocks

Resources in terraform can include nested blocks. To continue our sandwich example:

resource "sandwich" "tofurkey" {}resource "sandwich" "ham" {
extras {
cheese = "cheddar"
}
}
resource "sandwich" "roastbeef" {
extras {
cheese = "swiss"
}
}
resource "sandwich" "special" {
extras {
cheese = "${var.cheese}"
}
}

Remember our commitment to being declarative. These nested blocks create additional features in our resources, they are validated and passed to the resource provider, and in some cases are optional.

The downstream provider may accept our tofurkey sandwich with no extras declared. It may also only support cheese types swiss or cheddar and reject gouda simply because the provider doesn’t (yet) support it.

Our special sandwich has a variable in use for cheese. That variable, because of the validation, must be swiss or cheddar and no other value. In terraform, you can’t un-define a defined variable. If we want a special sandwich with no cheese terraform has no way to describe that scenario — If none is not implemented by the provider and cheese will not validate with any values other than swiss or cheddaryou are only left with abusing the language.

This is something that confuses many terraform users who have different experiences with different providers. A GitLab Professional Services Implementation Engineer got confused, argued it was possible in terraform v0.11, and cost someone else a position because of it. As a 1 year veteran of DevOps and with less than 6 months on the job it is understandable to be confused, but remember to be mindful of your actions on other people.

Don’t be that person. The next version of terraform will support dynamic nested blocks. :)

Someone, inevitably, will be collaborating with (or without) you on the terraform code you write. Any of the “tricks” above should be applied extremely conservatively — ideally, not at all. :)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store