Part 9— HumanGov Application — Terraform-7: Terraform Provisioners

Cansu Tekin
8 min readNov 24, 2023

--

HumanGov is a software-as-a-service (SaaS) cloud company that will create a Human Resources Management SaaS application for the Department of Education across all 50 states in the US and host the application files and databases in the cloud.

In this following project series, we are going to transition the architecture from a traditional virtual machine architecture to a modern container-based architecture using Docker containers and Kubernetes running on AWS. In addition, we will also be responsible for automating the complete software delivery process using Pipelines CI/CD using AWS services such as AWS CodeCommit, AWS CodePipeline, AWS CodeBuild, and AWS CodeDeploy. Finally, we will learn how to monitor and observe the cloud environment in real-time using tools such as Prometheus, Grafana, and automate one-off cloud tasks using Python and the AWS SDK.

In this section, we are going to introduce working with Terraform Provisioners and practice Terraform with cloud provider AWS before using it in the implementation of the HumanGov application. This is the 9th part of a project series. Check Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, Part 7 and Part 8 to follow up.

Terraform Provisioners

Provisioners are a set of features in Terraform that are used to configure and set up infrastructure resources with scripts or commands after they are created. For example, you created a VM instance and want to know which location/region it was created, what is its ID, or its tags. You can have all this information inside a file in the VM provided by provisioners. On the other hand, we cannot use provisioners for configuration management such as configuration setup, system update, software installation, setting firewall rules, etc., We will use Ansible for configuration management for the later projects.

Provisioners reside in the resource block and we can have multiple provisioners in the same block.

Terraform Provisioners

Local-exec Provisioner: Allows to run commands on the machine running Terraform(local), not on the resources. It is often used for running configuration scripts, initializing databases, or executing custom commands after resource creation. It allows only one command line. In the example below, an echo command is executed after the AWS instance is created.

resource "aws_instance" "example" {
ami = "ami-041feb57c611358bd"
instance_type = "t2.micro"

provisioner "local-exec" {
command = "echo 'Hello, Terraform!' > terraform_hello.txt"
}
}

Remote-exec Provisioner: Allows to run scripts on a remote resource over SSH or WinRM such as installing software, configuring settings, or initializing resources after resources are created. We can use multiple command lines. In the example below, commands are executed on the AWS instance after it’s created.

resource "aws_instance" "example" {
ami = "ami-041feb57c611358bd"
instance_type = "t2.micro"

provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
"sudo service nginx start",
]
}
}

File Provisioner: Copies files or directories from the machine running Terraforms to the newly created resources.

resource "aws_instance" "example" {
ami = "ami-041feb57c611358bd"
instance_type = "t2.micro"

provisioner "file" {
source = "local/path/file.txt"
destination = "/remote/path/instance/file.txt"
}

provisioner "file" {
source = "local/path/directory"
destination = "/remote/path/instance/"
}
}

Terraform will create an AWS instance and then copy the specified file (file.txt) from the local machine to the specified path on the AWS instance, and all files and subdirectories within the specified local directory will be copied to the remote directory on the AWS instance.

Desired state paradigm: Re-running a Terraform configuration should not affect the infrastructure if it’s already in the desired state. If a provisioner modifies a resource every time it’s run, it can lead to unexpected changes and make it challenging to maintain a stable infrastructure. Always consider alternative methods before using the provisioners.

Hands-on: Terraform Provisioners

We will create a new configuration first with a new EC2 instance and S3 bucket. We will connect to the EC2 instance with SSH connectivity to perform some provisionery tasks.

Step 1: Create a new directory for your Terraform project and create the Terraform files

Go to AWS Services and open Cloud9. Create a terraform-provisioners-example directory and create main.tf, resources.tf, outputs.tf files in it.

mkdir terraform-provisioners-example && cd terraform-provisioners-example
touch main.tf resources.tf outputs.tf

Then create an SSH key to be able to connect to the EC2 instance remotely. Provisioners also will use this key to establish a connection to the EC2 instance to perform tasks.

ssh-keygen -f ec2-key

This command will create a private and a public key.

Step 2: Edit the Terraform files

main.tf

Determine cloud provider and region.

provider "aws" {
region = "us-east-1"
}

resources.tf

We will create a security group and EC2 instance resources. We should open port 22 to establish a remote SSH connection. We also need to specify the key pairs for the EC2 instance. aws_key_pair resource will create new key pairs inside AWS which will allow us to access the EC2 instance using the private key which is a pair of the public key.

While creating an EC2 instance, we need to AMI. We will use Ubuntu at this time. To find out the AMI for your resource: go to AWS Services -> EC2 -> Instances -> Launch Instance -> Select Ubuntu-> Copy and paste the AMI ID in the EC2 instance of the resources block.

resource "aws_security_group" "example" {
name = "example"

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}

resource "aws_key_pair" "key" {
key_name = "aws_tf_key"
public_key = file("./ec2-key.pub")
}

resource "aws_instance" "ec2" {
ami = "ami-0fc5d935ebf8bc3bc"
instance_type = "t2.micro"
key_name = aws_key_pair.key.key_name
vpc_security_group_ids = [aws_security_group.example.id]
}

