Introduction
Terraform provisioners are resources that help with actions like copying files and running commands when you're setting up or tearing down your infrastructure. Similar to user data in AWS where we manually write commands for each instance, Terraform Provisioners automates the execution steps inside the instances.
Usually, provisioners only kick in when you're creating stuff. But you can set them up to also do their thing when you're destroying the resources. This way, you can make sure everything's tidy and set up just the way you want, whether you're building or tearing down your infrastructure.
Types of Provisioners
Local-exec provisioner
The local-exec
provisioner is used to run scripts or commands locally on the machine such as initializing a local database or configuring local resources. eg: local workstation
Remote-exec provisioner
The remote-exec
provisioner is used to run scripts or commands on a remote machine over SSH or WinRM connections. It's often used to configure or install software on provisioned instances.eg: EC2 instance
File provisioner
- The file provisoner transfers files or directories from the local to the remote repository. This is useful for deploying configuration files, scripts, or other assets to a provisioned instance. eg: local to remote location.
Use Cases
Provisioners are useful in bootstraping the configuration like installing software packages, configuration of services, setting up the network, managing configuration files etc.
Provisioners help in post deployment tasks like setting up the databases or setting up logging or monitoring agents.
Provisioners help to upload specific configuration files to a server.
Provisioners help in integration with other tools like puppet, chef, ansible for more complex configurations.
Demo
# Define the AWS provider configuration.
provider "aws" {
region = "us-east-1" # Replace with your desired AWS region.
}
# Variables
variable "cidr" {
description = "CIDR block for the VPC"
default = "10.0.0.0/16"
}
# Create AWS key pair
resource "aws_key_pair" "example" {
key_name = "terraform_demo_sur"
public_key = file("~/.ssh/id_rsa.pub") # Update with your public key path
}
# Create VPC
resource "aws_vpc" "myvpc" {
cidr_block = var.cidr
tags = {
Name = "terraform-vpc"
}
}
# Create subnet
resource "aws_subnet" "example_subnet" {
vpc_id = aws_vpc.myvpc.id
cidr_block = "10.0.0.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "example-subnet"
}
}
# Create Internet Gateway
resource "aws_internet_gateway" "example_ig" {
vpc_id = aws_vpc.myvpc.id
}
# Create Route Table
resource "aws_route_table" "RT" {
vpc_id = aws_vpc.myvpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.example_ig.id
}
}
# Associate Route Table with Subnet
resource "aws_route_table_association" "example_association" {
subnet_id = aws_subnet.example_subnet.id
route_table_id = aws_route_table.RT.id
}
# Create Security Group
resource "aws_security_group" "example_security_group" {
vpc_id = aws_vpc.myvpc.id
ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Create EC2 Instance
resource "aws_instance" "example_instance" {
ami = "ami-0c55b159cbfafe1f0" # Update with desired AMI
instance_type = "t2.micro"
key_name = aws_key_pair.example.key_name
subnet_id = aws_subnet.example_subnet.vpc_id
security_groups = [aws_security_group.example_security_group.name]
# Local-exec provisioner
provisioner "local-exec" {
command = "echo 'Local execution: Hello, Terraform!' > local_exec_output.txt"
}
# Remote-exec provisioner
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa") # Update with your private key path
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"echo 'Remote execution: Hello, Terraform!' > remote_exec_output.txt",
# Add any remote commands you wish to execute
]
}
# File provisioner
provisioner "file" {
source = "local_file.txt" # Update with path to your local file
destination = "/tmp/terraform_example.txt"
}
}
Limitations of Provisioners
Provisioners doesn't have the ability to validate if any configuration already exists in the instance. For eg: Provisioner might install a package without checking if it already exists in the system.
On provisioner failure, Terraform marks the resource as "tainted," which leads to it recreation of the instance only that causes difficulty in fixing the errors.
Provisionser lack in the ability of modification once created as they only run by default at the time of creation and only destruction if configured.
Best Practice of using Provisioners
Writing provisioner scripts with simplicity and clarity in mind as complex scripts leads to higher maintainance and troubleshooting.
Ensuring the scripts should be producing the same results by running any number of times.
Error handling should be managed clearly to avoid cascading errors.
Attach multiple provisioners to a single resource, and they will execute in the order they are declared.
As discussed above, if resource is marked tainted on provisioner failure, this can be adjusted by setting the
on_failure
tocontinue
, so, Terraform will ignore provisioner failures.
Alternatives to Provisioners
User data scripts or cloud-init can be used to bootstrap the instances during provisioning the cloud environments.
All the dependencies and configuration settings can be combined to create Docker images, reducing the need for provisioning tasks at the infrastructure level.
Tools with pre-configured software and settings can eliminate the need to runtime configuration during provisioning.
Database provisioning, monitoring setup and security configuration offered by cloud providers can be utilized despite of using provisioners.
Conclusion
In today blog, Terraform provisioners are helpful for setting up things when you're creating infrastructure. But, they have some limits, like sometimes they don't work perfectly or can cause problems. It's important to follow some good ways of using them, like keeping things simple and testing carefully.