Self-Service Cloud Infrastructure: From Ticket Queues to Claims
Replace ticket-driven app infrastructure with a bounded claim API: application teams declare approved needs, a control loop creates app-scoped resources, and the platform retains authority over foundations and policy.
The enterprise problem and today’s slice
Enterprise problem: The image-api team cannot ship object uploads without waiting for a platform ticket for a bucket, workload identity, and key access, so a human queue now controls delivery time and obscures who owns each change.
Whole-course context: The course has reached platform operations; today consumes a deployable service definition and turns its infrastructure dependencies into governed, reviewable desired state.
Today’s slice: We design one allowlisted claim that creates image-api’s app-scoped object store and identity while leaving the shared encryption key inside the platform-owned foundation boundary.
End-of-day evidence: A claim revision, reconciliation run, positive object probe, denied escalation probe, deletion-policy result, and immutable evidence ID prove the path.
Still unsolved: Multi-region failover, provider portability, billing allocation, secret rotation, and the application’s own end-user authorization remain outside this slice.
Thesis: Self-service is a constrained product API over infrastructure, not permission to author arbitrary infrastructure: claimed intent enters through policy-controlled reconciliation, and observed cloud reality returns as evidence.
Start with the smallest complete model
Teams often mistake self-service for direct cloud access, which replaces a ticket bottleneck with unbounded authority. The smallest complete model is safer: intent enters through a narrow claim, policy-controlled reconciliation changes reality, and observed evidence closes the loop.
The thesis is: self-service is a constrained product API over infrastructure, not permission to author arbitrary infrastructure. The loop is complete only when it can observe actual state, expose a reasoned status, and drive a customer decision: proceed, correct the claim, retry, or retire.
For image-api, the first valid outcome is intentionally small: one bucket, one non-human service identity, one approved object-access profile, and proof that the intended service succeeds while a neighbouring service fails. Kubernetes defines a ServiceAccount as a namespaced non-human identity, which makes it a useful workload-side subject rather than a human credential (Kubernetes ServiceAccounts).
Draw the foundation and app-scoped boundary
If shared perimeter resources enter the same lifecycle as one application, an innocent service deletion can damage every tenant. Classify infrastructure by lifecycle, sharing, and policy ownership before deciding what a claim may create.
| Resource | Lifecycle and sharing | Authority | Self-service decision |
|---|---|---|---|
| Cloud accounts, virtual networks, DNS zones, organization guardrails | Longer-lived than any service; shared security perimeter | Foundation platform team | Reference only; never created or mutated by an app claim |
| Shared encryption key | Shared across approved workloads; resource policy is perimeter-sensitive | Foundation key owner | Claim requests a bounded grant; foundation controller owns the grant |
image-api workload role | One service and environment; no payload data | App-scoped identity controller | Create, update, and delete with the service claim |
image-api object store | One service, but contains durable customer data | App-scoped infrastructure with app-owned payload | Create and update with the claim; retain on retirement; purge separately |
Use this decision rule:
- If multiple services depend on the resource, its policy defines a perimeter, or it must survive every one service, keep it in foundation.
- If exactly one service owns it and its authority can be bounded independently, it may be app-scoped.
- If it stores durable data, app-scoped does not imply auto-delete.
- If ownership is ambiguous, default to foundation until an owner, lifecycle, and revocation contract exist.
This is not a foundation-versus-self-service dichotomy. The two layers complement each other: foundation publishes safe references and grant interfaces; the app-scoped layer consumes those interfaces without co-owning foundation state.
Design the claim as an allowlisted contract
A flexible schema can become a disguised cloud console, so the claim must make safe requests easy and unsafe states unrepresentable. Here, a capability profile is a named, versioned bundle of allowed actions and constraints; it is selected from a catalog rather than supplied as raw IAM policy.
apiVersion: platform.example.io/v1alpha1
kind: ServiceInfrastructureClaim
metadata:
name: image-api
namespace: media-prod
spec:
claimId: clm-media-prod-image-api
ownerTeam: media
serviceId: svc-image-api
serviceAccountRef:
name: image-api
resources:
- name: images
kind: ObjectStore
deletionPolicy: Retain
access:
- capability: object-store-read-write
resourceRef: images
profileRevision: "2026-08-01"
- capability: kms-decrypt
foundationRef: media-shared-key
profileRevision: "2026-08-01"
The public API does not expose provider account IDs, arbitrary actions, raw policy JSON, wildcard subjects, or a field that edits the shared key. The schema validates shape; admission validates organization and environment rules; the catalog determines the exact expansion. A new legitimate need changes the catalog through a reviewed platform change rather than adding an escape hatch to one claim.
Crossplane is one implementation option, not the definition of this architecture. Its Composite Resource Definition supplies a custom API schema, while a Composition templates the resources created for that API; current Crossplane documentation describes Compositions as function pipelines that derive desired composed resources from observed and desired state (XRDs, Compositions). The generic design also works with another operator or an external workflow engine if it preserves the same contract and evidence.
Reconcile intent through a platform-owned composition
One-shot provisioning scripts leave partial resources and silent drift when a provider call fails. A reconciler repeatedly compares desired state with observed state and makes retry-safe changes until they converge; a composition is the platform-authored expansion from the high-level claim to concrete resources.
For image-api, the composition pins the target account and region, stamps organization, service, owner, environment, and claim tags, gives each resource a stable external name, attaches a permissions boundary to the new role, and requests rather than writes the shared-key grant. Crossplane reports separate Synced and Ready conditions for composite resources, so an accepted object is not automatically proof that all composed resources are usable (Crossplane composite-resource conditions).
Idempotency matters at every edge. Retrying after a timeout must look up the stable bucket and role before creating anything; status writes must use the claim generation they observed; evidence records must deduplicate on run_id plus probe name. Otherwise, the loop that should repair partial failure creates duplicate resources or reports stale success.
GitOps adds another control loop: tools such as Argo CD can automatically sync Git differences and can optionally prune removed resources, while self-healing is separately configurable (Argo CD automated sync). Therefore “Git is desired state” is not itself a deletion policy—prune, controller deletion, and external-resource retention must agree explicitly.
Make deletion a separate state machine
Treating absence from Git as permission to destroy everything turns a typo, branch error, or sync mistake into data loss. Retirement must distinguish revocable authority from durable state and must preserve an explicit record of human intent.
The stateful/stateless split is necessary but not sufficient. For the stateless identity, delete only after its grants are revoked and a negative assume-role probe succeeds. For the stateful bucket, remove active access, apply quarantine controls, retain its locator in the lifecycle job, and require a separate purge approval after retention and legal-hold checks.
Kubernetes finalizers delay object removal while a controller completes required cleanup; they do not perform the cleanup themselves and can leave an object stuck if the responsible controller cannot finish (Kubernetes finalizers). Crossplane similarly keeps a managed resource until its provider completes external deletion. Its managementPolicies feature is beta, and current provider support varies: omitting Delete can express retention, but verify that the selected provider implements those semantics before treating the omission as a retention guarantee (Crossplane managed resources). Those mechanisms implement a chosen policy; they do not choose the policy for you.
Bind one cloud identity to one service
A shared runtime role makes the compromise of the least secure service equivalent to compromise of the union of every service’s permissions. Per-service identity reduces that blast radius only when both the trust relationship and the permissions are scoped to the exact workload.
On Kubernetes, set spec.serviceAccountName explicitly rather than inheriting the namespace’s default identity. Prefer short-lived projected service-account tokens: Kubernetes documents that TokenRequest tokens expire, rotate, can carry an audience, and can be bound to a Pod, while long-lived Secret-based tokens are not recommended (Kubernetes ServiceAccounts).
For an AWS EKS implementation using IAM Roles for Service Accounts, the role trust policy can require exact aud and sub claims such as system:serviceaccount:media-prod:image-api; replacing the service-account value with a wildcard broadens who can assume the role (Amazon EKS service-account roles). Equivalent workload-identity products should pin cluster or issuer, namespace, service-account name, and intended audience.
The role permission policy answers “what may this identity do?”; the trust policy answers “who may become this identity?” Both must be narrow. Test both directions: image-api can access only its bucket, and thumbnail-worker cannot assume or use the image-api role.
Handle cross-boundary grants with layered enforcement
An app-scoped identity that needs a shared key can tempt the claim controller to edit foundation policy directly, erasing the ownership boundary. The safe interface is a grant request: app self-service owns the principal, the foundation service owns policy on its resource, and each side can revoke independently.
AWS distinguishes identity-based policies attached to identities from resource-based policies attached to resources such as S3 buckets and KMS keys; a resource policy specifies who may access that resource (AWS policy types). That supports the ownership rule: the identity adapter owns the app role policy, while the foundation controller owns the shared key’s policy.
Enforce the boundary in layers because no single layer sees the whole system:
| Layer | Required invariant | Failure it contains |
|---|---|---|
| Claim schema | Only named fields and resource kinds | Raw provider policy or arbitrary account selection |
| Namespace authorization | Team may write claims only for owned services and environments | Cross-team claim creation |
| Admission policy | Capability exists, owner is valid, deletion policy matches resource class, and raw managed/cloud resource kinds that bypass the approved claim path are rejected | Unknown profile, missing attribution, auto-delete of durable state, or direct provider-resource submission |
| Composition | Provider, account, tags, external names, and role boundary are pinned | Claim-controlled template expansion |
| Controller cloud credentials | Adapter can manage only approved app-scoped resource classes | Composition bug reaching foundation APIs |
| Cloud permissions boundary and organization policy | Maximum identity authority is capped | Over-broad generated identity policy |
| Foundation grant controller | Shared-resource grants follow foundation policy and expiry | Self-service mutation of perimeter state |
| Runtime federation | Exact issuer, audience, namespace, and service account | Neighbouring workload assuming the role |
| Drift and evidence probes | Desired policy matches effective runtime behaviour | Console edits or misleading Ready status |
Kubernetes admission controllers can validate API requests before objects persist, making admission the layer that rejects both invalid claims and raw provider-managed Kubernetes objects submitted outside the approved claim API; its denial evidence must retain the actor, object kind, rule ID, environment, time, and immutable audit or decision ID (Kubernetes admission control). Direct calls to cloud APIs are a different bypass path and must be denied by cloud IAM, not assumed visible to Kubernetes admission. In AWS, a permissions boundary sets a maximum for identity-based permissions but grants nothing by itself; AWS also documents important interactions with resource-based policies, so a boundary must not be treated as the entire authorization system (AWS permissions boundaries). The reusable rule is to constrain request, expansion, controller authority, cloud authorization, and observed behaviour independently.
Compare the operating trade-offs
Choosing a self-service mechanism without considering service diversity and platform capacity can produce either a rigid catalog nobody can use or an internal cloud console nobody can govern. Select the narrowest interface that covers repeated customer jobs and keeps authoritative ownership explicit.
| Approach | Delivery speed | Safety and cognitive load | Use when | Avoid when |
|---|---|---|---|---|
| Platform ticket plus platform-owned IaC | Slow and queue-bound | Strong human gate; platform absorbs context switching | Rare, ambiguous, foundation-changing work | Repeated app-scoped requests dominate the queue |
| Application-authored raw provider IaC | Fast for cloud experts | Large API surface, provider knowledge, and review burden move to every team | Teams genuinely own isolated accounts and full lifecycle operations | A central perimeter or shared account limits delegation |
| Allowlisted claim plus composition | Fast for paved-road needs | Small API; platform pays catalog, controller, migration, and support cost | Many teams repeat a bounded set of app-scoped resource patterns | Needs are mostly unique or cannot be safely parameterized |
| Namespace or account delegation with hard guardrails | Flexible inside a sandbox | Broader freedom; larger blast radius and policy burden | Mature teams need variation within strongly isolated boundaries | Workloads share accounts or organization controls are weak |
Catalog governance is the principal cost of the recommended claim model. Profiles need owners, semantic versions, compatibility rules, adoption metrics, deprecation windows, and a documented escape path back to a reviewed foundation ticket for genuinely novel needs. Without that product work, teams route around the platform.
Measure the result with customer and risk signals together: median claim-to-Ready time, denial precision, reconciliation error rate, orphan count, percentage of services on dedicated identity, time to revoke, retained-state age, and support load. Optimizing only provisioning latency can hide a growing cleanup or authorization problem.
Diagnose failure modes from evidence
When reconciliation fails, repeatedly reapplying the claim can amplify damage or hide the ownership fault. Diagnose by locating the last boundary with trustworthy evidence, then test the next interface with the same claim revision and correlation ID.
| Symptom | Likely cause | Evidence to inspect | Safe response |
|---|---|---|---|
| Claim rejected immediately | Unknown capability, wrong owner, forbidden foundation target, or invalid deletion policy | Admission decision, rule ID, manifest path, Git SHA | Correct intent or submit a catalog change; do not bypass admission with raw resources |
| Claim accepted but never Ready | Composition error, provider authorization failure, quota, or eventual consistency | Claim conditions, run ID, composed-resource state, provider request ID | Keep credentials withheld; retry only idempotent steps; escalate with exact failing edge |
| Bucket exists twice after a timeout | Unstable external name or missing idempotency lookup | Provider audit log and run attempts keyed by claim generation | Stop reconciliation, select the authoritative bucket, repair stable naming before retry |
| Neighbour can access the bucket | Wildcard trust subject, shared role, broad resource policy, or stale grant | Token claims, assume-role trace, effective policies, grant ID | Revoke the grant, quarantine access, narrow trust and permissions, then rerun both probes |
| Role remains after retirement | Finalizer failure, grant dependency, or controller lacks delete authority | Lifecycle-job state, finalizers, role trust activity, deletion provider ID | Revoke assumability first; repair the owning controller; never strip the finalizer blindly |
| Bucket disappeared on retirement | Delete policy expanded into a stateful resource or prune and provider policy disagreed | Deleted claim revision, composition revision, object-store audit events | Invoke recovery, freeze further retirements, correct policy, and add a retained-state negative test |
| Ready is green but runtime gets denied | Status observes resource creation, not effective federation or key policy | Positive probe trace, token audience and subject, identity policy, foundation grant | Keep release blocked; repair the specific trust or grant edge and rerun evidence |
| Permissions never get narrower | Parity phase became permanent or observation omitted rare-path ownership review | Migration phase, role policy diffs, activity analysis, exception expiry | Assign an owner and deadline; trim one reviewed statement at a time with rollback evidence |
The reusable diagnostic sequence is claim revision → admission decision → composition revision → provider request → cloud state → runtime token → effective authorization → probe evidence. Stop at the first broken edge; downstream symptoms cannot identify an upstream cause reliably.
Record the decisions before implementation
If policy choices remain implicit, two controllers can make opposite but individually plausible decisions about ownership or deletion. Write these decisions as versioned contracts before the first production claim.
| Decision | Recommended default | Revisit when |
|---|---|---|
| Self-service boundary | App-scoped resources only; ambiguous resources remain foundation-owned | Ownership, independent revocation, and lifecycle become unambiguous |
| Claim surface | Named versioned capabilities; no raw policy or provider-account fields | A repeated need cannot be represented and its safe bounds are understood |
| Identity granularity | One role per service and environment, bound to an exact workload subject | Provider limits force aggregation; compensate with stronger isolation and evidence |
| Shared-resource access | Foundation-owned grant request with independent revocation | Resource moves wholly into an app-owned boundary |
| Stateful deletion | Retain and quarantine on service retirement; purge separately | Data classification and recovery requirements authorize a different policy |
| Stateless deletion | Revoke grants, deny assumption, then delete | Audit or incident policy requires disabled retention instead |
| Reconciliation authority | Separate adapters with least cloud permissions and stable idempotency keys | Resource catalog or account topology changes |
| Migration | Parity, cutover, observe, trim, then revoke shared path | An incident requires a documented, time-boxed rollback |
| Evidence | Positive and negative runtime probes tied to immutable revisions and run IDs | A stronger independent verifier replaces a probe |
These decisions are architecture tests. A proposed feature that needs arbitrary IAM JSON, silent mutation of a shared key, destructive pruning of durable state, or a namespace wildcard fails the current contract and must return to design review rather than slip through as an exception field.
Inspect the claim and its evidence
Operators lose time when the only interface is a dashboard that hides revisions and provider correlation IDs. Keep a small command-line path that exposes desired state, controller conditions, composed resources, identity trust, and the terminal evidence envelope without printing credentials.
# Show the exact desired claim and its observed generation.
kubectl -n media-prod get serviceinfrastructureclaim image-api -o yaml
# Read status reasons, composition revision, and referenced resources.
kubectl -n media-prod describe serviceinfrastructureclaim image-api
# Select resources by the stable claim ID, not a mutable display name.
kubectl get managed -A -l platform.example.io/claim-id=clm-media-prod-image-api
# Inspect the exact Kubernetes workload identity binding.
kubectl -n media-prod get serviceaccount image-api -o yaml
# Inspect role trust and the attached permission ceiling; neither command returns credentials.
aws iam get-role --role-name media-prod-image-api
aws iam list-attached-role-policies --role-name media-prod-image-api
# Read one immutable reconciliation-and-probe envelope from the platform API.
platformctl evidence get ev-36-001 --output yaml
The expected evidence is concrete: the claim reports the intended observed generation and pinned profile revision; managed resources carry the same claim ID; the role trust names only the intended issuer, audience, namespace, and service account; the allowed object probe passes; the neighbouring-subject probe fails; and all outputs link to the same immutable run.
For a deletion rehearsal, use a non-production claim with a disposable object plus a retained-object checksum. Prove the grant and role disappear, the bucket remains, the checksum is unchanged, and an unrelated service still passes. Never trial the destructive half of a lifecycle policy against the first production bucket.
Key takeaways
The recurring risk is confusing removal of a human ticket with removal of control. Keep these rules available when designing another self-service resource type.
- Self-service is a constrained infrastructure API:
claim → policy-controlled reconciliation → observed evidence. - Classify resources by sharing, lifecycle, and policy ownership; ambiguous or perimeter-defining resources remain foundation-owned.
- Claims select allowlisted, versioned capabilities and never accept arbitrary provider permissions.
- App self-service owns
image-api’s bucket and role; the foundation service owns the grant on its shared key. - Reconciliation must use stable identities, idempotent provider operations, explicit conditions, and runtime probes.
- Stateful resources retain and quarantine on retirement; stateless authority is revoked and deleted; purge is a separate decision.
- One exact workload subject maps to one service role; test both intended access and neighbouring denial.
- Permission boundaries, admission, compositions, controller credentials, foundation grants, and probes are complementary layers.
- Migrate shared identity through parity, cutover, observation, trimming, and final revocation—not one combined leap.
- Measure delivery speed and safety together: readiness time, denials, errors, orphans, revocation time, and retained-state age.
Checklist
An implementation is not ready because its happy-path claim reconciled once. Review this checklist against an immutable revision and observed positive, negative, failure, and retirement evidence.
- [ ] The claim’s customer job, owning team, service, environment, and stable ID are explicit.
- [ ] Every resource is classified as foundation or app-scoped by lifecycle, sharing, and policy ownership.
- [ ] The schema exposes named capability profiles, not raw IAM policy, provider account selection, or wildcard subjects.
- [ ] The composition pins provider settings, ownership tags, stable external names, deletion policy, and a permission ceiling.
- [ ] Controller cloud credentials cannot mutate foundation resources.
- [ ]
image-apihas a dedicated role bound to the exact issuer, audience, namespace, and service account. - [ ] The foundation controller, not the app claim controller, owns the shared-key grant.
- [ ] Positive
image-apiaccess and negative neighbouring-service access are observed and linked to one evidence envelope. - [ ] Partial provider failure retries idempotently and cannot create duplicate resources or stale Ready status.
- [ ] Retirement revokes grants before deleting identity, retains and quarantines the bucket, and requires separate purge approval.
- [ ] Finalizers and GitOps pruning implement the chosen lifecycle policy and cannot silently override it.
- [ ] Shared-identity migration separates parity from trimming and finishes by proving the old path is denied.
- [ ] Operators can trace claim revision, admission decision, composition revision, provider request, cloud state, runtime token, and probe.
- [ ] Capability owners, versioning, deprecation, exceptions, service-level objectives, and support responsibilities are documented.