Provisioners

Sign Up to Build

About this Architecture

Here is some information about this architecture.

How to Build This Solution

Here are the steps you can follow to build this solution on your own.

As you already know, Terraform is an infrastructure as code tool that allows us to programmatically create resources for running software. Apart from creating, modifying, and destroying these infrastructures, Terraform does not, traditionally, interfere in what goes on in the infrastructure.

Oftentimes, after creating an infrastructure, you will want to run a script or an automation tool (such as Ansible) on the newly created infrastructure. To further extend its features and allow you to easily perform such actions after creating or deleting a resource, Terraform provides a block called provisioner.

What are Terraform Provisioners?

Provisioners are a Terraform feature that allows you to run scripts, commands or upload files after creating or destroying an infrastructure. When used in your configuration code, provisioners help to automatically invoke specific actions, commands, or scripts that you, otherwise, may have to run manually. You can use Terraform provisioners to install packages, pass information, or install configuration management tools on a resource.

Terraform offers three main types of provisioners: file, local-exec, and remote-exec.  More on that later in this lesson.

Before using Terraform provisioners, you must note the following rules:

  • A provisioner will only run once.

  • You can run a provisioner when applying a configuration or when destroying it.

  • You can run multiple provisioners in the same configuration file.

  • The provisioner block must be nested inside a resource block.

  • If an infrastructure has been created before you add the provisioner block, the provisioner will not execute. This is because provisioners only execute immediately after the creation or destruction of a resource.

Below is an example of the usage of a provisioner block:

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

Provisioner Arguments

Let’s learn about some of the Terraform blocks and arguments you will use when working with provisioners.

  • connection (required): a connection block is used to define the connection credentials for the provisoner to use to connect to the resource. The connection block supports both ssh and winrm connections. The following is an example of a provisioner block:

connection {
  type     = "ssh"
  user     = "root"
  password = var.root_password
  host     = self.public_ip
}

If you're running multiple provisioners in your Terraform configuration code, you can nest the connection block inside the provisioner block:

provisioner "remote-exec" {
 connection {    
  type     = "ssh"    
  user     = "ubuntu"    
  host     = self.public_ip    
  private_key = file("<path-to-private-key>")  
  }
#...other provisioner code
}

If there is no connection block nested inside a provisioner block, the provisioner will default to use a connection block declared outside it.

  • source: the source argument is used in the file and remote-exec provisioner types to specify the source of a file you want to copy to the newly created infrastructure. You can either use source or content argument for file provisioners.

#file source 
source = "conf/myapp.conf" 

#script  source 
source = "script.sh"

  • content: this is used in the file provisioner type. Instead of specifying a file source, you can also write content directly to the destination of the provisioner.

content = "hello world!"

  • destination (required): specifies the destination where you want to save a source file or content on the newly created resource. It is used in both the file and remote-exec provisioner types.

destination = "/etc/myapp.conf"

  • command (required): is used to specify a command Terraform should run after creating or destroying a resource. It is used in local-exec provisioners.

command = "echo ${self.private_ip} >> private_ips.txt"

  • inline: this argument accepts an array of command strings for a remote-exec provisioner to run. It is used in remote-exec provisioners.

inline = [ 
  "chmod +x /tmp/script.sh", 
  "/tmp/script.sh args", 
]

  • when: by default, Terraform executes providers after you run terraform apply. You can tell Terraform to run the provisioner after a terraform destroy by setting the value of when argument to destroy.

when = destroy

  • working_dir: it is used in local-exec provisioners to specify the directory where a command should be executed. The directory path can be written as a relative path to the current directory or an absolute path. The default is your current working directory.

  • interpreter: it is used in local-exec and remote-exec provisioners to specify which interpreter should be used to run the command. It is an array that accepts the actual interpreter as the first item, and a path as the second item.

interpreter = ["PowerShell", "-Command"]

  • on_failure: if a provisioner is not executed successfully when creating a resource, Terraform will mark it as a tainted. That is, when next you run terraform apply, Terraform will destroy the resource and recreate it. You can alter this behavior by setting the on_failure argument to continue. on_failure=”fail” is the default.

Types of Terraform Provisioners

Let's see some examples of each of the three common provisioner types.

File Provisioner

The file provisioner allows you to copy files or file directories from the machine you're running Terraform to a newly created resource. A typical file provisioner looks like the example below:

