13 — Provisioners and archive_file¶
Folder: none yet — paste this into a scratch root.
Two loosely related odds and ends, both genuinely useful to recognise: running arbitrary commands during apply, and building a zip as part of a config.
null_resource and local-exec¶
null_resource has no real backing anywhere. It exists purely to trigger provisioners or
hold triggers values that force re-evaluation.
locals {
greetings = {
en = "Hello"
fr = "Bonjour"
es = "Hola"
}
}
resource "local_file" "multi_lang" {
for_each = local.greetings
filename = "/tmp/greeting-${each.key}.txt"
content = each.value
}
resource "null_resource" "print_all" {
triggers = {
file_count = length(local.greetings)
}
provisioner "local-exec" {
command = "echo Created ${self.triggers.file_count} greeting files"
}
}
The local-exec output prints during apply. Change a value in local.greetings and
re-apply — the triggers map changes, so the null_resource is replaced and the
provisioner runs again. Without triggers, it would run once and never again.
This is not a dynamic block
This example is for_each over a map. A dynamic block is a different thing: it
generates repeated nested blocks inside one resource, not repeated resources. There
is no local-provider example in this repo, because the local providers have no nested
blocks worth repeating. Both real ones are cloud resources: the lifecycle_rule
generator in Lab 12 and
resources.tf in the core GCP root, and dynamic "rule" in
the sandbox's backend/infra/rbac.tf.
Why provisioners are a code smell¶
Modern Terraform guidance treats local-exec and remote-exec as a last resort. They run
imperative commands inside a declarative tool, they aren't tracked in state beyond "did it
run", and a failure mid-provisioner leaves the resource tainted.
Most cases are better handled by cloud-init, a startup script, or a real config-management
tool. Recognise them because they appear in older repos — and because the same mechanism is
an attack vector: a malicious PR adding a local-exec that exfiltrates state is the exploit
described in Theory §16. This is
harmless in a /tmp sandbox and very much not harmless in CI with cloud credentials
attached.
The GCP version is Lab 17.
archive_file¶
The archive provider zips files. Entirely local and offline, but it's a real pattern —
this is how you prepare a Cloud Function source bundle.
terraform {
required_providers {
archive = {
source = "hashicorp/archive"
version = "~> 2.4"
}
}
}
data "archive_file" "bundle" {
type = "zip"
source_dir = "/tmp/tf-practice"
output_path = "/tmp/tf-practice-bundle.zip"
}
output "bundle_hash" {
value = data.archive_file.bundle.output_md5
}
Note it's a data source, not a resource — it computes the zip during plan rather than
managing it as owned state.
Why it matters: the output_md5 is the point. Real configs feed that hash into the
deploying resource so that changing the source automatically triggers a redeploy, without
anyone having to remember to bump a version. Good bridge between "toy local lab" and "real
repo pattern", still without touching a cloud.
Key takeaway¶
Both of these are escape hatches from the declarative model, and they escape differently.
local-exec runs a command Terraform can't reason about, so its result lives outside the
graph — treat it as a signal to look for a better mechanism. archive_file escapes safely,
because what comes back is a hash that feeds into the graph. That's the distinction worth
keeping: whether the escape hatch hands you something Terraform can still track.
You've finished Stage 2. Next: Stage 3 — GCP, where the same mechanics run against a real project with quotas, org policies, propagation delays and a bill.