nrp-kubernetes
Skill4 starsChanged 15 days agoBSD-3-ClauseView on GitHub
Operating the NRP (National Research Platform / Nautilus) shared Kubernetes cluster with kubectl — deployments, pods, configmaps, rollouts, and diagnosing bad rollouts. Use whenever running kubectl against NRP (namespaces such as `biodiversity`, `geo-workflows` and `schmidtdse`). #1 safety rule: NEVER force-delete pods. A pod stuck in ContainerCreating or pulling an image for more than ~5 minutes is a bad node: gracefully delete it, never wait it out. Also covers batch Job requirements, secrets wiring, HAProxy ingress (CORS, timeouts), the dedicated Berkeley node, and NRP's pod usage policy (CPU/memory/GPU use vs request, the small-pod exemption, the 4-pod limit) and check-usage.py; read it before launching a batch job or when a rollout is denied with "utilization is too low".
- Commits and pushes
In this repository18 other files it gives its agents.
- boettiger-lab-stackSkill
- cng-datasetsSkill
- disclose-tool-useSkill
- duckdb-spatialSkill
- gdal-spatialSkill
- github-app-authSkill
- github-rulesetsSkill
- html-figuresSkill
- human-communication-consentSkill
- k8s-never-force-deleteSkill
- nrp-s3Skill
- oci-artifactsSkill
- pod-resourcesSkill
- python-envSkill
- reproducible-reportsSkill
- stacked-pr-squash-mergeSkill
- stac-navigationSkill
- writing-voiceSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
14 rules. About 0 are common, 3 are distinctive, and 11 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 26
Do not: run kubectl delete pod ...
- Very specific: line 35
If a pod is stuck Terminating, investigate (describe, events, node health) or wait it out.
- Very specific: line 61
If the replacement lands on the same node and hangs again, keep the workload off that node with a kubernetes.io/hostname NotIn [...] node …
- Very specific: line 95
Deployment name ≠ repo name: Check first: kubectl get deploy -n <ns>.
- Very specific: line 125
Pods that run under about an hour: use priorityClassName: opportunistic.
- Distinctive: line 127
Pods that must run long: use default priority, and their requests are sized to measured use: memory 20–150% of the request, CPU 20–200%.
- Distinctive: line 141
before a rollout, if anything large is running.
- Very specific: line 183
Add ephemeral-storage to requests/limits sized to the scratch disk the job needs.
- Very specific: line 244
Add mcp-session-id to cors-allow-headers for MCP servers.
- Very specific: line 263
Always kubectl apply ConfigMaps before restarting — rollout restart recycles pods using whatever is already in the cluster.
- Very specific: line 270
Stuck rollout?: If rollout status hangs >2 min, the pod likely landed on a broken node.
- Very specific: line 288
Don't kubectl delete PVCs, StatefulSets, or Namespaces casually — data loss.
- Distinctive: line 289
Don't request more CPU/memory than you use (see Usage policy).
- Very specific: line 291
Don't edit live objects with kubectl edit for tracked apps — change the manifest in the repo and kubectl apply, so git stays authoritative.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
NRP Nautilus — kubectl operations
NRP Nautilus is a shared, multi-tenant academic cluster. Be a good neighbor.
⛔ NEVER force-delete a pod
Do not run kubectl delete pod ... --force --grace-period=0 (or --force).
Force-delete removes the pod from the API server before the kubelet confirms the container stopped. On a shared cluster this:
- orphans the container — it can keep running on the node, consuming resources;
- corrupts StatefulSet / RWO-PVC workloads — the volume can end up double-mounted or two copies running at once;
- violates NRP etiquette and hides real problems.
If a pod is stuck Terminating, investigate (describe, events, node health)
or wait it out; escalate to NRP admins on Matrix. Never force.
To reschedule a pod off a bad node — normal delete
A crashlooping/stuck pod caused by a node problem (DNS timeout, bad mount) is fixed by a plain graceful delete — the controller recreates it, often on a healthy node:
kubectl delete pod <pod> -n <ns> # NO --force, NO --grace-period=0⛔ A pod stuck in ContainerCreating / Pulling is a bad node — delete it, do not wait
On NRP an image pull that has not finished after ~5 minutes is a network or DNS fault on
that node, not a slow download. It does not recover: on 2026-10-26 an orchestrator pod sat in
Pulling image "bitnami/kubectl:latest" on nrp-00.rcac.purdue.edu for 4 hours until the
runner's timeout killed it. One graceful delete would have cost a minute.
kubectl -n <ns> get pods | grep -E 'ContainerCreating|Pending' # age > ~5 min?
kubectl -n <ns> describe pod <pod> | grep -E '^Node:|Pulling|Failed|Back-off'
kubectl -n <ns> delete pod <pod> # graceful; the Job recreates itIf the replacement lands on the same node and hangs again, keep the workload off that node
with a kubernetes.io/hostname NotIn [...] node affinity (several manifests in
data-workflows already exclude known broken-egress hosts this way).
Never let a script wait hours on a pod that never started. A runner that only waits for
Complete with a long timeout turns a one-minute bad-node fix into a lost afternoon. Watch for
the stuck state explicitly and act on it:
# inside a wait loop: any pod of the job still not running after 5 min -> bad node, reschedule it
kubectl -n <ns> get pods -l job-name=<job> -o json | python3 -c '
import json, sys, datetime as d
now = d.datetime.now(d.timezone.utc)
for p in json.load(sys.stdin)["items"]:
cs = (p["status"].get("containerStatuses") or [{}])[0].get("state", {})
age = (now - d.datetime.fromisoformat(p["metadata"]["creationTimestamp"].replace("Z", "+00:00"))).total_seconds()
if "waiting" in cs and age > 300: print(p["metadata"]["name"])' | xargs -r kubectl -n <ns> delete podThe old ReplicaSet keeps serving until the new pod is Ready, so a crashlooping
new pod during a rollout is usually not yet an outage.
Everyday ops
- Always pass
-n <namespace>. Our apps live inbiodiversity(andschmidtdsefor some SchmidtDSE apps). Nothing is indefault. - Config change → apply + restart:
Pods re-renderkubectl apply -f k8s/configmap.yaml -n <ns> kubectl rollout restart deployment/<name> -n <ns> kubectl rollout status deployment/<name> -n <ns> --timeout=90sconfig.json(envsubst) at start, so a restart is required for a ConfigMap change to take effect. - Deployment name ≠ repo name. Check first:
kubectl get deploy -n <ns>. (e.g. the geo-agent template deploys aspadus; biodiversity asbiodiversity-maplibre.) - Diagnose a bad rollout:
kubectl get pods -n <ns> kubectl describe pod <pod> -n <ns> # State/Reason/Events kubectl logs <pod> -n <ns> -c <initContainer> # git-clone init failures show here
Usage policy
Pods must use what they request. The NRP portal checks running pods every 10 minutes, and a pod is outside the policy when its use falls outside these ranges of its request:
| Resource | Allowed use | Checked only when the pod requests |
| GPU | above 40% | any GPU |
| CPU | 20%–200% | more than 1 core |
| Memory | 20%–150% | more than 2 GiB |
Small pods are exempt. A pod requesting at most 1 core is never checked on CPU, and one requesting at most 2 GiB is never checked on memory. Our web apps and MCP servers are designed around this exemption: they request 1 core or less and 2Gi or less, so they are never checked even though they sit idle most of the time. Do not raise them above either threshold. An idle server just over 2Gi would be checked and out of range.
Never checked: opportunistic (negative-priority) pods, JupyterHub pods, and pods younger than 30 minutes.
The limit is 4 pods per user across all namespaces. A user may have no more than 4 pods outside the ranges, counted over every namespace they use (biodiversity, geo-workflows, schmidtdse, …). Past that, the admission webhook deployment.nrp-nautilus.io denies every Deployment change on the account, including restarts of small exempt apps, with "Your pods resources utilization is too low for account …". Namespaces that keep requesting more than they use risk being banned. When a restart is denied this way, the cause is almost always a batch job in another namespace, not the app being restarted.
Limits may be far above requests. Small requests with large limits are fine and let servers burst. A ratio-rule rejection usually means a malformed request or limit; check with kubectl apply --dry-run=server rather than shrinking limits.
Priority and sizing for batch jobs
- Pods that run under about an hour use
priorityClassName: opportunistic. They are exempt from the usage check, and a preempted pod costs little to redo. This covers setup jobs, daily CronJob runs and short fan-out units. - Long work is split into short units where that is practical, so it can run at opportunistic. Multi-hour pods at opportunistic are preempted repeatedly.
- Pods that must run long use default priority, and their requests are sized to measured use: memory 20–150% of the request, CPU 20–200%. Check them about 30 minutes after launch, before the portal does, and keep the job under the 4-pod limit.
Checking usage
check-usage.py in this skill's directory lists every pod the portal would check and flags those outside the ranges:
python3 <this skill's directory>/check-usage.py # biodiversity geo-workflows schmidtdse
python3 <this skill's directory>/check-usage.py geo-workflows # one namespaceIt reads pod specs with kubectl and use from NRP's Prometheus (https://prometheus.nrp-nautilus.io, no authentication needed), averaged over 30 minutes. It exits 1 when any checked pod is outside the policy. The portal's own measurement may differ in detail, so treat a result near a boundary as a warning. Run it:
- about 30 minutes after launching a job that requests more than 1 core or 2 GiB at default priority;
- before a rollout, if anything large is running;
- whenever the webhook denies a Deployment change.
Full policy: https://nrp.ai/documentation/userdocs/start/policies/ — per-user status: https://nrp.ai/userinfo/
Batch Job Requirements
CPU jobs whose pods run under about an hour use priorityClassName: opportunistic (see Usage policy for longer pods). All jobs need explicit resource requests and limits, and restartPolicy: Never. GPU node avoidance is recommended:
apiVersion: batch/v1
kind: Job
metadata:
name: my-job
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 10800
template:
spec:
priorityClassName: opportunistic
restartPolicy: Never
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: feature.node.kubernetes.io/pci-10de.present
operator: NotIn
values: ["true"]
containers:
- name: worker
image: ghcr.io/boettiger-lab/datasets:latest
command: ["bash", "-c", "echo hello"]
resources:
requests:
cpu: "4"
memory: "8Gi"
limits:
cpu: "4"
memory: "8Gi"Add ephemeral-storage to requests/limits sized to the scratch disk the job needs. In geo-workflows 50Gi is only the LimitRange default, not a cap; larger requests are admitted, and node free disk is the real ceiling.
Secrets
S3 credentials (aws secret):
env:
- name: AWS_ACCESS_KEY_ID
valueFrom: {secretKeyRef: {name: aws, key: AWS_ACCESS_KEY_ID}}
- name: AWS_SECRET_ACCESS_KEY
valueFrom: {secretKeyRef: {name: aws, key: AWS_SECRET_ACCESS_KEY}}Rclone config (rclone-config secret):
volumeMounts:
- name: rclone-config
mountPath: /root/.config/rclone
readOnly: true
volumes:
- name: rclone-config
secret:
secretName: rclone-configIngress
NRP uses HAProxy (not nginx). TLS is cluster-terminated — just list the hostname, no cert secret needed.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
haproxy-ingress.github.io/cors-enable: "true"
haproxy-ingress.github.io/cors-allow-origin: "*"
haproxy-ingress.github.io/cors-allow-methods: "GET, POST, OPTIONS"
haproxy-ingress.github.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,mcp-session-id"
haproxy-ingress.github.io/cors-allow-credentials: "true"
haproxy-ingress.github.io/cors-max-age: "86400"
haproxy-ingress.github.io/timeout-server: "600s"
haproxy-ingress.github.io/timeout-tunnel: "3600s" # required for SSE/WebSocket
spec:
ingressClassName: haproxy
tls:
- hosts:
- my-service.nrp-nautilus.io
rules:
- host: my-service.nrp-nautilus.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80Add mcp-session-id to cors-allow-headers for MCP servers.
Dedicated Node
stratus1.nrp-espm.berkeley.edu is our Berkeley-owned node — use when jobs get preempted on shared nodes. It carries a nautilus.io/issue taint; tolerate it with operator: Exists:
nodeSelector:
kubernetes.io/hostname: stratus1.nrp-espm.berkeley.edu
tolerations:
- key: "nautilus.io/issue"
operator: Exists
effect: NoScheduleTo discover taints on other nodes, check the scheduler error from a pending pod: kubectl -n biodiversity describe pod <pod> — the message lists every untolerated taint verbatim. Use exact key/value pairs (not operator: Exists globally — the admission webhook may reject it).
Deployment Rollouts
Always kubectl apply ConfigMaps before restarting — rollout restart recycles pods using whatever is already in the cluster; git push alone does nothing:
kubectl apply -f k8s/my-configmap.yaml
kubectl -n biodiversity rollout restart deployment/<name>Stuck rollout? If rollout status hangs >2 min, the pod likely landed on a broken node. Check with kubectl -n biodiversity get pods -o wide and describe pod. If it's a node issue (not your image), delete the stuck pod — it reschedules, and the old pod stays live throughout (maxUnavailable: 0):
kubectl -n biodiversity delete pod <stuck-pod>Common Pitfalls
- Default priority with oversized requests — a pod above 1 core or 2 GiB that uses less than 20% of its request counts against the 4-pod limit, and past that NRP blocks every Deployment change on the account. Use opportunistic for short pods, and size long ones to measured use
- Missing resource requests/limits — pod won't schedule
sleepin batch jobs — policy violation, grounds for banrollout restartwithoutkubectl apply— config changes won't take effect- nginx ingress annotations — NRP uses HAProxy; nginx annotations are silently ignored
- Max 200 completions per indexed job — hard cluster limit
Don'ts
- No
--force/--grace-period=0on pods (see above). - Don't
kubectl deletePVCs, StatefulSets, or Namespaces casually — data loss. - Don't request more CPU/memory than you use (see Usage policy); don't schedule CPU work onto GPU nodes.
- Don't edit live objects with
kubectl editfor tracked apps — change the manifest in the repo andkubectl apply, so git stays authoritative.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

