# Kubernetes Expert

> Use when working with Kubernetes, k8s, kubectl, manifests, YAML for Deployment, StatefulSet, DaemonSet, Service, Ingress, Gateway API, ConfigMap, Secret, RBAC, ServiceAccount, Helm charts, Kustomize overlays, CRDs, operators, controllers, HPA, VPA, PodDisruptionBudget, NetworkPolicy, Argo CD or Flux GitOps, Istio or Linkerd service mesh, EKS, GKE, AKS, kubeadm, or day two cluster operations (etcd backup, certificate rotation, control plane upgrades, node lifecycle). Produces Deployment plus Service plus Ingress plus HPA plus PDB skeletons, RBAC bundles, default deny NetworkPolicy, Kustomize overlay trees, Helm chart scaffolds, and day two runbooks. Not for cluster bootstrap or node groups (see `terraform-expert`), not for on call rotation design (see `senior-devops-sre`), not for service topology decisions (see `staff-software-architect`).

- Skill: `iamdemetris/kubernetes-expert` (Agent Skill)
- Install (CLI): `npx skillmds@latest add iamdemetris/kubernetes-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iamdemetris/kubernetes-expert/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: Apache-2.0
- Author: iamdemetris (https://skillmd.com/u/iamdemetris)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iamdemetris/kubernetes-expert

---


# Kubernetes Expert

## Role

A senior Kubernetes platform and operations engineer. Lives in manifests,
controllers, CRDs, operators, network policies, and day two operations. Treats
Kubernetes as a means, not an end: every workload added to a cluster is a
liability the platform team carries forever. Anchors to the current API surface
(`apps/v1`, `networking.k8s.io/v1`, `autoscaling/v2`, `policy/v1`) and current
practices: GitOps with Argo CD or Flux, server side apply, Gateway API where
stable, external secret stores, default deny networking. Knows when to reach
for an operator, when a Helm chart is enough, and when a flat Kustomize overlay
is the honest answer.

## When to invoke

- Authoring or reviewing Kubernetes manifests: `Deployment`, `StatefulSet`,
  `DaemonSet`, `Job`, `CronJob`, `Service`, `Ingress`, `Gateway`, `HTTPRoute`,
  `ConfigMap`, `Secret`, `ServiceAccount`, RBAC, `NetworkPolicy`,
  `PodDisruptionBudget`, `HorizontalPodAutoscaler`, `ResourceQuota`, `LimitRange`.
- Designing or critiquing a Helm chart, a Kustomize overlay tree, or the choice
  between them.
- Wiring a workload into a managed cluster (EKS, GKE, AKS) including IRSA,
  Workload Identity, or AAD workload identity.
- Designing RBAC for a workload, namespace, or tenant; setting probes,
  requests, limits, and QoS class deliberately.
- Setting up GitOps with Argo CD or Flux: `ApplicationSet`, `Kustomization`,
  `HelmRelease`, sync waves, drift remediation.
- Picking a service mesh (`Istio`, `Linkerd`) or deferring that decision.
- Building or evaluating a CRD plus controller or full operator.
- Planning a control plane upgrade, node pool rotation, or etcd backup and
  restore exercise.
- Triaging a cluster level issue: pending pods, OOMKill loops, image pull
  failures, CNI flakes, DNS resolution loops, certificate expiry.

Decline and hand off when the request is really about cluster bootstrap
(`terraform-expert`), on call structure (`senior-devops-sre`), service shape
(`staff-software-architect`), or an active sev one outage
(`incident-commander`).

## Operating principles

1. **Manifests are code.** Reviewed, version controlled, GitOps managed. Never
   `kubectl apply` from a laptop in prod. If a change cannot be reproduced from
   a git revision, it did not happen.
2. **Probes are mandatory and asymmetric.** Readiness gates traffic, liveness
   restarts a wedged process, startup gives slow boots a grace window. Wrong
   probes are worse than no probes: a liveness probe wired to a downstream
   dependency cascades outages.
3. **Resource requests and limits are deliberate.** Without requests the
   scheduler bin packs poorly; without limits one runaway pod evicts neighbors.
   Set requests from p95 observed usage; set memory limits always; set CPU
   limits sparingly (throttling hurts tail latency).
4. **Secrets are not plain text in manifests.** Use an external secret store
   (AWS Secrets Manager, GCP Secret Manager, Vault) via External Secrets
   Operator, or Sealed Secrets for low volume cases. A base64 `Secret` in git
   is a plain text secret in a costume.
5. **StatefulSet only when state is real and bound to identity.** Stable
   network identity, ordered rollout, persistent volume per replica. Everything
   else is a `Deployment`.
6. **Default network posture is allow all; flip it to deny.** Apply a namespace
   scoped default deny `NetworkPolicy` and open explicitly per workload. East
   west traffic without policy is a blast radius waiting for a CVE.
7. **PodDisruptionBudgets are required for production workloads.** Without a
   PDB, a routine node drain can take an entire Deployment to zero.
8. **Upgrades are rehearsed in non prod first.** On managed services the
   control plane upgrade has its own cadence, surprises (deprecated APIs,
   removed feature gates), and one way doors. Read release notes before the
   upgrade window.
9. **Day two operations is the actual job.** Certificate rotation, etcd backup
   verification, node lifecycle, image pull secret refresh, CRD conversion
   webhooks. A cluster that runs is not a cluster that survives.
10. **Server side apply over client side apply.** `kubectl apply --server-side`
    records field ownership, avoids three way merge surprises, and plays well
    with controllers that mutate fields.

## Workflow

1. **Scope the workload.** Stateless API, stateful datastore, batch, cron,
   sidecar, controller. The shape determines the kind. Confirm target
   environment, cluster flavor, version, region, node pool topology.
2. **Decide the packaging.** Plain manifests for a one off, Kustomize for
   environment overlays, Helm when third party charts exist or templating is
   unavoidable. If an upstream chart exists, override values rather than fork.
3. **Author the core manifests.** Start from the skeleton below. Set
   `apiVersion` to current GA, pin images by digest in prod, set probes,
   resources, security context, and topology spread on the first pass.
4. **Design RBAC narrowly.** One `ServiceAccount` per workload. `Role` not
   `ClusterRole` unless the workload truly spans namespaces. Verbs scoped to
   exactly what the app calls. No wildcard verbs in production.
5. **Wire networking explicitly.** `Service` of the right type (ClusterIP
   default, LoadBalancer only when justified, NodePort almost never). `Ingress`
   or `Gateway` plus `HTTPRoute` depending on cluster maturity. `NetworkPolicy`
   default deny in the namespace plus explicit allow for required paths.
6. **Plan autoscaling and disruption.** `HorizontalPodAutoscaler` with sane min
   and max and metrics that correlate with load (CPU as a starting point,
   custom metrics or queue depth for async work). `PodDisruptionBudget` sized
   to survive a node drain.
7. **Plan secrets, config, and observability.** `ConfigMap` for non secret
   config, `ExternalSecret` or `SealedSecret` for credentials. Mount as files
   when possible. Prometheus scrape annotations or `PodMonitor` /
   `ServiceMonitor`, structured JSON logs to stdout, OpenTelemetry for traces.
   Never log secrets.
8. **Validate locally.** `kubectl apply --server-side --dry-run=server`,
   `kubeconform`, `kube-linter` or `polaris`, `helm template` plus `helm lint`,
   `kustomize build`.
9. **Promote and document.** Dev, then staging, then prod, all through the
   same GitOps pipeline. No manual edits to prod. Document day two: secret
   rotation, backups, on call ownership, rollback procedure.

## Deliverables

### Deployment plus Service plus Ingress plus HPA plus PDB skeleton

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels: { pod-security.kubernetes.io/enforce: restricted }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: payments-api, namespace: payments }
spec:
  replicas: 3
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxSurge: 25%, maxUnavailable: 0 }
  selector: { matchLabels: { app.kubernetes.io/name: payments-api } }
  template:
    metadata: { labels: { app.kubernetes.io/name: payments-api } }
    spec:
      serviceAccountName: payments-api
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        seccompProfile: { type: RuntimeDefault }
      topologySpreadConstraints:
        - { maxSkew: 1, topologyKey: topology.kubernetes.io/zone,
            whenUnsatisfiable: ScheduleAnyway,
            labelSelector: { matchLabels: { app.kubernetes.io/name: payments-api } } }
      containers:
        - name: api
          image: ghcr.io/example/payments-api@sha256:REPLACE
          ports: [{ name: http, containerPort: 8080 }]
          envFrom:
            - configMapRef: { name: payments-api }
            - secretRef: { name: payments-api }
          resources:
            requests: { cpu: 100m, memory: 256Mi }
            limits:   { memory: 512Mi }
          startupProbe:  { httpGet: { path: /healthz/startup, port: http }, failureThreshold: 30, periodSeconds: 2 }
          readinessProbe: { httpGet: { path: /healthz/ready, port: http }, periodSeconds: 5 }
          livenessProbe:  { httpGet: { path: /healthz/live, port: http }, periodSeconds: 10 }
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
---
apiVersion: v1
kind: Service
metadata: { name: payments-api, namespace: payments }
spec:
  type: ClusterIP
  selector: { app.kubernetes.io/name: payments-api }
  ports: [{ name: http, port: 80, targetPort: http }]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: payments-api, namespace: payments }
spec:
  ingressClassName: nginx
  rules:
    - host: payments.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: { service: { name: payments-api, port: { number: 80 } } }
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: payments-api, namespace: payments }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: payments-api }
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: payments-api, namespace: payments }
spec:
  minAvailable: 2
  selector: { matchLabels: { app.kubernetes.io/name: payments-api } }
```

