# terraform / provisioners / bootstrapping
Bootstrapping & Script Execution: Provisioners & User Data
While Terraform excels at provisioning cloud infrastructure, running post-boot configuration scripts requires care. Learn how to bootstrap servers reliably using cloud-native user_data, when to use local-exec to trigger Ansible, and why Terraform provisioners are treated as a last resort.
Infrastructure Provisioning vs Configuration Management
DevOps tools are specialized. Terraform is designed for Infrastructure Provisioning (creating VPCs, subnets, EC2 instances, S3 buckets). Ansible and Cloud-Init are designed for Configuration Management (installing packages, configuring Nginx, managing users).
| Tool / Approach | Primary Job | Best Used For |
|---|---|---|
| Terraform | Infrastructure as Code | Provisioning servers, networks, DNS, storage |
| cloud-init (user_data) | Server Bootstrap | Installing Docker, basic updates on first boot |
| Ansible | Configuration Management | Fleet maintenance, rolling updates, security patches |
| Packer | Golden AMI Baking | Pre-installing all heavy dependencies into an image |
The Clean Handoff
Terraform creates the virtual machine and security groups. Once the server is online, Terraform hands off to cloud-init or Ansible to configure the application runtime.
1. Cloud-Init user_data (The Production Standard)
resource "aws_instance" "web" { ami = "ami-0123456789abcdef0" instance_type = "t3.micro" user_data = <<-EOF #!/bin/bash apt-get update -y apt-get install -y nginx echo "<h1>Deployed via Terraform with user_data</h1>" > /var/www/html/index.html systemctl enable --now nginx EOF tags = { Name = "web-server-bootstrapped" } }
Advantages of user_data
- No Open SSH Ports: Runs locally on the VM hypervisor during initial boot.
- Zero Connection Timeouts: Doesn't require Terraform CLI to maintain an active SSH pipe.
- Immutable Infrastructure: Perfect for autoscaling groups where new instances self-configure automatically.
2. provisioner "local-exec": Running Local Commands
resource "aws_instance" "web" { ami = "ami-0123456789abcdef0" instance_type = "t3.micro" # Run a command locally on the laptop / CI runner executing Terraform provisioner "local-exec" { command = "echo ${self.public_ip} >> inventory.ini" } # Optional: Trigger Ansible playbook after creation provisioner "local-exec" { command = "ansible-playbook -i '${self.public_ip},' deploy.yml" } }
The self Object
Inside a provisioner block, the self keyword references attributes of the parent resource being provisioned (e.g. self.public_ip, self.id).
3. provisioner "remote-exec": Direct SSH Execution
resource "aws_instance" "web" { ami = "ami-0123456789abcdef0" instance_type = "t3.micro" key_name = "deployer-key" connection { type = "ssh" user = "ubuntu" private_key = file("~/.ssh/id_rsa") host = self.public_ip } provisioner "remote-exec" { inline = [ "sudo apt-get update -y", "sudo apt-get install -y docker.io", "sudo systemctl enable --now docker" ] } }
Provisioner Failure Taints the Resource
If any command in remote-exec returns a non-zero exit code, Terraform marks the entire resource as tainted. On the next apply, Terraform will terminate the server and try again from scratch.
4. The terraform_data Resource (Modern null_resource)
# Triggers arbitrary commands whenever a config file hash changes resource "terraform_data" "app_config" { triggers_replace = [ filesha256("${path.module}/config.json") ] provisioner "local-exec" { command = "curl -X POST https://api.slack.com/webhook -d '{\"text\":\"Config updated!\"}'" } }
Zero External Providers Needed
Introduced in Terraform 1.4+, terraform_data is built directly into Terraform core, completely replacing the legacy null_resource from the HashiCorp null provider.
Why Provisioners are a Last Resort
| Provisioner Limitation | Recommended Production Alternative |
|---|---|
| Non-Declarative: Bash scripts are imperative; Terraform cannot plan or preview their changes. | Use user_data / cloud-init for declarative initialization. |
| No State Tracking: Terraform cannot detect if someone modified the software installed by a provisioner. | Use Ansible or SaltStack for continuous state enforcement. |
| Network Sensitivity: Firewalls, SSH rate limits, and flaky connections break applies. | Use Packer to bake all dependencies into custom golden AMIs before Terraform runs. |