Hey {{first name | there}}. Terraform has no built-in scheduler, and the first time you hit that wall, it reads like a missing feature. 

It is not. It is the boundary that keeps Terraform declarative, and the moment you bolt an imperative scheduler onto a declarative provisioning tool. 

You start collecting state lock fights and apply runs nobody can account for.

So the real question is how you schedule and orchestrate around Terraform without breaking that. 

Here is one answer, written as a single flow:

id: terraform-approval-plan-apply

namespace: company.team

description: |

  Put a human gate between terraform plan and apply. A webhook starts the run,

  the flow plans and shows the changes, pauses for approval, then applies only

  the reviewed plan and logs the result.

tasks:

  - id: plan

    type: io.kestra.plugin.terraform.cli.TerraformCLI

    description: Produce a saved plan for review.

    taskRunner:

      type: io.kestra.plugin.scripts.runner.docker.Docker

    containerImage: hashicorp/terraform:latest

    env:

      AWS_ACCESS_KEY_ID: "{ secret('AWS_ACCESS_KEY_ID') }"

      AWS_SECRET_ACCESS_KEY: "{ secret('AWS_SECRET_ACCESS_KEY') }"

    inputFiles:

      main. tf: |

📰 TECHNICAL NOTES: Why Terraform’s missing scheduler is the point

It comes down to declarative versus imperative. Terraform is declarative: you define the end state, what infrastructure should exist, and let it work out the steps. 

Schedulers and scripts are imperative: you define the step-by-step actions and the timing, when and how things happen. 

Both are fine on their own.

The trouble starts when you push the imperative half into the declarative tool. 

Cron wrapped around terraform apply -auto-approve, a CI job that reruns plan on a timer, a bash wrapper only one person fully understands. 

You end up with applies firing on a schedule while nobody is watching, and a state file that several jobs think they own. 

It works right up until two of them run at once.

Scheduling without going imperative

The fix is not to make Terraform imperative. It is to orchestrate it from a layer that is also declarative. 

Kestra is declarative from day one, so instead of writing an imperative code layer just to schedule a trigger, the tasks, their dependencies, and the schedule all live in one YAML file.

The flow above is the whole pattern.

A webhook triggers it, plan runs, the flow parks on the review pause and emails a named reviewer, and “apply” only moves once they resume it. 

Because apply runs the saved tfplan, the diff that ships is the exact one that got approved, not a fresh plan that drifted in the thirty minutes since. 

The approver and the timestamp get written into the run history on their own, so the review is part of the run instead of a Slack thread sitting next to it.

Terraform Plan and Apply with an Approval Gate Flow

What starts to matter with a team on it

On a solo project, none of this matters. It starts to matter the moment the flow is shared, and it is mostly what keeps the approval gate above from turning into a checkbox 

  • RBAC decides who can run, edit, and approve a flow, scoped per namespace. 

  • Assets keep a live inventory of the resources your flows touch, VMs, IPs, certs, tables, with lineage and an audit trail. 

  • Task Runners push the execution itself onto isolated runners on Kubernetes or cloud batch, on demand, air-gapped included

🌍IN THE ECOSYSTEM

⏱️UNTIL NEXT TIME

I have known and explored Kestra for a year now, but this is my first time writing about it publicly. 

What kept coming up, in my own use and from people running it in production, is boring in the way good infrastructure is boring. 

The flow is a YAML file, so it reviews like any other pull request, and someone who does not write the underlying scripts can still read it and approve it.

Know an engineer wrestling with this? Share this link with them

Thank you to Kestra for Sponsoring Today’s Newsletter!

Divine Odazie
CTO, EverythingDevOps

HOW DID WE DO?

Login or Subscribe to participate