Skip to content

Case study: from script to framework

provisio was extracted from a real, procedural provisioning CLI — roughly 600 lines across five files, with zero tests — that stood up a production Azure stack (resource group, storage account + file share, Key Vault, Container Apps, a Static Web App, a self-hosted CI runner VM, managed-identity RBAC, and CI secrets). Rebuilding that CLI as a provisio app is what shaped the SDK.

Before → after

Before (a procedural script) After (a provisio app)
Procedural functions, 0 tests Steps declared on provisio, fully unit-tested (fake runner)
CLI framework coupled into the domain Domain touches neither; the CLI is generated
Config declared twice (a dataclass and the CLI signature) One Setting schema drives CLI + resolution + redaction
Values threaded through module-level locals Typed OutputKeys validated before anything runs
Four copy-pasted patterns ensure / update_if_confirmed / poll_until
No audit trail, no state Redacted audit log + diff CI gate

A step is pure intent

@step("resource-group", "Create resource group")
def resource_group(ctx):
    az, cfg = ctx.tool("az"), ctx.settings
    ensure(ctx, describe=f"resource group '{cfg.resource_group}'",
           exists=lambda: az.exists("group", "show", "--name", cfg.resource_group),
           create=lambda: az("group", "create", "--name", cfg.resource_group, "--location", cfg.location))

The whole orchestration is one list

A hand-threaded try/except and a step table collapse to an explicit, reviewable plan:

def build_plan() -> Plan:
    return Plan([
        steps.prerequisites, steps.subscription, steps.resource_group, steps.storage,
        steps.keyvault, steps.container_env, steps.static_web_app, steps.kv_secrets,
        steps.data_upload, steps.container_app, steps.principal, steps.kv_access,
        steps.env_secrets, steps.file_share_mount, steps.registry, steps.runner_vm,
        steps.ci_secrets,
    ])

The CLI is a few lines

from provisio import build_cli
from myapp.application import app_spec   # InfraApplication(name, plan, settings, tools)
cli = build_cli(app_spec)                # -> `myinfra apply` / `myinfra diff`

Parity, with zero cloud

An end-to-end test runs the whole plan through the fake runner and asserts it issues the same az commands the original script did — parity, with no cloud access and no credentials.