Self-Service Cloud Infrastructure: From Ticket Queues to Claims
When every bucket, queue permission, or service identity requires a platform-team pull request, "wait on the platform team" quietly becomes the bottleneck. This day looks at the shift from ticket-driven provisioning to a **self-service claim model** — where application teams describe the cloud resources they need inside their own service config and the platform provisions them automatically — and at the security win that comes with it: replacing one shared cloud identity with a **least-privilege identity per service**.
The bottleneck: infrastructure divorced from the app lifecycle
On most platforms, application code ships through a fast, self-service path — a team merges a change, and continuous delivery rolls it out — while the infrastructure that code depends on travels a slower, human-gated road. Need a new object-storage bucket, permission to read a message queue, or a cloud identity for a brand-new service? You file a request, the platform team turns it into an infrastructure-as-code change only they can write and merge, and your work waits until they get to it. The two lifecycles are out of step: the app moves in minutes, its infrastructure in days.
App code path: write code -> merge -> CD rolls it out (minutes, self-service)
Infra path: file ticket -> platform writes IaC PR -> merge -> apply (days, gated)
This is not a tooling failure so much as an ownership mismatch. The team that knows what the service needs is not the team allowed to provision it, so every app-scoped resource becomes a round-trip through people who lack the context and have their own queue. Rule of thumb: when the thing blocking a deploy is a human in another team's backlog rather than a check that must pass, you have a self-service gap, not a safety control — and the cost compounds with every new service.
Two kinds of infrastructure: foundation vs app-scoped
The fix is not "let everyone provision everything." It is to notice that cloud resources fall into two very different buckets, and only one of them belongs in a self-service path. Foundation resources — the network, accounts, shared keys, DNS — are long-lived, shared across many services, and part of the security perimeter. App-scoped resources — a single service's own bucket, its own identity, its own cache — are created and destroyed with that one service and referenced only by it.
| Resource | Lifecycle & sharing | Belongs to |
|---|---|---|
| VPC / subnets / accounts | Perimeter, shared by all | Foundation (platform-owned IaC) |
| Shared encryption key, base IAM, DNS zone | Multi-tenant, security-sensitive | Foundation (platform-owned IaC) |
| A service's own bucket / identity / cache | Single-app, created & destroyed with it | App-scoped (self-service) |
The test is a single axis — lifecycle plus sharing. Owned by one app, born and destroyed with it, referenced only by it → app-scoped, safe to self-serve. Shared, perimeter, or longer-lived than any one app → foundation, stays gated. Rule of thumb: draw this boundary explicitly and make it unambiguous; when a resource is ambiguous, default it to foundation and raise it — never let the self-service layer silently claim something that belongs to the perimeter.
The claim model: describe what you need, in your own config
Self-service works when the application team declares its resource needs declaratively, right beside the code that uses them, and a controller reconciles reality to match. Instead of writing cloud-specific infrastructure code or filing a ticket, the team writes a small, high-level claim — "this service needs an identity that can read objects and decrypt with the shared key" — and commits it in the service's own configuration. The same GitOps flow that deploys the app now also provisions its infrastructure.
# lives in the service's own chart, next to its Deployment
kind: ServiceRoleClaim
metadata:
name: image-api
spec:
owner: media-team # attribution, stamped on every created resource
serviceAccount: image-api # the in-cluster identity to bind
access: [object-store, kms] # named capability profiles, not raw permissions
Three properties make this safe rather than a free-for-all. The claim is high-level (it asks for named capabilities like object-store, not arbitrary raw permissions); it is owned and attributed (every provisioned resource is tagged with the owning team); and it has no escape hatch (there is no "give me any permission" field — a genuinely new need means adding a reviewed capability profile to a shared catalog, one pull request, visible to all). Rule of thumb: a self-service claim should be a menu order from an allowlist, never a blank check — the allowlist is what lets you remove the human approver without removing the guardrail.
From claim to cloud: the reconcile loop
Under the hood, a controller in the cluster watches for claims and continuously reconciles them into real cloud resources — the same control-loop pattern Kubernetes uses for pods, now pointed at cloud APIs. The application team never touches a cloud console or writes provider-specific code; they commit a claim, and the platform's controller does the rest, using a composition — a platform-authored template that turns one high-level claim into the correct provider-specific resources with guardrails baked in.
The composition is where the platform keeps control without being in the loop for each request: it pins the target account and provider (so a claim can't point itself at something over-privileged), stamps ownership tags, validates the requested capabilities against the allowlist, and applies the right cleanup policy. Rule of thumb: put the guardrails in the composition, not in a review step — encode the policy once, in the template every claim flows through, so safety is automatic and uniform rather than dependent on who reviews which ticket.
Deletion is a first-class design decision
The moment infrastructure is created and destroyed with the app, deletion becomes a design decision you must make deliberately — because "reconcile to match the claim" also means "when the claim disappears, so should the resource." Get this wrong in one direction and you leak orphaned identities forever; wrong in the other and you delete a bucket full of data on an accidental config change. The answer is type-dependent deletion: stateless resources clean themselves up, stateful ones never delete automatically.
Stateless (identity, role, service account):
claim deleted -> resource DELETED (clean lifecycle, no orphans)
Stateful (bucket, cache, database):
claim deleted -> resource RETAINED (data-loss guard; delete is manual & explicit)
Two safety latches make this robust. First, retain by default for anything holding data — an accidental delete should never be able to destroy state. Second, protect even the stateless resources against accidental removal (finalizers, no automatic pruning) so that only an explicit claim deletion — not a transient sync glitch — ever tears something down. Rule of thumb: decide deletion policy per resource type before you ship the self-service path; the default for stateful resources is always retain, and every automatic deletion should trace to a deliberate human action, not a reconcile hiccup.
The security win: per-service least-privilege identity
Self-service is the velocity story, but the same shift fixes a quieter security problem. Many platforms start with one shared cloud identity that every service assumes — simple to set up, and disastrous under attack, because compromising any single service yields the union of every service's permissions. The blast radius is the whole platform. When each service instead claims its own identity, that identity can be scoped to exactly what that one service needs.
The trust is pinned tightly: each cloud identity is bound to exactly one in-cluster service account (an exact subject, not a namespace wildcard), so a neighbouring workload can't assume it. Crucially, you can adopt this without a risky big-bang migration — mint the new per-service identities and cut services over one at a time, giving each service exactly what it holds today first, then trimming its permissions down afterward under observation. Rule of thumb: migrate identity in two phases — first achieve parity (same access, new per-service identity) so nothing breaks, then trim toward least privilege per service; never try to re-scope and re-identity in the same step.
Keeping the boundary honest over time
A clean split on day one erodes unless something enforces it, because the tempting shortcuts all involve the self-service layer reaching across the boundary — grabbing a permission on a shared key, editing a foundation resource, claiming something ambiguous. The defense is layered validation, each layer independent, so a gap in one is caught by the next.
| Layer | Enforces |
|---|---|
| Claim schema validation | Requested capabilities ⊆ allowlist; owner required; obvious escalations rejected |
| Composition template | Only known capabilities expand; provider/account pinned, not claim-chosen |
| Admission policy | Reject raw resources that bypass the claim path; block perimeter-owned kinds |
| Cloud-side permission boundary | A hard ceiling the created identities can never exceed, whatever the claim says |
The most important discipline is the cross-boundary grant: when an app-scoped identity needs access to a foundation resource (say, a shared key), the self-service layer owns the app-side identity and the foundation layer owns the grant on its own resource — never the reverse. If the self-service layer could write policy onto a shared, perimeter resource, it would be co-managing something outside its lane, and the boundary would leak. Rule of thumb: the self-service plane may create and hold an app's own principal, but any grant on a foundation resource is issued by the foundation plane — the app requests access, the perimeter owner grants it, and no single object is ever owned by both planes.
Key takeaways
- Ticket-driven provisioning couples the app lifecycle to another team's backlog; the fix is self-service for app-scoped resources, keeping foundation resources gated.
- Split infrastructure by lifecycle + sharing: single-app, born-and-dies-with-it resources are self-service; shared, perimeter, or long-lived resources stay platform-owned. Ambiguous → default to foundation.
- The claim model lets teams declare high-level, allowlisted capabilities in their own config; a controller reconciles claims into real cloud resources via platform-authored compositions that carry the guardrails.
- Deletion is type-dependent: stateless identities clean up on claim delete (no orphans); stateful resources retain by default, and only an explicit delete — never a sync glitch — ever tears anything down.
- The same shift replaces one shared identity (blast radius = the whole platform) with a least-privilege identity per service, bound to an exact service account; migrate in two phases — parity first, then trim.
- Keep the boundary honest with layered enforcement (schema, composition, admission policy, a hard cloud permission ceiling), and never let the self-service plane write policy onto a foundation resource.
Checklist
- [ ] I can explain why ticket-driven infra provisioning becomes a bottleneck as service count grows.
- [ ] I can classify a resource as foundation vs app-scoped using the lifecycle-plus-sharing test, and say what to do when it's ambiguous.
- [ ] I can describe a self-service claim and name the three properties (high-level, attributed, no escape hatch) that keep it safe.
- [ ] I can trace a claim from git through the GitOps controller and composition to real cloud resources, and say where the guardrails live.
- [ ] I can state a type-dependent deletion policy and explain the two safety latches (retain-by-default for state; explicit-delete-only).
- [ ] I can explain the blast-radius problem of one shared identity and describe the two-phase (parity then trim) migration to per-service identity.
- [ ] I can list the enforcement layers and explain why a cross-boundary grant must be issued by the foundation plane, not the self-service plane.