Skip to content
All projects

IDP & GitOps Reference Architecture

Internal Developer Platform for zero-touch service onboarding and multi-tenant GitOps delivery, driven by one versioned catalog that holds both the golden paths and the output contract — no scaffolder code contains a hardcoded path.

Source code
  • IDP
  • GitOps
  • Argo CD
  • Kubernetes
  • Terraform
  • Go

The problem

Onboarding a new service means a ticket, a wait, and a platform engineer hand-assembling the same manifests again — the platform team becomes the bottleneck for every team it serves.

Constraints

Architecture

Catalog

Golden paths
Output contract

Developer

Scaffolder CLI
Tenant repos

Delivery

GitHub Actions
Rendered manifests

Control plane

Argo CD ApplicationSet
Kyverno admission

Key decisions

What was chosen, what it was chosen over, and why.

Also decided

  • Kyverno admission controlnotManual compliance review before merge

    Guardrails over gates: the control plane enforces the boundary at admission, so a tenant cannot opt out by editing their own manifests and the platform team is not a queue.

  • Tenant-first monorepo whose paths are the split prefixesnotSeparate repositories from day one

    Output is `<team>/{apps,infra,gitops}/`, and those three paths are exactly the `git subtree split` prefixes. The demo stays one repo while the production shape — code, Terraform and desired state under different reviewers and blast radius — is already encoded, so splitting is a no-op rather than a reshuffle.

  • Ownership enforced by CODEOWNERS at each would-be repo rootnotDocumenting which directories the platform team owns

    GitHub honours CODEOWNERS only at a repo root, so nesting it under the platform directory would have made it decorative. Inside `infra/` and `gitops/` the split is by ownership rather than taxonomy, which reduces the whole rule to two globs.

  • Backstage System as metadata, not a directory levelnotA `<system>/` directory under each team

    Reversed after building it. The justifications did not survive scrutiny — Terraform blast radius is set by where `apply` runs, and a team ApplicationSet globbing `apps/*/*` discovers services perfectly well. It now lives only in `catalog-info.yaml`, where it cannot drift from a second encoding in the path.

  • Two independent scaffolder engines against one catalognotA single implementation and a claim that the catalog is a contract

    Go and Python both render the catalog, and their output trees are diffed byte-for-byte in CI. A difference means either the engines drifted or the catalog under-specified something both had to guess — so the contract is falsifiable rather than asserted. In production you would ship one; here it is the test.