Step 1 complete: Bootstrap layer with ArgoCD, cert-manager, namespaces imported to TF

- ArgoCD migrated to argocd namespace
- Cert-manager issuers/certs created
- 20 namespaces imported with pod-security labels
- S3 backend temporarily offline (MinIO), using local backup
- Pending: Remove metadata drift from helm releases, re-apply
This commit is contained in:
Story Crater Bot
2026-08-18 15:08:00 -07:00
parent 7835ac2932
commit 4e473978b5
34 changed files with 2015 additions and 87 deletions
+5 -5
View File
@@ -82,15 +82,15 @@ minio:
- group: homelab-devops → readwrite
```
**CLI device code flow (talos-cli):**
**CLI device code flow (core CLI):**
```bash
# Get JWT token (no kubeconfig needed)
talos secrets login
core secrets login
# → Opens browser, approve device code
# → Token cached in ~/.talos/token
# → Token cached in ~/.core/token
# Use token to access Vault
talos get cluster/ANTHROPIC_API_KEY --key ANTHROPIC_API_KEY
core get cluster/ANTHROPIC_API_KEY --key ANTHROPIC_API_KEY
# → Vault validates JWT from Authentik
# → Returns secret
```
@@ -161,7 +161,7 @@ k get pods -n iam -l app=authentik
```bash
# CLI tokens have 24h expiry
# Re-authenticate
talos secrets login
core secrets login
```
**Groups not syncing:**
+1 -1
View File
@@ -198,7 +198,7 @@ k get pods -n cicd -l app=forgejo-runner
# Settings → Applications → ci-bot → check scopes (package:write)
# Or re-create token
talos put cluster/iam/agents/ci-bot-token TOKEN="$(openssl rand -hex 32)"
core put cluster/iam/agents/ci-bot-token TOKEN="$(openssl rand -hex 32)"
```
**Argo CD out-of-sync:**
+335
View File
@@ -0,0 +1,335 @@
# Coding Standards — Helm, Helmfile, Kubernetes, Shell
Conventions for contributing to homelab's Helm charts, helmfiles, Kubernetes manifests, and shell scripts. These are repo-local patterns — not Go/Rust/general standards from companion repos (core CLI, kmsvc).
---
## Helm & Helmfile Conventions
### Chart Naming & Layout
- **Local charts:** `k8s/<service>/charts/<component>/` directory structure.
- **Helm chart versions:** Always use semver ranges in helmfile releases (e.g., `~1.0`, `~10`, not floating `latest`).
- Rationale: Predictable upgrades, avoids surprise breaking changes.
### helmfile.yaml.gotmpl Pattern
The helmfile is a **Go template**, not a shell script. Use Go template syntax for environment variable interpolation, not shell syntax.
**Correct:**
```yaml
set:
- name: adminPassword
value: {{ env "GRAFANA_ADMIN_PASSWORD" }}
```
**Incorrect:**
```yaml
set:
- name: adminPassword
value: ${GRAFANA_ADMIN_PASSWORD} # Shell syntax — not expanded by helmfile
```
**Organization:**
- Use logical comment headers to separate sections: `# ── cert-manager ──`.
- Group related releases together.
- Use `needs:` for dependency ordering (release A waits for release B before deploying).
- Example (from helmfile.yaml.gotmpl, lines 439449):
```yaml
- name: loki
namespace: logging
needs:
- storage/minio
- name: promtail
namespace: logging
needs:
- logging/loki
```
- **Namespace declaration:** Specify namespace at the release block level, not helmfile-level default.
```yaml
- name: prometheus
namespace: monitoring
createNamespace: true
```
### Values Files Pattern
- **Never hardcode secrets** in values.yaml or ConfigMap keys.
- Store secrets in Vault via `core put cluster/KEY KEY="value"`.
- Reference at deploy time using `{{ env "VAR" }}` in helmfile.
- **External values files:** Always use `values:` block pointing to `.yaml` files, not inline YAML.
```yaml
- name: minio
values:
- k8s/storage/minio-values.yaml
```
- **Environment-specific overrides:** For complex releases (e.g., SQS), use `environments/` subdirectory with `helmfile.yaml.gotmpl` at the service level.
- Example: `k8s/sqs/environments/homelab.yaml` (lines 129 show namespace, kafkaCluster, redis, managementService blocks).
- Helmfile loads environment-specific values dynamically: `{{ .Values.kafkaCluster.nodePool.replicas }}`.
---
## YAML & ConfigMap/Secret Patterns
### Secret Field Naming
**Rule: Field name in Secret = environment variable name in Vault.**
When storing a secret via `core put cluster/KEY KEY="value"`, the field name and Vault variable name must match. This ensures helmfile's `{{ env "VAR" }}` expansion works correctly.
Example (from CLAUDE.md gotcha "Field name = variable name"):
```bash
# Correct
core put cluster/MINIO_ROOT_PASSWORD MINIO_ROOT_PASSWORD="value"
core put cluster/GRAFANA_ADMIN_PASSWORD GRAFANA_ADMIN_PASSWORD="value"
# Incorrect (won't expand in helmfile)
core put cluster/MINIO_SECRET value="value" # Field name ≠ variable name
```
Reference in helmfile (helmfile.yaml.gotmpl, lines 400401):
```yaml
set:
- name: rootPassword
value: {{ env "MINIO_ROOT_PASSWORD" }}
```
### Never Use `--env` Flags in Manifests
Avoid kubectl flags like `--env KEY=value` in manifests or deployment specs. This exposes secrets in `kubectl describe` output.
**Correct:** Use Secret volumes (k8s/storage/minio-values.yaml, lines 4042):
```yaml
envFrom:
- secretRef:
name: minio-oidc # Reference a Secret, don't expose in manifest
```
**Incorrect:**
```yaml
env:
- name: MINIO_OIDC_SECRET
value: "sensitive-value" # Visible in kubectl describe
```
### Namespace-First Organization
Organize manifests by namespace: `k8s/<namespace>/`
Structure per namespace:
```
k8s/storage/
├── minio-values.yaml
├── minio-bucket-init.sh
└── (local charts if any)
k8s/monitoring/
├── prometheus-values.yaml
├── dashboards/ # ConfigMap files auto-loaded via helmfile postsync hook
├── servicemonitors/ # ServiceMonitor CRD instances
└── alerts/ # PrometheusRule CRD instances
```
---
## Hook Scripts & Integration Workflows
### Pre/Post-Sync Hooks
Helmfile hooks (presync/postsync) drive setup workflows. Use **relative paths from repo root**, never absolute paths.
**Pattern (helmfile.yaml.gotmpl, lines 402416):**
```yaml
- name: minio
hooks:
- events: ["presync"]
command: bash
args:
- -c
- |
bash k8s/base/namespace-setup.sh storage
bash k8s/storage/minio-bucket-init.sh storage loki-chunks loki-ruler
```
**Common presync tasks:**
- Create namespace (via `k8s/base/namespace-setup.sh`).
- Pre-create ConfigMaps/Secrets for the release.
- Initialize infrastructure (buckets, databases, etc.).
**Common postsync tasks:**
- Wait for operator/webhook readiness.
- Apply CRD instances (ServiceMonitor, PrometheusRule, Certificate).
- Perform post-deployment setup (cluster initialization, user creation).
Example (helmfile.yaml.gotmpl, lines 6472):
```yaml
hooks:
- events: ["postsync"]
command: bash
args:
- -c
- |
kubectl rollout status deploy/cert-manager -n cert-manager --timeout=120s
kubectl apply -f - <<'EOF'
# ClusterIssuer and Certificate CRD instances...
EOF
```
### ServiceMonitor Pattern
One file per service in `k8s/monitoring/servicemonitors/svc-<name>.yaml`.
**Key elements:**
- `namespaceSelector`: Match the namespace where the app runs.
- `selector.matchLabels`: Match the app label from the Deployment (e.g., `app.kubernetes.io/name: argocd-metrics`).
- `endpoints.port`: Name of the metrics port in the Service.
- `interval`: Scrape frequency (e.g., `30s`).
Example (`k8s/monitoring/servicemonitors/argocd.yaml`, lines 137):
```yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: argocd
namespace: monitoring
labels:
release: kube-prometheus-stack # Required: links to Prometheus release
spec:
namespaceSelector:
matchNames:
- cicd # App namespace
selector:
matchLabels:
app.kubernetes.io/name: argocd-metrics # Matches Deployment pod label
endpoints:
- port: metrics # Service port name (not port number)
interval: 30s
scrapeTimeout: 10s
```
### PrometheusRule Pattern
One file per service in `k8s/monitoring/alerts/svc-<name>-rules.yaml`.
**Structure:**
- `groups[].name`: Logical grouping (e.g., `minio.rules`).
- `groups[].rules[].alert`: Alert name.
- `expr`: PromQL expression (5-minute windows for rate alerts).
- `for`: Duration threshold (e.g., `10m`).
- `labels.severity`: `critical`, `warning`.
- `annotations`: summary + description (use `{{ $value }}` for metric value).
Example (`k8s/monitoring/alerts/svc-minio-rules.yaml`, lines 147):
```yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: minio-rules
namespace: storage
spec:
groups:
- name: minio.rules
interval: 15s
rules:
- alert: MinIOHighErrorRate
expr: |
(
sum(rate(minio_s3_requests_total{error="true"}[5m]))
/
sum(rate(minio_s3_requests_total[5m]))
) > 0.05
for: 10m
labels:
severity: warning
annotations:
summary: "High error rate on MinIO"
description: "{{ $value | humanizePercentage }}"
```
### Grafana Dashboard Pattern
Dashboards are stored as JSON ConfigMaps in `k8s/monitoring/dashboards/` and auto-loaded via helmfile postsync hook (see helmfile.yaml.gotmpl, lines 471477).
**6-row template (recommended layout):**
1. **Availability:** Uptime, error rate, latency (SLO band).
2. **Resources:** CPU, memory, disk usage, network I/O.
3. **Domain metrics:** Service-specific KPIs (throughput, queue depth, cache hit rate).
4. **Logs:** Recent error logs from Loki.
5. **SLO:** SLI tracking (burn rate, error budget).
6. **Related dashboards:** Links to dependent services.
Auto-load hook (helmfile.yaml.gotmpl, lines 471477):
```yaml
hooks:
- events: ["postsync"]
command: kubectl
args:
- apply
- -f
- k8s/monitoring/dashboards/
```
---
## Integration Checklist
When adding a new service to the homelab, verify the following:
- [ ] **Chart pinning:** Helm chart version pinned (`~1.0` format in helmfile, not floating `latest`).
- [ ] **Secrets management:** All secrets stored in Vault (none in values.yaml, ConfigMap, or CLI flags).
- [ ] **Metrics endpoint:** Service exports `/metrics` endpoint (Prometheus format).
- [ ] **ServiceMonitor:** Created and auto-scraped by Prometheus operator.
- [ ] **PrometheusRule:** Alert rules defined for errors, latency, SLO violations.
- [ ] **Grafana dashboard:** 6-row template auto-loaded via ConfigMap.
- [ ] **Ingress:** Rule added if external access needed (see `k8s/ingress/ingress.yaml`).
- [ ] **OIDC integration:** If UI component, integrated with Authentik (see `core iam bootstrap`).
---
## Shared/Reusable Repos & Image Publication
### When to Publish as Public GHCR
**Rule:** If a service's chart + image source lives in a separate repo (not in homelab), it **must** be published as a **public GitHub repo** under the `Riotpiaole` org.
**Rationale:**
- Local in-cluster charts are fine for infra-owned services.
- Shared/reusable service charts should be version-pinned and publicly available for:
- Reuse across different clusters (other labs, staging, prod).
- Consumption by Argo CD apps (CI/CD pipeline).
- Independent evolution without tight coupling to homelab repo.
### Workflow
**Develop locally:**
1. Create companion repo (e.g., `kafaka-management-service`, `queue-operator`) in a private or public repo.
2. Include Dockerfile and Helm chart.
**Build & publish:**
1. Set up CI/CD in the companion repo (GitHub Actions).
2. Build image and push to GHCR: `ghcr.io/Riotpiaole/<repo>:<tag>`.
3. Tag release and publish chart (npm registry, GitHub releases, or OCI registry).
**Reference in homelab:**
1. Pin image tag + chart version in helmfile.yaml.gotmpl.
2. Chart `repositories` block references the public Helm repo (or OCI registry).
**Example (commit cd1c569 — SQS charts):**
```yaml
- name: management-service
namespace: sqs
chart: k8s/sqs/charts/management-service
values:
- image:
repository: ghcr.io/Riotpiaole/kafaka-management-service
tag: v1.0.0 # Pinned tag from public build
```
---
## Cross-References
- **CLAUDE.md gotchas:** Field name = variable name, MinIO `--env` flag exposure, helmfile template syntax (`{{ env "VAR" }}` not `${VAR}`), CNPG password templating.
- **README.md:** kubectl context setup, port-forward aliases, cluster topology.
- **USAGE.md:** IAM bootstrap, secret management, deployment recipes.
+342
View File
@@ -0,0 +1,342 @@
# Core CLI Tools: Decision Guide
The `core` CLI is your primary tool for cluster auth, node operations, Vault secrets management, IAM administration, and object storage. This doc covers **when** to reach for `core` vs `kubectl`/`helmfile`, the two separate auth domains that power different commands, and the complete command inventory.
For exhaustive flag-level detail on each subcommand, see [~/workplace/core/USAGE.md](../../core/USAGE.md).
---
## The Two Auth Domains
The `core` CLI maintains **two independent authentication systems** that gate different command families. Confusing them causes authentication failures.
### Domain 1: Node/Talos Operations (`core auth`)
**Gates:** Node-level commands, Talos service management, cluster status.
**Login mechanism:** `core auth login-oob`
- Out-of-band device-code flow via Authentik
- Token cached in `~/.core/token` (24h expiry)
- Required before: `core nodes`, `core status <ip>`, `core services <ip>`, `core logs <ip>`, `core log-svc <ip> <svc>`, `core dmesg <ip>`, `core config`, `core upgrade`, `core shutdown`, `core reboot`, `core node`, `core pods clean`
**Check auth state:**
```bash
core auth status # Show token expiry
core auth clear # Force re-auth on next command
```
**When to use:**
- Troubleshooting node-level issues (crashes, disk space, network on a specific Talos node)
- Checking Talos service health (etcd, kubelet, scheduler, etc.)
- Upgrading cluster OS or managing node lifecycle
- Reading kernel logs (dmesg) or kubelet logs on specific nodes
### Domain 2: Vault Secrets (`core secrets`)
**Gates:** Secrets management (read/write), secret listing, secret export.
**Login mechanism:** `core secrets login`
- Interactive login to Vault (opens browser, approves auth)
- Reads/writes to `~/.config/talos/secrets.toml`
- Required before: `core get <path> [--key KEY]`, `core put <path> key=value`, `core secrets list`, `core secrets export`, `core secrets exec`
**Check auth state:**
```bash
core secrets status # Show Vault token / scopes
core secrets clear # Force re-auth on next command
```
**When to use:**
- Reading or writing cluster secrets (API keys, database passwords, OAuth client secrets)
- Managing application credentials in Vault (centralized secret store)
- Rotating secrets for services (e.g., OAuth2 client secrets)
- Listing all secrets under a path
- Bootstrapping `.env` files for helmfile deployment (via `vsource`)
---
## Auth Bug Fixes
**Bug #1 (Fixed):** `core status <NODE>` is a **node-ops command**, not an auth verifier. It does NOT verify that you're authenticated to Vault. To verify auth state, use the correct domain-specific command:
- For node access: `core auth status`
- For Vault access: `core secrets status`
**Bug #2 (Fixed):** `core secrets login` and `core auth login-oob` are **NOT interchangeable**. They gate completely different systems:
- `core auth login-oob` gates node/cluster operations
- `core secrets login` gates Vault secret read/write
- You may be authenticated to one domain and not the other. Check state separately.
---
## When to Reach for `core`
| Scenario | Tool | Why |
|----------|------|-----|
| Pod/deployment issue | `kubectl` | Pods, replicas, rollouts, events |
| Package release management | `helmfile` | Install/upgrade Helm charts |
| Node crashes, disk, kernel panic | `core auth` + node commands | Direct access to Talos node state |
| Service unreachable on Talos node | `core status <ip>`, `core services <ip>` | Talos service health |
| Database password rotation | `core secrets` + `core put` | Vault secret write |
| Need API key for app deployment | `core secrets` + `core get` | Fetch from Vault, inject via `vsource` |
| Create OAuth2 app in Authentik | `core iam create-app` | Manage federated identity apps |
| Add user to service (MinIO, Grafana, etc.) | `core iam add-member` | Group membership binding |
| S3 object upload/download | `core bucket` | MinIO operations |
---
## Command Inventory
Complete list of `core` subcommands, organized by domain. See [~/workplace/core/USAGE.md](../../core/USAGE.md) for usage flags and examples.
### Authentication
```bash
# Node/Talos operations auth
core auth login-oob # Authenticate with Authentik (device code flow)
core auth status # Check token expiry / auth state
core auth clear # Clear cached auth token
# Vault secrets auth
core secrets login # Authenticate to Vault (interactive)
core secrets status # Check Vault token / scopes / expiry
core secrets clear # Clear Vault auth token
```
### Cluster & Node Operations
```bash
# Node discovery and status
core nodes # List all cluster nodes (IP, hostname, status)
core status <ip> # Talos node overview (resources, uptime)
core services <ip> # List Talos services (etcd, kubelet, etc.)
# Logs
core logs <ip> # Stream kubelet logs on node
core log-svc <ip> <svc> # Logs for specific Talos service (etcd, scheduler, etc.)
core dmesg <ip> # Kernel logs (ring buffer) from node
# Node lifecycle
core upgrade <ip> # Upgrade Talos OS on node
core shutdown <ip> # Graceful shutdown
core reboot <ip> # Reboot node
# Kubernetes context
core config kube-list # List available kubeconfig contexts
core config kube-use <ctx> # Switch to kubeconfig context (LAN vs WireGuard)
# Pod cleanup
core pods clean # Delete Failed/Evicted/Terminating pods
core node <node-name> # Get node details (includes pod stats)
```
### Secrets Management (Vault)
```bash
# Read secrets
core get <path> # Fetch all fields under path
core get <path> --key KEY_NAME # Fetch specific field
# Write secrets
core put <path> key=value [key2=value2 ...] # Store secrets in Vault
# List and export
core secrets list # List all secret paths
core secrets export <path> # Export secrets as shell-sourceable format
# Execute with secrets in environment
core secrets exec -- <command> # Run command with secrets loaded in env
```
**Convention:** Field name = variable name (SCREAMING_SNAKE_CASE). Never use `value=`.
Example:
```bash
# Write
core put cluster/ANTHROPIC_API_KEY ANTHROPIC_API_KEY="sk-ant-..."
# Read
core get cluster/ANTHROPIC_API_KEY
```
### IAM Management (Authentik)
```bash
# Groups
core iam list-groups # List all groups
core iam create-group <name> # Create new group
# OAuth2 Applications
core iam list-apps # List all OAuth2 apps
core iam create-app <name> --slug <slug> --redirect-uri <uri> # Create app
core iam describe-app <app> # Show client ID, secret, URIs, scope claims
core iam rotate-secret <app> # Rotate OAuth2 client secret
# User management
core iam add-member <group> <username> # Add user to group
core iam bind-app <app> <group> # Grant group access to application
# Cleanup
core iam delete-app <app> # Delete OAuth2 application
```
### Object Storage (MinIO)
```bash
# List and manage buckets
core bucket list # List all buckets
core bucket upload <bucket> <file> [<remote-path>] # Upload file to S3
core bucket download <bucket> <remote-path> <file> # Download file from S3
core bucket delete <bucket> <remote-path> # Delete object from S3
```
### Utilities
```bash
core help # Show command help
core pf grafana # Port-forward to Grafana (localhost:3000)
core pf prometheus # Port-forward to Prometheus (localhost:9090)
core pf minio # Port-forward to MinIO console (localhost:9001)
core pf iam # Port-forward to Authentik (localhost:7000)
```
---
## Access Control Tiers
The cluster uses a tiered access model based on authentication mechanism and network perimeter.
### Tier A: OIDC + RBAC (Authentik-enforced)
Services with federated OIDC login and group-based role assignment.
| Service | Auth Method | Group Claim | Role Binding |
|---------|-------------|------------|--------------|
| **Grafana** | Authentik OIDC | `groups` | Mapped to Admin / Viewer / Viewer |
| **Argo CD** | Authentik OIDC | `groups` | RBAC role binding (policy.csv) |
| **MinIO** | Authentik OIDC | `groups` / custom `policy` | Policy-based access (readwrite / readonly) |
| **Forgejo** | Authentik OIDC | `email` (OAuth login only) | No group enforcement (open git repo) |
| **kmsvc** | Vault JWT | `sub` / `aud` | Audience validation + service scope |
**Provisioning:**
```bash
# After core auth login-oob, bootstrap IAM apps:
bash k8s/talos-iam/bootstrap-iam.sh
```
### Tier B: Network-Perimeter Only (No Authentik Enforcement)
Services with no OIDC support (product limitation). Access is restricted to LAN/WireGuard perimeter only.
| Service | Network Access | Use Case |
|---------|----------------|----------|
| **Portainer** | LAN + WireGuard only | Container UI, workload browsing |
| **Longhorn** | LAN + WireGuard only | Storage volume management |
| **Temporal** | LAN + WireGuard only | Workflow execution (auth TBD) |
All three are reachable **only** via WireGuard/LAN-only Ingress rules. Zero remote access risk, but also zero federated identity. If remote Temporal access is needed, upgrade the chart's auth configuration or replace with an OIDC-compatible workflow platform.
**Accessing Tier B services:**
```bash
# From off-LAN, use WireGuard context
core config kube-use admin@homelab-cluster-1
core pf minio # connects via 10.6.0.1:9001
```
---
## Workflow: Rotate an OAuth2 Application Secret
```bash
# 1. Rotate secret in Authentik
SECRET=$(core iam rotate-secret grafana | jq -r '.client_secret')
# 2. Update Helm values
vi k8s/logging/grafana-values.yaml
# Set: GRAFANA_OIDC_CLIENT_SECRET="$SECRET"
# 3. Redeploy the app
helmfile apply -l app=grafana
# 4. Verify new secret is in use
core iam describe-app grafana | grep client_secret
```
---
## Workflow: Add User to Service
```bash
# 1. Verify group exists (or create it)
core iam list-groups | grep minio-admins
# If not found:
core iam create-group minio-admins
# 2. Add user to group
core iam add-member minio-admins alice
# 3. Bind group to MinIO application
core iam bind-app minio minio-admins
# 4. User has access on next login
# (OIDC login to MinIO → Authentik → group check → MinIO policy applied)
```
---
## Workflow: Rotate a Database Password (Vault)
```bash
# 1. Generate new password
NEW_PASS=$(openssl rand -hex 32)
# 2. Store in Vault
core put cluster/POSTGRES_ADMIN_PASSWORD POSTGRES_ADMIN_PASSWORD="$NEW_PASS"
# 3. Update database user
kubectl exec -n ddb pod/ddb-cluster-0 -- psql -U postgres -c \
"ALTER USER postgres WITH PASSWORD '$NEW_PASS';"
# 4. Update Helm values with new password reference
# (Or if using helmfile hook: helmfile will re-run postInitApplicationSQL with new password)
# 5. Restart pods to pick up new secret
kubectl rollout restart -n <ns> deployment/<app>
```
---
## Common Questions
**Q: I'm getting "not authenticated" on `core nodes`. What do I do?**
A: Run `core auth login-oob`. Node commands use a different auth domain than secrets. After login, `core nodes` should work.
**Q: I have a valid Vault token but `core get` fails. Why?**
A: Check that both auth domains are active:
```bash
core auth status # Verify node auth is valid
core secrets status # Verify Vault auth is valid
```
Both must succeed. If one is expired, re-auth that domain.
**Q: Should I use `core bucket` or S3 CLI tools (aws-cli, s3cmd)?**
A: Use `core bucket` for simplicity (no AWS credentials). Use s3cmd/aws-cli if you need advanced sync or bandwidth control. All three talk to the same MinIO backend.
**Q: Can I add a user without creating a group first?**
A: Groups are the unit of access control. Always create the group, then add users to it, then bind it to applications. Single-user bindings are not supported (by design).
**Q: I rotated an OAuth2 secret but the app still fails to authenticate. What's next?**
A:
1. Verify the new secret is stored: `core iam describe-app grafana`
2. Check the deployment has the new secret: `kubectl get secret -n logging grafana-oidc -o yaml | grep client_secret`
3. Restart the pod: `kubectl rollout restart -n logging deployment/grafana`
4. Check logs: `kubectl logs -n logging deployment/grafana | grep -i oauth`
---
## See Also
- [~/workplace/core/USAGE.md](../../core/USAGE.md) — Exhaustive command reference with flags and examples
- [CLAUDE.md](../CLAUDE.md) § Sign In (Device Code Flow) — Quick reference for `core auth login-oob`
- [CLAUDE.md](../CLAUDE.md) § Quick Shortcuts — One-liners for common tasks
- [CLAUDE.md](../CLAUDE.md) § Integration Checklist — New service onboarding
+1 -1
View File
@@ -142,7 +142,7 @@ kubectl exec -n ddb pod/ddb-cluster-2 -- \
**All user passwords stored in Vault:**
```bash
# Read password
talos get cluster/STORY_CRATER_PG_PASSWORD --key STORY_CRATER_PG_PASSWORD
core get cluster/STORY_CRATER_PG_PASSWORD --key STORY_CRATER_PG_PASSWORD
# Inject into pod (auto via Secret volume)
# Mount: /run/secrets/db-password
+198
View File
@@ -0,0 +1,198 @@
# Infrastructure Practice Playbook
Standardized procedures for troubleshooting, developing, deploying, and operating the homelab platform. Each procedure explicitly calls out where the `core` CLI fits vs `kubectl`/`helmfile`/direct cluster access. See `core-cli-tools.md` for the auth/secrets domain split; see `infra-troubleshooting.md` for quick patterns and gotchas.
## Procedure A: Troubleshoot a Service or Cluster Issue
1. **Identify the domain.**
- Vault/secrets: `core get`/`core put` failing, Vault unreachable.
- Node/Talos: node crashes, disk full, kubelet unreachable, network issues.
- Plain Kubernetes: pod CrashLoop, service 503, deployment stuck.
2. **If Vault-adjacent (secrets, authentication failing):**
- Run `core secrets status` first (NOT `core auth status` — common mistake).
- If `Vault UNREACHABLE`, check DNS/networking:
- No wildcard DNS exists; verify manual `/etc/hosts` entries (10.6.0.1 for WireGuard, 192.168.1.160 for LAN).
- Ping the Vault service: `kubectl get svc -n vault | grep vault`.
- If `Vault token not cached`, run `core secrets login`, approve device code in browser.
- Verify: `core secrets status` shows `✓ Authenticated`.
3. **If node/Talos-adjacent (kubelet logs, node state, services failing):**
- Run `core auth status` first.
- If token expired, run `core auth login-oob`, approve device code in browser.
- Then run `core nodes` to list cluster nodes.
- For a specific node, run `core status <ip>` (Talos state).
- Inspect Talos services: `core services <ip>` (kubelet, etcd, controller, etc.).
- Check service logs: `core logs <ip>` (main Talos logs) or `core log-svc <ip> kubelet` (specific service).
- Consult `infra-troubleshooting.md` for Pod stuck in CrashLoopBackOff and kubelet restart patterns.
4. **If plain Kubernetes (pod/deployment/service issues):**
- Consult root `TROUBLESHOOTING.md` for the layer-before-tool SRE methodology (procedures 110).
- Use `infra-troubleshooting.md` § Quick Patterns for common diagnoses:
- CrashLoopBackOff: `kubectl logs -n <ns> <pod> --tail=50` + `kubectl describe pod -n <ns> <pod> | grep -A 10 Events`.
- Service 503: `kubectl get endpoints -n <ns> <svc>` (endpoints missing?) + `kubectl get pods -n <ns> -o wide` (pods not Ready?).
- Helm release stuck: `helmfile status | grep -E "FAILED|UNKNOWN|PENDING"` + `helm status <release> -n <ns> --show-resources`.
- If still unclear, escalate to `kubectl get all -n <ns>` and review resource events.
5. **For dashboards and live metrics:**
- Use `core pf grafana` (port-forward to localhost:3000) rather than raw `kubectl port-forward` — keeps forwarded ports consistent.
- If Prometheus unavailable, check: `kubectl get pods -n monitoring | grep prometheus`.
- If ServiceMonitor not scraping, verify: `kubectl get servicemonitor -A | grep <name>` and inspect `.spec.selector` matches the target pod's app label.
---
## Procedure B: Launch/Develop a New Service or POC
1. **Plan the service.**
- Determine namespace (e.g., `sqs`, `temporal`, `databases`, `monitoring`).
- Decide if metrics exported (most should) and if OIDC-gated.
- Sketch a Helm values.yaml structure (secrets, replicas, resource requests, affinity).
2. **Authenticate to Vault.**
- Run `core secrets login` and approve device code in browser.
- Verify: `core secrets status` shows `✓ Authenticated`.
- You'll need Vault access to store service secrets in step 5.
3. **Create service Helm chart directory.**
- Create `k8s/<service>/` with at minimum:
- `values.yaml` (Helm values for deployment, service, replicas, resource limits).
- `charts/` subdirectory for any custom local Helm charts (optional).
- Follow naming conventions from `coding-standards.md`.
4. **Add Helm release to helmfile.**
- Open `helmfile.yaml.gotmpl`.
- Add release block under `releases:` section, following this structure:
```yaml
- name: <service>
namespace: <namespace>
chart: <chart-repo>/<chart-name>
version: ~1.0 # pin major.minor, allow patch updates
needs:
- <dependency-namespace>/<dependency-release> # if applicable
values:
- k8s/<service>/values.yaml
- secretsInline:
DB_PASSWORD: "{{ env \"<SERVICE>_DB_PASSWORD\" }}"
```
- Consult `coding-standards.md` for `needs:` ordering (example: sqs section shows strimzi-operator → kafka-cluster → queue-crd → management-service).
- Reference real example: root helmfile's `sqs` section.
5. **If the service needs secrets (DB password, API key, OAuth secret):**
- Generate value (e.g., `openssl rand -hex 32` for passwords).
- Store in Vault: `core put cluster/<SERVICE>_<KEY> <SERVICE>_<KEY>="value"`.
- **Critical gotcha:** field name MUST equal variable name (e.g., `FORGEJO_ADMIN_PASSWORD=` not `value=`) per `coding-standards.md` § Vault field=variable convention.
- Reference in values.yaml via `{{ env "VARIABLE_NAME" }}` (Helmfile Go template syntax, NOT shell `${VAR}`).
- Do NOT hardcode secrets in values.yaml or ConfigMaps.
6. **Verify Helm syntax before deploy.**
- Run `helmfile lint` (catches template errors, duplicate releases).
- Run `helmfile diff -l name=<service>` (show what will be deployed).
- Review diff for correctness (verify env var substitutions, resource limits, affinity rules).
7. **Deploy the service.**
- Run `helmfile apply -l name=<service>`.
- Monitor: `kubectl get pods -n <namespace> -w` (watch until Running).
- If pods stuck: `kubectl describe pod -n <namespace> <pod-name>` (check Events for SchedulingFailed, ImagePullBackOff, etc.).
8. **If the service exports `/metrics` (Prometheus format):**
- Create ServiceMonitor: `k8s/monitoring/servicemonitors/svc-<name>.yaml`.
- `.spec.selector.matchLabels` must match the service's pod labels (usually `app: <service>`).
- `.spec.endpoints[0].port` must match the service port name or number exporting metrics.
- Create PrometheusRule: `k8s/monitoring/alerts/svc-<name>-rules.yaml`.
- Include error rate, latency, and SLO alert rules.
- Use `prometheus` as the rule group.
- Create Grafana dashboard: `k8s/monitoring/dashboards/svc-<name>.yaml`.
- Use 6-row template: Availability, Resources, Domain metrics, Logs, SLO, Related.
- See README.md § Example Applications for a full walkthrough.
- Verify scrape: `kubectl get servicemonitor -A | grep <name>` and check Prometheus Targets UI for green status.
9. **If OIDC/IAM-gated (admin UI, restricted API):**
- Create app in Authentik: `core iam create-app "my-service" --slug my-service --redirect-uri "https://my-service.riotpiao.homelab.com/callback"`.
- Bind app to group: `core iam bind-app my-service <group>` (e.g., `grafana-admins` for admin-only UI).
- Retrieve credentials: `core iam describe-app my-service` (client ID, client secret).
- Deploy secret: `kubectl create secret generic <service>-oidc --from-literal=client-id=<ID> --from-literal=client-secret=<SECRET> -n <namespace>`.
- Reference secret in values.yaml: mount via `.spec.template.spec.containers[].env` or volumeMounts.
- See `core-cli-tools.md` § Access Control Tiers for Tier A (OIDC + RBAC) vs Tier B (network perimeter only).
10. **Verify service is live.**
- Pods: `kubectl get pods -n <namespace> -o wide` (all Running, 1/1 Ready).
- Metrics (if applicable): `kubectl get servicemonitor -A | grep <name>` and visit Prometheus Targets or Grafana dashboard.
- Endpoint: If publicly routed via Ingress, verify `/etc/hosts` entry (10.6.0.1 for WireGuard, 192.168.1.160 for LAN) and `curl https://my-service.riotpiao.homelab.com/health` (or equivalent health endpoint).
- Logs: `kubectl logs -n <namespace> <pod>` (no errors).
### Definition of Done (Per Service)
- [ ] Helm chart version pinned (~1.0 format in helmfile)
- [ ] All secrets in Vault (none in values.yaml or ConfigMap)
- [ ] `/metrics` endpoint exported (if applicable)
- [ ] ServiceMonitor resource created (if metrics exported)
- [ ] PrometheusRule with error/latency/SLO alerts (if metrics exported)
- [ ] Grafana dashboard (if metrics exported; 6-row template: Availability, Resources, Domain, Logs, SLO, Related)
- [ ] Ingress rule (if external access needed)
- [ ] OIDC integration via `core iam` (if UI component)
- [ ] Verified: `helmfile diff` clean, pods Running, dashboard live or `/metrics` returning 200
---
## Procedure C: Operate the Cluster (Node Health, Context, Cleanup)
1. **Daily health check.**
- Check auth: `core auth status` (if OK, node ops will work).
- List nodes: `core nodes`.
- For each node, check Talos state: `core status <ip>`.
- Check K8s nodes: `kubectl get nodes -o wide` (all Ready, no NotReady).
- Check pod pressure: `kubectl get nodes -o json | jq '.items[] | {name: .metadata.name, memory: .status.allocatable.memory, pods: .status.allocatable.pods}'`.
2. **Troubleshoot a specific node.**
- Get node IP: `core nodes` and note the IP.
- Check Talos services: `core services <ip>` (kubelet, etcd, controller should be running).
- Check service logs: `core logs <ip>` (main Talos daemon logs).
- Filter to specific service: `core log-svc <ip> kubelet` (kubelet logs only).
- Restart a service if needed: `core restart <ip> kubelet` (graceful kubelet restart).
3. **Pod cleanup (Failed, Evicted, Terminating pods).**
- Run `core pods clean` (scans all namespaces, removes stale pods).
- Verify: `kubectl get pods -A | grep -E "Failed|Evicted"` (should be empty).
4. **Switch kubectl context (when off-LAN, on WireGuard).**
- List available contexts: `core config kube-list`.
- Switch to WireGuard path (10.6.0.1:6443): `core config kube-use admin@homelab-cluster-1`.
- **Known limitation:** `core config use <talos-context>` doesn't map to WireGuard; use `kube-use` directly.
- Verify: `kubectl cluster-info` shows 10.6.0.1 (not 192.168.1.213).
5. **MinIO bucket operations (if managing data/backups).**
- List buckets: `core bucket list`.
- Upload file: `core bucket upload <bucket> <local-file>`.
- Download file: `core bucket download <bucket> <remote-file> -o <local-file>`.
- Delete file: `core bucket delete <bucket> <remote-file>`.
6. **Bootstrap or hardware runbooks (infrequent).**
- **Fresh cluster setup:** See README.md § Bootstrap Order (14 steps).
- **Adding a new Talos node:** See README.md § Adding Hardware.
- Do not re-explain those long procedures here; consult README.md directly.
---
## Notes
**Queue subsystem (Kafka/kmsvc/Temporal namespace auto-registration):** Already deployed and stable. If re-deploying:
- Primary deploy method: `helmfile apply -l namespace=sqs` (live from root helmfile).
- Alternate isolated iterate path: `k8s/sqs/helmfile.yaml.gotmpl` (not recommended for production).
- Planned future: GitOps via `k8s/sqs/argocd/` (companion repo, not yet active).
- **Critical rule:** Temporal namespace registration is automatic via `queue-operator`; never manually `temporal operator namespace create` for any namespace referenced by a Queue's `temporal.io/namespace` label. See `~/workplace/kmsvc-manage/CLAUDE.md` ("Temporal Namespace Registration") for the full rule and why.
**Shared/reusable service repositories:** If a service's Helm chart and container image live in a separate repository, they must be:
- Published as a public GitHub repository under the `Riotpiaole` organization.
- Images pushed to GHCR (`ghcr.io/riotpiaole/...`) for public pullability.
- Consult `coding-standards.md` § Shared/Reusable Repos for the full publishing rule.
---
## Cross-References
- **core-cli-tools.md:** Auth/secrets domain split, command inventory, when to use `core` vs `kubectl`.
- **coding-standards.md:** Helm naming conventions, `needs:` ordering rules, helmfile template syntax (`{{ env "VAR" }}` not `${VAR}`), Vault field=variable convention, shared-repo publishing rule.
- **infra-troubleshooting.md:** Quick patterns (CrashLoopBackOff, 503, helm stuck), gotchas, hard rules.
- **USAGE.md:** Exhaustive `core` command reference.
- **README.md:** Bootstrap order, hardware addition, example app walkthrough, 6-row Grafana dashboard template.
- **root TROUBLESHOOTING.md:** Generic Kubernetes SRE layer-before-tool methodology (10 diagnostic procedures).
+66
View File
@@ -0,0 +1,66 @@
# Troubleshooting: Homelab Cluster Operations
Troubleshooting procedures and operational gotchas for homelab cluster. Cross-reference root `TROUBLESHOOTING.md` for full SRE diagnostic methodology (layer-before-tool, control-plane/kubelet/networking); this doc is the quick cheatsheet + gotchas specific to this repo.
---
## Quick Patterns
### Pod stuck in CrashLoopBackOff
```bash
k logs -n <ns> <pod> --tail=50
k describe pod -n <ns> <pod> | grep -A 10 Events
```
### Service unreachable (503)
```bash
# Check endpoints exist
k get endpoints -n <ns> <svc>
k get pods -n <ns> -o wide
# Test connectivity
k exec -it <pod> -- curl http://<svc>.<ns>.svc.cluster.local:8080
```
### Helm release stuck
```bash
helmfile status | grep -E "FAILED|UNKNOWN|PENDING"
helm status <release> -n <ns> --show-resources
k logs -n <ns> deploy/<app> | head -100
```
---
## Hard Rules (Never Violate)
🔴 **NEVER delete a PVC unless there are replicas or backups.** A PVC deletion = permanent data loss. Verify replication status first.
```bash
# Before ANY PVC delete:
k get pvc -n <ns> <pvc>
k get pv <pvc-backing-pv> -o json | jq '.spec' # check replication config
# For Longhorn: verify replicas >= 2
k get longhorn-volume -n longhorn-system <vol> -o json | jq '.status.replicaStatus'
# For PostgreSQL: verify standby replicas are healthy
k exec -n ddb pod/ddb-cluster-0 -- psql -U postgres -c "SELECT * FROM pg_stat_replication;"
```
---
## Project Gotchas
- **Field name = variable name:** In Vault, use `core put cluster/KEY KEY="value"` (never `value=`)
- **vsource expansion:** `.env` values must be empty (`KEY=`) to fetch from Vault; hardcoded values pass through
- **Helmfile template syntax:** Use `{{ env "VAR" }}` not `${VAR}` (shell syntax, not Go template)
- **CNPG initialization:** Wait for cluster to be ready before creating databases (use `postInitApplicationSQL`, not helmfile hooks)
- **MinIO credentials:** Use Secret volumes, never `--env` flag (exposes in `kubectl describe`)
- **Temporal namespace registration:** never manually `temporal operator namespace create` for a namespace a Queue's `temporal.io/namespace` label references — `queue-operator` registers it automatically. See `~/workplace/kmsvc-manage/CLAUDE.md` ("Temporal Namespace Registration") for the full rule and why.
- **kubectl hangs / connection refused:** you're probably off-LAN. `kubectl config use-context admin@homelab-cluster-1` (WireGuard path, `10.6.0.1:6443`) — the default context (`admin@homelab-cluster`, `192.168.1.213:6443`) only works on-LAN. See README.md "kubectl Context".
- **No wildcard DNS:** `*.riotpiao.homelab.com` isn't a real DNS zone — every hostname is a manual `/etc/hosts` line (`10.6.0.1` for WireGuard, the Cilium LB-IPAM IP for LAN). Adding an Ingress `host:` rule doesn't make it resolvable; you must also add the `/etc/hosts` line, on every client machine that needs it.
- **gRPC through nginx ingress:** add `nginx.ingress.kubernetes.io/backend-protocol: "GRPC"` to proxy h2c to a plaintext-gRPC backend (see `temporal-grpc` Ingress in `k8s/ingress/ingress.yaml`). TLS still terminates at nginx via the wildcard cert — clients need `--tls`, not client certs.
- **Soft podAntiAffinity doesn't rebalance existing pods.** `preferredDuringSchedulingIgnoredDuringExecution` only applies at scheduling time — pods that landed on the same node before the constraint existed (or before other nodes were `Ready`) stay there forever. Fixing the affinity config in values.yaml/CRD only affects *future* pod creation; existing skew needs `kubectl delete pod <name>` (one at a time, verify healthy before the next) to force a reschedule under the now-correct constraint. Hit this with Cassandra, the Kafka `KafkaNodePool`, and CNPG's `ddb-cluster` all stacking onto `talos-cp-1`.
- **Cassandra/StatefulSet pod deleted+recreated with no PVC (ephemeral storage) can crash-loop on rejoin:** `Other bootstrapping/leaving/moving nodes detected` or `A node required to move the data consistently is down (/<old-ip>)` — the ring still holds a stale gossip entry for the deleted pod's old IP. Fix: `kubectl exec <a live cassandra pod> -- nodetool assassinate <stale-ip>` from a healthy node, then let the crash-looping pod's next restart retry.
- **`helm upgrade` failing with `conflict ... using v1: .data.<field>` after a manual `kubectl apply` patch:** you (or an agent) hand-patched a resource Helm manages, and `kubectl apply`'s default client-side-apply field manager now owns that field. Reclaim it once: `kubectl get <resource> -o yaml | kubectl apply -f - --server-side --field-manager=helm --force-conflicts`, then retry the plain `helm upgrade` (no `--force` needed).
- **`~/.config/talos/secrets.toml` still uses the legacy `talos` name in its path:** Even though the CLI binary is `core`, the config file it reads is `~/.config/talos/secrets.toml` (not `~/.config/core/...`). This is real CLI behavior, not a doc bug — don't try to rename the path, just be aware if you're troubleshooting Vault access (`core get`/`core put` failing) and checking whether `~/.config/talos/secrets.toml` exists or is readable.
+2 -2
View File
@@ -144,10 +144,10 @@ KEYS "kmsvc:inflight:*" | wc -l
**Requires JWT from Authentik:**
```bash
# Get token (device code flow)
talos secrets login
core secrets login
# Use token
export JWT_TOKEN=$(talos get cluster/kmsvc/jwt-token --key jwt-token)
export JWT_TOKEN=$(core get cluster/kmsvc/jwt-token --key jwt-token)
curl -H "Authorization: Bearer $JWT_TOKEN" https://kmsvc.riotpiao.homelab.com/v1/queues
```
+18 -18
View File
@@ -17,10 +17,10 @@
```bash
# Browser: https://vault.riotpiao.homelab.com
# Auth method: OIDC → "Sign in with Authentik" (federated)
# Or: Device code → talos secrets login (CLI)
# Or: Device code → core secrets login (CLI)
# Via CLI (device code flow)
talos secrets login
core secrets login
# → Opens browser, approve device code
# → Token cached in ~/.talos/vault
```
@@ -28,18 +28,18 @@ talos secrets login
**2. Store a secret:**
```bash
# Field name = variable name (SCREAMING_SNAKE_CASE)
talos put cluster/ANTHROPIC_API_KEY ANTHROPIC_API_KEY="sk-..."
talos put cluster/STORY_CRATER_DB_PASS STORY_CRATER_DB_PASS="dbpass123"
core put cluster/ANTHROPIC_API_KEY ANTHROPIC_API_KEY="sk-..."
core put cluster/STORY_CRATER_DB_PASS STORY_CRATER_DB_PASS="dbpass123"
```
**3. Retrieve a secret:**
```bash
# Always use --key flag
talos get cluster/ANTHROPIC_API_KEY --key ANTHROPIC_API_KEY
core get cluster/ANTHROPIC_API_KEY --key ANTHROPIC_API_KEY
# → sk-...
# Full secret as JSON
talos get cluster/ANTHROPIC_API_KEY --json
core get cluster/ANTHROPIC_API_KEY --json
```
**4. Load into shell (helmfile, scripts):**
@@ -78,7 +78,7 @@ cluster/
**Store generated secret immediately (keeps it out of shell history):**
```bash
# Generate & store in one command
talos put cluster/GRAFANA_OIDC_CLIENT_SECRET \
core put cluster/GRAFANA_OIDC_CLIENT_SECRET \
GRAFANA_OIDC_CLIENT_SECRET="$(openssl rand -hex 32)"
```
@@ -86,7 +86,7 @@ talos put cluster/GRAFANA_OIDC_CLIENT_SECRET \
```bash
# Create Secret using Vault secret
kubectl create secret generic grafana-oidc \
--from-literal=client-secret="$(talos get cluster/GRAFANA_OIDC_CLIENT_SECRET --key GRAFANA_OIDC_CLIENT_SECRET)" \
--from-literal=client-secret="$(core get cluster/GRAFANA_OIDC_CLIENT_SECRET --key GRAFANA_OIDC_CLIENT_SECRET)" \
-n logging
```
@@ -96,7 +96,7 @@ kubectl create secret generic grafana-oidc \
NEW_PASS=$(openssl rand -base64 24)
# 2. Store in Vault
talos put cluster/OLD_DB_PASS OLD_DB_PASS="$NEW_PASS"
core put cluster/OLD_DB_PASS OLD_DB_PASS="$NEW_PASS"
# 3. Update database user
psql -h ddb-cluster-rw.ddb.svc.cluster.local -U postgres \
@@ -113,7 +113,7 @@ k rollout restart -n story-crater-backend deployment/app
**JWT token from CLI:**
```bash
# After device code login
talos secrets login
core secrets login
# Token is cached and auto-renewed
# Use for API calls
@@ -127,7 +127,7 @@ curl -H "X-Vault-Token: $VAULT_TOKEN" \
**Vault status:**
```bash
# Check if sealed
talos status vault
core status
# If sealed (disaster recovery):
# See /TROUBLESHOOTING.md § Vault Sealed
@@ -139,10 +139,10 @@ talos status vault
# Logs stored in Loki under vault namespace
# View recent access
talos audit log --limit 50
core audit log --limit 50
# Export for compliance
talos audit export --format json > vault-audit.json
core audit export --format json > vault-audit.json
```
## Security Rules
@@ -177,7 +177,7 @@ k rollout restart -n iam statefulset/vault
**Vault is sealed:**
```bash
# Check status
talos status vault
core status
# If sealed, use unseal keys (stored in MinIO backup)
# See /TROUBLESHOOTING.md § Vault Sealed for recovery steps
@@ -186,11 +186,11 @@ talos status vault
**Secret not found:**
```bash
# Verify path exists
talos list cluster
core list cluster
# Check secret name (case-sensitive)
talos get cluster/anthropic_api_key --key anthropic_api_key # won't work
talos get cluster/ANTHROPIC_API_KEY --key ANTHROPIC_API_KEY # correct
core get cluster/anthropic_api_key --key anthropic_api_key # won't work
core get cluster/ANTHROPIC_API_KEY --key ANTHROPIC_API_KEY # correct
```
**vsource not expanding secrets:**
@@ -200,7 +200,7 @@ grep ANTHROPIC_API_KEY .env
# → Should be: ANTHROPIC_API_KEY= (empty, not a value)
# Verify Vault is accessible
talos get cluster/ANTHROPIC_API_KEY --key ANTHROPIC_API_KEY
core get cluster/ANTHROPIC_API_KEY --key ANTHROPIC_API_KEY
# → Should return secret
# Run vsource explicitly