Internal platforms

Building an Internal Developer Platform with Helm

Treat the platform as a product with a stable contract: one reusable Helm chart, a bridge GitOps repo for desired state, and shared Gateway API infrastructure. Developers ship services without editing Deployment YAML.

Chart fidelity

Examples use Universal Helm Chart application 0.4.3 on Artifact Hub. Image names and hostnames are sanitized examples. The chart package leaves securityContext: {} empty; the platform golden path must set non-root and related keys in values.

Chart 0.4.3 — args / multi-port / service.enabled

From application 0.4.3: main container args (default []); Deployment revisionHistoryLimit (default 10); multi-port via containerPorts and service.ports; optional chart-managed Service via service.enabled (default true). When service.ports is set, Ingress / simple HTTPRoute / NOTES use the first entry (templates/_helpers.tpl application.servicePort). Legacy single-port service.name / service.port / service.protocol / service.appProtocol remain supported. Set service.enabled: false when the workload does not need a chart-managed Service. Do not invent other port keys.

Chart 0.4.2 — externalSecretHooks

Behavior change in application 0.4.2: ExternalSecret objects in extraManifests are ordinary resources by default. The chart does not add Helm hooks automatically. Recommend explicit lifecycle/ordering annotations on the manifest (for example argocd.argoproj.io/sync-wave: "-5"). Set externalSecretHooks.enabled: true (default false) only when legacy automatic helm.sh/hook: pre-install,pre-upgrade / hook-weight: "-5" / hook-delete-policy: before-hook-creation behavior is required (templates/1_extra-manifests.yaml). Do not invent other externalSecret* keys.

Versions used

What this article covers

This is an implementable IDP shape: catalog inputs, golden-path Helm contract, GitOps promotion, shared kgateway, policy, secrets, observability, ownership, scorecards, exceptions, and upgrades. It is not a product brochure. Delivery CI stays with Senior DevOps; error-budget policy stays with SRE. Shared-vs-dedicated chart ownership is covered in One Helm chart vs many application charts; the shorter primer is Why one Helm chart is enough. For Ingress NGINX cutover, see Ingress NGINX to kgateway.

Platform product, tooling, and process

Keep three layers distinct. When they blur, the platform becomes either a ticket desk or an unmanaged toolbox.

Product is the paved road: create service, build image, promote values, sync, observe. Measure it with time-to-first-deploy, percent of services on the golden path, and support ticket rate.

Tooling is the Helm chart, Argo CD ApplicationSets, shared Gateway and TLS, External Secrets, and policy engines. Tools change; the application contract should stay stable.

Process is RACI, the exception path, chart upgrade rings, and scorecard reviews. Process without tooling does not scale. Tooling without process becomes tribal knowledge.

Users and jobs-to-be-done

Architecture (repo-native)

┌──────────────┐   catalog request    ┌─────────────────────┐
│ Developer    │ ───────────────────► │ Service catalog /   │
│ (self-serve) │                      │ scaffolding job     │
└──────────────┘                      └─────────┬───────────┘
                                                │ generates
          ┌─────────────────────────────────────┼──────────────────────────┐
          ▼                                     ▼                          ▼
 ┌─────────────────┐                 ┌──────────────────┐      ┌────────────────────┐
 │ Service repo    │  CI: build/push │ Bridge GitOps    │      │ Shared platform    │
 │ + shared CI     │ ──────────────► │ values + Chart   │      │ Gateway / TLS /    │
 │ template        │  imageTag only  │ pin (application)│      │ ESO / policies     │
 └─────────────────┘                 └────────┬─────────┘      └─────────┬──────────┘
                                              │ ApplicationSet            │
                                              ▼                           ▼
                                     ┌────────────────────────────────────────────┐
                                     │ Cluster: Deployment/Service/HTTPRoute via │
                                     │ Universal Helm Chart + kgateway listeners │
                                     └────────────────────────────────────────────┘

Ownership notes and the same diagram live in the evidence pack under examples/internal-developer-platform/.

Service catalog inputs

The catalog form is the public API of the platform. Minimum fields (see examples/internal-developer-platform/catalog/):

# catalog/template-inputs.example.yaml (abbreviated)
serviceName: payments-api
ownerTeam: payments
language: fastapi
environments: [dev, prod]
route:
  hostname: payments-api.dev.example.com
  gateway: external
  gatewayNamespace: kgateway-system
  sectionName: https-wildcard
secretRefs:
  - secretName: payments-api-secrets
    keys: [DATABASE_URL]

Golden path and reusable Helm contract

The paved road for a conventional HTTP service:

  1. Scaffold the service repo with the shared CI template.
  2. Create the bridge path environments/<env>/<service>/ with a wrapper Chart.yaml that pins application 0.4.3 and nested application: values.
  3. Platform defaults inject securityContext, probes, resources, and route.* pointing at the shared Gateway.
  4. CI updates only application.image / application.imageTag. Wrong values nesting breaks promotions.
  5. Argo CD ApplicationSet discovers the path; sync produces Deployment, Service, and HTTPRoute.
# golden-path wrapper Chart.yaml
dependencies:
  - name: application
    version: "0.4.3"
    repository: https://chaser100.github.io/u-helm-chart

# values.yaml (nested)
application:
  replicaCount: 2
  image: ghcr.io/example-org/payments-api
  imageTag: "1.0.0"
  serviceAccount:
    create: true
    automount: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    allowPrivilegeEscalation: false
    readOnlyRootFilesystem: true
    capabilities:
      drop: ["ALL"]
  route:
    enabled: true
    gateway: external
    gatewayNamespace: kgateway-system
    sectionName: https-wildcard
    hostname: payments-api.dev.example.com
  envSecrets:
    enableEnv: true
    envs:
      - name: DATABASE_URL
        secretName: payments-api-secrets
        secretKey: DATABASE_URL

Golden-path values and wrapper live under examples/internal-developer-platform/ (chart pin 0.4.3).

GitOps workflow

Four layers, one job each (from the battle delivery model):

Bridge layout and ownership live in examples/gateway-api-helm-contract/BRIDGE-GITOPS-LAYOUT.md. The IDP delivery contract (ApplicationSet discovery, nested application.imageTag CI updates, and jittered GitOps push) is in examples/internal-developer-platform/DELIVERY-CONTRACT.md with matching excerpts under ci/ and gitops/. At scale, GitOps push conflicts need bounded fetch+rebase+jitter retries in the CI updater. Without that, demos look stable and production goes flaky.

kgateway / Gateway API as a platform service

Treat ingress as shared infrastructure, not an app-team CRD playground.

Policy, secrets, observability

Operating model (RACI summary)

The full matrix is in the evidence pack (examples/internal-developer-platform/).

Scorecards, SLOs, and adoption metrics

Measurable success (targets are platform-local; publish your own numbers):

Watch for mixed chart fleets after partial upgrades, escape-hatch sprawl, CI updating the wrong values nesting, and apps creating private Gateways.

Exceptions, upgrades, feedback loops

  1. Exception: file a request with blast radius, labeled extraManifests, and an expiry date. If most of the release is escape hatches, move to a dedicated chart.
  2. Upgrade: platform ships a new chart version, canary the wrapper pin, verify Deployment + HTTPRoute, roll ApplicationSet paths, then inventory pins. Rollback means revert the wrapper dependency and sync.
  3. Feedback: quarterly scorecard review, catalog NPS, and a single backlog for contract changes (not per-repo forks).

End-to-end self-service flow (generated artifacts)

  1. Developer submits a catalog request (template-inputs.example.yaml).
  2. Scaffold job creates the service repo, bridge Chart.yaml/values.yaml, and namespace Secret placeholder refs.
  3. First push builds the image; CI writes imageTag into the bridge.
  4. Argo syncs. The verify script/golden asserts Deployment, Service, HTTPRoute, and runAsNonRoot: true.
  5. Developer receives dashboard links: Argo app, route hostname, runbook.

Artifact tree: examples/internal-developer-platform/golden-path/ plus golden/ from the verify script.

Build vs buy — when Helm alone is insufficient

Helm + GitOps is enough for conventional Deployment/Service/(Ingress|HTTPRoute) fleets with rare labeled exceptions.

Buy or build a portal when catalog UX, RBAC across many teams, and scorecards need a product surface (Backstage, Port, and similar). Keep Helm as the runtime contract underneath.

Dedicated charts or operators win when CRDs dominate the release, multi-workload values explode, or compliance freezes templates off the platform train. See the decision matrix.

Helm is not a secrets manager, a policy engine, or a progressive-delivery controller. Compose External Secrets, admission policy, and Argo Rollouts/Flagger beside the chart.

Validation evidence

Official documentation

Why one Helm chart is enough → · One chart vs many → · Ingress NGINX to kgateway → · Gateway API examples → · Browse examples →

Related in this series

Platform engineering with Helm → · GitOps repository structure → · Deploy 100 microservices → · Kubernetes Secrets best practices → · One chart vs many → · Why one Helm chart is enough → · Gateway API examples → · Ingress NGINX to kgateway →