While Terraform actually discourages the use of its three "generic" provisioners, it is certainly worth learning about what they can do so that you can choose to use them when appropriate. You should only plan to use them for simpler operations and perhaps only when working on a smaller scale. If your needs go beyond that, you may want to investigate using a stand-alone DevOps provisioning tool like Ansible, Chef or Puppet.

One reason that Terraform doesn't endorse its own provisioners is that they cannot provide the same exacting control over instances that the Terraform deployment functionality provides. In the well-controlled realm of deployment, dependencies are known and failures have consequences that Terraform understands. When using provisioners, you are on your own and must manage dependencies and handle any failures yourself.

Syntax

Provisioners are specified as blocks within a Resource. A Resource block may contain more than one Provisioner, in which case they are processed in their order of appearance. In this example, the instance Resource block named "leader" has a provisioner of type "local-exec" that prints some text.


resource "openstack_compute_instance_v2" "leader" {
  # (other blocks and arguments)
  
  provisioner "local-exec" {
     command "echo 'provisioning the leader'"
  }
}

Provisioners don't have names and their type must be either "file", "local-exec" or "remote-exec" (This is true for the generic provisioners we are discussing here. Terraform recommends strongly that you not use their other "vendor" provisioners). A provisioner's arguments will differ depending on its type, and those arguments are discussed on the coming pages.

Connections

The "file" and "remote-exec" provisioners need credentials in order to operate on your instances, and those are provided through a Connection block. Connection blocks can be specified either within a Provisioner block (in which case they affect only that provisioner), or inside the parent Resource block (in which case they affect all provisioners in the resource). Here is a sample Connection block:


connection {
  type        = "ssh"
  host        = openstack_networking_floatingip_v2.leader.address
  user        = "ubuntu"
  agent       = false
  private_key = file(var.private_key_file)
}

This block connects to the instance using SSH (specified using the "type" argument). The "host" argument is set from the floating IP address of the instance to which we are connecting, and we want to log in with username "ubuntu". Setting the "agent" argument to false indicates that the private key will come from a file, not from an agent process running on the Terraform system. The private part of the SSH key is defined to be the contents of a file identified using a Terraform variable.

It is also possible to provide password credentials, though password logins are disabled by default on Jetstream2 instances. Many other arguments are available for Connections.

Where To Define A Provisioner

Intuitively, you might think a provisioner should be placed inside the Resource block of the instance that the provisioner is affecting. Because the provisioner will be invoked once that instance has been created, this would seem like a good plan. But in fact you may need more than just the instance to be created before you can work with it. For example, you would also need a floating IP address to be allocated before you can associate it with an instance. So, you will often need to place your provisioner where you can be confident that all of its requirements will be satisfied.

Because a floating IP association Resource cannot be created until both the instance and the IP address on which it depends have been created, it is an excellent location for a provisioner. But Terraform also provides another type of Resource block that you may find even more convenient: the "null resource". This type of resource will be used by some of the examples on the following pages, and has the following syntax:


resource "null_resource" "INTERNAL_NAME" {
  depends_on = [LIST_OF_RESOURCES]
  connection { SYNTAX_AS_SHOWN_ABOVE }
  provisioner "PROVISIONER_TYPE" {
    PROVISIONER_CONTENTS
  }
}

"depends_on" is a meta-argument, so you won't find it in the null_resource documentation. It is a list of names of other resources upon which the null_resource depends. When all of those resources have been created, the null_resource will invoke its provisioners. This allows your provisioner to depend on multiple conditions before it runs. In order to use a null_resource, the "null" provider must be included in the Terraform configuration. This provider is already in our main.tf, so the coming examples will work without adjustments.

A Time to Destroy

By default, provisioners run when their parent resource is created. However, you can specify that they should instead be run when their parent resource is destroyed by setting their when meta-argument to "destroy". The provisioner will then be run before the resource is destroyed, and if the provisioner fails the resource will not be destroyed.

Editing a Provisioner

Whether your provisioner is inside an OpenStack Resource or a null_resource, you must remember that Terraform accepts little responsibility for its contents. One consequence of this is that when you change those contents, Terraform will not consider it a change to the resource itself. When you are developing a provisioner, once your components have successfully been deployed, it can be difficult to get Terraform to re-invoke your provisioner because its dependencies may no longer need to be created.

To get around this problem, you can tell Terraform on the command line that it should replace one of the resources upon which your provider depends. This is done through the "apply" command's -replace option, which identifies a specific OpenStack resource that should be replaced when applying the configuration, regardless of whether or not it needs to be replaced. The command below does this for our ongoing example, causing the "leader" instance's floating IP address association to be replaced. This association is handy, as replacing it doesn't take long and doesn't change the IP address itself. However, if other resources reference the one you are replacing, they may also be replaced. You may specify more than one of these arguments at a time. Try this now with the following command:


terraform apply -var-file=terratest.tfvars -replace="openstack_compute_floatingip_associate_v2.leader"
 
©  |   Cornell University    |   Center for Advanced Computing    |   Copyright Statement    |   Inclusivity Statement