Command Palette

Search for a command to run...

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.

70 min ~$0.02/hr per small RDS instance 4 steps 2 break-it drills

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).

One request, many resourcesdiagram
Rendering diagram…

Part 2

Your project after this mission · 4 files change

shoplite-platform/
  • 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. 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. 2

    Implement it (Composition)

    A patch-and-transform pipeline: map size to 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.io API groups and authenticate through the ClusterProviderConfig from 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. 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: 7
    terminal
    $ kubectl -n wishlist get postgresqlinstance wishlist
    kubectl -n wishlist get secret wishlist-db-conn -o jsonpath='{.data}' | jq 'keys'
    ── expected output ──
    NAME SYNCED READY COMPOSITION AGE
    wishlist True True postgresql-aws 9m
    ["endpoint","password","port","username"]
  4. 4

    Offer it in Backstage too

    A small template (Mission 1.2) with a size dropdown whose only step opens a PR adding database.yaml to the team's GitOps folder. Developers who prefer YAML write it directly; others click. Both go through Git review.

Checkpoint — you should now have

  • ✓PostgreSQLInstance exists 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'.

terminal
$ kubectl -n wishlist apply -f database.yaml
── what you'll see ──
The PostgreSQLInstance "wishlist" is invalid: spec.size: Unsupported value: "xlarge": supported values: "small", "medium", "large"

Break #2

Composite stuck not Ready

The subnet group selector label doesn't match any SubnetGroup.

terminal
$ kubectl -n wishlist describe postgresqlinstance wishlist | tail -3; crossplane beta trace postgresqlinstance wishlist -n wishlist
── what you'll see ──
Warning ComposeResources cannot resolve references: mg.Spec.ForProvider.DBSubnetGroupName: no resources matched selector
NAME SYNCED READY STATUS
PostgreSQLInstance/wishlist True False Creating...
└─ Instance/wishlist-x7k2p False - ReconcileError: cannot resolve references

Part 5

Interview questions from this mission

01

What problem do Crossplane compositions solve for a platform team?

Before you stop

Clean up

terminal
$ kubectl -n wishlist delete postgresqlinstance wishlist
# deletionPolicy Orphan: delete the RDS instance itself in AWS after confirming you don't need it
0/4 · 0%