Terraform template conditional

Hello. I have the following template for which I’m using a conditional (%{ if vault_role == "transit" ~}):

#cloud-config
${yamlencode({
  users = [
    {
      name   = "root"
      groups = "users"
      ssh_authorized_keys = [
        root_ssh_key,
	provision_node_ssh_key,
      ]
      shell = "/bin/bash"
    },
  ]
  write_files = [
        {
        content = bashrc
        path = "/root/.bashrc"
        },
        {
        content = consul_config
        path = "/etc/consul.d/consul.hcl"
        },
        {
        content = consul_acl
        path = "/etc/consul.d/consul-acl.hcl"
        },
        {
        content = ssh_template
        path = "/etc/consul-template.d/ssh-template.json"
        },
        {
        content = consul_ca
        path = "/etc/consul.d/certs/ca.crt"
        },
        {
        content = consul_server_crt
        path = "/etc/consul.d/certs/host.crt"
        },
        {
        content = consul_server_key
        path = "/etc/consul.d/certs/host.key"
        },
        {
        content = vault_config
        path = "/etc/vault.d/vault.hcl"
        },
        {
        content = vault_ca
        path = "/opt/vault/tls/ca.crt"
        },
        {
        content = vault_crt
        path = "/opt/vault/tls/tls.crt"
        },
        {
        content = vault_key
        path = "/opt/vault/tls/tls.key"
        },
	%{ if vault_role == "transit" ~}
	{
	content = vault_bootstrap_transit
	path = "/tmp/bootstrap_transit.py"
	},
	%{ endif ~}
  ]
})}

And this is the cloud init config in main.tf:

data "template_cloudinit_config" "vault_transit" {
        # split in parts - 1st is cloud-init cfg as such; from 2nd onwards, shell scripts.
        # default gzip is true + base64 encoded (for proxmox don't encode or zip the cloud-init)
        gzip = false
        base64_encode = false
        part {
                filename = "cloud-init.cfg"
                content_type = "text/cloud-config"
                content = templatefile("${path.module}/files/vault/vault-userdata.tpl", {
                        vault_role = "transit"
                        consul_ca = file("${path.module}/files/ssl-certs-consul/ca/ca.crt")
                        consul_server_crt = file("${path.module}/files/ssl-certs-consul/certs/host.crt")
                        consul_server_key = file("${path.module}/files/ssl-certs-consul/certs/host.key")
                        root_ssh_key = file("${path.module}/files/id_ed25519.pub")
                        provision_node_ssh_key = file("${path.module}/files/provision_node_ssh_key.pub")
                        # we are using the same certificate for all consul instances at this point
                        consul_config = templatefile("${path.module}/files/consul/consul-client-config.tftpl", {
                                consul_datacenter = var.consul_datacenter
                                ui_config = "false"
                                server_role = "false"
                                retry_join = var.retry_join
                                consul_domain = var.consul_domain
                                consul_keygen = var.consul_keygen
                        })
                        ssh_template = templatefile("${path.module}/files/consul/ssh-template.tftpl", {
                                tf_hostname = each.key
                        })
                        consul_acl = templatefile("${path.module}/files/consul/consul-client-acl.tftpl", {
                                consul_bootstrap_token = data.external.bootstrap_consul.result.token
                        })

                        bashrc = templatefile("${path.module}/files/common/bashrc", {
                                role = "true"
                        })
                        vault_ca = file("${path.module}/files/ssl-certs-vault/ca/ca.crt")
                        vault_crt = file("${path.module}/files/ssl-certs-vault/certs/host.crt")
                        vault_key = file("${path.module}/files/ssl-certs-vault/certs/host.key")
                        vault_config = templatefile("${path.module}/files/vault/vault-config.tftpl", {
                                vault_hostname = "${each.key}"
                        })

                })
        }
        part {
                filename = "initialise.sh"
                content_type = "text/x-shellscript"
                content = templatefile("${path.module}/files/vault-transit/initialise.sh", {
                        tf_hostname = "vault-transit"
                        root_ssh_key = file("${path.module}/files/id_ed25519.pub")
                })
        }
}

So when I try to run it, I get the following error:

Call to function “templatefile” failed: ./files/vault-transit/vault-userdata.tpl:59,2-3: Invalid expression; Expected the start of an expression, but found an invalid expression token…

Maybe I cannot use conditionals within the yamlencode directive?

Hi @lethargosapatheia,

The important thing to notice here is that in Terraform templates there are two separate “parsing modes”:

The parser starts in literal mode where anything you’ve written is taken as a literal string to include directly in the output. When it encounters the ${ sequence, which represents string interpolation, it changes to expression parsing mode where the syntax is exactly the same as you’d use when assigning a value to an argument inside a resource block (for example).

The %{ sequence is also valid only in the literal mode, because it’s a template language marker rather than an expression marker. A template for sequence is for repeated string concatenation, which is not suitable for describing elements of an object as you are doing inside this yamlencode call.

I think the expression language equivalent of what you wrote here would be to set the write_files attribute to be the result of the concat function, building a list from multiple parts. Then your first argument to concat will be the list of unconditional elements, while your second argument will be a conditional expression (the condition ? result : result syntax) which chooses between a list with one element or a list with zero elements, to decide dynamically whether to include that last item.

1 Like

Thanks, I’ll give it a try :slight_smile:

On that note, is it possible to use a conditional based on some attribute defined in the resource? I guess what I’m saying is, can I make use of the values given by for_each and act change the template according to that?

Ok, so what I tried to do (being inspired by your answer here Terraform conditional operator and long string) was this:

  packages = [
  (can(regex("^kube-.+", each.key)) ?
    [
      "kubeadm",
      "1.19.16-00"
    ],
    [
      "kubectl",
      "1.19.16-00"
    ],
    [
      "kubelet",
      "1.19.16-00"
    ],
    [
      "kubeadm",
      "1.19.16-00"
    ], :
    [
      "kubeadm",
      "1.19.16-00"
    ],
  )
  ]

Unfortunately it doesn’t like the syntax, and I get:

│ Call to function "templatefile" failed: ./files/kubernetes/userdata.tpl:28,6-7: Missing false expression in conditional; The conditional operator (...?...:...) requires a false expression, delimited by a
│ colon..

I’m not sure how I’m supposed to do this right.

For testing purposes I turned this into:

  packages = [
  (each.key == "etcd-1" ?
    "[ 'kubeadm', '1.19.16-00' ]," :
    "[ 'kubelet', '1.19.16-00' ],"
  )
  ]

This seems to be the right syntax, at least terraform doesn’t output that error anymore. But now I have the issue that the template doesn’t have access the to the for_each variable:

Invalid value for “vars” parameter: vars map does not contain key “var”, referenced at ./files/kubernetes/userdata.tpl:24,4-7.

But in the data block I do have it defined:

data "template_cloudinit_config" "kubernetes" {
        for_each = var.kubernetes_servers

        # split in parts - 1st is cloud-init cfg as such; from 2nd onwards, shell scripts.
        # default gzip is true + base64 encoded (for proxmox don't encode or zip the cloud-init)
        part {
                filename = "cloud-init.cfg"
                content_type = "text/cloud-config"
                content = templatefile("${path.module}/files/kubernetes/userdata.tpl", {
[...]

[Later edit:]
Ok, I think I got it. This is of course related to the fact that the template cannot directly access a variable defined in terraform, it needs to be passed somehow, so I added node_name = each.key in the templatefile function and called node_name directly in the template.
So it seems to be working, I guess :slight_smile: I’ll get back if I come across something else.

Thank you!