| name | osdc-cli-debugging |
|---|---|
| description | Read-only CLI commands for debugging OSDC clusters: kubectl, aws, helm, tofu. Includes command references, safety boundaries, and the deploy-audit ConfigMaps in osdc-system (what version is running where, who deployed it, when, did it succeed, is a deploy stuck). Applies to ~/meta/ci-infra/osdc. Load when investigating cluster state, debugging pods, inspecting infrastructure, comparing deployed versions across clusters, or diagnosing a failed or stuck deploy. |
You are encouraged to run read-only CLI commands to debug, investigate, and understand cluster and infrastructure state. This is the fastest way to diagnose issues — use it proactively. All tools below are managed by mise via osdc/mise.toml.
Run commands from the osdc/ working directory so mise activates the correct tool versions. If running from elsewhere, prefix with mise exec --:
mise exec -- kubectl get pods -n arc-runnersAll clusters are defined in clusters.yaml in the project root. Run just list for full details. Read the file directly for config values.
The <cluster> argument to every just recipe is the YAML key (e.g. meta-prod-aws-ue1, meta-staging-aws-uw1, arc-cbr-production, arc-cbr-production-uw1), not the cluster_name field — they sometimes differ (e.g. arc-cbr-production resolves to cluster_name: pytorch-arc-cbr-production).
The lf-prod-aws-ue1 / lf-prod-aws-ue2 clusters (state buckets lf-osdc-tfstate-prod-*, admin role lf_osdc_admin) are operated by another org. From this laptop the operator has no AWS credentials, no kubeconfig access, and no tofu state read access for them. Any just kubeconfig lf-prod-*, kubectl against an lf-* context, aws eks describe-cluster --name lf-prod-*, or tofu init against an lf-osdc-* state bucket will fail on authentication.
Accessible from this laptop (admin role osdc_gha_prod / osdc_gha_staging):
meta-staging-aws-uw1(us-west-1)meta-staging-aws-ue1(us-east-1)meta-prod-aws-ue1(us-east-1)arc-cbr-production→pytorch-arc-cbr-production(us-east-2)arc-cbr-production-uw1→pytorch-arc-cbr-production-uw1(us-west-1)
For lf-* investigations: read clusters.yaml, read the module code, read the manifests in base/ and modules/ — but do not attempt live cluster commands. If the user explicitly asks for lf-* live state, tell them you cannot reach the cluster and ask them to run the command and paste output.
Before running any kubectl or helm commands, configure kubeconfig using the just recipe — never run aws eks update-kubeconfig directly.
just kubeconfig <cluster>The just recipe reads clusters.yaml to resolve the cluster name and region automatically and delegates to scripts/kubeconfig-lock.sh (which serializes concurrent kubeconfig writes via a file lock). It aliases the kubectl context to the cluster_name (e.g. pytorch-arc-cbr-production, meta-prod-aws-ue1, meta-staging-aws-uw1) — there is no eks-... prefix. Switch contexts with kubectl config use-context <cluster_name> or scope a single call with kubectl --context <cluster_name> .... Running aws eks update-kubeconfig by hand risks wrong cluster name, wrong region, or wrong kubeconfig context.
This sandbox inherits Meta's x2p proxy (HTTPS_PROXY=http://localhost:10054), which blocks the EKS API endpoints. Every ad-hoc kubectl, aws eks, or helm call against the cluster MUST inline the bypass — export does not persist across separate Bash tool invocations, so prefix each command:
NO_PROXY="${NO_PROXY:-},.eks.amazonaws.com" no_proxy="${no_proxy:-},.eks.amazonaws.com" kubectl get pods -A
NO_PROXY="${NO_PROXY:-},.eks.amazonaws.com" no_proxy="${no_proxy:-},.eks.amazonaws.com" aws eks describe-cluster --name <cluster-name> --region <region>The shipped recipes (just kubeconfig, just deploy-*, scripts/destroy-cluster.sh, etc.) already inject the bypass. The raw kubectl/aws/helm examples in the sections below omit the prefix only for readability — you still must add it when you actually run them.
aws calls that do NOT touch the EKS API endpoint (aws s3 ..., aws ec2 describe-*, aws autoscaling ..., aws ecr ..., aws iam ..., etc.) generally work without the bypass, because Meta's proxy already whitelists the regular AWS service endpoints. Add the bypass when in doubt — it is harmless on non-EKS calls.
| Namespace | What lives there |
|---|---|
arc-systems |
ARC controller + per-runner-set listener pods |
arc-runners |
Runner pods, AutoscalingRunnerSets, zombie-cleanup CronJob |
karpenter |
Karpenter controller |
harbor-system |
Harbor + harbor-cache-recovery CronJob |
buildkit |
BuildKit builder pods |
monitoring |
Alloy metrics + kube-prometheus-stack (default; overridable via monitoring.namespace in clusters.yaml) |
logging |
Alloy logging + Alloy events (default; overridable via logging.namespace in clusters.yaml) |
pypi-cache |
PyPI wheel cache pods |
kube-system |
node-compactor, cache-enforcer, NVIDIA device plugin, registry mirror config, algif mitigation, dirtyfrag mitigation, node-performance-tuning, image-cache-janitor, node-local-dns DaemonSet (see base/kubernetes/ for the full list) |
osdc-system |
Deploy-audit ConfigMaps (labeled app.kubernetes.io/managed-by=osdc-deploy-log) read by just deploy-status and just deploy-history |
kubectl get nodes # List nodes and status
kubectl get pods -A # All pods across namespaces
kubectl get pods -n arc-runners # Runner pods
kubectl get pods -n arc-systems # ARC controller pods
kubectl get pods -n karpenter # Karpenter pods
kubectl get pods -n harbor-system # Harbor pods
kubectl get pdb -n harbor-system # list Harbor PDBs and disruption budgets
kubectl describe pdb harbor-core -n harbor-system # see selector matches and current/desired healthy
kubectl get pods -n buildkit # BuildKit builder pods
kubectl get pods -n monitoring # Alloy metrics + kube-prometheus-stack pods
kubectl get pods -n logging # Alloy log collector pods
kubectl get pods -n pypi-cache # PyPI wheel cache pods
kubectl get ds -n logging # Logging DaemonSet (should match node count)
kubectl get nodepools # Karpenter NodePools
kubectl get nodeclaims # Karpenter NodeClaims (what Karpenter has provisioned)
kubectl get ec2nodeclasses # Karpenter EC2NodeClasses (AMI / IAM / tag config)
kubectl get autoscalingrunnersets -n arc-runners # ARC runner scale sets (one per runner type, named arc-<runner_name>)
kubectl get pods -n arc-systems -l app.kubernetes.io/component=runner-scale-set-listener # ARC listener pods
kubectl describe pod <pod> -n <ns> # Pod details and events
kubectl logs <pod> -n <ns> # Pod logs
kubectl get events -n <ns> --sort-by=.lastTimestamp # Recent events
kubectl top nodes # Node resource usage
kubectl top pods -n <ns> # Pod resource usage
kubectl get pods -A --field-selector=status.phase=Pending # All pending pods (often Karpenter is missing capacity)
kubectl get pods -A --sort-by=.status.containerStatuses[0].restartCount | tail -20 # Top restart counts (crashloop suspects)
kubectl get pods -A -o wide | awk '$4 != "Running" && $4 != "Completed" && NR>1' # Anything not Running/Completedaws eks describe-cluster --name <cluster-name> --region <region>
aws eks list-access-entries --cluster-name <cluster-name> --region <region> # Cluster auth entries (who can get in)
aws eks describe-access-entry --cluster-name <cluster-name> --principal-arn <arn> --region <region>
aws ec2 describe-instances --filters "Name=tag:eks:cluster-name,Values=<cluster-name>" --query 'Reservations[].Instances[].{ID:InstanceId,Type:InstanceType,State:State.Name,AZ:Placement.AvailabilityZone,Launch:LaunchTime}' --output table --region <region>
aws ec2 describe-nat-gateways --filter "Name=tag:Cluster,Values=<cluster-name>" --query 'NatGateways[].{ID:NatGatewayId,State:State,Subnet:SubnetId}' --output table --region <region>
aws ec2 describe-vpc-endpoints --filters "Name=tag:Cluster,Values=<cluster-name>" --query 'VpcEndpoints[].{ID:VpcEndpointId,Service:ServiceName,State:State}' --output table --region <region>
aws ecr describe-repositories --region <region>
aws autoscaling describe-auto-scaling-groups --query 'AutoScalingGroups[].{Name:AutoScalingGroupName,Desired:DesiredCapacity,Min:MinSize,Max:MaxSize}' --output table --region <region>
aws s3 ls s3://<state_bucket>/ --recursive | head # Peek at tofu state object layout for a clusterUse the access-entries commands when troubleshooting "why can't I get into this cluster" — clusters.yaml has an access_config block that controls EKS access entries.
For Karpenter-provisioned nodes the source of truth is the NodeClaim CR (kubectl get nodeclaims), not aws autoscaling — Karpenter nodes are NOT in an ASG. ASGs only back the base node group (provisioned by EKS managed node groups via tofu). aws ec2 describe-instances will show both kinds.
helm list -A # All installed releases
helm list -n arc-runners # Per-namespace (arc-runners has many: arc-<runner_name> per runner type)
helm status <release> -n <ns> # Release status
helm get values <release> -n <ns> # Current values
helm history <release> -n <ns> # Release historyjust list # All clusters with details
just show <cluster> # Dry-run: cluster name, region, bucket, modules, tofu vars
just plan <cluster> # Tofu plan (no apply, no k8s/helm side effects — safe from CI on PRs)
just deploy-status <cluster> [module] # Deployed versions for cluster (reads ConfigMaps in osdc-system, labeled app.kubernetes.io/managed-by=osdc-deploy-log). Optional second arg narrows to one module.
just deploy-history <cluster> # Recent deploy history (same ConfigMap source)just deploy-status / just deploy-history require kubeconfig to be set first (just kubeconfig <cluster>) — they read live ConfigMaps from the cluster.
Every just deploy, just deploy-base, and just deploy-module records itself as ConfigMaps in the osdc-system namespace via scripts/deploy-log.sh. These are the source of truth for what version is running where, who deployed it, when, did it succeed, and is something stuck. The osdc-system namespace exists for nothing else — it is purely the audit log.
Always start here: just deploy-status <cluster> — it parses the ConfigMaps and prints color-coded Current Versions + recent history. Drop to raw kubectl only when you need a field the renderer hides (branch, duration) or want to diff across clusters.
Every recipe writes three ConfigMaps per recorded event, all labelled app.kubernetes.io/managed-by=osdc-deploy-log:
| Name pattern | When written | Status field |
|---|---|---|
osdc-deploy-<scope>-start-<name> |
Immediately when the recipe begins | started |
osdc-deploy-<scope>-finish-<name> |
When the recipe exits (success or via ERR trap on failure) | completed or failed |
osdc-deploy-<scope>-history-<name> |
Both events appended to .data.entries (JSONL, trimmed to last 50 lines) |
n/a |
<scope> is cmd for the top-level just recipe and module for the per-module record. <name> is the recipe or module name. Examples:
just deploy meta-prod-aws-ue1→osdc-deploy-cmd-{start,finish,history}-deployjust deploy-base meta-prod-aws-ue1→osdc-deploy-cmd-{start,finish,history}-deploy-basejust deploy-module meta-prod-aws-ue1 karpenter→ writes two records:osdc-deploy-cmd-{...}-deploy-module-karpenterANDosdc-deploy-module-{...}-karpenter
A full just deploy of an N-module cluster emits one outer deploy record, one deploy-base record, and 2N module records (cmd-scope + module-scope per module) — roughly 100 ConfigMaps for a 16-module cluster.
Common keys on both start and finish:
| Key | Source | Example |
|---|---|---|
commit |
git rev-parse --short HEAD on the deploy machine (or unknown) |
4fe8f56 |
branch |
git branch --show-current (or detached for SHA checkouts; CI is almost always detached) |
main |
user |
$USER on the deploy machine (NOT the GH actor) |
jschmidt |
cluster |
The positional arg passed to the just recipe |
meta-prod-aws-ue1 |
timestamp |
date -u +%Y-%m-%dT%H:%M:%SZ — ISO-8601 UTC, second precision, lexicographically sortable |
2026-06-30T14:23:45Z |
module or command |
The recipe/module name | karpenter / deploy-base |
status |
started (on start CM); completed or failed (on finish CM) |
completed |
Only on finish: duration — wall-clock seconds as a string (e.g. "197").
History entries are \n-separated JSON lines with the same fields plus event: start|finish.
All commands assume just kubeconfig <cluster> has been run and need the EKS proxy bypass — NO_PROXY=...,.eks.amazonaws.com no_proxy=...,.eks.amazonaws.com prefix, omitted below for readability.
What version of module X is on this cluster?
kubectl get cm osdc-deploy-module-finish-<module> -n osdc-system -o yamlRead .data.commit, .data.branch, .data.timestamp, .data.user, .data.status.
Does prod match staging for module X?
just kubeconfig meta-prod-aws-ue1
PROD_COMMIT=$(kubectl get cm osdc-deploy-module-finish-karpenter -n osdc-system -o jsonpath='{.data.commit}')
just kubeconfig meta-staging-aws-uw1
STAGING_COMMIT=$(kubectl get cm osdc-deploy-module-finish-karpenter -n osdc-system -o jsonpath='{.data.commit}')
echo "prod=$PROD_COMMIT staging=$STAGING_COMMIT"
git log --oneline "$PROD_COMMIT..$STAGING_COMMIT" -- modules/karpenter/Who deployed last on this cluster, and when?
just deploy-status <cluster> # color-coded; reads all CMs at onceOr raw, sorted by actual deploy time (NOT creationTimestamp):
kubectl get cm -n osdc-system -l app.kubernetes.io/managed-by=osdc-deploy-log \
-o jsonpath='{range .items[*]}{.data.timestamp}{"\t"}{.metadata.name}{"\t"}{.data.user}{"\t"}{.data.status}{"\n"}{end}' \
| sort | tail -20Why did the last deploy fail?
just deploy-status <cluster> # red "failed" rows identify the failing scope
# Or list every failure on the cluster:
kubectl get cm -n osdc-system -l app.kubernetes.io/managed-by=osdc-deploy-log -o json \
| jq -r '.items[] | select(.data.status=="failed") | "\(.data.timestamp)\t\(.metadata.name)\t\(.data.commit)\t\(.data.user)"'The ConfigMap only records THAT it failed, not WHY — pair this with the actual deploy logs (CI job, terminal scrollback). When deploy-base fails inside just deploy, you typically see TWO failed records (the inner deploy-base and the outer deploy).
Is a deploy in progress right now?
just deploy-status <cluster> | grep -i "in progress"deploy-status flags "in progress" when the start ConfigMap's timestamp is newer than the finish one's. There is no TTL or liveness check — an orchestrator that was SIGKILL'd hours ago shows as in-progress forever until somebody re-runs the same scope/name. Sanity-check the start timestamp: anything >1h old with no recent module activity is almost certainly a dead orchestrator, not a live deploy.
Timeline of every deploy event today, narrowed to one scope:
just deploy-status <cluster> <name> # name = "deploy" | "deploy-base" | "deploy-module-<MOD>" | "<MOD>"The optional <name> filter switches the history table to include BOTH start and finish events (richer timeline) and bumps the row cap to 20. Without it, only finish events are shown, capped at 10.
Full JSONL history for one record:
kubectl get cm osdc-deploy-module-history-karpenter -n osdc-system -o jsonpath='{.data.entries}'.metadata.creationTimestampis NOT the deploy time — it is when the ConfigMap was first created (possibly weeks ago).just deploy-historysorts bycreationTimestampand is therefore mostly an inventory of which records exist, not a timeline. Always use.data.timestampfor actual deploy time.- "In progress" can be stale. No TTL, no heartbeat. A re-run of the same scope/name is the only thing that clears it.
useris$USER, not the GH actor. CI runs as whatever user the runner pod uses — usually meaningless for attribution. Cross-reference with the CI job URL.branch=detachedis normal in CI (SHA checkouts). Not a bug.- Removed modules leak ConfigMaps. Records for a module dropped from
clusters.yamlstay inosdc-systemuntil someonekubectl delete cms them. - Audit writes are non-fatal and silent on failure. If the audit
kubectlcall itself fails (network blip, RBAC), the deploy still proceeds and the audit record simply doesn't exist — only aWarning: deploy-log failed …on stderr. - Direct
./modules/foo/deploy.shinvocations produce zero audit records. Module scripts don't sourcedeploy-log.sh; only thejustrecipes do. - History caps at 50 entries combined (start + finish) → effectively the last ~25 deploys per record. High-churn clusters lose old history fast.
NEVER run terraform — this is OpenTofu-only. Running terraform against the same state directory will corrupt state with no recovery. Use tofu or the just recipes (which call tofu internally) for everything.
Base infra terraform code lives in modules/eks/terraform/ even though state is keyed under ${CLUSTER}/base/terraform.tfstate (base here is the state slot name; the directory base/terraform/ does not exist — the real code is in modules/eks/terraform/).
Each cluster's state lives in its own state_bucket (defined in clusters.yaml, e.g. ciforge-tfstate-meta-staging-aws-uw1, ciforge-tfstate-arc-cbr-prod, ciforge-tfstate-arc-cbr-prod-ue1, ciforge-tfstate-arc-cbr-prod-uw1). Lock table is shared: ciforge-terraform-locks. To inspect state for a specific cluster, first init with the cluster's backend config:
cd modules/eks/terraform
tofu init -reconfigure \
-backend-config="bucket=ciforge-tfstate-meta-staging-aws-uw1" \
-backend-config="key=meta-staging-aws-uw1/base/terraform.tfstate" \
-backend-config="region=us-west-2" \
-backend-config="dynamodb_table=ciforge-terraform-locks"
tofu show # Current state
tofu output # Output values
tofu state list # All managed resources
tofu state show <addr> # Inspect a single resourceFor module state (karpenter, arc, monitoring, etc.) the pattern is the same but the code lives in modules/<module>/terraform/ and the key is <cluster>/<module>/terraform.tfstate. The simplest way to plan-only a whole cluster is just plan <cluster> — it handles init + plan for base and every module in order.
The state bucket and lock table always live in us-west-2 regardless of the cluster's own workload region. This is the hardcoded STATE_REGION constant in scripts/state-config.sh (the single source of truth) and is what every justfile recipe and module deploy.sh uses. mise.toml also exports AWS_REGION=us-west-2 by default for the same reason. Don't substitute the cluster's region here, even for clusters running outside us-west-2.
lf- state buckets* (lf-osdc-tfstate-prod-ue1, lf-osdc-tfstate-prod-ue2) live in another AWS account and are not readable from this laptop — tofu init against them will fail on S3 auth.
This skill is READ-ONLY. Everything below is forbidden without explicit user instruction — and even then, prefer surfacing the appropriate just recipe over running the raw command.
- NEVER run
terraform(any subcommand, includingterraform init/plan) — this project is OpenTofu-only and runningterraformagainst the same directory will corrupt state with no recovery. Always usetofuorjustrecipes. - NEVER
kubectl apply/create/delete/edit/patch/replace/scale/cordon/drain/taint/label/annotate— usejustrecipes for deployments - NEVER
kubectl execinto a pod for anything other than running idempotent read-only diagnostic commands (ls,cat,env,ps,netstat,nslookup). No mutating commands, no shell sessions left running. - NEVER
helm install/upgrade/uninstall/rollback— usejust deploy-*recipes - NEVER
tofu apply/destroy/import/state rm/state mv/taint/untaint/refresh -auto-approve— usejust deploy-*recipes;tofu show/output/state list/state show/planare the only safe verbs - NEVER
awswrite operations (create-, delete-, modify-, update-, put-, terminate-, attach-, detach-, associate-, disassociate-, run-instances, etc.). Onlydescribe-*,list-*,get-*,head-*verbs are safe. - NEVER scale, cordon, drain, or taint nodes directly
- NEVER write to the project's git repo (no
git commit,git push,git merge,git rebase,git tag, branch deletion). Read-only git commands are fine. - NEVER edit kubeconfig by hand — always go through
just kubeconfig <cluster>(which usesscripts/kubeconfig-lock.shto serialize concurrent writes)
If you need to make a change, find or suggest the appropriate just recipe. Don't experiment with the cluster — even on staging.
# Controller logs
kubectl logs -n kube-system deploy/node-compactor -f
# Check taint state on nodes
kubectl get nodes -o custom-columns='NAME:.metadata.name,TAINTS:.spec.taints[*].key'NLD is a per-node DaemonSet (hostNetwork: true, listens on fd00::10 and the kube-dns Service ClusterIP). Pods continue resolving via the unchanged kube-dns ClusterIP — NLD intercepts via NOTRACK ip6tables (IPv6-only EKS) on a dummy nodelocaldns interface.
# DaemonSet health snapshot (DESIRED/CURRENT/READY should match node count)
kubectl get ds node-local-dns -n kube-system
# Per-node pod placement (verify one pod per node, identify any not-Running)
kubectl get pods -n kube-system -l k8s-app=node-local-dns -o wide
# Recent logs across all NLD pods
kubectl logs -n kube-system -l k8s-app=node-local-dns --tail=50
# Verify DNS resolution from a debug pod (both paths)
kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never -- \
nslookup kubernetes.default.svc.cluster.local fd00::10
kubectl run dns-test --rm -it --image=busybox:1.36 --restart=Never -- \
nslookup kubernetes.default.svc.cluster.local # via kube-dns ClusterIPContainer is a minimal image — kubectl exec ... -- env (or any shell/busybox cmd) does NOT work. For runtime env var inspection use /proc/1/environ if accessible; otherwise rely on functional DNS queries as the authoritative test. Setup errors surface via the coredns_nodecache_setup_errors_total metric (note: NOT nodelocaldns_setup_errors_total).
Legacy — inert under IPv6 mode. VPC CNI Custom Networking is unsupported in IPv6-only EKS; the ENIConfig CRs and the ipam.osdc.internal/eni-config node labels are kept inert as a rollback fallback to IPv4 + Custom Networking. Under IPv6 the commands below still execute but the labels carry no behavioral meaning.
# List ENIConfig CRs (one per AZ for base nodes)
kubectl get eniconfigs.crd.k8s.amazonaws.com
# Verify base nodes carry the AZ-matched eni-config label
kubectl get nodes -l role=base-infrastructure \
-o custom-columns='NAME:.metadata.name,ZONE:.metadata.labels.topology\.kubernetes\.io/zone,ENI:.metadata.labels.ipam\.osdc\.internal/eni-config'