#...resource declaration code
provisioner "file" {
  source      = "conf/myapp.conf"
  destination = "/etc/myapp.conf"
}

This code passes the myapp.conf file from the local path to the /etc/myapp.conf destination path in the newly created infrastructure.

Local Provisioner

local-exec provisioner is used to invoke an action, command, or script in the local machine that is running Terraform. Once used as the type of a provisioner block, it executes the command that is defined in the command argument.

#...resource declaration code
provisioner "local-exec" {
  command = "echo ${self.private_ip} >> private_ips.txt"
  interpreter = ["PowerShell", "-Command"]
}

The code above creates a file named private_ips.txt file, then saves the private IP of the resource inside.

Remote Provisioner

Similar to local-exec, remote-exec executes commands or scripts. However, this type of provisioner runs the command or script on the newly created resource.

#...resource declaration code
  provisioner "remote-exec" {
  inline = [
    "puppet apply",
    "consul join ${aws_instance.web.private_ip}",
  ]
}

Lab Time!

In this lab session, you will create file, local-exec, and remote-exec provisioners to invoke specific actions after creating an EC2 instance.

Get Your AWS Credentials

If you're using the Skillmix Labs feature, open the lab settings (the beaker icon) on the right side of the code editor. Then, click the Start Lab button to start hte lab environment.

Wait for the credentials to load. Then run this in the terminal.

Be sure to enter in your own access key and secret key and name your profile 'smx-lab'.

$ aws configure --profile smx-lab
AWS Access Key ID [None]: 
AWS Secret Access Key [None]: 
Default region name [None]: us-west-2
Default output format [None]: 

Note: If you're using your own AWS account you'll need to ensure that you've created and configured a named AWS CLI profile named smx-lab.

Create an EC2 Key pair

  • Navigate to the EC2 dashboard from the AWS console.

  • In the navigation pane on left, under Network & Security, choose Key Pairs

  • Choose Create Key pair on top right corner

  • Fill in key information

Name: skillmix-instance-key

Key pair type : RSA

Private key file format: .pem.

Then, click on create key pair.

AWS will automatically download your private key pair once the creation is successful. You will use this key pair later in the lab.

Create Project Directory

Open your terminal/command line and, run the following command to create our project directory:

#create the project directory
$ mkdir terraform-provisioners

Create Workspaces

Since we intend to create three different provisioners independently, this is a good avenue to use Terraform workspaces. If you haven’t learned about workspaces before, that’s ok. Just follow the steps for now.

We will create 3 workspaces, one for each type of provisioner.

Run the following commands, one after the other to create the workspaces:

#create local-exec provisioner workspace
$ terraform workspace new local-provisioner

#create file provisioner workspace
$ terraform workspace new file-provsioner

#create remote-exec provisoner workspace
$ terraform workspace new remote-provisioner

Now, we will create a configuration file and populate it.

Firstly, run terraform workspace select local-provisoner to switch into the local-provisioner workspace.

Create a main.tf file inside the workspace:

# switch to the workspace
$ terraform workspace select local-provisoner

# create the main.tf
$ touch main.tf

Open the main.tf file; input, and save the following code:

terraform {
  required_providers {
  aws = {
  source  = "hashicorp/aws"
  version = "~> 4.0"
  }
 }
}
provider "aws" {
  profile = "skillmix-lab"
  region  = "us-west-2"
}
resource "aws_instance" "web_server" {
  ami           = "ami-08d4ac5b634553e16"
  instance_type = "t2.micro"
  tags = {
    Name = "skillmix-lab-instance"
  }

 provisioner "local-exec" {
  command = "echo ${self.private_ip} >> my_private_ip.txt"
 }
}

Code Review

  • The self keyword in the code allows us to tap into the newly created EC2 instance.

  • The command argument saves the private IP of the EC2 instance inside a my_private_ip.txt file in our current directory.

Create the EC2 Instance Resource

Now we will run the terraform workflow commands to see the local-exec provisioner in action

#initialize terraform
$ terraform init

#apply the configuration
$ terraform apply -auto-approve

Once the EC2 instance has been successfully created, you can inspect your current working directory, and you will see the newly created my_private_ip.txt file

You can open the file and will find the private IP address in it.

After seeing the local-exec provisioner in action, run terraform destroy to destroy the EC2 instance we created.

Create File Provisioner

Next we will switch to the file-provisioner workspace and write a new configuration.

Run terraform workspace select file-provisoner to switch into our file-provisioner workspace.

