Self-Service Infrastructure
Letting teams provision what they need without a ticket, by constraining what can be asked for rather than by reviewing every request.
The question, the obvious approach, and why it breaks
Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.
How does a team get a database, a queue or a bucket without waiting for someone, and without being able to create anything at all?
Infrastructure requests are frequent, mostly routine, and answered by a small number of people who hold the credentials — so a routine need becomes a queue, and the queue becomes the reason people build workarounds.
Give teams direct access to the cloud console. They know what they need better than a central team does, and it removes the bottleneck immediately.
Console-created resources exist outside any plan, so the infrastructure definition and reality diverge from the first click (Drift).
- Console-created resources exist outside any plan, so the infrastructure definition and reality diverge from the first click (Drift).
- Nothing carries the defaults that matter and nobody remembers: encryption, backups, private networking, retention, ownership tags.
- A resource created by a person leaves with that person. Six months later nobody knows what it is for, and deleting it is a risk nobody wants to take.
- Broad console access is broad production access, and the blast radius of a mistake is the whole account (Least Privilege in Production).
- Spend rises with no way to attribute it, because the resources carry no owner (Cost Per Request).
What is actually happening
Underneath the tooling, which is the part that survives a change of tool.
- Self-service is not "everyone can do anything". It is a *narrowed interface*: teams can ask for the small number of things the platform models, with the parameters the platform exposes, and nothing else.
- The narrowing is what makes review unnecessary. If the only thing that can be requested is a database from a reviewed module, with private networking and backups fixed and only size and version as parameters, then approving each request adds latency and no safety (Guardrails, Not Gates).
- The request must be a *declaration* rather than an action. A team declares that this service needs a database of this size; the platform reconciles reality towards that declaration, so re-running is safe and drift is detectable (Declarative vs Imperative Infrastructure).
- The identity performing the provisioning is the platform's, not the team's. Teams never hold credentials broad enough to create infrastructure directly — which is what lets the interface be the only control point.
- Every provisioned resource must be traceable to a declaration, an owner and a reason, or day two is impossible.
The request is a declaration, not an action
The structural choice that decides everything else: does the team perform an action against the cloud, or declare a desired state that the platform reconciles? The first cannot be reviewed, cannot be re-run and leaves no record. The second is code, and inherits everything you already know how to do with code.
This is also what makes the interface narrow enough to skip approvals. The team is not asking for arbitrary infrastructure; it is filling in the parameters of a shape someone already reviewed.
1apiVersion: platform/v12kind: ManagedPostgres3metadata:4 name: orders-primary5 owner: team-orders # required: no owner, no resource6 costCentre: retail7spec:8 environment: production9 size: small # small | medium | large, from the catalogue10 version: "16"11 retentionDays: 1412 13# Not exposed as parameters, because they are not decisions:14# encryption at rest, private networking only, automated backups,15# deletion protection, audit logging, maintenance window policy.What is absent from spec is the design. Every field the platform does not expose is a decision it has already made on the team's behalf, and every field it does expose is one it accepts will vary. Getting that split wrong in either direction is how a catalogue becomes either useless or pointless.
What happens between the declaration and the resource
The steps below are what an approval used to be standing in for. Each one is faster than a human, runs on every request, and — unlike an approver — does not get tired at the fortieth request of the week.
- 1Declare
Team commits the claim to its repository and opens a change.
fails by Provisioning done out of band, leaving no reviewable artifact.
evidence The claim exists in version control with an author.
- 2Validate
Schema and catalogue check: is this a shape we model, with parameters in range?
fails by Accepting free-form configuration, which is console access in YAML.
evidence Invalid claims are rejected in seconds, with the reason.
- 3Evaluate policy
Required tags, no public exposure, size within envelope, environment rules (Policy as Code).
fails by Policy checked only after creation, when the violation already exists.
evidence A deliberately non-compliant claim is refused before anything is created.
- 4Plan
Compute what will actually change, including what will be replaced or destroyed (The Plan: Desired vs Current).
fails by Applying without reading the plan, which is how a rename deletes a database.
evidence The plan is attached to the change and read by the reviewer.
- 5Apply
Platform identity provisions; the team's identity never could.
fails by Teams holding provisioning credentials directly, so the interface is not the only path.
evidence Cloud audit log shows the platform identity, referencing the change.
- 6Record
Write owner, cost centre, module version and the originating change into the inventory.
fails by Resource exists, provenance does not.
evidence Every live resource resolves to a claim and an owner.
- 7Hand back
Return connection details through workload identity or the secret store — never as a copied credential (Workload Identity).
fails by Credentials pasted into a chat message and then into CI variables.
evidence No static credential exists for the new resource.
- 8Reconcile
Detect drift between declaration and reality on a schedule.
fails by Nobody notices someone changed it in the console until an incident.
evidence Scheduled plan reports no unexplained differences (Drift).
The failures that arrive on day two hundred
Self-service infrastructure rarely fails at provisioning. It fails months later, in ways that all trace back to something the request did not capture.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| A team is reorganised | Resources with an owner tag that names a team that no longer exists | Ownership captured once, never reconciled | Reconcile owner tags against the current org record; unowned means decommission or adopt |
| A misconfigured loop in a provisioning change | Dozens of instances created in minutes | No quota on the shape or the environment | Quota per environment and per shape; the request fails loudly instead of succeeding expensively (The Automation Trap) |
| Someone fixes something in the console during an incident | The next apply reverts the fix, at the worst possible time | Emergency change made outside the declaration | Expect it, detect it, and require the change to be back-filled into the declaration (Break-Glass Access) |
| A module default is changed | The next unrelated apply on every consuming service proposes a large change | Modules not versioned, so consumers float onto new defaults | Pin module versions per consumer; upgrade deliberately (Dependency Pinning) |
| A service is decommissioned | Database still running, still backed up, still billed | Deletion never modelled as part of the interface | Model decommission as a first-class claim removal with a retention window |
| A stateful resource is renamed in the declaration | Plan proposes destroy and create | Identity of the resource is its name in state | Read the plan; move state deliberately rather than applying (Destructive Changes: What a Rename Really Does) |
How to do it properly
Most important first.
- Model a small catalogue of shapes with opinionated defaults, and expose only the parameters that genuinely vary between teams (Modules: Reuse Without Hiding).
- Make the request a versioned artifact in the team's repository, reviewed the way code is, applied by the platform's identity (Infrastructure as Code).
- Enforce the non-negotiables as policy evaluated on the declaration, before anything is created (Policy as Code).
- Attach ownership, cost centre and environment at creation time as required fields; a resource that cannot be attributed should not be creatable.
- Give every request a budget and a quota, so a mistake is bounded by a number rather than by whether anyone noticed.
- Make deletion self-service too, with the same narrowing — and make it the platform's job to know what deleting will take with it (Destructive Changes: What a Rename Really Does).
How much can this affect
Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.
A bad module or a runaway request is contained by quotas, per-environment credential scoping and policy evaluated before creation. Without those, the containment is the size of the account the platform identity can reach.
What can go wrong
- The catalogue is too narrow, so most real needs fall outside it and the ticket queue reappears for everything interesting.
- The catalogue is too wide, so it is effectively console access with extra steps and no defaults worth having.
- The platform identity is over-privileged, making the provisioning pipeline the most dangerous credential in the organisation (Identity and Access Management (IAM)).
- Resources are created and never reconciled, so declarations and reality diverge and nobody trusts either.
- No quota, so one misconfigured loop provisions a hundred instances and the containment is a billing alert three days later (Cost Drivers).
- Deletion is not modelled, so decommissioning is manual and therefore incomplete.
- "Self-service means teams have cloud access." It means teams have a narrowed interface. Direct access is the thing self-service exists to avoid, because it is the version with no defaults and no record.
- "If nobody approves the request, nothing is being checked." Policy, module defaults and quotas are all checks. They run in milliseconds, on every request, without a queue.
- "We can add ownership tags later." Later means a fleet of resources you cannot attribute, and a manual archaeology project to fix it.
Operating it
Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.
- A team provisions a routine dependency without a human in the path, and the resource comes up with encryption, backups and private networking without anyone having chosen them.
- A plan run against the live environment reports no unexplained differences.
- Every resource can be traced to a declaration in a repository, with an owner.
- A deliberate over-request hits a quota rather than succeeding.
- Provisioning must be reversible as a unit: a failed request removes exactly what it created rather than leaving fragments.
- For stateful resources, reversal is not symmetric — destroying a database is not the inverse of creating one. Model deletion as a two-step with a retention window and a deliberate second action (Destructive Changes: What a Rename Really Does).
- A bad module version can be rolled back for new requests immediately; existing resources created from it need migration, not rollback.
- Automate provisioning, defaults, tagging, quota enforcement and drift detection.
- Automate the paperwork that used to justify the ticket: the ownership record, the cost attribution, the audit trail of who declared what (The Audit Trail).
- Do not automate away the review of the *declaration* — it is code, and it gets reviewed like code. What is removed is the approval of each apply, not the reading of what is being asked for.
- Keep destructive actions on stateful resources deliberately manual at the final step, and make that one of the very few places a human confirmation genuinely buys something.
- Modelling a shape properly is real work, so the catalogue grows slowly and there is always something not in it.
- Opinionated defaults are wrong for someone, and that someone is usually the team with the most demanding workload.
- The platform now holds the credentials that matter, concentrating risk in one place — which is easier to protect and worse to lose.
Where this applies
This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.
- CLOUD-SPECIFICThe quota, tagging and account-boundary mechanisms differ by provider, and so does what a single over-privileged identity can reach. On a provider where account boundaries are cheap, per-team accounts are the strongest containment available; where they are expensive, the containment has to come from policy and scoped roles instead.
- SCALE-SPECIFICWith one team and a handful of resources, a shared IaC repository and a person who runs the apply is self-service enough. The catalogue starts paying for itself when requests arrive faster than the credential holder can answer them without becoming the bottleneck.
Where the depth lives
This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.