on
Advent of Code in Terraform
Motivation
Back in the start of December, Ned Bellavance asked a question in the HashiCorp Ambassador slack regarding a challenging mapping in Terraform that sparked lots of engagement and resulted in this blog post regarding Data Transformation in Terraform.
Around the same time colleagues at work were starting to work through the Advent of Code puzzles. As Terraform has been my primary work tool for 1.5 years now I decided it would be fun to see how far I can get using Terraform, and it would be good drilling on how to do more complex transformations in Terraform so that I can make better modules.
Status
I didn’t expect to get very far as Terraform is clearly not the right tool for the job but I did manage to complete 14 stars, out of 50 possible and learnt some things about Terraform along the way.
For the impatient the code is published on Gitlab, but be aware I haven’t been good at commenting the code as I go.
What I do intend to do is to publish a series of blog posts with little examples of what I have learnt while solving the puzzles. First I’ll give a quick walkthrough of the first solution so you can see the general way I approached things.
Parsing the input
First lets get the data into Terraform. For the explanation of the puzzle there is a small example dataset, for Day 1 this looks like the following;
199
200
208
210
200
207
240
269
260
263
This example text I saved to a file example.txt
and I downloaded the real
puzzle input to a file input.txt
. Reading files is easy enough in Terraform
through the use of the file
function.
locals {
input = file("${path.module}/example.txt")
}
Now we have the whole file as a single string but we need a list of numbers to
work with, so first lets split it into individual lines with the split
function. As I like to have newlines at the end of my files to
make git happy I also add compact
to avoid an empty string at the the end of
my list.
input_as_string_list = compact(split("\n", local.input))
Now we’ve got a list of strings but we need numbers if we are going to do maths
on them, which is where the for
expression with tonumber
comes in handy to
convert all of the strings to numbers.
input_as_number_list = [for v in local.input_as_string_list : tonumber(v)]
Finally, a dataset we can work with.
Solving the puzzle
Described briefly, the first part of the puzzle involves counting the numbers that are larger than the previous number in the list. In order to do this I want to do a map operation on the list, outputting the string “increased” for each number that is higher than the previous, and the empty string for everything else.
199 => ""
200 => "increased"
208 => "increased"
210 => "increased"
200 => ""
207 => "increased"
240 => "increased"
269 => "increased"
260 => ""
263 => "increased"
To do this I used the following for
expression with some nested ternary
expressions;
increased_or_not = [for i, v in local.input_as_number_list :
i == 0 ? "" : (v - local.input_as_number_list[i - 1] > 0 ? "increased" : "")
]
Lets break this down a little;
[for i, v in local.input_as_number_list : <some logic> ]
This first bit says to map over each element in the list, placing the value in
the variable v
and the list index in the variable i
.
i == 0 ? "" : (<more logic>)
Here, we use a ternary expression to create a special case returning ""
for
the first element in the list (index == 0), as we have no previous value to
compare it to, then delegating to the nested expression.
v - local.input_as_number_list[i - 1] > 0 ? "increased" : ""
This part is the real logic, stating that if the current value in the list
minus the previous value in the list is greater than 0, we should output the
string "increased"
otherwise output ""
.
Finally we want to count all of the entries in the list that aren’t the empty
string. To do this we can first remove all the empty strings the same way as
earlier with the compact
function. Then use length
to count how many
entries remain in the list.
output_1 = length(compact(local.increased_or_not))
First gold star
Putting all of that together you end up with the following
locals {
input = file("${path.module}/example.txt")
input_as_string_list = compact(split("\n", local.input))
input_as_number_list = [for v in local.input_as_string_list : tonumber(v)]
increased_or_not = [for i, v in local.input_as_number_list :
i == 0 ? "" : (v - local.input_as_number_list[i - 1] > 0 ? "increased" : "")
]
output_1 = length(compact(local.increased_or_not))
}
output "output_1" {
value = local.output_1
}
Running the code with terraform refresh
we get the following result;
$ terraform refresh
╷
│ Warning: Empty or non-existent state
│
│ There are currently no resources tracked in the state, so there is nothing to refresh.
╵
Outputs:
result_1 = 7
7
is the correct result for the example in the puzzle description, so lets
try again with the real input file;
terraform refresh
╷
│ Warning: Empty or non-existent state
│
│ There are currently no resources tracked in the state, so there is nothing to refresh.
╵
Outputs:
output_1 = 1139
Submitting this to the Advent of Code webpage revealed that this was indeed the correct answer for my input.
Part two?
When you complete the first part of the puzzles in Advent of Code, a second part of the challenge is revealed. Depending on how you solved the first part, the second part can either be really easy, or require a complete rewrite of the solution.
In my case, an intentional limit in Terraform tripped me up a little but with a little out of the box thinking I was able to work around the issue. The details of that will have to wait for the next blog post.