Here is some information about this architecture.
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
.
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"
}
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.
Let's see some examples of each of the three common provisioner types.
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-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.
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}",
]
}
In this lab session, you will create file, local-exec, and remote-exec provisioners to invoke specific actions after creating an EC2 instance.
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.
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.
Open your terminal/command line and, run the following command to create our project directory:
#create the project directory
$ mkdir terraform-provisioners
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.
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
In the connection block:
we're connecting to the instance through SSH.
Since we're using an Ubuntu AMI to create our instance, the default user value is "ubuntu
"
the host argument taps into the new instance we create to extract and use the public IP as its value.
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.
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.
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.
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.
Once you complete this lab, run terraform destroy
to remove all the resources you created.