Learning Terraform – Part 3: Provisioners and Modules

This post is a continuation of my series of blog posts on learning Terraform.  In my previous post I covered variables, count, conditional expressions and functions.  In this post, I want to look at provisioners and modules. 

Provisioners

Provisioners are used to execute scripts or actions on a local or remote machine as part of the resource construction or destruction process.  Provisioners can supplement Terraform functionality but should only be used as a last resort since they don’t support Terraform state. There are two different categories of provisioners used within Terraform… Generic provisioners and Vendor provisioners.  Let’s get started with Generic provisioners.

There are three different types of Generic provisioners: file provisioner, local-exec provisioner and remote-exec provisioner.  These are described below.

File Provisioner

The file provisioner is used to copy files or directories from the machine executing the terraform apply to the newly created resource.  The file provisioner can connect to the resource using either ssh or winrm connections.  The following example illustrates a file provisioner.

resource "azurerm_linux_virtual_machine" "vm-linux" {
# Copies app.bak file from /dev/ to /home/
provisioner "file" {
source = "/dev/app.bak"
destination = "/home/app.bak"

# Connection Settings
connection {
type = "ssh"
user = var.user
password = var.password
host = var.host
}
}
}

Local-exec provisioner

The local-exec provisioner executes code on the local machine after a resource is created.  This is useful if you want to save information about the newly created resource to your local machine.  Below is an example of a local-exec provisioner.

resource "aws_instance" "my_vm" {
# This copies the private ip from the AWS resource to the private_ips.txt file on the local machine

provisioner "local-exec" {
command = "echo ${aws_instance.web.private_ip} >> private_ips.txt"
}
}

Remote-exec provisioner

The remote-exec provisioner executes a script on a remote resource.  This allows you to execute commands or scripts on a resource after it’s created.  Below is an example of a remote-exec provisioner installing Apache and MySQL on a Linux VM.

resource "azurerm_linux_virtual_machine" "vm-linux" {
provisioner "remote-exec" {
inline = [
"sudo apt-get -y install apache2",
"sudo apt-get -y install mysql-server"
]

# Connection Settings
connection {
type = "ssh"
user = var.user
password = var.password
host = var.host
}
}
}

These provisioners are known as creation-time provisioners and by default only run when a resource they are defined within is created. Their primary use case is to bootstrap a system during a terraform apply.  If a creation-Time provisioner fails during the terraform apply, the resource is marked as tainted and will be destroyed and recreated during the next terraform apply.  This is done because a failed provisioner could leave a resource in a semi-configured state and the only way to ensure it is created correctly is to recreate the resource. 

If when = destroy is specified within the provisioner block, the provisioner will only run when the resource is destroyed.

resource "azurerm_linux_virtual_machine" "vm-linux" {

provisioner "local-exec" {
inline = [
when = destroy
command = "echo 'Destroy-time provisioner'"
]
}
}

Modules

Modules provide a way to create reusable components that are written once and used throughout a Terraform configuration. Modules are Terraform files which are called from module blocks and have exposed input and output variables that allow them to be reused. All module blocks require a source argument which defines the location of the configuration files.

In this example, the prod folder should contain a <name>.tf file.

module "module_name" { 
source = "../../modules/prod" //module source
}

The <name>.tf file describes the resources Terraform needs to create.

resource "azurerm_linux_virtual_machine" "linux_vm" { 
name = "linux_vm"
resource_group_name = var.rg
location = var.location
size = var.vm_size[0]
}

Additionally, you can use a valid Git repository as a source location.

module "module_name" {
source = "git::https://example_repo.com/azure_vm.git"
}

When a Git repository is used as a source location, Git uses the default branch from the referenced repository. This can be override using the ref argument which allows you to reference specific branch or tag names.

module "module_name" {
source = "git::https://example_repo.com/azure_vm.git?ref=branch_name"
}

Terraform Registry

The Terraform Registry is a repository of modules written and maintained by the Terraform community. To reference a module from the registry in a configuration, use the source argument to reference the module path.

module "linuxservers" { 
source = "Azure/compute/azurerm"
resource_group_name = var.rg
vm_os_simple = "UbuntuServer"
public_ip_dns = ["linsimplevmips"]
#...
}

It’s recommended to constrain the version number when using modules from the registry to avoid unexpected results or changes.

module "ec2" { 
source = "terraform-aws-modules/ec2-instance/aws"
version = "~> 2.0"
name = "my-ec2-VM"
instance_count = 5
#...
}

Currently there are over 4,000 modules in the Terraform Registry. In certain cases, HashiCorp verifies that these modules are compatible with Terraform. Verified modules will appear with a blue badge next to them.

For more details on Terraform modules, browse to the Terraform documentation.

Summary

In this blog post I discussed Terraform provisioners and modules. These features extend Terraform functionality as well as allow you to apply software development best practices to infrastructure development. In part 4 of this Learning Terraform series I’ll look at state management in Terraform.

Thank you for reading!