TL;DR: Kill it in almost every case: provisioners are imperative escape hatches outside Terraform's model, a default creation-time failure taints the resource, and their effects rot invisibly. Move bootstrapping into user_data or baked images, move orchestration into CI, and keep provisioners only for destroy-time cleanup carrying an expiry comment.
How to approach it
Concede the legitimate niches early so the position reads as judgement rather than dogma, then anchor on mechanics: what a provisioner failure does to the resource, and what the plan shows about a script's effects, which is nothing.
A strong answer
A provisioner is a shell escape hatch: local-exec, remote-exec, or file upload that runs at create (or destroy) time and whose effects Terraform neither models nor tracks. Three consequences follow directly. A failed creation-time provisioner marks the resource tainted by default; destroy-time failures and on_failure = continue have different behavior. The resulting taint can make the next apply replace infrastructure that was otherwise healthy. The script's effects are invisible to plan, so drift accumulates in silence. And remote-exec drags connectivity requirements (SSH keys, bastion hops, security-group exceptions) into a tool whose promise was declaring outcomes instead of reaching into machines.
The replacements are boring and effective:
resource "aws_instance" "app" {
ami = data.aws_ami.golden.id
instance_type = "t3.small"
user_data = file("${path.module}/bootstrap.sh")
user_data_replace_on_change = true
# Explicitly choose replacement when this plaintext bootstrap changes.
# Use user_data_base64 instead if supplying pre-encoded content.
}
Bootstrap logic moves into user_data or cloud-init for simple cases, or into a Packer-baked image when startup time and determinism matter; image changes then roll through an ASG like any other deployment. Post-create orchestration belongs in the deploy pipeline or in a configuration-management layer with its own idempotence guarantees, never inside Terraform's create path where a script failure can leave partial effects and a tainted resource. The maintainers' line stands: provisioners are a last resort, and a codebase reaching for them constantly is reporting missing architecture.
Two niches survive contact with reality. Destroy-time provisioners clean up child resources no provider supports deleting, and one-shot glue during a migration earns its keep with a loud comment and a removal date. Both should read as acknowledged debts, not patterns.
The honest interview answer includes the scar: nearly everyone has shipped one provisioner to unblock a launch. What separates candidates is knowing exactly what it cost (a tainted resource at the worst hour) and having a named replacement queued.
What interviewers probe next
"Everything wants to destroy after a provisioner failed. Fix?" That is the taint mechanic: clear it with terraform untaint once the root cause is understood, or accept deliberate recreation. Untainting before understanding just relocates the outage.
"Packer image or user_data?" user_data keeps one artifact and pays minutes at every boot; images shift installation into a build pipeline. Measure boot time and keep runtime configuration controlled; image bytes alone do not make the full environment identical.
"Where does Ansible fit then?" As a separate layer with its own inventory and idempotence model, triggered by CI or instance bootstrap, not embedded in Terraform's apply path.
Common mistakes
Defending provisioners as configuration management because the README documents them.
Ignoring the credentials dimension: remote-exec needs network reachability and keys, which fights private-subnet designs and drips secrets into logs.
Leaving a working provisioner untouched for years, so it references apt mirrors that no longer exist, and the first recreation after a real incident fails twice.
Sources: AWS instance user data, Provisioner failures.