Conditional provisioner blocks in Terraform

January 5, 2022 – Samuli Seppänen

I'll start with a spoiler: what the title suggests is not possible. It is, however, possible to accomplish this with cloud-init and Terraform templates as described in the Multi-part cloud-init provisioning with Terraform blog post.

If you need to use SSH/WinRM provisioning, then there are various workarounds you can apply, and this article explains some of them and goes into depth into one in particular.

The first workaround is relatively clean, but is not usable in all cases, especially when working with complex modules. The idea is to have two resources, here of type aws_instance. Those resources are identical except for the provisioner blocks:

variable "provision" {
  type = bool
}

resource "aws_instance" "provisioned_instance" {
  count = var.provision : 1 ? 0
  --- snip ---

  provisioner "remote-exec" {
    --- snip ---
  }
}

resource "aws_instance" "non_provisioned_instance" {
  count = var.provision : 0 ? 1
  --- snip ---
}

Depending on what value you give var.provision Terraform will create either aws_instance.provisioned_instance or aws_instance.non_provisioned_instance. This approach works ok, but may get you into trouble if any other resources depend on those aws_instance resources (as they may be present or not). It also duplicates most of the code which may introduce bugs.

The second workaround is less complete, but works more easily with complex modules, and does not change the title of the resource. The idea is to use the remote-exec provisioner blocks but replace the provisioning commands on the fly. First you define a bool value which tells whether to run the provisioning scripts or not:

variable "provision" {
  type    = bool
}

Then you define the commands you'd like to run if you want to run the provisioner as a local:

locals {  
  provisioner_commands = ["/do/something", "/do/something/else"]
}

If you don't use any variables in your provisioning commands you can put the commands into a variable as well. This is because variables can't contain variable interpolations, whereas locals can.

Now define the instance:

resource "aws_instance" "provisioned_instance" {
  --- snip ---

  provisioner "remote-exec" {
    inline = concat(["echo Provisioning"], [for command in local.provisioner_commands: command if var.provisioning])
  }
}

What this basically does is create a new list based on local.provisioner_commands by filtering out every entry if var.provisioning is not true. As the inline parameter can't be empty a dummy command is added to the list to keep Terraform happy.

I should note that this approach does not work with other provisioner types (e.g. file), but may still be an acceptable workaround in many cases.

Want to talk to an expert?

If you want to reach us, just send us a message or book a free call!
menucross-circle