outputs.tf

We want to print the public IP of the EC2 instance to the console after configuring the infrastructure.

output "ec2_pub_ip" {
value = aws_instance.ec2.public_ip
}

Step 3: Provision the resources

Initialize the configuration.

terraform init && terraform apply -auto-approve

Copy the IP address of the EC2 instance. We will use it to test SSH connectivity.

Step 4: Test the SSH connectivity

We need to private key we created and the public IP address of the EC2 instance to connect to this instance.

ssh -i ec2-key ubuntu@54.89.73.180

Now we are inside of the Ubuntu virtual machine.

Step 5: Destroy the resources

Connectivity is working. Exit from the Ubuntu virtual machine with exit command and destroy the resources.

terraform destroy -auto-approve

Step 6: Create a dump file to copy from the local Cloud9 environment to the EC2 instance using provisioners

First, create a dump file.

echo "This is a sample dump file" >> dump

Now, we are going to add provisioners to do some tasks inside the EC2 instance. Add the Terraform Provisioners to the resources.tf file.

  • The 1st provisioner is a local-exec provisioner that runs locally on your machine running the Terraform. It will grab the public IP of the machine running Terraform and store it in a txt file.
provisioner "local-exec" {
command = "echo ${self.public_ip} >> ec2-public-ip.txt"
}
  • The 2nd provisioner is the file provisioner. It will copy the dump file we created in our local system to the remote destination inside of the EC2 instance.
provisioner "file" {
source = "./dump" ## Do not forget to create fictitious 'dump' file
destination = "/tmp/dump-onp"
}
  • The 3rd provisioner is again a file provisioner. It will create a new file called ec2-az.txt in the EC2 instance and put specific content in this file.
provisioner "file" {
content = "EC2 AZ: ${self.availability_zone}"
destination = "/tmp/ec2-az.txt"
}
  • The 4th provisioner is a remote-exec provisioner. This provisioner allows us to run commands on the remote destination. In our case an EC2 instance. These commands in inline will run in the order we write. We will put some information regarding to EC2 instance into corresponding txt files inside the EC2 instance.
provisioner "remote-exec" {
inline = [
"echo EC2 ARN: ${self.arn} >> /tmp/arn.txt",
"echo EC2 Public DNS: ${self.public_dns} >> /tmp/public_dns.txt",
"echo EC2 Private IP: ${self.private_ip} >> /tmp/private_ip.txt"
]

}

Step 7:Create connection

As you can see, some of the provisioners need to establish a remote SSH connection to be able to complete the tasks. Add a connection block inside the aws_instance resources block.

connection {
type = "ssh"
user = "ubuntu"
private_key = file("./ec2-key")
host = self.public_ip
}

This is the last version of the resources.tf file.

resource "aws_security_group" "example" {
name = "example"

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}

resource "aws_key_pair" "key" {
key_name = "aws_tf_key"
public_key = file("./ec2-key.pub")
}

resource "aws_instance" "ec2" {
ami = "ami-0fc5d935ebf8bc3bc"
instance_type = "t2.micro"
key_name = aws_key_pair.key.key_name
vpc_security_group_ids = [aws_security_group.example.id]

tags = {
"Name" = "webserver01"
}

provisioner "local-exec" {
command = "echo ${self.public_ip} >> ec2-public-ip.txt"
}

provisioner "file" {
source = "./dump" ## Do not forget to create fictitious 'dump' file
destination = "/tmp/dump-onp"
}

provisioner "file" {
content = "EC2 AZ: ${self.availability_zone}"
destination = "/tmp/ec2-az.txt"
}

provisioner "remote-exec" {
inline = [
"echo EC2 ARN: ${self.arn} >> /tmp/arn.txt",
"echo EC2 Public DNS: ${self.public_dns} >> /tmp/public_dns.txt",
"echo EC2 Private IP: ${self.private_ip} >> /tmp/private_ip.txt"
]

}

connection {
type = "ssh"
user = "ubuntu"
private_key = file("./ec2-key")
host = self.public_ip
}

}

Step 8: Provision the resources

Check formatting with terraform fmt command first and then validate with terraform validate before initializing Terraform to make sure everything is correct.

It seems everything is successfully executed.

Terraform local-exec provisioner created the ec2-public-ip.txt file locally with the IP address.

Step 9: Check if the provisioners executed as planned for the remote destination; the EC2 instance

Create an SSH connection.

ssh -i ec2-key ubuntu@54.82.10.220

Go to the EC2 instance /tmp/ folder to check the 2nd, 3rd, and 4th Terraform provisioners.

As you can see 2nd provisioners created dump-onp, 3rd provisioners created ec2-az.txt file with the specified content “EC2 AZ: us-east-1bubuntu@ip-172–31–28–29”, 4th provisioner created public_dns.txt, private_ip.txt, and arn.txt files.

Step 10: Destroy the resources

terraform destroy -auto-approve

CONGRULATIOTIONS!!

--

--

Cansu Tekin

AWS Community Builder | Full Stack Java Developer | DevOps | AWS | Microsoft Azure | Google Cloud | Docker | Kubernetes | Ansible | Terraform