Terraform Tricks: simulating conditional logic
You can simulate conditional logic using interpolation functions like distinct and coalesce to modify how a string looks, depending on your variables
September 27, 2016
terraform
I have been using Terraform in production since version 0.4.0, and most of that experience has been pretty awesome. Especially considering Terraform is still pre-1.0 software. However, Terraform does have some shortcomings here and there. This series of ‘Terraform Tricks’ blogs will be about working around some of the shortcomings of Terraform, as well as exploring some of the less-known (or undocumented) awesomeness.
Background: evolving platforms
At my current project I’m building a container platform on AWS, and I use Terraform to deploy it. However, since we’re using all kinds of new technology that is still evolving heavily, we have completely re-worked our stack multiple times in the past year. It’s one of the hazards of working with cutting-edge platforms.
Currently, we’re in the process of transitioning from a setup where we have a single platform that contains everything, to a setup where each environment, or stage, consists of at least 2 dedicated, isolated platforms. The inner workings of our platform remain largely unchanged, but on a Terraform level, the changes are massive.
Both in an attempt to reduce duplication, and simply to challenge ourselves, we try to adapt our Terraform modules for things like RDS databases, S3 buckets, ElasticSearch clusters, etcetera so that they work with both the old and the new setups. And sometimes that is quite the challenge.
Use case: backwards compatibility in a module
Consider a Terraform module for creating RDS database instances. On the current platform, its implementation would look something like this:
module "rds-foo-dev" {
...
platform_name = "${var.platform_name}"
stage = "dev"
db_name = "foo"
db_subnet_group_name = "${aws_db_subnet_group.rds.name}"
rds_security_group_id = "${aws_security_group.dev-rds.id}"
}
The resulting RDS instance would get a name using the following scheme:
<platform_name>-<db_name>-<stage>
For instance: infra-prod-foo-dev
.
The module implements this in the following way:
resource "aws_rds_instance" "db" {
identifier = "${var.platform_name}-${var.db_name}-${var.stage}"
...
}
Now on our new setup, we no longer use platform_name
, but both db_name
and stage
persist. The naming scheme also changes, to:
<stage>-<db_name>
Challenge: simulating conditional logic in Terraform
If Terraform had supported conditional logic, like if-statements, life would have been simple. I could write something like this:
if ( ${var.platform_name} != "" ) {
identifier_string = "${var.platform_name}-${var.db_name}-${var.stage}"
} else {
identifier_string = "${var.stage}-${var.db_name}"
}
resource "aws_db_instance" "db" {
identifier = "${identifier_string}"
...
}
Or something like that. But unfortunately, Terraform doesn’t support this kind of conditional logic. So, is there really no way to make this work?
Tricks to the rescue!
If we look at the RDS identifier string, we see it consists of 2 or 3 parts:
- Platform Name (optional,
platform_name
) -> A - Stage Name (
stage
) -> B - Database Name (
db_name
) -> C
For the existing setup, we need both A, B, and C and the formula is: id = A-C-B
For the new setup, we need just B and C, and A will be unset. The formula is: id = B-C
The key here is that we know that on the new setup, A will be unset. So we could combine the two formula’s as follows:
field 1: A or B
field 2: C
field 3: B (if A is set) or empty (if A is unset)
So:
id = (A or B) - C - (( A => B ) or ( !A => !B))
Great! Now how do we implement that in Terraform? Well, a little like this:
resource "aws_db_instance" "db" {
identifier = "${join("-", distinct(list(coalesce(var.platform_name, var.stage), var.db_name, var.stage)))}"
...
}
Hold the phone. What exactly just happened? What’s all this join
, distinct
, coalesce
stuff, and why is var.stage
in there twice? Let me break it down:
coalesce
takes a list of strings and picks the first string that is not empty. In this case, ifplatform_name
is set, it will pick that, and otherwise it will pickstage
. This implements the logic for field 1list
takes a set of strings, and turns them into a listdistinct
is the magic trick for field 3. Ifplatform_name
(A) is unset, field 1 will default tostage
(B), in which case field 3 should be empty. To simulate this conditional logic, we let field 3 default tostage
and then apply thedistinct
function. This function will iterate over a list, and remove every subsequent occurrence of an item (in this casestage
). So if field 1 defaults tostage
, thedistinct
function will removestage
from field 3, making it emptyjoin
wraps everything, and adds the dashes-
in between the fields.join("-", A, B, C)
would result inA-B-C
.