Provisioning automation usually starts as a script that creates a machine, and ends as an unmaintainable collection of scripts that each create a slightly different machine. The difference between those two outcomes is structure: separating the stages of a provisioning request so that each stage has one job and can be reasoned about on its own.
Five stages, kept separate
A provisioning workflow that holds up over time has five distinct stages. Keeping them separate is what lets you add a platform, change a policy or tighten verification without rewriting everything.
- Intake — turn whatever arrived into a normalised specification.
- Policy — decide whether this request is permitted at all.
- Validation — confirm the target can actually satisfy it.
- Build — perform the provisioning through the platform's own API.
- Verification — prove the result matches the specification.
Intake: normalise early
Requests arrive in wildly different shapes: a ticket written in prose, a form submission, an order from a billing system, an API call from another service. The first stage's only job is to produce one canonical specification object regardless of source.
A workable specification shape covers size, image or template, network placement, storage, location, owner, tenant, environment, tags and lifecycle expectations. Make every field explicit, including the ones you would otherwise default silently — a defaulted field that nobody chose is where inconsistency enters.
{
"tenant": "acme",
"environment": "production",
"location": "ams-1",
"compute": { "vcpu": 8, "memory_gb": 32, "dedicated": true },
"storage": [{ "size_gb": 400, "class": "nvme" }],
"network": { "vlan": 418, "public_ipv4": false },
"image": "ubuntu-24.04-base",
"owner": "platform-team",
"tags": { "cost-center": "eng-infra", "backup-policy": "nightly-30d" },
"lifecycle": { "expires_at": null }
}Normalising at the boundary means the four stages behind it never need to know whether the request came from a ticket or an API call.
Policy: answer 'may this happen' before 'can this happen'
Policy evaluation is cheap, deterministic and refusable. Run it first so that an impermissible request fails immediately with a clear reason, rather than after a capacity query that cost you an API round trip.
The policy questions worth encoding are consistent across most organisations: is this requester entitled to request in this environment; is the requested size within their ceiling; is this image on the approved list; does this request exceed a threshold that requires named approval; does the specification satisfy mandatory tagging.
Make approval a workflow step, not a side channel
Validation: ask the platform, do not assume
Validation is where you query live state: available capacity in the target cluster, quota remaining for the tenant, free addresses in the requested network, placement constraints, template availability in that location.
The important design decision here is that validation failures are a normal outcome, not an error. A request that cannot be satisfied right now should produce a clear, reportable result — 'no capacity in ams-1 for this shape' — rather than an exception somebody has to interpret.
Build: idempotent and resumable
The build stage is the one that creates side effects, so it needs two properties. First, each step declares whether it is safe to repeat. Second, the run records what it has created, so a retry resumes rather than duplicates.
A practical way to get this is to generate a deterministic resource identifier during intake and use it as the name or tag on every created object. A retry can then query for existing objects with that identifier before creating anything.
Build should also handle everything that makes a machine a managed machine rather than just a running one: inventory registration, monitoring enrolment, backup policy assignment, configuration management handoff, DNS and address records.
Verification: compare against the specification
The build stage reports what it did. The verification stage independently checks what exists. These are not the same claim, and keeping them separate catches a real class of failure.
- Is the instance reachable on the expected interface and port?
- Does the running configuration match the requested shape — not the requested shape as recorded, but as read back from the platform?
- Is it present in monitoring, and is it reporting?
- Is it covered by a backup policy, and has a first copy succeeded?
- Are the mandatory tags present on the object itself?
A build that fails verification should be reported as unverified, not as failed and not as successful. The distinction matters: failed means nothing was created, unverified means something was created that does not match expectations and needs a human decision.
Handling multiple platforms without forking the workflow
The common mistake is building one workflow per platform. Intake, policy and verification logic are platform-neutral; only the build stage is platform-specific. Structure the workflow so that the build stage dispatches to a platform-specific sub-workflow chosen by location, plan or availability.
This keeps the governance consistent — every request passes the same policy and comes out the other side with the same verification — while allowing the mechanics to differ per platform.
Lifecycle is part of provisioning
A provisioning system that only creates resources produces a slowly growing estate that nobody can account for. Resize, re-tag, suspend, snapshot and decommission deserve the same five-stage treatment, with the same approvals and the same verification.
Decommission in particular benefits from structure: confirm no dependent services, take the final retained copy, require owner sign-off, release the resource, then update inventory, monitoring, backup policy and billing. Done by hand, at least one of those last four is routinely missed.
A checklist for reviewing your own workflow
- Can a request be refused with a reason before anything is created?
- Does a retry resume rather than duplicate?
- Does verification read state back, rather than trusting the build's own report?
- Is every mandatory post-build registration part of the run, or still a human step?
- Can you answer, for any machine, which workflow version built it and who approved it?
About this guide
Written by the StackHeal team as original material for this site. It describes practices and product design, and does not cite customer deployments or performance figures we have not measured. If something here is wrong or incomplete, tell us and we will correct it.
Send a correction or suggest a topic