Infra: Basics of Terraform Provisioners

Asish M Madhu
DevOps for you
Published in
6 min readMar 28, 2023

--

A mechanisms in Terraform that allow executing scripts or commands on a local or remote machine during resource creation or destruction.

Terraform is a tool for defining infrastructure as code and it allows the automation of creating resources on various cloud platforms. Provisioners provide capability to configure and customize resources after they are created. For example when we create a VM using terraform, we might have to install some packages on the VM or perform some local action after the VM is created. We are somewhat deviating from the declarative model of terraform to perform something extra on the resource.

This is similar to user_data in AWS, where we can specify the commands which can be run while launching an instance. Below are some of the major types of Terraform provisioners

  1. Local-exec Provisioner

This provisioner executes a command on the machine from where terraform code is run.

2. Resource Provisioner

This is similar to a local provisioner, which will be run on the machine where terraform is running. The only difference is that it will be run at the end of the terraform execution, while a local provisioner will be run for each resource after resource creation.

2. Remote-exec Provisioner

This provisioner connects to a remote machine and executes commands inside that machine. This can be helpful to configure software or install packages on the remote machine

3. File Provisioner

Helps to copy file from local machine to the remote machine, helpful for copying configuration files etc.

4. Null Resource Provisioner

This provisioner does not create any resource, but can be used to trigger local-exec or remote-exec based on a condition.

5. Chef Provisioner

This provisioner is used to bootstrap a machine using Chef configuration management. It requires a Chef server to be configured beforehand.

6. Puppet Provisioner

This provisioner is used to bootstrap a machine using Puppet configuration management tool.

7. Ansible Provisioner

This provisioner is used to execute Ansible playbooks on a machine.

Lets try to use some of these provisioners to get a basic understanding.

We will first spin up an AWS instance through terraform. As a best practice we will split the terraform files into different files as below.

aws
├── instance.tf
├── provider.tf
├── terraform.tfvars (Secret credentials. Include in .gitignore)
└── vars.tf
  1. provider.tf

In this file we define the AWS provider. We need to provide access and a secret key along with the region where we are spinning up the vm.

provider "aws" {
access_key = "${var.AWS_ACCESS_KEY}"
secret_key = "${var.AWS_SECRET_KEY}"
region = "${var.AWS_REGION}"
}

2. vars.tf

In this file we define AWS credential variables. I am using some defaults for AWS_REGION and AMIS.

variable "AWS_ACCESS_KEY"
variable "AWS_SECRET_KEY"
variable "AWS_REGION" {
default = "us-west-2"
}
variable "AMIS" {
type = "map"
default = {
us-east-1 = "ami-13be557e"
us-west-2 = "ami-06b94666"
eu-west-1 = "ami-0d729a60"
}
}

3. instance.tf

We will have different instance.tf files for each of the provisioners mentioned earlier.

Local Exec Provisioner

We can use local exec to gather some details during resource creation and write to a local file. This can be much helpful to get resource details after terraform apply.

resource "aws_instance" "example-local-exec" {
ami = "ami-12345"
instance_type = "t2.micro"
key_name = "example-key"

provisioner "local-exec" {
command = "echo 'Instance created with ID: \
${aws_instance.example-local-exec.id}' >> instance-id.txt"
}
}

Remote Exec Provisioner

Remote executor is helpful to install some package onto the remote resource after creating the resource. We need to define a connection block with a private_key for connecting to the remote instance. Here I am creating a new key pair for the instance and using the private key connecting to the instance for installing nginx.

resource "aws_key_pair" "sample_aws_key" {
key_name = "my-key-pair"
public_key = file("~/.ssh/my-public-key.pub")
}

resource "aws_instance" "sample-remote-exec" {
ami = "ami-12345"
instance_type = "t2.micro"
key_name = aws_key_pair.sample_aws_key.key_name

provisioner "remote-exec" {
connection {
type = "ssh"
user = "user"
host = "remote_host"
private_key = file("~/.ssh/my-private-key.pub")
}
inline = [
"echo 'Hello, World!'",
"echo 'This is a remote-exec provisioner.'",
"sudo apt-get update",
"sudo apt-get install -y nginx",
]
}
}

File Provisioner

Sometimes we need to copy files to remote instance. We can use file provisioner for this.