# switch to the right workspace
$ terraform workspace select file-provisoner

Open the main.tf file; update it with following code:

terraform {
  required_providers {
    aws = {
    source  = "hashicorp/aws"
    version = "~> 4.0"
    }
  }
}
provider "aws" {
  profile = "skillmix-lab"
  region  = "us-west-2"
}

resource "aws_instance" "web_server" {
    ami           = "ami-08d4ac5b634553e16"
    instance_type = "t2.micro"
    tags = {
    Name = "skillmix-lab-instance"
    }

  #connection block
  connection {
    type     = "ssh"
    user     = "ubuntu"
    host     = self.public_ip
    private_key = file("<path-to-private-key>")
  }

  #file provisioner block
  provisioner "file" {
    content = "My Skillmix EC2 Instance"
    destination = "/home/ubuntu/skillmix-file.txt"
  }
}

#output the public ip
output "ip" {
  value = aws_instance.web_server.public_ip
}

Code Review

  1. In the connection block:

  2. we're connecting to the instance through SSH.

  3. Since we're using an Ubuntu AMI to create our instance, the default user value is "ubuntu"

  4. the host argument taps into the new instance we create to extract and use the public IP as its value.

  5. the private_key argument specifies the path to your private key. Replace <path-to-private-key> with the path to the key pair you downloaded earlier.

  6. The provisioner block will create a skillmix.txt file with the defined content in the /home/ubuntu/skillmix-file.txt path of the EC2 instance to be created.

  7. In the output block, we are printing the public ip of the created instance to the terminal so that we can use it to SSH into the instance.

Create the EC2 Instance Resource

Now we will run the terraform commands to see the file provisioner in action.

Run following commands:

#initialize terraform
$ terraform init

#apply the configuration
$ terraform apply -auto-approve

Take note of the IP address that Terraform outputs on the CLI.

After applying the configuration and Terraform successfully creates our EC2 instance, we will SSH into the instance to be sure that the provisioner block worked.

Run the following commands:

#SSH into the destination folder using the outputted ip address
$ ssh -i "/home/ubuntu/skillmix-file.tx" ubuntu@<public_ip>
#view the content of the file
$ cat skillmix-file

This will display our content value from the provisioner block on the command line.

Run terraform destroy to destroy the EC2 instance resource.

Create Remote Provisioner

Lastly, we will work with the remote provisioner. To start, switch to the right workspace:

# switch workspaces
$ terraform workspace select remote-provisoner

Open the main.tf file and update it with the following code:

terraform {
  required_providers {
    aws = {
    source  = "hashicorp/aws"
    version = "~> 4.0"
    }
  }
}
provider "aws" {
  profile = "skillmix-lab"
  region  = "us-west-2"
}

resource "aws_instance" "web_server" {
  ami           = "ami-08d4ac5b634553e16"
  instance_type = "t2.micro"
  tags = {
  Name = "skillmix-lab-instance"
  }

#connection block
connection {
  type     = "ssh"
  user     = "ubuntu"
  host     = self.public_ip
  private_key = file("<path-to-private-key>")
}

#remote provisioner
provisioner "remote-exec" {
  inline = [
    "touch /home/ubuntu/skillmix.txt"
  ]
}
}

#output the public ip
output "ip" {
  value = aws_instance.web_server.public_ip
}

Code Review

  • The connection and output blocks are the same as in the file provisioner code we used earlier.

  • The provisioner block will create a skillmix.txt file with the defined content in the /home/ubuntu/skillmix-file.txt path of our EC2 instance.

Create the EC2 Instance Resource

Now, we will run the terraform workflow commands to see the remote-exec provisioner in action.

Run the following commands:

#initialize terraform
$ terraform init

#apply the configuration
$ terraform apply -auto-approve

Take note of the IP address that is printed as an output on the CLI.

After applying the configuration and Terraform successfully creates our EC2 instance, we will SSH into the instance to be sure that the provisioner block worked.

Run the commands below to SSH into the instance and view the files in the directory:

#SSH into the destination folder using the outputted ip address
$ ssh -i "/home/ubuntu/" ubuntu@<public_ip>
#list the content of the directory
$ ls

This will list the folders and files in that directory. You should see the skillmix.txt file we created by running a touch command via the remote provisioner as one of the listed files.

Delete resources

Once you complete this lab, run terraform destroy to remove all the resources you created.