Platform Engineering & Internal Developer Platforms

Build internal developer platforms. Covers platform engineering principles, golden paths, self-service infrastructure, developer portals, and measuring platform adoption and developer productivity.

Orientation

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.

Scaffold a New Service from a Golden Path

1.1 Initialize a Service Repo

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:

Customization 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.

Promote Code Through Environments

2.1 GitOps‑Based Push/Pull

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.

Inject Secrets and Configuration

3.1 Create and Reference a Secret

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

4.1 Minimal Platform CI Config

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.

Query Platform Health and Debug

5.1 Listing and Inspecting Resources

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.

Sharp Edges – What Silently Fails

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.