Provisioners
Terraform allows executing commands or shell scripts on provisioned virtual servers, such as EC2 instances, by passing user_data during creation. Many cloud providers, including AWS, Google Cloud, and Azure, support this functionality. However, it’s important to note that Terraform only provisions the infrastructure and passes the execution responsibility to the cloud provider. Once the EC2 instance is created, Terraform marks the process as complete and does not wait for the VM to initialize before executing the given commands. To address this limitation, Terraform provides provisioners, which allow executing scripts and commands directly from Terraform, ensuring better control and visibility over the initialization process.
remote-exec
The remote-exec provisioner allows executing commands on a remote server using the inline attribute but Terraform requires an explicit connection block (SSH or WinRM) to establish access using self.public_ip, ec2-user, and a private key.
connection { type = “ssh” host = self.public_ip user = “ec2-user” private_key = file(var.private_key_location) } provisioner “remote-exec” { inline = [ “export ENV=dev”, “mkdir newdir” ] } provisioner “file” { source = “entry-script.sh” destination = “/home/ec2-user/entry-script.sh” } provisioner “remote-exec” { inline = [“/home/ec2-user/entry-script.sh”]
|
Difference from user_data: While user_data hands over commands to AWS for execution, remote-exec directly SSHs into the instance and runs commands, providing real-time execution feedback in Terraform logs.
Executing Scripts: Instead of inline commands, a script can be referenced for execution; ensure it has proper permissions by running chmod +x entry-script.sh before execution.
provisioner “remote-exec” { script = “entry-script.sh” } ** make sure entry-script.sh file should have execute permissions before running terraform script. |
inline vs script methods in the remote-exec provisioner ?
Both inline and script methods in the remote-exec provisioner execute commands on a remote machine via SSH, but they work slightly differently. Here’s a breakdown of their differences:
1. Using inline
- Executes commands directly on the remote instance.
- You provide a list (inline = []) of shell commands to execute.
- Each command in the list is run sequentially.
- The script does not need to have execution (chmod +x) permissions.
- You must explicitly specify bash or sh if needed.
Pros:
✅ Allows multiple commands to be executed one after another.
✅ No need to set script permissions (chmod +x).
✅ More control over the execution environment.
Cons:
❌ More prone to formatting issues when passing complex scripts.
❌ Harder to maintain for large scripts.
2. Using script
How it Works:
- Terraform copies the script file to the remote instance automatically.
- The script is executed as a file, rather than as inline commands.
- The script must have execution permissions (chmod +x entry-script.sh).
- Runs the script in the default shell (sh or bash depending on the system).
Pros:
✅ Ideal for running large scripts.
✅ Easier to maintain and update separately.
✅ Terraform handles script transfer automatically.
Cons:
❌ Requires execution permissions (chmod +x).
❌ Cannot run multiple commands directly like inline.
Examples:
provisioner “remote-exec” {
inline = [
“echo ‘Running setup…'”,
“yum update -y”,
“bash /home/ec2-user/setup.sh”
]
}
provisioner “remote-exec” {
script = “/home/ec2-user/setup.sh”
}
provisioner “file” {
source = “entry-script.sh”
destination = “/home/ec2-user/entry-script.sh”
}
provisioner “remote-exec” {
inline = [
“chmod +x /home/ec2-user/entry-script.sh”,
“/home/ec2-user/entry-script.sh”
]
}
And local exec is basically commends that will be executed locally. So, for example, if you want to execute some commands locally on our laptop then we can execute them using local exec. So, we’re going to have a command attribute, and we can execute any command like we would directly by typing it on our local computer. If you wanted to print out.
provisioner “local-exec” {
command = “echo \”${self.public_ip}\” > output.txt”
}
Terraform discourages provisioners: They are considered a last resort and are not recommended due to unpredictable behaviour, potential script failures, and lack of state tracking. Instead, Terraform suggests using cloud provider features like user_data for initialization.
Provisioners break idempotency: Terraform cannot track script execution results, making it difficult to determine state changes. Configuration management tools (e.g., Ansible, Chef, or Puppet) are better suited for post-provisioning tasks, as they can manage system state more effectively.
Provisioner failures taint resources: If a provisioner fails (e.g., missing script file), Terraform marks the resource as failed and requires recreation, potentially deleting a successfully provisioned instance. For local tasks, Terraform recommends using the local provider instead of local-exec, as it properly tracks state changes.
Terraform Trigger for Inline Command Changes
- user_data_replace_on_change applies only to user_data in AWS resources like aws_instance or aws_launch_template, ensuring instance replacement when user_data changes. It does not apply to provisioners like remote-exec or local-exec, meaning changes to inline commands do not trigger instance replacement by default.
- To force resource replacement on provisioner changes, use the triggers argument by hashing inline commands, ensuring Terraform detects changes. Additionally, using create_before_destroy = true in the lifecycle block helps minimize downtime by creating a new resource before deleting the old one.
resource “aws_instance” “example” { ami = “ami-12345678” instance_type = “t2.micro” provisioner “remote-exec” { connection { …} inline = [ “sudo apt-get update”, “sudo apt-get install -y nginx” ] } # ‘triggers’ argument to force replacement when the ‘inline’ commands change lifecycle { create_before_destroy = true } triggers = { inline_commands = sha256(join(“”, provisioner[“remote-exec”][0].inline)) } } |