The Plan-Apply Pattern
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)
}