36

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.

ResourceLifecycle and sharingAuthoritySelf-service decision
Cloud accounts, virtual networks, DNS zones, organization guardrailsLonger-lived than any service; shared security perimeterFoundation platform teamReference only; never created or mutated by an app claim
Shared encryption keyShared across approved workloads; resource policy is perimeter-sensitiveFoundation key ownerClaim requests a bounded grant; foundation controller owns the grant
image-api workload roleOne service and environment; no payload dataApp-scoped identity controllerCreate, update, and delete with the service claim
image-api object storeOne service, but contains durable customer dataApp-scoped infrastructure with app-owned payloadCreate and update with the claim; retain on retirement; purge separately

Use this decision rule:

  1. If multiple services depend on the resource, its policy defines a perimeter, or it must survive every one service, keep it in foundation.
  2. If exactly one service owns it and its authority can be bounded independently, it may be app-scoped.
  3. If it stores durable data, app-scoped does not imply auto-delete.
  4. 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:

LayerRequired invariantFailure it contains
Claim schemaOnly named fields and resource kindsRaw provider policy or arbitrary account selection
Namespace authorizationTeam may write claims only for owned services and environmentsCross-team claim creation
Admission policyCapability exists, owner is valid, deletion policy matches resource class, and raw managed/cloud resource kinds that bypass the approved claim path are rejectedUnknown profile, missing attribution, auto-delete of durable state, or direct provider-resource submission
CompositionProvider, account, tags, external names, and role boundary are pinnedClaim-controlled template expansion
Controller cloud credentialsAdapter can manage only approved app-scoped resource classesComposition bug reaching foundation APIs
Cloud permissions boundary and organization policyMaximum identity authority is cappedOver-broad generated identity policy
Foundation grant controllerShared-resource grants follow foundation policy and expirySelf-service mutation of perimeter state
Runtime federationExact issuer, audience, namespace, and service accountNeighbouring workload assuming the role
Drift and evidence probesDesired policy matches effective runtime behaviourConsole 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.

Migrate from a shared identity without a big bang

Changing identity and reducing permissions simultaneously makes any denial ambiguous and raises rollback risk. Separate identity parity from permission trimming, then remove the shared path only after every service has proved its dedicated path.

PhaseChange for image-apiRequired evidenceRollback boundary
0. InventoryMap current shared-role actions, resources, trust subjects, rare jobs, and ownersCloud audit samples, service inventory, current positive probes, known gapsNo runtime change
1. Mint at parityCreate the dedicated role with only image-api’s known existing access, but do not trim uncertain required actions yetExact-subject assume-role test, policy diff, successful staging probesDelete unused new role
2. Cut overBind media-prod:image-api to the new role and canary the workloadProduction positive probes, expected negative neighbour probe, error and latency comparisonTime-boxed rebind to shared role; record renewed blast radius
3. Observe and trimRemove unused services, actions, and resource wildcards after a representative observation window and owner reviewDenial monitoring, rare-path test suite, policy analysis, approved diffRestore only the specific removed statement
4. Revoke shared pathRemove image-api from the shared trust path; repeat until no service uses it; then delete the shared roleShared-role assumption denied for every migrated subject, dedicated positive controls pass, no cloud events use the old roleRestore one subject only under incident approval

Observed usage is evidence, not proof of completeness: seasonal jobs and data-plane events may be absent. AWS IAM Access Analyzer can generate a policy template from CloudTrail activity, but AWS notes that some data events are unavailable and the output still requires review and resource scoping (IAM Access Analyzer policy generation). Combine observation with owned use cases and explicit rare-path tests.

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.

ApproachDelivery speedSafety and cognitive loadUse whenAvoid when
Platform ticket plus platform-owned IaCSlow and queue-boundStrong human gate; platform absorbs context switchingRare, ambiguous, foundation-changing workRepeated app-scoped requests dominate the queue
Application-authored raw provider IaCFast for cloud expertsLarge API surface, provider knowledge, and review burden move to every teamTeams genuinely own isolated accounts and full lifecycle operationsA central perimeter or shared account limits delegation
Allowlisted claim plus compositionFast for paved-road needsSmall API; platform pays catalog, controller, migration, and support costMany teams repeat a bounded set of app-scoped resource patternsNeeds are mostly unique or cannot be safely parameterized
Namespace or account delegation with hard guardrailsFlexible inside a sandboxBroader freedom; larger blast radius and policy burdenMature teams need variation within strongly isolated boundariesWorkloads 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.

SymptomLikely causeEvidence to inspectSafe response
Claim rejected immediatelyUnknown capability, wrong owner, forbidden foundation target, or invalid deletion policyAdmission decision, rule ID, manifest path, Git SHACorrect intent or submit a catalog change; do not bypass admission with raw resources
Claim accepted but never ReadyComposition error, provider authorization failure, quota, or eventual consistencyClaim conditions, run ID, composed-resource state, provider request IDKeep credentials withheld; retry only idempotent steps; escalate with exact failing edge
Bucket exists twice after a timeoutUnstable external name or missing idempotency lookupProvider audit log and run attempts keyed by claim generationStop reconciliation, select the authoritative bucket, repair stable naming before retry
Neighbour can access the bucketWildcard trust subject, shared role, broad resource policy, or stale grantToken claims, assume-role trace, effective policies, grant IDRevoke the grant, quarantine access, narrow trust and permissions, then rerun both probes
Role remains after retirementFinalizer failure, grant dependency, or controller lacks delete authorityLifecycle-job state, finalizers, role trust activity, deletion provider IDRevoke assumability first; repair the owning controller; never strip the finalizer blindly
Bucket disappeared on retirementDelete policy expanded into a stateful resource or prune and provider policy disagreedDeleted claim revision, composition revision, object-store audit eventsInvoke recovery, freeze further retirements, correct policy, and add a retained-state negative test
Ready is green but runtime gets deniedStatus observes resource creation, not effective federation or key policyPositive probe trace, token audience and subject, identity policy, foundation grantKeep release blocked; repair the specific trust or grant edge and rerun evidence
Permissions never get narrowerParity phase became permanent or observation omitted rare-path ownership reviewMigration phase, role policy diffs, activity analysis, exception expiryAssign 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.

DecisionRecommended defaultRevisit when
Self-service boundaryApp-scoped resources only; ambiguous resources remain foundation-ownedOwnership, independent revocation, and lifecycle become unambiguous
Claim surfaceNamed versioned capabilities; no raw policy or provider-account fieldsA repeated need cannot be represented and its safe bounds are understood
Identity granularityOne role per service and environment, bound to an exact workload subjectProvider limits force aggregation; compensate with stronger isolation and evidence
Shared-resource accessFoundation-owned grant request with independent revocationResource moves wholly into an app-owned boundary
Stateful deletionRetain and quarantine on service retirement; purge separatelyData classification and recovery requirements authorize a different policy
Stateless deletionRevoke grants, deny assumption, then deleteAudit or incident policy requires disabled retention instead
Reconciliation authoritySeparate adapters with least cloud permissions and stable idempotency keysResource catalog or account topology changes
MigrationParity, cutover, observe, trim, then revoke shared pathAn incident requires a documented, time-boxed rollback
EvidencePositive and negative runtime probes tied to immutable revisions and run IDsA 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-api has 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-api access 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.