Conditional Provisioning? (Probably Not Possible)

Hi,

I have a aws instance resource defined like this:

resource "aws_instance" "amazon-linux" {
  count                       = length(var.instance_names)

  provisioner "file" {
    source      = "ansible/"
    destination = "/home/ec2-user"
  }
}

How do I make the provisioner run only when there is an instance named “foo”?

Thanks
-T

Hi @gamename1,

There is no direct way to get that done as you described it here. I can think of two alternatives that could work, though:

My preferred option would be to send those files up to the instance via user_data and have software such as cloud-init retrieve it and install it, which can then avoid the need for Terraform to SSH into the instance at all. Whether this would be tenable for your use-case will depend on how large the contents of that “ansible” directory are, because there’s a 16kB size limit on user_data. But assuming it’s a relatively small set of configuration files, you could perhaps build a cloud-init configuration like this:

locals {
  ansible_cloudinit = yamlencode({
    write_files = [
      for p in fileset("${path.module}/ansible", "**") : {
        encoding = "b64"
        content  = filebase64("${path.module}/ansible/${p}")
        owner    = "ec2-user:ec2-user"
        path     = "/home/ec2-user/${p}"
      }
    ]
  })
}

(I based this on the Writing out arbitrary files example.)

With that defined, you could then set the user_data conditionally based on the current instance name:

  user_data = var.instance_names[count.index] == "foo" ? local.ansible_cloudinit : null

If your file packet is too big to send in this way, or if there are other blockers for that technique, then as a last resort I’d consider moving the provisioner out into a separate resource that is itself conditional on the instance names. This would be marginally easier if you can use for_each instead of count because then you can use each.key as the filtering criteria:

resource "aws_instance" "amazon_linux" {
  for_each = toset(var.instance_names)

  # ...
}

resource "null_resource" "amazon_linux_ansible" {
  for_each = {
    for k, v in aws_instance.amazon_linux : k => v
    if k == "foo"
  }

  provisioner "file" {
    source      = "${path.module}/ansible/"
    destination = "/home/ec2-user"
  }
}

Here I used a conditional for expression as a concise way to filter out all but the foo element. If a particular instantiation of this module doesn’t have a foo element at all then the null_resource will have zero instances and thus nothing to provision. If there is a foo element then it’ll have one instance and thus the provisioner in there will execute once.

The usual tradeoffs with null_resource apply here, including:

  • You’ll need to put a connection block in the other resource that refers to attributes of each.value, like each.value.private_ip, to match with the corresponding EC2 instance.
  • If you expect to be taking actions that will replace the instance in future, you should include something in the triggers that will force Terraform to replace the null_resource instance in that case too. In this case I think each.value.id would be good enough, because the id will change if you ever replace the EC2 instance.