The Plan-Apply Pattern#

My favorite way to supercharge nearly any administrative script is the Plan-Apply Pattern. Instead of directly mutating infrastructure, you split up your script into two logical steps:

  • Plan - Gather the info about what you want to modify, figure out how you need to modify it, and create an object representing each operation you want to perform
  • Apply - Take the object generated from the plan, and apply it

This is especially helpful for scripts that modify or delete infrastructure. Tools like Terraform do this automatically, but I often have to roll my own scripts for things like bulk cleanup or modification.

Instead of doing this:

for _, resource := range GetAllResources() {
  if ShouldDelete(resource) {
    Delete(resource)
  }
}

Do this:

func Plan() []Resource {
  var resourcesToDelete []Resource
  for _, resource := range GetAllResources() {
    if ShouldDelete(resource) {
      resourcesToDelete = append(resourcesToDelete, resource)
    }
  }
  
  return resourcesToDelete  
}

func Apply(plan []Resource) {
  for _, resource := range plan {
    Delete(resource)
  }
} 

plan := Plan()
Apply(plan)

This comes with a ton of benefits.

You can clearly display the plan before applying it, making it clear exactly what will be done ahead of time.

plan := Plan()
Print(plan)
Apply(plan)

You can add an approval step, so that the operator has a chance to back out.

plan := Plan()
// ...
if GetApproval() {
  Apply(plan)
}

You can add a dry-run mode to automatically skip the apply.

dryRun := GetFlag("--dry-run")
plan := Plan()
// ...
if !dryRun && GetApproval() {
  Apply(plan)
}

You can serialize the object and save it for review. For example, you could store the plan file into a Jira ticket or PR to be implemented only after it’s been reviewed for correctness.

plan := Plan()
SavePlan("plan.json", plan)

// ... later on
plan := RestorePlan("plan.json")
Apply(plan)

You can add logic to detect higher risk changes, and prompt only if you see something risky

plan := Plan()
hasHighRiskChanges := GetHighRiskChanges(plan)
if !hasHighRiskChanges || GetApproval() {
  Apply(plan)
}