resource "aws_instance" "example-file" {
ami = "ami-1234"
instance_type = "t2.micro"
key_name = "example-key"

provisioner "file" {
source = "config_files/nginx.conf"
destination = "/etc/nginx/nginx.conf"
}
}

Null Resource Provisioner

The null_resource provisioner is used when we need to create a resource that doesn't have any associated Terraform provider, but need to perform some local actions or execute some external command after the resource has been created. It is often used as a placeholder resource, that you can attach a provisioner block inorder to run certain actions.

A common use case for the null_resource provisioner is to execute a local script or command after creating an AWS EC2 instance. In the below example after creating an aws_instance, we use null_resource to perform a local-exec provisioner. The depends_on parameter ensures that the null_resource is created only after aws_instance resource has been successfully created. The null_resource is not a real resource and it is used just to perform a provisioner based on a dependency.

resource "aws_instance" "example" {
ami = "ami-123"
instance_type = "t2.micro"
key_name = "example-key"
}

resource "null_resource" "example" {
depends_on = [aws_instance.example]

provisioner "local-exec" {
command = "echo 'Resource ${aws_instance.example.id} is created.'"
}
}

Ansible Provisioner

This is my favourite provisioner. We can run ansible playbook from terraform. In the below example I am defining ansible inventory and playbook file along with ansible vault file for passing sensitive variables.

There are two ways we can use the provisioner. We can run ansible-playbook either remotely (on the target vm) or locally (on the machine where the terraform command is executed). Let’s understand both these approaches.

a) Executing ansible playbook remotely.

We have to install ansible, python on the remote instance. We use remote-exec for this. We should also define the connection block when we ue remote-exec. We can copy ansible files to remote instance using the file provisioner.

resource "aws_instance" "example" {
ami = "ami-12345"
instance_type = "t2.micro"
key_name = "example-key"

provisioner "file" {
source = "inventory.ini"
destination = "/home/ubuntu/inventory.ini"
}

provisioner "file" {
source = "playbook.yml"
destination = "/home/ubuntu/playbook.yml"
}

provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y python3-pip",
"sudo pip3 install ansible"
]
}

provisioner "null_resource" {
triggers = {
always_run = timestamp()
}

provisioner "ansible" {
inventory_file = "/home/ubuntu/inventory.ini"
playbook_file = "/home/ubuntu/playbook.yml"
extra_arguments = [
"--vault-password-file =/home/ubuntu/vault_pass.txt",
"-e",
"variable=value1",
"-e",
"variable2=value2",

]
}
}

connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
}

Did you observe the null resource. We mentioned it to run every time this terraform is run, with the always_run parameter inside null_resource block.

b) Executing ansible playbook locally (from terraform host)

I prefer this approach, since we do not have to install ansible on target hosts. But we still need to install python on the remote machine. Ansible-playbook will be run from the local machine where terraform will be run. So make sure ansible is installed on the local machine

resource "aws_instance" "example" {
ami = "ami-12345"
instance_type = "t2.micro"
key_name = "example-key"

provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y python3-pip"
]
}
}

data "template_file" "ansible_inventory" {
template = "${file("${path.module}/inventory.tpl")}"
vars = {
instance_ip = aws_instance.example.public_ip
}
}

resource "local_file" "ansible_inventory" {
content = data.template_file.ansible_inventory.rendered
filename = "${path.module}/inventory"
}

provisioner "null_resource" {
triggers = {
always_run = timestamp()
}
provisioner "ansible" {
playbook_file = "path/to/playbook.yml"
inventory_file = "${path.module}/inventory"
extra_arguments = ["--vault-password-file=path/to/vault_pass.txt"]
run_on = "local"
}
}

We have defined “run_on” to be local. This will run ansible-playbook on the local machine. We also use “template_file” and “local_file” to generate the inventory. We are passing instance_ip as a variable to the template file. The template file will look like below

[hosts]
${aws_instance.example.public_ip}

Conclusion

Provisioners are really helpful to extend terraform, though it deviates from the declarative model of terraform. I hope you enjoyed different provisioners with terraform and have got some basic understanding of the concept. If you liked my article, do like and share. Follow me for more such articles.

--

--

I enjoy exploring various opensource tools/technologies/ideas related to CloudComputing, DevOps, SRE and share my experience, understanding on the subject.