Build internal developer platforms. Covers platform engineering principles, golden paths, self-service infrastructure, developer portals, and measuring platform adoption and developer productivity.
Platform Engineering & Internal Developer Platforms is the practice of building self‑service abstractions that sit between a development team and the underlying infrastructure. It matters when an organization has more than a handful of teams, when manual infrastructure requests become a bottleneck, when compliance and security policies need to shift left without slowing delivery, and when you want consistent environments, observability, and dependency management across many services. An IDP is not a single tool; it is a pattern of APIs, scaffolds, automated pipelines, and golden paths that let a developer go from git push to production‑ready traffic through a repeatable, governed flow.
When a developer needs a new service, the platform provides a golden‑path template rather than a blank repo. The template is a git repository pre‑populated with baseline manifests, a Dockerfile, a Makefile, and a CI workflow. The developer runs:
git clone https://git.internal.example.com/platform/service-template my-service
cd my-service
The template typically contains:
Dockerfile that builds the app and exposes $PORThelm/Chart.yaml with apiVersion: v2 and name: my-servicek8s/deployment.yaml with replicas: 2 and an empty resources: block.github/workflows/ci.yml that checks out code, runs hadolint, builds and pushes an image, and applies manifests via kubectlCustomization is done through values.yaml overrides or kustomize edit set image. When the platform updates the base image tag or security baseline, repos cloned before the update do not change automatically; a git pull in each service repo is required, or the platform provides a platform upgrade command that patches values.yaml across all cloned repos. The sharp edge here is template drift: if a team modifies values.yaml locally and later pulls a platform update, the changes can be overwritten unless the platform uses a merge strategy or a separate overlays directory.
Promoting code through environments is usually modeled as a GitOps loop. The developer pushes a branch; a CI pipeline builds the image, tags it with the SHA, and merges to main. The platform’s GitOps controller (watching the main branch) detects the new tag and runs helm upgrade --install with the new image tag. A concrete example of the helm command the platform executes:
helm upgrade --install my-service ./helm \
--namespace production \
--set image.tag=sha-$(git rev-parse --short HEAD) \
--set replicaCount=4
In staging, the same command runs with --namespace staging and --set replicaCount=1. The platform may expose a CLI wrapper, but the underlying mechanism is always a helm upgrade against a declarative manifest. A common confusion is between helm upgrade and helm install; if the release name doesn’t exist, helm upgrade installs it, which can mask a misconfigured releaseName in the chart. The error you see is:
Error: release "my-service" has already been installed
which forces you to use --recreate-pods or uninstall first.
Injecting secrets and configuration is one of the most frequently botched steps. A concrete, platform‑agnostic pattern is to create a Kubernetes secret from a literal, then reference it in a pod spec:
kubectl create secret generic db-creds \
--from-literal=password=$PG_PASSWORD \
--dry-run=client -o yaml | kubectl apply -f -
The resulting secret YAML looks like:
apiVersion: v1
kind: Secret
metadata:
name: db-creds
type: Opaque
data:
password: UGFzc3dvcmQ=
The pod spec then references it:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-creds
key: password
A sharp edge: if the secret is updated in etcd but the pod’s container already running does not reload the environment variable, the app continues using the old value. The platform may inject a postStart exec hook or a preStart script that runs kill -SIGTERM 1 to force a reload, but many teams forget this and deploy a new replica only to find the env var still stale. The error text that surfaces is typically a connection failure:
FATAL: password authentication failed for user "postgres"
because the rotated password was never read by the running instance.
CI/CD integration is where the platform’s pipeline hooks into the developer’s workflow. A minimal platform CI config lives in .github/workflows/ci.yml and uses standard actions:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: internal-registry.example.com/my-service:${{ github.sha }}
- name: Apply manifests
run: |
kubectl set image deployment/my-service my-service=internal-registry.example.com/my-service:${{ github.sha }}
kubectl rollout status deployment/my-service -n staging
The platform may add gates: a required-approvals check that blocks merge until two team leads approve, or a security-scan job that fails the pipeline if trivy finds CVEs above a severity threshold. A confused option is placing the kubectl step before the image push succeeds; the error you get is:
Error: failed to set image: rpc error: code = NotFound "deployment "my-service" not found
because the manifest hasn’t been applied to the target namespace yet. The fix is to ensure the manifest exists in the repo or use kubectl apply -f k8s/ in the same job.
Querying platform health and debugging a stuck service relies on the same tools the platform exposes, but the way you use them matters. To list all running pods in a namespace:
kubectl get pods -n my-service -o wide
To see why a deployment isn’t rolling out:
kubectl rollout status deployment/my-service -n production --timeout=300s
If the rollout is stuck, kubectl describe deployment my-service -n production gives events such as:
Warning FailedScheduling 2m x5 kubernetes.io/unschedulable
or
Warning FailedCreatePodSandBox 1m kubelet
A frequent sharp edge is mixing up resources.limits and resources.requests; if only limits is set and requests is left at the cluster default (often 0), the scheduler may place the pod on a node with insufficient capacity, and the pod enters Pending indefinitely. The event you’ll see is:
0/3 nodes are available: 3 Insufficient cpu.
Another common confusion is imagePullPolicy: Always versus IfNotPresent; if the platform builds a new image on every push but the node has the old image cached, IfNotPresent means the pod starts with the stale image. Switching to Always resolves it, but you then incur a pull latency on every restart.
kubectl apply fails with admission webhook "platform.validator.internal" denied the request: the server could not find the requested resource or simply hangs. The operator assumes a network issue and retries, but the real fix is checking the webhook pod logs and its TLS certificates.helm template on a laptop with Chart v5 but the CI runner has v3 produces different output. The error is not always clear; sometimes helm template silently omits hook sections, leading to a partially applied manifest. Pin apiVersion: v2 in Chart.yaml and helm globally to avoid this.Terminating: The platform often adds finalizers (e.g., kubernetes.io/cluster-finalizer) to gate cleanup. Deleting a namespace yields The namespace "staging" is invalid: The namespace "staging" is terminating. The cure is kubectl patch namespace staging -p '{"metadata":{"finalizers":null}}', but if the platform’s controller re‑adds the finalizer, you’re in a loop. The lesson: always check which finalizers exist and whether the platform provides a cleanup API endpoint.401 Unauthorized errors in the app logs, followed by a surge of successful requests after a kubectl rollout restart.imagePullSecret omission: A new developer clones a golden‑path repo, forgets to add the private registry secret to their service account, and runs kubectl apply. The error is Pulling image "internal-registry.example.com/my-service:latest" was skipped: insufficient permissions. The platform’s golden path should include a default service account with the secret mounted, but if it doesn’t, the developer has to manually kubectl create secret docker-registry reg-cred and patch the service account—something that’s easy to skip in a hurry.This page was rewritten on 16 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