Provisioners
You can use provisioners to model specific actions on the local machine or on a remote machine in order to prepare servers or other infrastructure objects for service.
Provisioners are a Last Resort
OpenTF includes the concept of provisioners as a measure of pragmatism, knowing that there are always certain behaviors that cannot be directly represented in OpenTF's declarative model.
However, they also add a considerable amount of complexity and uncertainty to OpenTF usage. Firstly, OpenTF cannot model the actions of provisioners as part of a plan because they can in principle take any action. Secondly, successful use of provisioners requires coordinating many more details than OpenTF usage usually requires: direct network access to your servers, issuing OpenTF credentials to log in, making sure that all of the necessary external software is installed, etc.
The following sections describe some situations which can be solved with provisioners in principle, but where better solutions are also available. We do not recommend using provisioners for any of the use-cases described in the following sections.
Even if your specific use-case is not described in the following sections, we still recommend attempting to solve it using other techniques first, and use provisioners only if there is no other option.
Passing data into virtual machines and other compute resources
When deploying virtual machines or other similar compute resources, we often need to pass in data about other related infrastructure that the software on that server will need to do its job.
The various provisioners that interact with remote servers over SSH or WinRM can potentially be used to pass such data by logging in to the server and providing it directly, but most cloud computing platforms provide mechanisms to pass data to instances at the time of their creation such that the data is immediately available on system boot. For example:
- Alibaba Cloud:
user_data
onalicloud_instance
oralicloud_launch_template
. - Amazon EC2:
user_data
oruser_data_base64
onaws_instance
,aws_launch_template
, andaws_launch_configuration
. - Amazon Lightsail:
user_data
onaws_lightsail_instance
. - Microsoft Azure:
custom_data
onazurerm_virtual_machine
orazurerm_virtual_machine_scale_set
. - Google Cloud Platform:
metadata
ongoogle_compute_instance
orgoogle_compute_instance_group
. - Oracle Cloud Infrastructure:
metadata
orextended_metadata
onoci_core_instance
oroci_core_instance_configuration
. - VMware vSphere: Attach a virtual CDROM to
vsphere_virtual_machine
using thecdrom
block, containing a file calleduser-data.txt
.
Many official Linux distribution disk images include software called cloud-init that can automatically process in various ways data passed via the means described above, allowing you to run arbitrary scripts and do basic system configuration immediately during the boot process and without the need to access the machine over SSH.
If you are building custom machine images, you can make use of the "user data" or "metadata" passed by the above means in whatever way makes sense to your application, by referring to your vendor's documentation on how to access the data at runtime.
This approach is required if you intend to use any mechanism in your cloud provider for automatically launching and destroying servers in a group, because in that case individual servers will launch unattended while OpenTF is not around to provision them.
Even if you're deploying individual servers directly with OpenTF, passing data this way will allow faster boot times and simplify deployment by avoiding the need for direct network access from OpenTF to the new server and for remote access credentials to be provided.
Provisioning files using cloud-config
You can add the cloudinit_config
data source to your OpenTF configuration and specify the files you want to provision as text/cloud-config
content. The cloudinit_config
data source renders multi-part MIME configurations for use with cloud-init. Pass the files in the content
field as YAML-encoded configurations using the write_files
block.
In the following example, the my_cloud_config
data source specifies a text/cloud-config
MIME part named cloud.conf
. The part.content
field is set to yamlencode
, which encodes the write_files
JSON object as YAML so that the system can provision the referenced files.
data "cloudinit_config" "my_cloud_config" {
gzip = false
base64_encode = false
part {
content_type = "text/cloud-config"
filename = "cloud.conf"
content = yamlencode(
{
"write_files" : [
{
"path" : "/etc/foo.conf",
"content" : "foo contents",
},
{
"path" : "/etc/bar.conf",
"content" : file("bar.conf"),
},
{
"path" : "/etc/baz.conf",
"content" : templatefile("baz.tpl.conf", { SOME_VAR = "qux" }),
},
],
}
)
}
}
Running configuration management software
As a convenience to users who are forced to use generic operating system distribution images, OpenTF includes a number of specialized provisioners for launching specific configuration management products.
We strongly recommend not using these, and instead running system configuration steps during a custom image build process. For example, HashiCorp Packer offers a similar complement of configuration management provisioners and can run their installation steps during a separate build process, before creating a system disk image that you can deploy many times.
If you are using configuration management software that has a centralized server component, you will need to delay the registration step until the final system is booted from your custom image. To achieve that, use one of the mechanisms described above to pass the necessary information into each instance so that it can register itself with the configuration management server immediately on boot, without the need to accept commands from OpenTF over SSH or WinRM.
First-class provider functionality may be available
It is technically possible to use the local-exec
provisioner to run the CLI
for your target system in order to create, update, or otherwise interact with
remote objects in that system.
If you are trying to use a new feature of the remote system that isn't yet supported in its provider, that might be the only option. However, if there is provider support for the feature you intend to use, prefer to use that provider functionality rather than a provisioner so that OpenTF can be fully aware of the object and properly manage ongoing changes to it.
Even if the functionality you need is not available in a provider today, we
suggest to consider local-exec
usage a temporary workaround and to also
open an issue in the relevant provider's repository to discuss adding
first-class provider support. Provider development teams often prioritize
features based on interest, so opening an issue is a way to record your
interest in the feature.
Provisioners are used to execute scripts on a local or remote machine as part of resource creation or destruction. Provisioners can be used to bootstrap a resource, cleanup before destroy, run configuration management, etc.
How to use Provisioners
-> Note: Provisioners should only be used as a last resort. For most common situations there are better alternatives. For more information, see the sections above.
If you are certain that provisioners are the best way to solve your problem
after considering the advice in the sections above, you can add a
provisioner
block inside the resource
block of a compute instance.
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo The server's IP address is ${self.private_ip}"
}
}
The local-exec
provisioner requires no other configuration, but most other
provisioners must connect to the remote system using SSH or WinRM.
You must include a connection
block so that OpenTF knows how to communicate with the server.
OpenTF includes several built-in provisioners. You can also use third-party provisioners as plugins, by placing them
in %APPDATA%\terraform.d\plugins
, ~/.terraform.d/plugins
, or the same
directory where the OpenTF binary is installed. However, we do not recommend
using any provisioners except the built-in file
, local-exec
, and
remote-exec
provisioners.
All provisioners support the when
and on_failure
meta-arguments, which
are described below (see Destroy-Time Provisioners
and Failure Behavior).
The self
Object
Expressions in provisioner
blocks cannot refer to their parent resource by
name. Instead, they can use the special self
object.
The self
object represents the provisioner's parent resource, and has all of
that resource's attributes. For example, use self.public_ip
to reference an
aws_instance
's public_ip
attribute.
-> Technical note: Resource references are restricted here because references create dependencies. Referring to a resource by name within its own block would create a dependency cycle.
Suppressing Provisioner Logs in CLI Output
The configuration for a provisioner
block may use sensitive values, such as
sensitive
variables or
sensitive
output values.
In this case, all log output from the provisioner is automatically suppressed to
prevent the sensitive values from being displayed.
Creation-Time Provisioners
By default, provisioners run when the resource they are defined within is created. Creation-time provisioners are only run during creation, not during updating or any other lifecycle. They are meant as a means to perform bootstrapping of a system.
If a creation-time provisioner fails, the resource is marked as tainted.
A tainted resource will be planned for destruction and recreation upon the
next opentf apply
. OpenTF does this because a failed provisioner
can leave a resource in a semi-configured state. Because OpenTF cannot
reason about what the provisioner does, the only way to ensure proper creation
of a resource is to recreate it. This is tainting.
You can change this behavior by setting the on_failure
attribute,
which is covered in detail below.
Destroy-Time Provisioners
If when = destroy
is specified, the provisioner will run when the
resource it is defined within is destroyed.
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
when = destroy
command = "echo 'Destroy-time provisioner'"
}
}
Destroy provisioners are run before the resource is destroyed. If they
fail, OpenTF will error and rerun the provisioners again on the next
opentf apply
. Due to this behavior, care should be taken for destroy
provisioners to be safe to run multiple times.
~> Note: Destroy provisioners of this resource do not run if create_before_destroy
is set to true
.
Destroy-time provisioners can only run if they remain in the configuration at the time a resource is destroyed. If a resource block with a destroy-time provisioner is removed entirely from the configuration, its provisioner configurations are removed along with it and thus the destroy provisioner won't run. To work around this, a multi-step process can be used to safely remove a resource with a destroy-time provisioner:
- Update the resource configuration to include
count = 0
. - Apply the configuration to destroy any existing instances of the resource, including running the destroy provisioner.
- Remove the resource block entirely from configuration, along with its
provisioner
blocks. - Apply again, at which point no further action should be taken since the resources were already destroyed.
Because of this limitation, you should use destroy-time provisioners sparingly and with care.
~> NOTE: A destroy-time provisioner within a resource that is tainted will not run. This includes resources that are marked tainted from a failed creation-time provisioner or tainted manually using opentf taint
.
Multiple Provisioners
Multiple provisioners can be specified within a resource. Multiple provisioners are executed in the order they're defined in the configuration file.
You may also mix and match creation and destruction provisioners. Only the provisioners that are valid for a given operation will be run. Those valid provisioners will be run in the order they're defined in the configuration file.
Example of multiple provisioners:
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo first"
}
provisioner "local-exec" {
command = "echo second"
}
}
Failure Behavior
By default, provisioners that fail will also cause the OpenTF apply
itself to fail. The on_failure
setting can be used to change this. The
allowed values are:
continue
- Ignore the error and continue with creation or destruction.fail
- Raise an error and stop applying (the default behavior). If this is a creation provisioner, taint the resource.
Example:
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo The server's IP address is ${self.private_ip}"
on_failure = continue
}
}