Provisioners
Terraform is an open-source infrastructure as code (IAC) tool that allows you to manage your infrastructure as code. Provisioners in Terraform are used to execute scripts or commands on a resource after it has been created. This can be useful for tasks such as configuring software or setting up a database. However, it is important to be careful when using provisioners, as they can make your infrastructure less predictable and harder to manage.
When to use a Provisioner
Provisioners can be used to automate tasks that need to be executed after a resource has been created. Here are some examples of when you might use a provisioner:
- When you need to install software on a resource after it has been created
- When you need to configure software on a resource after it has been created
- When you need to execute a script on a resource after it has been created
- When you need to run a test on a resource after it has been created
- When you need to migrate data to a resource after it has been created
When not to use a Provisioner
While provisioners can be useful, there are some cases where you should avoid using them. Here are some examples:
- When you need to make changes to an existing resource
- When you need to configure a resource at creation time
- When you need to configure a resource using a configuration management tool like Chef or Puppet
- When you need to manage stateful resources like databases and queues
Examples
Installing Software on an EC2 Instance
Suppose you have an EC2 instance that needs to have some software installed on it. You can use a provisioner to install the software after the instance has been created. Here's an example:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "example-instance"
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
]
}
}
In this example, we're using the remote-exec
provisioner to execute commands on the EC2 instance after it has been created. The commands will install the Nginx web server.
Configuring a Database
Suppose you have a database that needs to be configured after it has been created. You can use a provisioner to execute commands on the database after it has been created. Here's an example:
resource "aws_db_instance" "example" {
allocated_storage = 10
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
name = "example-db"
username = "admin"
password = "password"
parameter_group_name = "default.mysql5.7"
skip_final_snapshot = true
provisioner "remote-exec" {
inline = [
"mysql -u admin -ppassword -e 'CREATE DATABASE example;'",
]
}
}
In this example, we're using the remote-exec
provisioner to execute a command on the database after it has been created. The command will create a new database called example
.
Warning about Provisioners
- Be careful when using provisioners in Terraform
- Provisioners can make infrastructure less predictable and harder to manage
- Only use provisioners when absolutely necessary
- Avoid using provisioners to configure resources at creation time
- Use configuration management tools like Chef or Puppet instead if you need to configure a resource at creation time
- Use specialized tools like Kubernetes or Docker Swarm instead of Terraform provisioners when working with stateful resources like databases and queues
Types of Provisioners
Create Time Provisioners
These provisioners are invoked as part of resource creation. They run after the resource is created, allowing you to use output data from the resource in your provisioning actions.
Example of a create time provisioner (local-exec):
resource "aws_instance" "example" {
ami = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
instance_type = "t2.micro"
provisioner "local-exec" {
command = "echo 'The instance ID is ${aws_instance.example.id}' > instance_id.txt"
}
}
In this example, after the instance is created, the local-exec provisioner writes the ID of the instance to a text file.
Destroy Time Provisioners
These provisioners are invoked just before a resource is destroyed. They can be used to gracefully shut down or cleanup an application or service before its resources are removed.
Example of a destroy time provisioner (local-exec):
resource "aws_instance" "example" {
ami = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
instance_type = "t2.micro"
provisioner "local-exec" {
when = "destroy"
command = "echo 'Destroying the instance with ID ${self.id}' > destroy.log"
}
}
In this example, just before the instance is destroyed, the local-exec provisioner writes a log message to a file.
Remember to replace "ami-0c94855ba95c574c8"
with the latest Amazon Linux 2 AMI ID for your region. The self.id
attribute in the second example refers to the ID of the resource being destroyed.
Usage
Here is the official documentation link for Terraform Provisioners: https://www.terraform.io/docs/provisioners/index.html
This link provides the latest information on Terraform Provisioners and their usage.
Local-Exec Provisioner
- Executes a command locally on the machine running Terraform
- Useful for tasks such as generating files or running local scripts
- Not recommended for production use
- Here is an example of how to use the
local-exec
provisioner with theaws_instance
resource in Terraform:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "example-instance"
}
provisioner "local-exec" {
command = "echo The instance ID is ${aws_instance.example.id}"
}
}
- You can use Terraform's
local-exec
provisioner to run an Ansible playbook. Here's an example of how you might set that up:
resource "null_resource" "ansible_provisioner" {
triggers = {
build_number = "${timestamp()}"
}
provisioner "local-exec" {
command = "ansible-playbook -i '${aws_instance.example.public_ip},' --private-key '~/.ssh/id_rsa' playbook.yml"
}
depends_on = [aws_instance.example]
}
resource "aws_instance" "example" {
ami = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
instance_type = "t2.micro"
key_name = "key-name" # Update this with your AWS key pair name
tags = {
Name = "terraform-example-instance"
}
}
In this example, the local-exec
provisioner runs the ansible-playbook
command to execute your playbook. The command includes an inventory (-i
) that consists of the public IP address of the AWS instance you want to configure, and --private-key
which specifies the SSH key for the Ansible to use when connecting to the instance.
Please note:
- You should replace
'~/.ssh/id_rsa'
with the path to your private SSH key. - Replace
'key-name'
with the name of your AWS key pair. - Replace
'playbook.yml'
with the path to your Ansible playbook. - Replace
"ami-0c94855ba95c574c8"
with the latest Amazon Linux 2 AMI ID for your region.
Also, ensure Ansible is installed on the system where you are running Terraform. This method executes Ansible on the same system where Terraform runs, rather than on the provisioned resource.
Remote-Exec Provisioner
- Executes a command on a resource after it has been created
- Useful for tasks such as installing software or configuring a resource
- Can make infrastructure less predictable and harder to manage
- Here's an example of how to use the
remote-exec
provisioner with theaws_instance
resource in Terraform: - Example:
provider "aws" {
region = "us-west-2"
}
resource "null_resource" "ssh_keygen" {
provisioner "local-exec" {
command = "ssh-keygen -t rsa -f ${path.module}/id_rsa -q -N ''"
}
triggers = {
always_run = "${timestamp()}"
}
}
resource "aws_key_pair" "deployer" {
key_name = "deployer-key"
public_key = file("${path.module}/id_rsa.pub")
depends_on = [null_resource.ssh_keygen]
}
resource "aws_security_group" "allow_http" {
name = "allow_http"
description = "Allow HTTP inbound traffic"
ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
instance_type = "t2.micro"
key_name = aws_key_pair.deployer.key_name
vpc_security_group_ids = [aws_security_group.allow_http.id]
provisioner "remote-exec" {
inline = [
"sudo yum install -y nginx",
"sudo service nginx start"
]
}
connection {
type = "ssh"
user = "ec2-user"
private_key = file("${path.module}/id_rsa")
host = self.public_ip
}
}
This script adds a null_resource
that triggers a local-exec provisioner to generate the SSH keys. The newly generated keys are then used in the aws_key_pair
resource. Be aware that this will overwrite any existing keys in the file paths specified.
Again, be sure to replace "ami-0c94855ba95c574c8"
with the latest Amazon Linux 2 AMI ID for your region. The keys will be created in the same directory as your Terraform script (specified by ${path.module}
), and they should be kept secure.
File Provisioner
- Copies files or directories to a resource after it has been created
- Useful for tasks such as copying configuration files or scripts
- Can be used in conjunction with other provisioners to configure a resource
- Here is an example of using the file provisioner in a production scenario:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "example-instance"
}
provisioner "file" {
source = "file.txt"
destination = "/tmp/file.txt"
}
}
In this example, the file
provisioner is used to copy the file.txt
file from the local machine to the /tmp
directory on the EC2 instance after it has been created.
Null Provisioner
- Used to execute local scripts or commands that do not require access to a resource
- Useful for tasks such as generating random data or writing to a local file
- Does not interact with a resource and does not have any effect on it
-
To use the
null
provisioner in Terraform, you must define anull_resource
block and then specify theprovisioner
block within it. The block is used to create a resource that does not perform any action, and the block is used to execute local scripts or commands that do not require access to a resource.Here's an example of how to use the
null
provisioner in Terraform:
resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo This is an example of the null provisioner"
}
}
In this example, we're using the null
provisioner to execute a command locally on the machine running Terraform. The command will simply print the message "This is an example of the null provisioner" to the console.
Keep in mind that the null
provisioner should be used sparingly, as it does not interact with any resources and does not have any effect on the infrastructure.
To use the null
provisioner to generate random data, you can write a script that generates the data and outputs it to a file. Then, you can use the file
provisioner to copy the file to the desired location:
resource "null_resource" "example" {
provisioner "local-exec" {
command = "echo ${random_integer.value} > random.txt"
}
provisioner "file" {
source = "random.txt"
destination = "/tmp/random.txt"
}
}
resource "random_integer" "value" {
min = 1
max = 100
}
In this example, we're using the random_integer
resource to generate a random integer between 1 and 100. We then use the local-exec
provisioner to output the integer to a file called random.txt
. Finally, we use the file
provisioner to copy the file to the /tmp
directory on the machine running Terraform.
Using Triggers
The null_resource
provisioner in Terraform is used to execute scripts or commands that are not directly related to infrastructure management. The triggers
argument is used to define conditions under which the provisioner should be run.
- Purpose:
triggers
is a map of arbitrary strings that, when changed, will taint thenull_resource
and force re-provisioning. - Use Case: Useful in scenarios where you want to perform some action when the value of a variable or output changes.
- Example:
resource "aws_instance" "example" {
ami = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
instance_type = "t2.micro"
}
resource "null_resource" "example_provisioner" {
triggers = {
instance_public_ip = aws_instance.example.public_ip
}
provisioner "local-exec" {
command = "echo 'The public IP of the instance has changed to ${self.triggers["instance_public_ip"]}' > change.log"
}
}
In this example, the null_resource
with the local-exec
provisioner will run whenever the public IP of the AWS instance changes, writing a log message to a file.
It's important to note that the null_resource
does not monitor the aws_instance.example.public_ip
for changes in real-time. The changes are only checked when you run a terraform plan
or terraform apply
.
Chef Provisioner
- Automates the installation and configuration of software using Chef
- Requires a Chef server and a cookbook
- Useful for tasks such as installing and configuring web servers or databases
- To use the Chef provisioner in Terraform, you need to have a Chef server and a cookbook. Here's an example of how to use the
chef
provisioner with theaws_instance
resource in Terraform:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "example-instance"
}
provisioner "chef" {
server_url = "<https://chef.example.com/organizations/example>"
node_name = "example-node"
run_list = [ "recipe[example-cookbook::default]" ]
client_key = "${file("~/.chef/example.pem")}"
chef_server_ca_cert = "${file("~/.chef/chef-server.crt")}"
}
}
In this example, we're using the chef
provisioner to install and configure software on the EC2 instance after it has been created. The server_url
specifies the URL of the Chef server, the node_name
specifies the name of the node, and the run_list
specifies the list of recipes to run. The client_key
specifies the path to the client key, and the chef_server_ca_cert
specifies the path to the Chef server certificate.
Puppet Provisioner
- Automates the installation and configuration of software using Puppet
- Requires a Puppet master and a Puppet manifest
- Useful for tasks such as installing and configuring web servers or databases
- To use the Puppet provisioner in Terraform, you need to have a Puppet master and a Puppet manifest. Here's an example of how to use the
puppet
provisioner with theaws_instance
resource in Terraform:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "example-instance"
}
provisioner "puppet" {
server = "puppet.example.com"
manifest_file = "/path/to/manifest.pp"
modules = [
"/path/to/modules",
]
}
}
In this example, we're using the puppet
provisioner to automate the installation and configuration of software on the EC2 instance after it has been created. The server
specifies the hostname or IP address of the Puppet master, the manifest_file
specifies the path to the Puppet manifest, and the modules
specifies the path to the Puppet modules.
Handling Provisioner Failures in Terraform
Normal Behavior:
- On resource creation: If a provisioner fails, Terraform taints the resource and plans for its destruction and recreation during the next apply.
- On resource destruction: If a provisioner fails, Terraform logs an error but continues with the resource destruction.
Overriding Normal Behavior:
- Use
on_failure
attribute and set it tocontinue
to prevent resource tainting on provisioner failure during creation. Example:
resource "aws_instance" "example" {
ami = "ami-0c94855ba95c574c8" # Update this with latest Amazon Linux 2 AMI ID
instance_type = "t2.micro"
provisioner "local-exec" {
command = "echo 'The instance ID is ${aws_instance.example.id}' > instance_id.txt"
on_failure = "continue"
}
}
This will allow the resource to be marked as successfully created even if the provisioner fails. Be cautious, as this could leave your resource in an unexpected state.