Mission 2.2 · Stage 2 — Self-Service Infrastructure, Automation, and Measuring the Platform
Compositions: A Self-Service Database API
Goal: A team requests kind: PostgreSQLInstance with size: small and gets an RDS database, parameter group, subnet group, and credentials Secret, all within platform standards.
By the end of this mission
- Design a platform API with a CompositeResourceDefinition (XRD)
- Implement it with a Composition using a composition function pipeline
- Hide complexity while enforcing standards (encryption, backups, sizes)
- Offer it through a Backstage template
Part 1
Understand it first
XRDs and Compositions
A COMPOSITE RESOURCE DEFINITION (XRD) defines a NEW API, your platform's product: its name, and a schema with only the fields teams should choose (size, engine version, backup retention within limits). A COMPOSITION implements that API: it expands one composite resource into several managed resources (DB instance, subnet group, security group, parameter group) with all the defaults the platform team decided. Teams see a two-field API; the platform owns the 200 fields behind it.
Since Crossplane 2.0, composite resources are namespaced by default, so teams create them in their own namespace with normal RBAC and quotas (the older 'claim' indirection is only needed for cluster-scoped legacy XRs). Compositions run as a PIPELINE of COMPOSITION FUNCTIONS: function-patch-and-transform for simple mappings, function-go-templating or KCL/Python functions for logic.
Designing a good platform API
Offer OUTCOMES, not knobs: size: small|medium|large instead of instance classes; defaults for everything else. Bake in the non-negotiables (encryption at rest, deletion protection and deletionPolicy: Orphan in prod, backups, private subnets, tags for cost allocation). Version the API (v1alpha1 → v1) and evolve it compatibly, just like any API (Stateful Systems course, schema evolution).
Part 2
Your project after this mission · 4 files change
- shoplite-gitops/
- apps/
- wishlist/
- base/
- database.yamlnew
- shoplite-platform/
- apis/
- postgresql/
- composition.yamlnew
- definition.yamlnew
- shoplite-templates/
- postgresql/
- template.yamlnew
Part 3
Build it, step by step
- 1
Define the API (XRD)
Teams choose a size, a version from an allow-list, and backup days within a range. Everything else is the platform's decision.
shoplite-platform/apis/postgresql/definition.yamlwhole fileyaml apiVersion: apiextensions.crossplane.io/v2 kind: CompositeResourceDefinition metadata: name: postgresqlinstances.platform.shoplite.dev spec: group: platform.shoplite.dev names: { kind: PostgreSQLInstance, plural: postgresqlinstances } scope: Namespaced versions: - name: v1alpha1 served: true referenceable: true schema: openAPIV3Schema: type: object properties: spec: type: object required: [size] properties: size: { type: string, enum: [small, medium, large] } version: { type: string, enum: ["16", "17"], default: "17" } backupDays: { type: integer, minimum: 1, maximum: 35, default: 7 } status: type: object properties: endpoint: { type: string } - 2
Implement it (Composition)
A patch-and-transform pipeline: map
sizeto an instance class, fix the rest to standards, and write the connection details to a Secret in the team's namespace. Namespaced composites compose NAMESPACED managed resources, which live in the*.m.upbound.ioAPI groups and authenticate through theClusterProviderConfigfrom Mission 2.1. Subnet and security groups are found by label selectors on resources Terraform created.shoplite-platform/apis/postgresql/composition.yamlwhole fileyaml apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: postgresql-aws spec: compositeTypeRef: { apiVersion: platform.shoplite.dev/v1alpha1, kind: PostgreSQLInstance } mode: Pipeline pipeline: - step: resources functionRef: { name: function-patch-and-transform } input: apiVersion: pt.fn.crossplane.io/v1beta1 kind: Resources resources: - name: db base: apiVersion: rds.aws.m.upbound.io/v1beta1 kind: Instance spec: deletionPolicy: Orphan forProvider: region: ap-south-1 engine: postgres allocatedStorage: 20 storageType: gp3 storageEncrypted: true publiclyAccessible: false deletionProtection: true autoGeneratePassword: true username: app passwordSecretRef: { key: password, name: "" } # name patched below dbSubnetGroupNameSelector: { matchLabels: { platform.shoplite.dev/network: private } } vpcSecurityGroupIdSelector: { matchLabels: { platform.shoplite.dev/sg: databases } } skipFinalSnapshot: false writeConnectionSecretToRef: { name: "" } patches: - type: FromCompositeFieldPath fromFieldPath: spec.size toFieldPath: spec.forProvider.instanceClass transforms: - type: map map: { small: db.t4g.small, medium: db.m7g.large, large: db.m7g.2xlarge } - { type: FromCompositeFieldPath, fromFieldPath: spec.version, toFieldPath: spec.forProvider.engineVersion } - { type: FromCompositeFieldPath, fromFieldPath: spec.backupDays, toFieldPath: spec.forProvider.backupRetentionPeriod } - type: FromCompositeFieldPath fromFieldPath: metadata.name toFieldPath: spec.writeConnectionSecretToRef.name transforms: [{ type: string, string: { type: Format, fmt: "%s-db-conn" } }] - type: FromCompositeFieldPath fromFieldPath: metadata.name toFieldPath: spec.forProvider.passwordSecretRef.name transforms: [{ type: string, string: { type: Format, fmt: "%s-db-password" } }] - { type: ToCompositeFieldPath, fromFieldPath: status.atProvider.address, toFieldPath: status.endpoint } - 3
A team requests a database
This lives in the team's GitOps folder and deploys through Argo CD. It's reviewed like code, versioned, and the same in every environment (just a different size in the prod overlay).
shoplite-gitops/apps/wishlist/base/database.yamlwhole fileyaml apiVersion: platform.shoplite.dev/v1alpha1 kind: PostgreSQLInstance metadata: name: wishlist spec: size: small version: "17" backupDays: 7terminal$ kubectl -n wishlist get postgresqlinstance wishlistkubectl -n wishlist get secret wishlist-db-conn -o jsonpath='{.data}' | jq 'keys'── expected output ──NAME SYNCED READY COMPOSITION AGEwishlist True True postgresql-aws 9m["endpoint","password","port","username"] - 4
Offer it in Backstage too
A small template (Mission 1.2) with a size dropdown whose only step opens a PR adding
database.yamlto the team's GitOps folder. Developers who prefer YAML write it directly; others click. Both go through Git review.
Checkpoint — you should now have
- ✓
PostgreSQLInstanceexists as a namespaced API in the cluster. - ✓A two-line request produces an encrypted, backed-up, private RDS instance and a connection Secret.
- ✓Invalid requests (size
huge, backupDays 90) are rejected by the schema.
Part 4
Break it on purpose
Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.
Break #1
Request outside the API
A team sets size: xlarge to 'get more performance'.
Break #2
Composite stuck not Ready
The subnet group selector label doesn't match any SubnetGroup.
Part 5
Interview questions from this mission
What problem do Crossplane compositions solve for a platform team?
Before you stop