Homelab Day 14
Day 14
· 4 min read
See intro for what this is about.
Per previous days, entirely written by Claude.
A day mostly spent cleaning up after the gitops→argocd migration, then adding persistent storage for vmstorage using iSCSI-backed PVCs. Also hit a good lesson about Helm values: a typo at a non-existent key path is silently dropped.
victoria-metrics operator deadlock #
Alertmanager was firing TooManyScrapeErrors for the victoria-metrics-operator
target. The operator pod showed Running / 1/1 Ready but port 8080 (metrics) had
gone dark. The liveness probe was a TCP socket check on port 8081 (the health
endpoint), which was still accepting connections — so kubelet saw the pod as
healthy and never restarted it.
Root cause: on startup, the operator tries to expose PSI (Pressure Stall
Information) cgroup metrics. k3s nodes don’t expose cpu.pressure in the cgroup
hierarchy. The operator logs the failure and continues, but the goroutine involved
doesn’t fully unwind — it deadlocks the metrics HTTP server on port 8080 while
leaving 8081 (a separately written handler) responsive. The pod then sits in this
half-alive state indefinitely until manually restarted.
Immediate fix: kubectl rollout restart deployment/victoria-metrics-operator.
Port 8080 responded within seconds.
Prevention: add an HTTP liveness probe on port 8080 so the next deadlock triggers a kubelet restart within 45 seconds (3 × 15s period).
The probe fix that did nothing #
The liveness probe override was nested under the wrong Helm key:
# wrong — operator: is a real key but livenessProbe under it is silently ignored
victoria-metrics-operator:
operator:
livenessProbe:
httpGet:
path: "/metrics"
port: 8080
Helm silently drops values at unrecognised key paths. ArgoCD reported
Synced / Healthy because the rendered manifest was unchanged — the running pod
still had the original tcpSocket:8081 probe. The operator deadlocked again
several hours later.
The correct key, confirmed by helm show values:
victoria-metrics-operator:
probe:
liveness:
httpGet:
path: "/metrics"
port: 8080
initialDelaySeconds: 5
periodSeconds: 15
failureThreshold: 3
The lesson: Synced/Healthy in ArgoCD means the cluster matches the rendered
chart. It does not mean your override did anything. Verify against the live
object with kubectl get ... -o jsonpath=..., not the sync status.
ArgoCD migration fallout: four separate errors #
The gitops→argocd migration from Day 13 left a mess in ArgoCD’s internal state that produced four different errors.
“not our ref”: ArgoCD fetches commits by SHA (git fetch origin <sha>). This
requires uploadpack.allowAnySHA1InWant = true on the Git server, which the
newly created chris/argocd repo didn’t have. Setting it in Gitea’s app.ini
only affects repos created after the change; existing repos are not
retroactively updated. Fix: sudo git config --system uploadpack.allowAnySHA1InWant true
on the Gitea host (/etc/gitconfig, applies to all repos immediately).
“Host key verification failed”: After the repo-server pod restarted during
debugging, it loaded argocd-ssh-known-hosts-cm fresh — and git.implicit.net
was not in it. Previous connections had been using insecure: "true" in the
repository secret, which bypasses host key checking. Added the three key types
for git.implicit.net (RSA, ECDSA, ed25519) via ssh-keyscan.
“Permission denied (publickey)”: The application status objects contained
references to old repository URLs from before the migration (ansible.git,
gitops.git). ArgoCD’s repo-server was trying to fetch those old SHAs from the
old repos to render history in the UI — and had no credentials for them.
Stale operationState: Even after adding credentials for the old repos, the
underlying issue was that status.history and status.operationState across
six applications contained stale repo URLs and SHAs. The cleanest fix was to
clear them:
for app in cert-manager eloise gitea gitea-runner metallb monitoring-ingress victoria-metrics; do
kubectl patch app -n argocd $app \
--type=json -p='[{"op":"replace","path":"/status/history","value":[]}]'
done
All seven applications went clean after this.
democratic-csi: iSCSI-backed PVCs #
Up to this point, vmstorage was using local-path PVCs, which pin pods to the
node where the data lives. If that node is down, the pod can’t reschedule.
Added democratic-csi, which provisions iSCSI volumes from a TrueNAS-compatible
target. The storage class (zfs-iscsi) uses the org.democratic-csi.iscsi
provisioner and creates ZFS datasets on dan (10.1.1.251) on demand.
Advantages over local-path:
- Volumes are network-attached — pods can reschedule to any node
reclaimPolicy: Retain— PVs survive PVC deletion (safe for databases)allowVolumeExpansion: true— resize is a one-liner
The Helm chart installs the controller and node components. The controller needs credentials to reach the TrueNAS API; the node components handle iSCSI initiator setup on each k3s node.
Migrated vmstorage to use the new zfs-iscsi StorageClass. The three vmstorage
pods each got a 10 GiB PVC backed by a ZFS dataset on dan.