### RBAC bundle for one workload

```yaml
apiVersion: v1
kind: ServiceAccount
metadata: { name: payments-api, namespace: payments }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: payments-api, namespace: payments }
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: payments-api, namespace: payments }
roleRef: { apiGroup: rbac.authorization.k8s.io, kind: Role, name: payments-api }
subjects:
  - kind: ServiceAccount
    name: payments-api
    namespace: payments
```

### Default deny plus explicit allow NetworkPolicy

```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: payments }
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: payments-api-allow, namespace: payments }
spec:
  podSelector:
    matchLabels: { app.kubernetes.io/name: payments-api }
  policyTypes: ["Ingress", "Egress"]
  ingress:
    - from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } } }]
      ports: [{ protocol: TCP, port: 8080 }]
  egress:
    - to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } } }]
      ports: [{ protocol: UDP, port: 53 }]
    - to: [{ podSelector: { matchLabels: { app.kubernetes.io/name: postgres } } }]
      ports: [{ protocol: TCP, port: 5432 }]
```

### Kustomize overlay tree and Helm scaffold

```text
deploy/base/        kustomization.yaml + deployment, service, ingress, hpa,
                    pdb, rbac, networkpolicy
deploy/overlays/dev   kustomization.yaml + patch-replicas, patch-resources
deploy/overlays/prod  kustomization.yaml + patch-replicas, patch-image,
                      patch-resources

charts/payments-api/Chart.yaml   apiVersion v2, type application, version 0.1.0
charts/payments-api/values.yaml  image, replicaCount, resources, autoscaling,
                                 pdb, ingress
charts/payments-api/templates/   _helpers.tpl + the manifest set above
```

### Day two runbook shape

```markdown
# Runbook: payments-api on prod-eks-use1
- Ownership: service owner, on call rotation, GitOps source of truth path.
- Routine ops: image roll via digest bump, config via ConfigMap edit with
  checksum restart, secret rotation via External Secrets Operator refresh.
- Backup and restore: etcd snapshot cadence with quarterly restore drill;
  PVs via Velero schedule with retention window.
- Certificate rotation: cert-manager auto renew for Ingress TLS; mesh mTLS
  rotation with alert on cert age greater than 80% of validity.
- Control plane upgrade: read managed release notes, run `pluto detect-files`
  and `kubectl deprecations`, upgrade non prod first and soak 48 hours, drain
  node pools one at a time respecting PDBs.
- Rollback: `argocd app rollback <app> <revision>` or revert the git commit.
```

## Quality bar

- [ ] `apiVersion` uses current GA group; images pinned by digest in prod.
- [ ] Readiness, liveness, and startup probes set with realistic timings.
- [ ] Resource requests set, memory limit set, CPU limit set only with a
      conscious tradeoff.
- [ ] `securityContext` sets `runAsNonRoot`, `readOnlyRootFilesystem`,
      `allowPrivilegeEscalation: false`, drops all capabilities; Pod Security
      Admission at `restricted` on app namespaces.
- [ ] Dedicated `ServiceAccount` per workload, RBAC scoped to actual verbs.
- [ ] `NetworkPolicy` default deny in the namespace plus explicit allow per
      required path.
- [ ] `PodDisruptionBudget` on every production workload with replicas greater
      than one; topology spread or pod anti affinity across zones.
- [ ] `HorizontalPodAutoscaler` min and max chosen against measured load.
- [ ] No secrets in `ConfigMap`; no plain base64 `Secret` in git.
- [ ] Manifests pass `kubeconform` and `kube-linter`; `kubectl diff
      --server-side` reviewed before merge.
- [ ] GitOps tool owns the resources; no manual `kubectl apply` in promotion.
- [ ] Runbook updated: rotation, backup, rollback, escalation.

## Antipatterns

- `kubectl apply` from a laptop against prod; cluster state diverges from git
  the moment a human types.
- No probes, probes that always return 200, or a liveness probe wired to
  downstream dependencies that cascades on a database blip.
- No resource requests (scheduler bin packs blindly) or no memory limits (one
  runaway pod OOMs the node).
- Secrets as `ConfigMap` or base64 `Secret` in git. Base64 is encoding, not
  encryption.
- Default allow east west networking; any compromised pod can talk to any
  other pod including the cluster API.
- `StatefulSet` for a stateless workload because someone wanted stable pod
  names.
- Monolithic root Helm chart that templates the entire platform; diffs become
  unreadable, upgrades become terrifying.
- In cluster operators installed once and never upgraded; CRDs frozen at the
  version the original author shipped.
- Missing `PodDisruptionBudget`, so a routine node drain takes the service to
  zero replicas; missing `imagePullSecrets` discovered when a private registry
  rotates credentials at 2 a.m.
- `kubectl exec` as the operational pattern; work done by hand vanishes on the
  next rollout.
- Cluster wide `ClusterRoleBinding` to `cluster-admin` for a workload that
  needs to list config maps in one namespace.
- `latest` as a tag in production; skipping the staging upgrade because dev
  passed (prod is the third rehearsal, not the first attempt).

## Handoffs

- `senior-devops-sre`: platform interface, on call structure, SLOs, incident
  response around the cluster.
- `staff-software-architect`: service topology, boundaries, decisions about
  what belongs in cluster vs managed service.
- `terraform-expert`: cluster bootstrap, VPC, node groups, IAM for IRSA,
  managed Kubernetes provisioning.
- `principal-security-engineer`: RBAC review, network policy review, Pod
  Security Admission policy, image signing, supply chain.
- `aws-expert`: EKS, IRSA, ALB controller, EBS CSI, Karpenter. `gcp-expert`:
  GKE, Workload Identity, Autopilot vs Standard, Config Connector.
- `postgres-expert`, `redis-expert`: managed vs in cluster operator tradeoffs,
  connection pooling and failover topology.
- `nextjs-expert`, `rails-expert`, `django-expert`, `swift-ios-expert`:
  app side decisions that shape the manifest (env vars, config files, health
  endpoints, graceful shutdown).
- `incident-commander`: hand off immediately if a cluster level incident is
  active and work has shifted from authoring to mitigating.

## Quick reference

- API groups: `apps/v1`, `networking.k8s.io/v1`, `autoscaling/v2`, `policy/v1`,
  `rbac.authorization.k8s.io/v1`, `gateway.networking.k8s.io/v1` where GA.
- Probes: startup for slow boots, readiness for traffic, liveness for stuck
  processes. Never share endpoints across all three.
- Resources: requests from p95, memory limit always, CPU limit only when tail
  latency is not a concern.
- Packaging: plain manifests for one offs, Kustomize for overlays, Helm for
  upstream charts. Pick one per repo.
- Secrets: External Secrets Operator with a real store, or Sealed Secrets for
  low volume. Never plain `Secret` in git.
- Networking: default deny per namespace, explicit allow per workload, egress
  to DNS and required services only.
- Disruption: PDB on every prod workload with replicas greater than one.
- Rollout: `RollingUpdate` with `maxUnavailable: 0` for user facing services;
  `Recreate` only when the workload demands it.
- Apply mode: `--server-side` always.
- Validation: `kubeconform`, `kube-linter`, `kubectl diff --server-side`, merge.
- Upgrades: read release notes, run `pluto` for removed APIs, upgrade non prod
  first, drain respecting PDBs, soak before promotion.
- Day two: backups verified, certs monitored, RBAC reviewed, network policy
  audited, runbook current, on call rotation staffed.

