Problems with Terraform due to the restrictive HCL.

Terraform is a great tool to define cloud environments, virtual machines and other resources, but sadly it’s default usage of HCL (Hashicorp Configuration Language) is very restrictive and makes IaaC (Infrastructure-as-a-code) look more like Infrastructure-as-copy-paste. Here are my findings on all different issues which makes writing Terraform .tf files with HCL pain (as on Terraform v0.7.2). This is usually fine for most simple scenarios, but if you think like a programmer you will find HCL really restrictive when you encounter any of its limitations.

Make no mistake: Nothing said here are showstoppers, but they simply make you write more code than what would be required and you will also need to repeat yourself a lot by doing copy-paste and that’s always a recipe for errors.

Template evaluation.

Template evaluation with template_file. Variables must all be primitives. So you can’t pass a list to a template and then use that list to iterate when rendering a template. Also you can’t use any control structures inside a template. You can still use all built-in functions and you also need to escape the template file if the syntax collides with the Terraform template syntax.

Module variables

Modules can’t inherit variables without explicitly declaring them each time a module is used, or there are no global variables a module could access. This leads to the need to pass every possible required variable in every possible moment when a module is called. Consider rather static variables like “domain name”, “region” or the amazon ssh “key_name”. This leads to manual copy-paste repetition. Issue #5480 tries to address this.

Also when you use a module, you will declare a name for it (standard terraform feature). But you can’t access that name as a variable inside that module (#8706).

module "terminal-machine" {
  source = "./some-module"
  hostname = "terminal-machine" # there is no way to avoid writing the "terminal-machine" twice as you can't access the module name.
}

Variables are not really variables

Terraform supports variables which can be used to store data and then later pass that to a resource or a module, or use those to evaluate expressions when defining a module variable. But there are caveats:

  • You can’t have intermediate variables. This for example prevents you for setting a map which values evaluate from variables and then later merge that map with a module input. You can kinda work around with this with a null_resource, but it’s a hack.
  • You can’t use a variable when defining a resource name: “interpolated resource names are considered an anti-pattern and thus won’t be supported.”
  • You can’t evaluate a variable which name contains a variable. So you can’t do something like this “${aws_sns_topic.${topic.$env}.arn}”.
  • If you want to pass a list to a resource variable which requires a list, you need to encapsulate it again into a list: security_groups = [${var.mylist}]. This looks weird to a programmer.

Control structures and iteration

No control structures, iteration nor loops. HCL is just syntactic sugar for JSON. This means that (pretty much) all current features for iteration in Terraform are implemented inside Terraform itself. So you can have a list of items and use that list to spawn a resource which is customised using the list entries using interpolation syntax:

variable "names" {
  type = "map"
  default = {
    "0" = "Garo"
    "1" = "John"
    "2" = "Eve"
  }
}

resource "aws_instance" "web" {
  count = "${length(var.names)}"

  tags {
    Name = "${element(var.names, count.index)}'s personal instance"
  }
}

This works so that Terraform evaluates the length() function to assign count amount of items there are in the map names and then instantiating the resource aws_instance that many times. Each instantiation evaluates the element() function, so we can customise that instance. This doesn’t however extend to more depth. Say you want that each user has three different instances, one for testing and another for staging. You can’t define another list environments and expect to use both names and environments to declare resources in a nice ways. There are couple of workarounds [1] [2], but they usually really complex and error prone. Also you can’t easily reference a property (such as arn or id) of a created resource in another resource, if the first resource tries to use this kind of interpolation.

A programmers approach would be something like this:

variable "topics" {
  default = ["new_users", "deleted_users"]
}

variable "environments" {
  default = ["prod", "staging", "testing", "development]
}

for $topic in topics {
  # Define SNS topics which are shared between all environments
  resource "aws_sns_topic" "$topic.$env" { ... }

  for $env in environments {
    # Then for each topic define a queue for each env
    resource "aws_sqs_queue" "$topic.$env-processors" { ... }

    # And bind the created queue to its sns topic
    resource "aws_sns_topic_subscription" "$topic.$env-to-$topic.$env-processors" {
      topic_arn = "${aws_sns_topic.${topic.$env}.arn}"
      endpoint = "${aws_sqs_queue.{$topic.$env-processors}.arn}"
    }
  }
}

But that’s just not possible, at least currently. Hashicorp argues that control structures remove the declarative nature of HCL and Terraform, but I would argue that you can still have a language with declarative nature which declares resources and constant variables, but still have control structures like in pure functionally programming languages.

Workarounds?

There have been few projects which have created for example a Ruby DSL which outputs JSON directly into Terraform but they aren’t really maintained. Other options would be to use for example C preprocessor, or just hit a nail into your head and accept that you need to do a lot of copy-pasting and try to minimize the amount of infrastructure which is provisioned using Terraform. It’s still a great tool once you have your .tf files ready: It can query current state, helps with importing existing resources and the dependency graph works well. Hopefully Hashicorp realises that the current HCL could be extended much more while still maintaining its declarative nature.