New to Kestra?
Use blueprints to kickstart your first workflows.
Terraform plan says No changes while the resource is dead. Probe it, plan a replace, apply behind approval, then re-probe.
The flow emits one verdict per run: the HTTP status the health probe got,
the terraform plan detailed exit code, the container id recorded in state
and, after a replacement, the new container id. A degraded verdict next to
a plan exit code of 0 is the case no drift detector reports.
Read them as {{ outputs.probe_and_plan.vars.health_status }},
{{ outputs.probe_and_plan.vars.plan_exit }} and
{{ outputs.probe_and_plan.vars.container_id }}, with the same three names
on outputs.reverify.vars after a replacement. The raw text stays on the
execution as files: {{ outputs.probe_and_plan.outputFiles['plan.txt'] }},
{{ outputs.plan_replacement.outputFiles['replace_plan.txt'] }} and the
binary plan at
{{ outputs.plan_replacement.outputFiles['replace.tfplan'] }}.
Read-only by default. dry_run is true and auto_remediate is false, so a
plain run probes the resource, saves the replacement plan and stops. The
apply needs auto_remediate true, dry_run false and a human answering
approved: true on the paused execution. An unanswered pause changes
nothing.
Point docker_host at the remote daemon that owns the resource. Never at
the daemon running Kestra. terraform apply -replace destroys before it
recreates, so a provider aimed at your own host would churn Kestra and
every other container beside it.
A resource can match its configuration byte for byte and still be useless.
A container is RUNNING, so the Docker provider reads back the same name,
the same image and the same ports the state file records. terraform plan
prints "No changes. Your infrastructure matches the configuration." The
process inside it died twenty minutes ago.
probe_and_plan runs one Terraform container that does both reads.
It fetches the health URL over HTTP, then runs
terraform plan -detailed-exitcode. The probe status and the plan exit
code come back as task outputs.report_finding logs both numbers side by side, so the paradox is in
the run log rather than in somebody's head.plan_replacement runs only when the probe failed. It produces
terraform plan -replace=<address> and saves the binary plan, so the
change a human reviews is the change that gets applied.approve_replacement pauses. apply_replacement applies the saved plan
behind that approval.reverify probes the health URL again and re-runs plan.
fail_when_still_degraded fails the run when the probe is still bad,
because a rebuilt resource that is still broken means the fault was
never in the resource.The Terraform project is declared once in probe_and_plan then travels to
the later tasks as a task output file. State lives in Postgres through
Terraform's built-in pg backend, so every task in the flow reads and
writes the same state and the saved plan stays valid across the pause.
terraform apply -replace run against a reviewed plan, never a fresh one.Platform and SRE teams who manage long-lived resources with Terraform and
have been paged for something plan swore was fine. Anyone running
containers, VMs or managed instances where liveness is not visible in the
provider's read.
The pieces are a probe, a plan, an approval and a second probe, with a destructive step in the middle. A shell script can run the commands. It cannot hold a plan file across a one hour human approval. It cannot keep the state backend credentials off the command line or record both reads as outputs an operator can inspect weeks later. It cannot fail the run on the re-probe instead of on the apply. Kestra gives the pause, the typed inputs that keep the default read-only, the task outputs and the failure branch.
Run on Kestra 2.0.5 with plugin-terraform 1.2.10, Terraform 1.9.8 and the
kreuzwerker/docker provider 3.9.0 against a Docker-in-Docker daemon and a
Postgres state backend.
plan exit 0, zero findings, nothing
planned.plan exit 0 with "No changes. Your
infrastructure matches the configuration." The detector fired on the same
inputs that returned nothing a minute earlier.dry_run: true: the replacement plan was saved and logged
(1 to add, 1 to destroy), nothing applied, container id unchanged.auto_remediate: true, dry_run: false, approved: the container was
replaced and the re-probe returned 200.terraform init and
the errors block posted to Slack.POSTGRES_USERNAME, POSTGRES_PASSWORD: the Terraform state backend.SLACK_WEBHOOK_URL: incoming webhook for the failure alert.dry_run (BOOL, default true): report only.auto_remediate (BOOL, default false): allow the apply.docker_host: the remote Docker daemon the provider manages.health_url, expected_status, probe_timeout_seconds: the probe.resource_address: what -replace targets.container_name, app_image: the managed resource.state_pg_endpoint, state_pg_database: the pg state backend.main.tf in probe_and_plan with your own configuration
and backend. Keep an output that exposes the resource id so the before
and after values show up in the log.docker_host to the remote daemon, health_url to something your
users actually hit, resource_address to the address you would type
after -replace.auto_remediate: true and dry_run: false, then
resume the paused execution with approved: true.core.flow.Loop and
concurrencyLimit: 1, so two replacements never race on one state lock.plan_replacement and apply -replace directly when you trust the
detector enough to skip the review step.This blueprint was created by zkasuran.
