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.