09 · Platform Engineering on Kubernetes¶
Not run against a live cluster
Manifests and CLI output below follow documented Crossplane/Backstage behavior; not executed against a live cluster in this session.
Why platform engineering exists¶
Once an organization has more than a handful of teams deploying to
Kubernetes, exposing raw kubectl/YAML to every developer creates two
opposing failure modes: either every team reinvents its own
Deployment/Ingress/monitoring boilerplate (inconsistent, insecure,
expensive to support), or a central platform team becomes a ticket queue
manually provisioning things for everyone (slow, doesn't scale with
headcount). Platform engineering is the practice of building a
self-service layer — a golden path — so developers get safe, consistent
infrastructure without either extreme.
Golden-path abstraction: a Crossplane Composite Resource¶
Crossplane lets a platform team define a simplified custom API
(XRD — Composite Resource Definition) that developers use, which expands
into the full set of real Kubernetes/cloud resources underneath.
# platform team defines the developer-facing API:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.platform.example.com
spec:
group: platform.example.com
names:
kind: XDatabase
plural: xdatabases
claimNames:
kind: DatabaseClaim
plural: databaseclaims
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
size: {type: string, enum: ["small", "medium", "large"]}
engine: {type: string, enum: ["postgres", "mysql"]}
# a developer's entire interaction with cloud provisioning:
apiVersion: platform.example.com/v1alpha1
kind: DatabaseClaim
metadata:
name: payments-db
namespace: payments
spec:
size: medium
engine: postgres
kubectl apply -f payments-db-claim.yaml
kubectl get databaseclaim payments-db -n payments
# NAME SYNCED READY CONNECTION-SECRET
# payments-db True True payments-db-conn
Behind that one DatabaseClaim, the platform team's Composition (not
shown) provisions whatever the real cloud resources are (an RDS instance,
security groups, a connection Secret) — the developer never touches
Terraform, cloud IAM, or VPC configuration, and the platform team can
change the underlying implementation (swap RDS for CloudSQL, change
instance sizing defaults) without any developer-facing API change.
Developer portal surface: Backstage software catalog¶
# catalog-info.yaml, checked into each service's repo
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payments-api
annotations:
backstage.io/kubernetes-id: payments-api
spec:
type: service
lifecycle: production
owner: team-payments
dependsOn:
- resource:default/payments-db
Backstage aggregates every service's catalog-info.yaml into a searchable
catalog with ownership, dependency graphs, and (via its Kubernetes plugin)
live cluster status — the goal is a developer can find "who owns this
service, what does it depend on, is it healthy right now" without needing
kubectl access or tribal knowledge of who to ask.
Self-service scaffolding: a Backstage software template¶
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: new-microservice
spec:
parameters:
- title: Service details
properties:
name: {type: string}
team: {type: string}
steps:
- id: fetch-base
action: fetch:template
input:
url: ./skeleton
values:
name: "${{ parameters.name }}"
- id: publish
action: publish:github
input:
repoUrl: "github.com?repo=${{ parameters.name }}"
- id: register
action: catalog:register
input:
catalogInfoPath: /catalog-info.yaml
This is the "golden path" made concrete: a developer fills out a form, gets a new repo with CI, a Dockerfile, base Kubernetes manifests, RBAC, and observability wiring already correct by construction — the platform team's opinions about "how services should be built" are encoded once, in the template, instead of re-explained per team per onboarding.
Worked example: request-to-running-service flow¶
1. Developer runs "new-microservice" Backstage template -> new repo scaffolded,
registered in the catalog, CI pipeline wired automatically.
2. Developer's first commit triggers CI: build, trivy scan, cosign sign
(Module 05), push image.
3. GitOps controller (Module 02) picks up the new manifest in Git, deploys
into a namespace already governed by a Pod Security Standard (Module 04)
and network policy the platform team set as a default.
4. Developer needs a database: applies a `DatabaseClaim` (Crossplane) instead
of filing a ticket or writing Terraform.
5. Backstage catalog shows the new service, its owner, its dependency on the
claimed database, and live health from the cluster.
None of steps 2-5 require the platform team to be paged or to click anything — every step after the initial template run is self-service, governed by policy encoded in the platform rather than enforced by a human gatekeeper per request.
How It Actually Works¶
- Crossplane's
Compositionmechanism is itself just a Kubernetes controller pattern layered on CRDs — "Composite Resources" (XRs) are real custom resources, and Crossplane's core controller reconciles them the same way any operator reconciles its CRD. When aDatabaseClaimis created, Crossplane's claim controller creates a correspondingXDatabasecomposite resource; the Composition (a template mapping XR fields to a set of "managed resources," each itself a CRD representing one real cloud API object, e.g.RDSInstance) drives the actual provider controller (Crossplane's AWS/GCP provider, running its own reconcile loop against the cloud API) — the entire chain is composed of ordinary Kubernetes control loops, several layers deep, each unaware of the layers around it. - The claim/composite split exists specifically for namespace
isolation and RBAC.
DatabaseClaimis namespaced (so a team's RBAC can be scoped to claims in their own namespace, the normal Kubernetes RBAC boundary) while the underlyingXDatabasecomposite and the real cloud-provider managed resources are cluster-scoped — this lets the platform team keep the powerful, cluster-wide provisioning objects out of reach of individual developer RBAC roles while still letting those developers create and own claims freely within their namespace. - Backstage's catalog is a static-ish graph built from repository
metadata plus periodic Kubernetes plugin polling — it is not itself
a Kubernetes controller and has no admission-time enforcement power.
catalog-info.yamlfiles are discovered by Backstage's catalog processor (via configured integrations — GitHub search, a static location list) and re-ingested on a polling interval; the Kubernetes plugin separately queries the cluster's API server (scoped via a service account) for live status keyed by thebackstage.io/kubernetes-idlabel/annotation. This means catalog freshness lags real cluster state by the poll interval, and a service running in the cluster without a matchingcatalog-info.yamlis simply invisible to the portal — the catalog reflects what's been registered, not what's actually deployed. - A scaffolder template's
publishandregistersteps chain ordinary external API calls (GitHub's REST API, the Backstage catalog's own ingestion API) inside a Backstage-managed workflow engine — no Kubernetes object is created until the developer's first CI run actually deploys something. This is why "self-service scaffolding" and "GitOps deployment" are separate systems that happen to compose: the template produces a Git repo and a catalog entry; only the subsequent CI/CD pipeline (wired into the scaffolded repo by the template) is what eventually causes any actual Kubernetes resource to exist.
Exercise¶
Design (in YAML, no live cluster required) a Crossplane CompositeResourceDefinition
and a matching DatabaseClaim example for a "cache" abstraction (e.g. a
managed Redis instance) that exposes only size and lets the platform
team's Composition decide the actual cloud resource, instance class, and
network configuration. Then sketch a catalog-info.yaml for a service
that depends on that cache resource, and describe what a developer would
see in a Backstage catalog page for it.