Hacking the Model
Provisioners (local-exec, remote-exec, file) allow you to run scripts as part of a Terraform resource lifecycle. They look useful — you can SSH into a new server and install a package right after it boots, with the connection details and the shell commands to run declared right inside the resource block. In practice, they are a source of fragility and should be treated as a last resort.
Why provisioners are problematic:
- Not idempotent by default: Terraform only runs provisioners on resource creation, not on subsequent applies. If the script fails halfway, the resource is marked "tainted" and will be destroyed and recreated on the next apply — potentially causing data loss.
- No state tracking: If you change the script content, Terraform has no way to know the script needs to be re-run. The resource appears unchanged in the plan.
- Network fragility:
remote-execrequires SSH access from wherever Terraform runs (your laptop, a CI runner) to the new instance. This requires open security group rules and working key management — both of which are operational burdens. - Breaks the declarative model: Terraform cannot reason about what the script does. It cannot show you a diff of the script's effects in
terraform plan.
Better alternatives:
| Use Case | Better Tool |
|---|---|
| Install software on boot | User Data / cloud-init (runs on the instance itself, no network dependency) |
| Pre-bake software into an image | Packer (build an AMI with everything installed; Terraform just launches it) |
| Configure running servers | Ansible, Chef, Puppet (purpose-built for configuration management) |
| Run a one-off command locally | local-exec is acceptable for non-critical tasks like triggering a webhook after deployment |
The golden rule: if you find yourself writing a provisioner, ask whether the work belongs in a Packer image build or a cloud-init script instead.