Terraform Tricks: override variables

This blog shows how you can override Terraform variables beyond the scope of a module, and without the need for duplicating default values.

February 11, 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.

Use Case: Override Variables

Consider a setup where you are building a CoreOS cluster on AWS. The CoreOS cluster has master nodes and worker nodes, where worker nodes are dedicated to DTAP stages. For each type of CoreOS node we have a dedicated autoscaling group with a corresponding launch configuration. By default, all nodes are built using the current CoreOS ‘stable’ AMI.

However, to test out new CoreOS versions, you occasionally want to use a CoreOS ‘beta’ AMI for your dev and or tst instances.

(tl;dr: Terraform has a built-in function called coalesce that you can use to select either an override variable or a default variable. Keep reading if you want to know how it works and why it’s awesome.)

Terraform variables

Terraform variables are either strings or maps. Other types like booleans, arrays, or integers are not supported, even though Terraform resources can use those types in their attributes.

Looking at our use case, however, a map might look like the perfect choice. We could do something like this:

variable "coreos_amis" {
	default = {
    	master = "ami-c26bcab1"
        dev	= "ami-c26bcab1"
        tst	= "ami-c26bcab1"
        acc	= "ami-c26bcab1"
        prd	= "ami-c26bcab1"
	}
}

...

resource "aws_autoscaling_group" "coreos-dev" {
	...
	launch_configuration = "${aws_launch_configuration.coreos-dev.name}"
}

resource "aws_launch_configuration" "coreos-dev" {
	...
    image_id = "${var.coreos_amis.dev}"
    ...
}

If we wanted to change the AMI for the dev instances, all we have to do is update the dev variable, and we’re done. Or we could add a file called dev-ami_override.tf that looks like this:

resource "aws_launch_configuration" "coreos-dev" {
	image_id = "ami-abcd1234"
}

However, this would mean that we need to specify the very same AMI 5 times in our variable map! Also, if the code is part of a module that we’re implementing, overrides become a bit more difficult. How do we override a value in a map from outside the module? Surely, something like this would be nice:

module "coreos-platform" {
	source = "../modules/coreos-platform"
    
    coreos_amis.dev = "ami-abcd1234"
}

As it turns out, that doesn’t work. If we want to be able to override individual AMIs for individual types of instances from outside the module, they should have separate variables inside the module.

So, something like this, then?

# module code
...
variable "coreos-dev_ami" {
	default = "ami-c26bcab1"
}

variable "coreos-tst_ami" {
	default = "ami-c26bcab1"
}
...

and:

module "coreos-platform" {
	source = "../modules/coreos-platform"
    
    coreos-dev_ami = "ami-abcd1234"
}

Yay! That actually works! Except for that tiny fact that if a new CoreOS stable AMI is released, I now have to update 5 variables.

There must be a nicer way to fix this…

Tricks to the rescue!

If we can’t override a single item in a map, maybe we can have a default_ami variable and then ‘override variables’ that we can leave unset if we don’t want to use them? Well, no, we can’t, since variables must have a value if they are referenced.

But fortunately, we can have something pretty close to that. We can have a default_ami variable, and then override variables that have an empty string as their default value. Then we use the coalesce function in Terraform to select either the override variable (if set), or the default variable. It looks like this:

variable "coreos_default_ami" {
	default = "ami-c26bcab1"
}

variable "coreos_master_ami" {
	default = ""
}

variable "coreos_dev_ami" {
	default = ""
}

...

resource "aws_launch_configuration" "coreos-dev" {
	...
    image_id = "${coalesce(var.coreos_dev_ami, var.coreos_default_ami)}"
    ...
}

If we just implement the module, without any overrides, the dev instances will use the default AMI (ami-c26bcab1). However, if we set one of the override variables, like this:

module "coreos-platform" {
	source = "../modules/coreos-platform"
    
    coreos_dev_ami = "ami-abcd1234"
}

Now the dev instances will use the override AMI (ami-abcd1234). As an added bonus, we only need to update a single line of code when a new CoreOS stable AMI is released. And if you want to deploy your platform using only beta AMIs you can simply override the coreos_default_ami variable.