← Projects
Platform Engineering

An Internal Developer Platform That Hides Kubernetes

Crossplane compositions that turn a Git repo into a fully-wired, secure service that ~60 engineers ship without ever touching raw YAML.

~60 engineers
self-serve, no K8s expertise required
10 engines
one database composition, one `engine:` flag
250+ services
shipped on the platform
secure by default
Pod Identity, policies, backups baked in

A platform is only as good as the day-to-day experience of the engineers on it. I built an internal developer platform where a service is just a Git repo plus two short specs, and everything that makes it production-grade, the rollout, the identity, the routing, the storage, the backups, is generated for you, correctly, every time.

A service is just a Git repo

On this platform, a service is a Git repo with a small k8s/ folder: a Kustomize base, a per-environment overlay, and two short Crossplane specs. No Helm, no raw Deployments, no hand-written RBAC. The two files that matter are app.yaml (an XApplication) and db.yaml (one XDatabase per datastore); everything else is Kustomize plumbing and per-environment config.

Here is the entire folder a team maintains. Click through it.

What a typical app repo looks like

The whole k8s/ folder a developer maintains. Click a file to read it (contents sanitized).

k8s/
base/
environments/
dev/
prod/
k8s/base/app.yaml
apiVersion: platform.example.com/v1alpha1
kind: XApplication
metadata:
  name: example
spec:
  image: 000000000000.dkr.ecr.us-west-2.amazonaws.com/example:0.0.0
  replicas: 1
  resources:
    cpu: "2"
    memory: "1000Mi"
  secrets:
    - secretName: db-credentials
      key: DB_PASSWORD
    - secretName: partner-api
      key: PARTNER_API_KEY
  databases:
    - name: exports-store
      engine: object-aws
    - name: orders-table
      engine: document-aws
  networking:
    routes:
      - path: /orders
        methods: [GET]
      - path: /orders/{orderId}
        methods: [GET]
      - path: /orders/{orderId}/status
        methods: [PUT]
    corsOrigins: true
  critical: true
  vpa: true

The XApplication: ~20 simple fields the developer writes. The composition turns this into a full service.

A real app repo's k8s/ folder, sanitized. app.yaml and db.yaml are the whole developer-facing surface.

The application composition

app.yaml is an XApplication: around twenty simple fields. Behind it, a Crossplane composition (written as a function-pythonic program) expands those fields into every resource a real service needs. The developer never writes a Rollout, an IAM role, or an Istio config; they just declare intent.

Only genuinely cross-cutting knobs are exposed. The clearest example is critical: true: one boolean that places the workload on on-demand capacity with the right node affinity, tolerations, and disruption budgets. That single flag is also the fix from a 28-minute outage I wrote up.

One XApplication, fully wired

The developer writes ~20 fields. The composition expands them into all of this. Click a resource.

kind: XApplication  ·  image · resources · secrets · databases · networking · critical · vpa · hpa
Workload
Identity (AWS)
Networking (Istio)
Autoscaling & scheduling
GitOps (dev)
Argo Rolloutfrom: image, replicas, resources

Progressive delivery in place of a Deployment: a canary that only proceeds when its AnalysisRuns pass. If spec.schedule is set, a CronJob is emitted instead.

Everything one XApplication becomes. Each tile maps back to a field the developer set.
Where critical: true came from
A 28-minute outage, a spot interruption, and the one flag that fixed the whole class of bug.

The database composition

Persistence is a second composition, XDatabase, driven by one engine: flag. A developer asks for the datastore they need and gets a correctly-operated backing store: the right operator, the right topology, and the unglamorous parts provisioned for them.

This is the more interesting of the two, because 'give me a database' hides an enormous amount of per-engine operational knowledge. Ten engines are supported, each mapping to a purpose-built operator or a set of Crossplane managed resources.

One XDatabase, ten engines

A developer sets engine: and gets a correctly-operated datastore. Click an engine.

engine: postgresPostgreSQL· CloudNativePG operator
  • A CNPG Cluster: primary plus read replicas, with a -rw and -ro service
  • A generated-password Secret (write and read-only principals)
  • Barman backups to S3 on a schedule

Most engines get backups baked in, usually to S3, so each also provisions the bucket, its policy, versioning, and the IAM/Pod Identity role to write them. In-cluster stores also get a TLS-deny AuthorizationPolicy and are pinned to the dedicated database node pool.

Ten engines behind one flag. Click any for the operator and what it provisions.

Backups, buckets, and least privilege, for free

The pattern that ties the database composition together is that requesting any engine also provisions everything that engine needs to be safe and durable. Most stores back up to S3, so selecting an engine also creates the S3 bucket, its policy, versioning, and the IAM/Pod Identity role that lets the store write backups into it. In-cluster stores get a TLS-deny AuthorizationPolicy and land on a dedicated database node pool.

On the application side, access is additive and least-privilege: when an app declares databases: [{engine: object-aws}], the composition attaches a scoped S3 RolePolicy to that app's own role, so one bucket can be shared by many apps without anyone holding broad access.

The compositions, generalized

Both compositions are function-pythonic programs: Python that reads the simple spec and emits the resource graph. The application composition's entry point reads almost like a table of contents for a production service, each line a resource the developer never has to think about:

I've generalized both, stripped the company-specific config, and open-sourced them (linked below) so the pattern is reusable.

composition-app.py · compose()
def compose(self):
    self._setup()
    self._service_account()
    self._iam_role()                  # one EKS Pod Identity role per app
    self._pod_identity_association()  # SA -> role, no static keys
    self._secrets_role_policy()       # + S3 / DynamoDB / Bedrock, additively
    if self._spec.secrets:
        self._secret_provider_class() # AWS Secrets Manager CSI
    if self._scheduled:
        self._cronjob()
    else:
        self._rollout()               # Argo Rollout, not a Deployment
    if self._spec.vpa and not self._scheduled:
        self._vpa()
    if self._spec.hpa and not self._scheduled:
        self._scaled_object()         # KEDA
    if self._has_routes and not self._scheduled:
        self._virtual_service()       # Istio routing
        self._auth_policy_ext_authz(self._authz_eligible_routes())
    if self._defaults.get('emit_imageupdater'):   # dev
        self._image_updater_stable()
        self._image_updater_test()
        self._git_repository()        # GitOps Promoter
        self._promotion_strategy()
The Death of the YAML Engineer
What this much abstraction does for developer productivity, and whether hand-written YAML is on its way out.

A single pane of glass

Backstage ties it together: service, library, and pipeline docs, OpenAPI specs, ML-model and access metadata, and the composition specs themselves, plus software templates that scaffold a brand-new service in a few clicks. The result is a paved road that's secure by default, where the fast path and the safe path are the same path.

How Backstage becomes the single pane
The developer portal that ties docs, software templates, and the composition specs into one paved road.
Stack
Crossplanefunction-pythonicKustomizeArgo CDArgo RolloutsIstioEKS Pod IdentityKEDAVPACloudNativePGExternal SecretsBackstage