Terraform how-to: create a variable list of numbered items

Terraform how-to: create a variable list of numbered items

In a couple of templates I worked on I came across the need to create a variable list of numbered strings that is a product of a different list.

Dealing with this kind of thing is easy when you’re dealing with resources (that support count). It’s less straight-forward when you need to create a local variable, say, to use for a resource’s value, or an output, that don’t.

Let’s have a look at a dirty hack that can help. 🙂

Task 1

I needed to generate an output that is a list of URLs with an incrementing port number, e.g.,:

http://server.com:9000
http://server.com:9001
...etc

 

Trick was, I needed a different number of these based on the length() of a list variable. After poking around the Interwebz, I’ve managed to pull together a solution that worked for me, like so:

variable "var1" {
  default = [
    "something",
    "something else",
    "something yet else"
  ]
}

variable "fqdn" {
  default = "server.com"
}

resource "null_resource" "port_list" {
  count = "${length(var.var1)}"

  triggers {
    port = "${9990 + count.index}"
  }
}

output "My_Cool_Servers" {
  value = "${formatlist("http://${var.fqdn}:%s",
    null_resource.port_list.*.triggers.port)}"
}

 

How this works:

  • The null_resource of course supports count, which we can use in setting the triggers.port attribute. We can then read it as null_resource.port_list.[x].triggers.port, or access as a list just as we do above – null_resource.port_list.*.triggers.port.
  • We do the later, and use the result as the input to the formatlist function, that we can stick directly into an output’s value.

Task 2

This one involved adding something to a template_file. You of course have already discovered that resource (or datasource) parameters don’t support count. Thus the way to insert something akin to the above into a template_file is to prepare a local variable with the complete value, and then do the substitution.

Say my template file is a BIND DNS zone, and I need to insert some NS records; except I don’t know ahead of time how many of those I’ll have!

Let’s see what we can do:

variable "nameservers" {
  type = "list"
  # default for illustration purposes:
  default = ["1.1.1.1", "2.2.2.2", "3.3.3.3"]
}

variable "dns_domain" {
  default = "blah.com"
}

variable "dns_subdomain" {
  default = "test"
}

variable "zone_serial" {
  default = "2018050101"
}

# Let's set up our null_resource with the bits we need
resource "null_resource" "ns" {
  # This creates a number of strings we need for NS records, based on the
  # list of nameserver IPs
  count = "${length(var.nameservers)}"

  triggers {
    # This creates "nsN" string, N based on the count.index
    ns_host = "ns${format("%d", count.index + 1)}"

    # This creates "nsN.test.blah.com" string, N based on the count.index
    ns_fqdn = "ns${format("%d", count.index + 1)}.${var.dns_subdomain}.${var.dns_domain}"
  }
}

# Now, we need to prepare the list of NS and A records for our nameservers
#
locals {
  # Create a list of "  IN NS nsN.test.blah.com." strings
  in_ns_list = "${formatlist("    IN NS %s.",
    null_resource.ns.*.triggers.ns_fqdn)}"

  # Pull the list into a single string, joined by the new line character
  in_ns_string = "${join("\n", local.in_ns_list)}"

  # Create a list of "nsN  IN A x.x.x.x"
  ns_in_a_list = "${formatlist("%s  IN A %s",
    null_resource.ns.*.triggers.ns_host,
    var.nameservers)}"

  # Pull the list into a string joined by new lines
  ns_in_a_string = "${join("\n", local.ns_in_a_list)}"
}

# Finally, we're ready to render our template
#
data "template_file" "glb_zone" {
  template = <<EOF
; $TTL used for all RRs without explicit TTL value

$TTL    30

$ORIGIN $${dns_subdomain}.$${dns_domain}.

@  1D  IN  SOA ns1.$${dns_subdomain}.$${dns_domain}. hostmaster.$${dns_subdomain}.$${dns_domain}. (
                $${zone_serial} ; serial
                3H ; refresh
                15 ; retry
                1w ; expire
                3h ; nxdomain ttl
                )

$${in_ns}

$${ns_in_a}

EOF

  vars {
    dns_domain    = "${var.dns_domain}"
    dns_subdomain = "${var.dns_subdomain}"
    zone_serial   = "${var.zone_serial}"
    in_ns         = "${local.in_ns_string}"
    ns_in_a       = "${local.ns_in_a_string}"
  }
}

output "my_zone" {
  value = "${data.template_file.glb_zone.rendered}"
}

 

What we do here is:

  • Create a number of copies of null_resource, one per entry in var.nameservers, and format them appropriately. The ns_fqdn one isn’t strictly necessary, but it is useful to make template more DRY (I used it in a couple more places in addition to what’s shown above).
  • We then formatlist() both with the subdomain/domain/IP(s), so we have a list of IN NS and IN A entries we need.
  • Then we convert each list into a single string (which is what var parameters of our template_file expect), where the original list items are joined by a newline \n character.
  • Last, we use the resulting stings “blah-blah\nblah-blah” as values to substitute template vars.

Closing comments

I don’t pretend to be the ultimate authority on Terraform – what you see served my needs, and I’ll just leave it at that.

If you know of a better way to do this – please kindly leave a comment, and thank you!

About Dmitri Kalintsev

Some dude with a blog and opinions ;) View all posts by Dmitri Kalintsev

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: