Design and build an Internal Developer Platform (IDP) that eliminates toil. Covers golden paths, self-service infrastructure, developer portals, and the organizational patterns that make platforms succeed.
Internal Developer Platforms (IDPs) sit between an organization’s tooling and its engineers, exposing curated self‑service capabilities so that developers can request, configure, and operate infrastructure without opening a ticket for every provisioning step. They matter when team velocity is throttled by manual request processes, when onboarding new engineers requires weeks of environment setup, or when compliance and security gates need to be enforced consistently across dozens of services. This reference page covers the practical patterns for building that layer, the concrete commands and configuration that keep it operable, and the sharp edges that catch teams off guard after they’ve already deployed.
The first self‑service surface most engineers encounter is a local or ephemeral development environment that mirrors the production stack without requiring a ticket to ops. The goal is to reduce “it works on my machine” friction while keeping the underlying cluster deterministic.
Cloning and bootstrapping
git clone https://github.com/example/service.git
cd service
docker compose up -d
If the service depends on a cluster, a one‑liner creates the namespace and applies the manifest:
kubectl create namespace dev --dry-run=client -o yaml | kubectl apply -f -
A typical kubernetes/dev.yaml might look like this (generic, no product‑specific keys):
apiVersion: v1
kind: Namespace
metadata:
name: dev
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: dev
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: registry.example.com/api:dev
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
Common exact error text you’ll see when something is mis‑configured:
Error: failed to pull image: ImagePullBackOff – the node cannot resolve the registry reference or the image tag is missing.context deadline exceeded – often from a slow‑responding ingress controller or a service mesh sidecar that hasn’t attached yet.the server could not find the requested resource – usually a missing CRD, API group, or namespace.Configuration keys that appear across most manifests (these are patterns, not vendor‑locked fields):
replicaCount – integer that drives the replicas field in a Deployment.image.repository and image.tag – the registry path and version label.service.type – ClusterIP, LoadBalancer, or NodePort.resources.limits and resources.requests – CPU and memory caps expressed as 500m or 256Mi.If the platform uses a package manager, the same values are typically passed as flags or in a values file, but the underlying data flow is always a key/value merge into the manifest before kubectl apply.
Once code is merged to a default branch, the next self‑service need is a reproducible path from dev → staging → production. The pattern is rarely a manual script; it is usually a git‑watching controller that watches the repository and runs kubectl apply against the appropriate overlay.
Git‑based promotion flow
git checkout main
git merge feature/x
git push origin main
When the push occurs, a controller (any standard Kubernetes operator, Flux, or a custom webhook) detects the change and executes:
kubectl apply -f overlays/production.yaml
If the platform uses kustomize, the command the controller effectively runs is:
kustomize build overlays/production | kubectl apply -f -
Exact flags you’ll encounter in typical workflows:
kubectl apply -f - – reads manifest from stdin, common in pipe‑from‑kustomize or pipe‑from‑helm templates.helm upgrade --install my-service . – while Helm is a specific tool, the flag pattern --install combined with --upgrade is standard across package managers that manage releases; the flag semantics (install if absent, upgrade if present) are universal.kustomize edit set image registry.example.com/api:v2 – a real kustomize edit command that changes the image tag across all resources in the folder.Configuration key confusion – a frequent sharp edge is treating image.tag as a immutable version vs. a mutable “latest” alias. Setting image.tag: latest in a values file will cause the controller to pull the tag on every reconcile, which can silently shift the running version without a git change. The inverse confusion—pinning a specific commit hash in the chart metadata while expecting the runtime image to update—leads to “stale” pods that never reconcile.
Self‑service platforms must mediate who can do what, across both the underlying cluster and the platform’s own API surfaces.
Exact kubectl auth command
kubectl auth can-i list pods --namespace dev --as=user:alice
Return values are yes or no (or the error Error: you must be authenticated to the server (Unauthorized) if the token is missing or expired).
RBAC manifest pattern (generic, no product‑specific API fields):
kubectl create role dev-reader \
--verb=get,list,watch \
--resource=pods \
--namespace=dev
The equivalent YAML that the command generates:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dev-reader
namespace: dev
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
Authentication flow – most IDPs expose an OIDC or GitHub Actions workload‑identity endpoint. A developer’s CI job requests a short‑lived token via:
curl -X POST https://auth.example.com/token \
-H "Accept: application/json" \
-d '{"client_id":"gitops","scope":"openid profile"}'
The returned JWT is then used as the KUBERNETES_TOKEN environment variable for subsequent kubectl calls. A silent failure in this flow is a token that has expired but is still cached in the agent’s environment, causing kubectl auth can-i to return no unexpectedly, or worse, silently succeed against a permissive cluster role while the intended scope was narrower.
Common confusion – mixing up clusterrole vs role. A clusterrole grants permissions across all namespaces; a role is namespace‑scoped. Applying a cluster‑wide role when only namespace access was intended is a frequent mis‑step that surfaces as “why can this user see pods in every namespace?”
Even after a platform is “working,” the edges surface in production‑grade scenarios.
--set and -f values.yaml are passed, the last‑seen value wins. A team that runs helm upgrade --set replicaCount=3 -f prod.yaml assuming the file‑based value takes precedence will silently keep the CLI override. The reverse—expecting --set to override a file flag—causes confusion during on‑call debugging.kubectl create secret generic db-credentials --from-literal=url=postgres://... creates a secret object immediately, but pods that mount it via volumeMount may still see the old value for up to one reconciliation cycle. If the platform auto‑rebuilds pods on secret change, the delay is invisible; if it requires a manual restart, the team learns this only after a rollout stalls.configmap.yaml followed by kubectl apply -f - does not hot‑reload application config. The app must either watch the Kubernetes API for changes or be restarted. Many teams only discover this after deploying a config‑only change and seeing no effect until a pod restart occurs.kubectl apply vs kubectl replace – apply is a strategic merge patch; it adds or updates fields without destroying others. replace sends the full manifest and deletes any fields not present. A common foot‑gun: running kubectl replace -f deployment.yaml after manually editing the YAML to add a single label results in the deletion of all other labels that weren’t copied into the edited file. The error message resource mapping not found or an unexpected rollback often follows.replicaCount: 2 and PR #2 sets replicaCount: 3, the final state depends on merge order, not intent. The platform may report “success” for both, but the running replica count toggles between 2 and 3 as each PR’s webhook fires. The lesson: enforce a single source‑of‑truth branch for critical values, or use a controller that deduplicates or rejects conflicting applies.automountServiceAccountToken: false – by default, pods receive an automatically mounted service account token. Setting automountServiceAccountToken: false in the pod spec silences this, which breaks internal service‑to‑service calls that rely on in‑cluster auth. The failure mode is a 401 Unauthorized from the API server, with no obvious log clue that the token volume was simply absent.kubectl delete namespace dev --grace-period=0 --force attempts immediate removal, but if a deployment or StatefulSet has a kubernetes.io/metadata.name finalizer, the namespace stays in Terminating status until the controller removes the finalizer or the objects inside are deleted first. Operators often waste minutes debugging “stuck namespace” only to realize a finalizer on a child resource is holding the parent open.Each of these edges is not a bug in a single product; they are structural realities of how Kubernetes resources, git‑watching controllers, and auth tokens interact. Knowing the exact command that triggers the behavior, and the exact error text that surfaces when it goes wrong, lets a platform team write guardrails (pre‑flight checks, CI linting, documentation) before the failure reaches production.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy