kubernetes-ops
Kubernetes and Helm in practice: Deployments, Services, probes, resources, autoscaling, disruption budgets, NetworkPolicy, Pod Security Standards, Gateway API (ingress-nginx is retired), Helm 4 charts
Files8
View on GitHubSKILL.mdskills/kubernetes-ops/SKILL.md
GitHubKubernetes Operations
Manifests, charts and day-two kubectl for Kubernetes 1.35-1.37. Facts verified 2026-10-07:
Kubernetes 1.37 "Garhwal" is current (1.34 reaches end of life on 2026-10-27), Helm 4 is
current, and Gateway API v1.6 is the routing API. docker-ops'
scripts/check-docker-facts.py --live watches these versions weekly.
Why: a manifest that applies cleanly can still take production down. A pod with no readiness probe takes traffic before it can serve; a namespace delete takes its volumes with it; an Ingress controller that stopped getting security fixes in March 2026 is still routing traffic in most clusters.
Hard rules
- Know where you are before you write. Run
kubectl config current-contextbefore any apply, delete or Helm command, and name the context when you report. Deletes,apply --prune,helm uninstalland namespace teardown are Tier 2: list what goes, ask, wait (docker-ops destructive-commands.md has the Kubernetes and Helm table). - New routes use Gateway API, not ingress-nginx. ingress-nginx was retired: no releases or security fixes since March 2026, and the repo is archived. Existing installs keep working until a CVE lands. InGate was retired too, so it is not the successor. Migrate with Ingress2Gateway. gateway-api.md
- Pods meet the restricted Pod Security Standard.
runAsNonRoot: truewith a numericrunAsUser,allowPrivilegeEscalation: false,capabilities.drop: [ALL],seccompProfile.type: RuntimeDefault.readOnlyRootFilesystem: trueis recommended, but Restricted does not require it. Enforce with the namespace labelpod-security.kubernetes.io/enforce: restricted. - Every container has requests, a memory limit, and probes that mean different things. readiness = "send me traffic", liveness = "restart me", startup = "I'm still booting". A liveness probe that checks a database restarts healthy pods when the database blips.
- Images by version, and by digest in production. Never
:latest(and neverimagePullPolicy: Alwaysas a substitute for pinning). The image itself is docker-ops' job. - A Secret is base64, not encryption. Never commit Secret manifests with data. Use
External Secrets Operator, Sealed Secrets or the Secrets Store CSI driver, and limit
get secretsin RBAC. - Helm 4 changed defaults. New releases use server-side apply.
--atomicis now--rollback-on-failureand--forceis--force-replace(the old flags warn). Post-renderers are plugins. Rendering withhelm template | kubectl applyloses release tracking and rollback. helm-patterns.md - Preview before changing a shared cluster:
kubectl diff -f ...andkubectl apply --dry-run=server -f .... Prefer GitOps over hand-applies for anything long-lived.
Routing
| The task | Where |
|---|---|
| Deployment, Service, probes, HPA, PDB, NetworkPolicy, RBAC, Jobs | k8s-manifests.md |
| Charts, values, Helm 4 upgrades, OCI charts | helm-patterns.md |
| Ingress, TLS, routing, leaving ingress-nginx | gateway-api.md |
| ECS vs Kubernetes, Fargate basics | ecs-fargate.md; details: AWS's aws-containers skill |
| EKS specifics (Auto Mode, Karpenter, Load Balancer Controller) | AWS's aws-containers skill (aws/agent-toolkit-for-aws) |
| Flux GitOps | fluxcd/agent-skills (gitops-knowledge, gitops-repo-audit, gitops-cluster-debug) |
| Dockerfiles, images, Compose, the local engine | docker-ops |
| Cluster infrastructure as code | terraform-ops |
| Metrics, alerts, dashboards | monitoring-ops |
| Pipelines that build, scan and deploy to a cluster | ci-cd-ops |
| A security review of RBAC, secrets handling or the app itself | security-ops |
Argo CD has no official agent skill (argoproj-labs ships an MCP server).
What changed recently (1.33-1.37)
| Release | Change that matters in manifests |
|---|---|
| 1.33 | sidecar containers GA (initContainers with restartPolicy: Always); the Endpoints API is deprecated, use EndpointSlice |
| 1.34 | trafficDistribution: PreferSameZone replaces PreferClose; PreferSameNode added; PreStop sleep GA |
| 1.35 | in-place pod resize GA; kubelet refuses cgroup v1 nodes by default; kube-proxy ipvs mode deprecated (use nftables) |
| 1.36 | user namespaces GA (hostUsers: false); OCI image volumes GA; Service externalIPs deprecated |
| 1.37 | KYAML output (kubectl get -o kyaml); configurable HPA tolerance GA; static pods can no longer reference Secrets or ConfigMaps |
A workload in one screen
apiVersion: apps/v1
kind: Deployment
metadata: {name: web, labels: {app.kubernetes.io/name: web}}
spec:
replicas: 3
selector: {matchLabels: {app.kubernetes.io/name: web}}
template:
metadata: {labels: {app.kubernetes.io/name: web}}
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile: {type: RuntimeDefault}
containers:
- name: web
image: ghcr.io/acme/web:1.4.0@sha256:<digest>
ports: [{name: http, containerPort: 8080}]
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {memory: 256Mi}
readinessProbe: {httpGet: {path: /ready, port: http}, periodSeconds: 5}
livenessProbe: {httpGet: {path: /live, port: http}, periodSeconds: 20, failureThreshold: 3}
startupProbe: {httpGet: {path: /live, port: http}, failureThreshold: 30, periodSeconds: 2}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}
volumeMounts: [{name: tmp, mountPath: /tmp}]
volumes: [{name: tmp, emptyDir: {}}]
---
apiVersion: v1
kind: Service
metadata: {name: web}
spec:
selector: {app.kubernetes.io/name: web}
ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: {name: web}
spec:
parentRefs: [{name: public, namespace: gateway}]
hostnames: [app.example.com]
rules: [{backendRefs: [{name: web, port: 80}]}]
No CPU limit is deliberate: CPU limits throttle bursty services, while a memory limit is what prevents a noisy neighbour. Add a PodDisruptionBudget and an HPA for anything with more than one replica (k8s-manifests.md).
Debugging pods
kubectl get pods -o wide
kubectl describe pod <pod> # Events at the bottom say why
kubectl logs <pod> -c <container> --previous # the crashed run, not the restarted one
kubectl get events --sort-by=.lastTimestamp -n <ns>
kubectl debug -it <pod> --image=busybox:1.37 --target=<container> # ephemeral container, no restart
| Status | Usually | Look at |
|---|---|---|
Pending |
no node fits (requests too big, taints, affinity, an unbound PVC) | describe pod events |
ImagePullBackOff |
wrong tag, a private registry without imagePullSecrets, or a platform the node lacks |
the exact image; check-image-tags.py --live (docker-ops) |
CrashLoopBackOff |
the app exits; a liveness probe kills it before it's up | logs --previous; add a startupProbe |
OOMKilled (exit 137) |
memory limit below real usage | kubectl top pod; raise the limit or fix the leak |
CreateContainerConfigError |
a missing Secret or ConfigMap key | describe pod |
| Running but no traffic | readiness failing, or the Service selector doesn't match the pod labels | kubectl get endpointslices -l kubernetes.io/service-name=<svc> |
References
| File | Read when |
|---|---|
| k8s-manifests.md | Writing manifests: config, secrets, autoscaling, disruption budgets, NetworkPolicy, RBAC, Jobs |
| helm-patterns.md | Writing or upgrading charts; moving to Helm 4 |
| gateway-api.md | Routing and TLS with Gateway API; migrating off ingress-nginx |
| ecs-fargate.md | Choosing ECS on Fargate vs Kubernetes; the ECS basics before AWS's own skill |