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 supportscount
, which we can use in setting thetriggers.port
attribute. We can then read it asnull_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 anoutput
’svalue
.
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 invar.nameservers
, and format them appropriately. Thens_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 ofIN NS
andIN A
entries we need. - Then we convert each list into a single string (which is what
var
parameters of ourtemplate_file
expect), where the original list items are joined by a newline\n
character. - Last, we use the resulting stings “blah-blah
\n
blah-blah” as values to substitute templatevars
.
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!
Leave a Reply