# Homelab Kubernetes Cluster

A bare-metal three-node Kubernetes cluster running Talos Linux with **18 Helm releases** across 22 namespaces. Includes distributed storage (MinIO + Longhorn), full observability (Prometheus + Grafana + Loki), federated SSO (Authentik OIDC), secrets management (Vault), CI/CD (Forgejo + Argo CD), messaging (Kafka + kmsvc), and workflow orchestration (Temporal).

## Use Cases & Architecture

**Why this stack?**

This homelab replicates **production-grade cloud-native infrastructure** on bare metal, enabling:

1. **Learning & Prototyping** — Test distributed systems patterns (HA databases, event-driven messaging, GitOps workflows) before deploying to cloud
2. **Self-Hosted Services** — Run applications (Story Crater, etc.) with zero cloud lock-in; full control over data, compliance, and networking
3. **Infrastructure as Code** — Git-driven cluster state via Helmfile + Forgejo Actions + Argo CD; every change is auditable and reproducible
4. **Observability Sandbox** — Experiment with Prometheus metrics, Loki log aggregation, and custom Grafana dashboards at scale

**Typical workflow:**

```
Developer pushes to Forgejo (git forge)
    ↓
Forgejo Actions CI runs tests + builds OCI image
    ↓
Image pushed to Forgejo registry (private, on-cluster)
    ↓
Argo CD detects deployment repo change (pull-based GitOps)
    ↓
New pods roll out; Grafana alerts on errors/latency
    ↓
Temporal workflows coordinate long-running operations (e.g., async jobs)
    ↓
Kafka queues decouple services (fire-and-forget messaging)
    ↓
All logs + metrics centralized in Grafana for debugging
```

**Architecture principles:**

- **Immutable OS** — Talos Linux (no SSH, declarative machine configs)
- **No external dependencies** — All data stored locally (MinIO, CloudNativePG, Longhorn)
- **High availability** — 3-replica databases, multi-node storage, cross-AZ readiness (on bare metal: cross-rack affinity)
- **Federated identity** — Single Authentik OIDC provider for all services (Grafana, MinIO, Forgejo, Argo CD)
- **Secrets at rest** — Vault + encrypted etcd; credentials never in logs or ConfigMaps
- **Infrastructure-as-code** — Every service deployed via Helmfile; one `helmfile apply` recovers from total failure

## Quick Start — Deploying the Cluster

### 1. Bootstrap Talos Nodes
Bootstrap each Talos node with your cluster schematic (see `CLAUDE.md` or `README` step 1–8).

### 2. Set Up Secrets
All secrets are managed via environment variables sourced from `.env` (gitignored). The helmfile template expands them at deploy time.

**Step 1: Copy the template**
```bash
cp .env.example .env
```

**Step 2: Populate required secrets**
Edit `.env` and fill in cluster configuration. See `.env.example` for all options:

```bash
# Cluster configuration
CLUSTER_DOMAIN=riotpiao.com           # Your cluster domain
POSTGRES_HOST=ddb-cluster-rw.ddb.svc.cluster.local
MINIO_ENDPOINT=minio.storage.svc.cluster.local:9000
KAFKA_BOOTSTRAP=kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092
REDIS_ADDR=kmsvc-redis-master.sqs.svc.cluster.local:6379

# Service credentials (generate with: openssl rand -hex 32)
MINIO_ROOT_PASSWORD=<random>
GRAFANA_ADMIN_PASSWORD=<random>
AUTHENTIK_SECRET_KEY=<random>
AUTHENTIK_BOOTSTRAP_PASSWORD=<random>
AUTHENTIK_PG_PASSWORD=<random>
```

**Step 3: Load and deploy**
```bash
# Load .env into current shell
vsource .env

# Preview all changes before deployment
helmfile diff

# Deploy the entire stack
helmfile apply
```

### 3. Verify Deployment
```bash
# Check all pods are running
kubectl get pods -A

# Confirm key services are ready
kubectl wait deploy/authentik-server -n iam --for=condition=Available --timeout=300s
kubectl wait deploy/grafana -n logging --for=condition=Available --timeout=300s

# Access Grafana
make pf-grafana    # localhost:3000 (login: admin / GRAFANA_ADMIN_PASSWORD)
```

### 4. First-Time Access

**Default Credentials:**
- **Authentik:** https://authentik.$(CLUSTER_DOMAIN) → login: `akadmin` / `AUTHENTIK_BOOTSTRAP_PASSWORD` (change immediately)
- **Grafana:** https://grafana.$(CLUSTER_DOMAIN) → login: `admin` / `GRAFANA_ADMIN_PASSWORD`
- **MinIO:** https://minio.$(CLUSTER_DOMAIN) → login: `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD`
- **Argo CD:** https://argocd.$(CLUSTER_DOMAIN) → login via Authentik OIDC
- **Forgejo:** https://forgejo.$(CLUSTER_DOMAIN) → login via Authentik OIDC

**Next Steps:**
1. Change default passwords in each service
2. Configure OIDC redirects (see `k8s/talos-iam/` for details)
3. Set up GitOps: push infrastructure to Forgejo, configure Argo CD
4. Review dashboards in Grafana (Prometheus + Loki)

---

## Architecture

```
                         192.168.1.0/24 (LAN)
                                │
         ┌──────────────────────┼──────────────────────┐
         │                      │                      │
    192.168.1.*          192.168.1.*          192.168.1.*
  ┌────────────────┐      ┌────────────────┐    ┌────────────────┐
  │  talos-cp-1    │      │ talos-worker-1 │    │ talos-worker-2 │
  │ Control-Plane  │      │  Worker        │    │  Worker        │
  │   + Workloads  │      │  (Storage)     │    │  (Storage)     │
  │   (az-a)       │      │   (az-b)       │    │   (az-c)       │
  ├────────────────┤      ├────────────────┤    ├────────────────┤
  │ Pods:          │      │ Pods:          │    │ Pods:          │
  │ • ingress-nginx│      │ • kube-system  │    │ • kube-system  │
  │ • authentik    │      │ • storage      │    │ • storage      │
  │ • vault        │      │   └─ minio-2   │    │   └─ minio-3   │
  │ • logging      │      │                │    │                │
  │   ├─ loki      │      │                │    │                │
  │   ├─ promtail  │      │                │    │                │
  │   └─ grafana   │      │                │    │                │
  │ • monitoring   │      │                │    │                │
  │   ├─ prom      │      │                │    │                │
  │   └─ blackbox  │      │                │    │                │
  │ • storage      │      │                │    │                │
  │   └─ minio-1   │      │                │    │                │
  │ • cicd         │      │                │    │                │
  │   ├─ forgejo   │      │                │    │                │
  │   ├─ argocd    │      │                │    │                │
  │   └─ runner    │      │                │    │                │
  │ • sqs          │      │ • sqs          │    │ • sqs          │
  │   ├─ kafka-0   │      │   ├─ kafka-1   │    │   ├─ kafka-2   │
  │   └─ kmsvc     │      │   └─ redis     │    │                │
  │ • ddb          │      │ • ddb          │    │ • ddb          │
  │   └─ postgres-0│      │   └─ postgres-1│    │   └─ postgres-2│
  │ • temporal     │      │ • temporal     │    │ • temporal     │
  │   ├─ server    │      │   ├─ cassandra │    │   ├─ cassandra │
  │   └─ cassandra │      │   │   -1       │    │   │   -2       │
  │       -0       │      │   └─ (replica) │    │   └─ (replica) │
  │ • llm          │      │                │    │                │
  │   └─ ollama    │      │                │    │                │
  └────────────────┘      └────────────────┘    └────────────────┘

Replication & Fault Tolerance:
  Data Layer:
    • MinIO:      minio-1 ↔ minio-2 ↔ minio-3  (3-way active-active S3)
    • PostgreSQL: postgres-0 ↔ postgres-1 ↔ postgres-2  (primary + 2 standbys, HA streaming replication)
    • Kafka:      kafka-0 ↔ kafka-1 ↔ kafka-2  (3 brokers, RF=3, min-ISR=2, cross-AZ)
    • Temporal:   cassandra-0 ↔ cassandra-1 ↔ cassandra-2  (3-node distributed)
    • Loki:       loki → MinIO (chunks stored in s3://loki-chunks, 10-day retention)
  
  Single-Replica Services (Protected by PodDisruptionBudget minAvailable=1):
    • Observability: Grafana, Prometheus, Loki (Recreate strategy for RWO PVCs)
    • IAM: Authentik, Vault (Recreate strategy for RWO PVCs)
    • CI/CD: Argo CD server/repo-server, Forgejo (Recreate strategy for RWO PVCs)
    • LLM: Ollama

Networking:
  Remote access: UR_OWN.duckdns.org → home IP → cp-1
```

## Stack

| Layer | Technology | Namespace | Purpose |
|-------|-----------|-----------|---------|
| OS | Talos Linux v1.13.3 | — | Immutable, Kubernetes-native OS |
| Kubernetes | v1.36.1 | — | Container orchestration |
| CNI | Cilium (eBPF) | kube-system | Networking, replaces kube-proxy |
| Ingress | Nginx Ingress Controller | ingress-nginx | Reverse proxy, hostname-based routing |
| Block Storage | Longhorn v1.7.0 | longhorn-system | Default StorageClass |
| Object Store | MinIO (multi-AZ) | storage | S3-compatible, site-replicated across az-a/az-b |
| IAM / SSO | Authentik | iam | OIDC provider for Grafana, MinIO, Forgejo, Argo CD |
| Secret Store | HashiCorp Vault | iam | KV secrets backend, JWT auth via Authentik |
| Git Forge *(planned)* | Forgejo | forge | Git server, built-in OCI registry, Actions CI |
| CI Runner *(planned)* | Forgejo Actions + DinD | cicd | Privileged build pod; images pushed to Forgejo OCI |
| CD *(planned)* | Argo CD | argocd | Pull-based GitOps; never holds kubeconfig in CI |
| Log Backend | Loki (SingleBinary) | logging | 10-day retention, backed by MinIO |
| Log Collector | Promtail (DaemonSet) | logging | Scrapes pod logs + Talos journal |
| Metrics | kube-prometheus-stack | monitoring | Prometheus + node-exporter + kube-state-metrics |
| Log/Metrics UI | Grafana | logging | Dashboards for Loki + Prometheus |
| Cluster UI | Portainer CE | dashboard | Container/workload management UI |
| LLM Inference | Ollama | llm | Local LLM model serving (open-source models) |

## Repository Structure

```
homelab/
├── helmfile.yaml             # Single source of truth — deploys everything
├── .env.example              # Required env vars template (copy to .env, gitignored)
│
├── cluster-config/           # Talos + Kubernetes bootstrap
│   ├── cilium-values.yaml
│   ├── longhorn_bootstrap.sh
│   ├── controlplane.yaml     # gitignored — contains secrets
│   ├── worker-1.yaml         # gitignored
│   ├── secrets.yaml          # gitignored
│   ├── kubeconfig            # gitignored
│   └── talosconfig           # gitignored
│
├── k8s/
│   ├── ingress/              # Nginx Ingress Controller + all Ingress rules
│   │   ├── nginx-values.yaml
│   │   └── ingress.yaml
│   ├── storage/              # MinIO multi-AZ object store
│   │   ├── minio-az-a-values.yaml
│   │   ├── minio-az-b-values.yaml
│   │   ├── minio-az-a-pvc.yaml
│   │   ├── minio-service.yaml
│   │   ├── minio-legacy-alias.yaml
│   │   └── minio-replication-job.yaml
│   ├── logging/              # Observability stack
│   │   ├── loki-values.yaml
│   │   ├── promtail-values.yaml
│   │   └── grafana-values.yaml
│   ├── monitoring/           # Prometheus stack + alerting + Grafana dashboards-as-code
│   │   ├── prometheus-values.yaml
│   │   ├── blackbox-exporter-values.yaml   # Active uptime probes (feeds Service Availability dashboard)
│   │   ├── ingress-alerts.yaml             # PrometheusRule: ingress 5xx rate, p95 latency
│   │   └── dashboards/                     # ConfigMaps picked up live by Grafana's sidecar
│   │       ├── service-availability.yaml      # Uptime probes + cert expiry (operator glance)
│   │       ├── service-golden-signals.yaml     # Latency & Golden Signals (ingress RED)
│   │       ├── service-internals.yaml          # Per-service deep-dive (MinIO/Forgejo/Argo CD/Vault/Longhorn/certs)
│   │       ├── kube-controller-health.yaml      # API server RED + kube-state-metrics controller-health proxy
│   │       ├── hardware-overview.yaml           # Per-node CPU/mem/disk/network/load summary
│   │       └── control-plane-logs.yaml          # kube-system + add-on logs (Loki)
│   ├── portainer/            # Portainer CE
│   │   └── portainer-values.yaml
│   ├── talos-iam/            # Authentik + Vault IAM
│   │   ├── authentik-values.yaml
│   │   ├── vault-values.yaml
│   │   ├── setup_vault.sh    # One-time Vault init (not replaced by Helmfile)
│   │   └── provision_oidc.py # Authentik OIDC provisioning
│   ├── coredns/              # CoreDNS hostname rewrites (in-cluster DNS)
│   │   └── coredns-configmap.yaml
│   ├── talos-ci-cd/          # CI/CD stack (planned — not yet applied)
│   │   ├── talos_version_control.html  # Implementation plan + build runbook
│   │   ├── forgejo-values.yaml         # Forgejo Helm values (gitea-charts/gitea)
│   │   ├── argocd-values.yaml          # Argo CD Helm values
│   │   └── charts/forgejo-runner/      # Local Helm chart for the Actions runner
│   │       ├── Chart.yaml
│   │       ├── values.yaml
│   │       └── templates/
│   │           ├── deployment.yaml     # Runner + DinD sidecar, Recreate strategy
│   │           ├── pvc.yaml            # runner-reg (1 Gi) + runner-dind (30 Gi)
│   │           └── networkpolicy.yaml  # Egress: forge ns + DNS + internet only
│   └── duckdns/              # DuckDNS DDNS updater CronJob
│
├── talos-cli/                # Rust CLI for Vault secret access
├── project_context.md        # Authoritative live-state reference
├── refine_cluster.md         # Known-issues runbook
└── LOG.md                    # Append-only change journal
```

## Access

### Discover Ingress LoadBalancer IP

```bash
# Find the external IP assigned by Cilium LB-IPAM
kubectl get svc -n ingress-nginx ingress-nginx
# Example output:
# LoadBalancer IP: 192.168.1.160 (Cilium LB-IPAM assignment)
```

Add to `/etc/hosts` on every client machine (Mac/Linux):

```
# WireGuard access (remote — via talos-cp-1)
10.6.0.1  grafana.riotpiao.com authentik.riotpiao.com vault.riotpiao.com minio.riotpiao.com prometheus.riotpiao.com portainer.riotpiao.com longhorn.riotpiao.com loki.riotpiao.com forgejo.riotpiao.com temporal.riotpiao.com temporal-grpc.riotpiao.com kmsvc.riotpiao.com

# LAN access (on the home network — use actual LoadBalancer IP from above)
192.168.1.160  grafana.riotpiao.com authentik.riotpiao.com vault.riotpiao.com minio.riotpiao.com prometheus.riotpiao.com portainer.riotpiao.com longhorn.riotpiao.com loki.riotpiao.com forgejo.riotpiao.com temporal.riotpiao.com temporal-grpc.riotpiao.com kmsvc.riotpiao.com
```

**Note:** `192.168.1.160` is an example Cilium LB-IPAM assignment. Verify with `kubectl get svc -n ingress-nginx ingress-nginx`.

**There is no real DNS wildcard for `*.riotpiao.com`** — every hostname must be added to `/etc/hosts` explicitly (as above) before it resolves. Adding a new Ingress host doesn't make it reachable by itself; add the line too.

### kubectl Context

Two contexts exist in `cluster-config/kubeconfig`, pointed at the same cluster over different paths:

| Context | Server | Use when |
|---|---|---|
| `admin@homelab-cluster` | `192.168.1.213:6443` (LAN) | On the home network |
| `admin@homelab-cluster-1` | `10.6.0.1:6443` (WireGuard) | Remote / off-LAN |

If `kubectl` commands hang or refuse the connection, switch: `kubectl config use-context admin@homelab-cluster-1`.

Then access services at:

| Service | URL | Credentials |
|---------|-----|-------------|
| Grafana | http://grafana.riotpiao.com | admin / `GRAFANA_ADMIN_PASSWORD` or Authentik SSO |
| Authentik | http://authentik.riotpiao.com | akadmin / see `.env` |
| Vault | http://vault.riotpiao.com | root token / see `setup_vault.sh` output |
| MinIO console | http://minio.riotpiao.com | `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` |
| Prometheus | http://prometheus.riotpiao.com | no auth |
| Portainer | http://portainer.riotpiao.com | set on first visit |
| Longhorn | http://longhorn.riotpiao.com | no auth |
| Forgejo *(planned)* | https://forgejo.forge.riotpiao.com | `rock` / `FORGEJO_ADMIN_PASSWORD`, or Authentik SSO |
| Argo CD *(planned)* | `kubectl port-forward -n argocd svc/argocd-server 8080:443` | Authentik SSO (admins only) |

Grafana → "Homelab" folder has the operator dashboards (sidecar-loaded from `k8s/monitoring/dashboards/`, no restart needed on change):
- **Service Availability & Certificate Expiration** — uptime probes + cert-manager expiry
- **Latency & Golden Signals** — ingress request rate/error %/p50-p99 latency
- **Kube-Controller Health** — API server RED metrics + kube-state-metrics controller-health signals
- **Hardware Statistics** — per-node CPU/mem/disk/network/load
- **Service Internals** — per-service deep-dive (MinIO/Forgejo/Argo CD/Vault/Longhorn)

## Deploy

```bash
# 1. Install helmfile (once)
brew install helmfile

# 2. Set credentials
cp .env.example .env
# edit .env with your passwords

# 3. Deploy everything
helmfile apply

# Deploy a single stack
helmfile apply -l namespace=logging
helmfile apply -l name=grafana
helmfile apply -l namespace=ingress-nginx

# Preview changes before applying
helmfile diff
```

## Bootstrap Order (fresh cluster)

```
1.  Provision nodes:        talosctl apply-config (make apply-cp / apply-worker-new)
2.  Bootstrap Kubernetes:   talosctl bootstrap
3.  Install Cilium:         helm install cilium -f cluster-config/cilium-values.yaml
4.  Install Longhorn:       bash cluster-config/longhorn_bootstrap.sh
5.  Deploy everything else: helmfile apply
6.  Vault init (one-time):  bash k8s/talos-iam/setup_vault.sh
7.  Authentik OIDC:         python3 k8s/talos-iam/provision_oidc.py
8.  Label worker:           kubectl label node talos-worker-1 node-role.kubernetes.io/worker=

# ── CI/CD (planned — run after step 8) ─────────────────────────────────────
9.  Private CA + TLS:       see k8s/talos-ci-cd/talos_version_control.html §12 Block 0
10. Deploy Forgejo + runner: helmfile apply -l name=forgejo && helmfile apply -l name=forgejo-runner
11. Authentik SSO for CI:   Forgejo + Argo CD OIDC (§12 Block 1.5)
12. Talos node CA trust:    talosctl patch machineconfig (§12 Block 2)
13. Deploy Argo CD:         helmfile apply -l name=argocd (§12 Block 4)
14. Wire deploy repo:       argocd app create + push first manifests (§12 Block 4)
```

## IAM & Auth Flow

Authentik is the central OIDC identity provider. Vault stores secrets and delegates authentication back to Authentik.

```
  User / core-cli
       │
       │  OAuth2 / OIDC
       ▼
  Authentik  (authentik.riotpiao.com)
  ├── grafana app      → Grafana OIDC login (group → Admin/Viewer role)
  ├── minio app        → MinIO OIDC login (group → readwrite/readonly policy)
  ├── vault-browser    → Vault UI OIDC login / `vault login -method=oidc`
  └── core-cli-shell  → CLI device code flow (public client, no secret)
       │
       │  JWKS endpoint for JWT validation
       ▼
  HashiCorp Vault  (vault.riotpiao.com)
  ├── auth/jwt   — core-cli authenticates with device code JWT
  ├── auth/oidc  — browser/UI login via Authentik
  └── secret/    — KV v2: mcp/*, cluster/*, cloud/*
```

**core CLI device code login:**
```bash
core secrets login        # prints URL + code → approve in browser → Vault token cached
core put cluster/DUCKDNS_TOKEN DUCKDNS_TOKEN="abc"            # field name = variable name, never `value`
```

**One-time IAM setup (after `helmfile apply`):**
```bash
# 1. Provision OIDC apps and groups in Authentik
GRAFANA_URL=http://grafana.riotpiao.com \
MINIO_URL=http://minio.riotpiao.com \
python3 k8s/talos-iam/provision_oidc.py

# 2. Init Vault, wire JWT + OIDC auth, seed secrets
bash k8s/talos-iam/setup_vault.sh
```

**CoreDNS hostname rewrites** (`k8s/coredns/coredns-configmap.yaml`) ensure in-cluster pods (Grafana, Vault, Forgejo runner, Argo CD) resolve internal hostnames to cluster services, avoiding hairpin NAT through LB IPs.

## CI/CD Pipeline *(planned)*

> **Implementation plan & exact build commands:** [`k8s/talos-ci-cd/talos_version_control.html`](k8s/talos-ci-cd/talos_version_control.html)

### Components

| Component | Helm chart | Namespace | Notes |
|-----------|-----------|-----------|-------|
| Forgejo | `gitea-charts/gitea` (Forgejo image override) | `forge` | Git + OCI registry + Actions engine; SQLite on Longhorn PVC; `strategy: Recreate` |
| Forgejo runner | local chart `charts/forgejo-runner` | `cicd` | DinD sidecar; PodSecurity privileged; NetworkPolicy fenced |
| Argo CD | `argo/argo-cd` | `argocd` | Pull-based CD; single replica; no external ingress (port-forward only) |

### Pipeline flow

```
Developer
  │
  │  git push
  ▼
Forgejo (forge ns)  ─── webhook ───►  Runner (cicd ns)
  │                                        │
  │  SSO login (Authentik OIDC)            │  ACTIONS_RUNTIME_TOKEN → git checkout
  ▼                                        │  ci-registry-token (ci-bot) → docker push → Forgejo OCI
Forgejo UI / Argo CD UI                    │  ci-deploy-token (ci-bot)   → git commit → rock/deploy
                                           │
Forgejo (rock/deploy repo)  ◄─────────────┘
  │
  │  argocd-bot token (repo:read, poll every 3 min)
  ▼
Argo CD (argocd ns)
  │
  │  kubectl apply (cluster-admin ServiceAccount — never in CI)
  ▼
K8s workloads (images from Forgejo OCI)
```

### Key security decisions

- **No kubeconfig in CI.** The runner can only push commits + OCI images. Argo CD bridges the gap autonomously.
- **Scoped machine credentials.** `ci-bot` tokens are narrowly scoped: `package:write` for OCI, `repo:write` on `rock/deploy` only. A compromised runner cannot read other repos or call the K8s API.
- **Private CA TLS.** Forgejo self-terminates HTTPS with a homelab CA (EC P-256, 10-year). The CA cert is distributed to Talos nodes via `machineconfig` patch and to the runner via K8s Secret. `ca.key` never enters the cluster.
- **Authentik SSO for humans.** All interactive logins (Forgejo UI, Argo CD UI) route through Authentik. `homelab-admins` group → Forgejo admin + Argo CD `role:admin`; `homelab-devs` group → Forgejo user + no Argo CD access.
- **Forgejo LB IP pinned.** Cilium LB-IPAM annotation `io.cilium/lb-ipam-ips: <LB_IP>` fixes the Forgejo LoadBalancer IP so the TLS SAN and DNS entries never need updating. Configure in `k8s/talos-ci-cd/forgejo-values.yaml`.

### Workflow file location

Forgejo Actions uses GitHub Actions syntax. Workflow files live in `.forgejo/workflows/` in each source repo:

```
rock/source/
└── .forgejo/
    └── workflows/
        ├── ci.yml    # build + test + push OCI image
        └── cd.yml    # on: push to main → bump image tag in rock/deploy
```

## Log Data Flow

```
Pods / Talos journal (both nodes)
       │
  Promtail (DaemonSet, all nodes)   reads /var/log/pods + /var/log/journal
       │
       ▼
    Loki (logging ns)               indexes + compacts, 10-day retention
       │  stores chunks via S3
       ▼
    MinIO frontend service          minio.storage.svc.cluster.local:9000
    (active-active, round-robin)
       │
  minio-az-a (cp-1) ↔ minio-az-b (worker-1) ↔ minio-az-c (worker-2)
  3-way site replication (bidirectional, automatic)
       │
  Grafana (logging ns)              queries Loki + Prometheus via dashboards
       │
  Nginx Ingress → grafana.riotpiao.com   browser access
```

## Example Applications & Workloads

This cluster runs production-like applications and infrastructure services:

### Story Crater Backend
**Type:** Distributed message-driven application  
**Namespace:** `story-crater-backend`  
**Architecture:**
- gRPC server + REST gateway (Envoy)
- PostgreSQL database (story_crater in CloudNativePG cluster)
- Kafka topic consumers (via kmsvc message queue)
- Temporal workflow integration for long-running operations
- Prometheus metrics (processed messages, latency, errors)
- Grafana dashboard (message throughput, queue depth, LLM token usage)

**Typical flow:**
```
POST /api/messages → gRPC handler
  ↓
Publish to Kafka topic (Story Crater queue)
  ↓
Message consumer processes async (may trigger LLM inference)
  ↓
Results stored in PostgreSQL + emitted as event
  ↓
Prometheus increments counters (story_crater_messages_handled_total)
  ↓
Grafana renders message throughput + duration histograms
```

### Infrastructure Services (Essential)

| Service | Purpose | Namespace | Example Use |
|---------|---------|-----------|-------------|
| **Authentik** | OIDC identity provider | iam | User login, group management, SSO for Grafana/MinIO/Forgejo |
| **Vault** | Secrets backend | iam | Database passwords, API keys, JWT token validation |
| **CloudNativePG** | PostgreSQL 3-replica cluster | ddb | Auth database (Authentik), app database (Story Crater) |
| **Loki** | Log aggregation | logging | Centralize pod logs, Talos kernel logs (10-day retention) |
| **Prometheus** | Metrics collection | monitoring | Scrape kube-state-metrics, kubelet, ServiceMonitors (every 30s) |
| **Grafana** | Observability dashboards | logging | Query Prometheus + Loki, alert on latency/error spikes |
| **Kafka + kmsvc** | Message queue | sqs | Decouple services, async job processing, at-least-once delivery |
| **MinIO** | S3-compatible object store | storage | Loki log chunks backend, Vault unseal keys, config backups |
| **Longhorn** | Persistent block storage | longhorn-system | All PVCs (Postgres replicas, Kafka broker disks, MinIO) |

### Development Services (Optional)

| Service | Purpose | Namespace | Example Use |
|---------|---------|-----------|-------------|
| **Forgejo** | Self-hosted git + OCI registry | cicd | Version control, CI Actions runner, private Docker images |
| **Argo CD** | Pull-based GitOps | cicd | Continuous deployment (deployment repo → Kubernetes) |
| **Temporal** | Workflow orchestration | temporal | Schedule long-running jobs, retry logic, state machines |
| **Portainer** | Container management UI | dashboard | Pod inspection, image management, quick debugging |

### Monitoring Example: Dashboard Walk-Through

Open **Grafana** → **Homelab** folder → **"Story Crater Backend — Service Overview"**:

**Row A — Availability & Golden Signals**
- Requests/sec (blue = success, red = errors)
- Error rate % (goal: < 0.1%)
- p50/p95/p99 latency (goal: p99 < 500ms)
- Alert: If error rate > 1% for 5 min, page on-call

**Row B — Resource Usage**
- CPU (request/limit)
- Memory (request/limit)
- Restart count (goal: 0; alerts if > 2)

**Row C — Domain-Specific Metrics** (Story Crater only)
- Messages processed/sec (bucketed by status: success, dlq, retry)
- Queue depth (Kafka partitions lag)
- Dedup window retention (FIFO redelivery tracking)
- LLM inference tokens used/sec
- External API call latency (e.g., OpenAI)

**Row D — Logs**
- Live Loki panel: filter by pod + search for errors
- Example: `{namespace="story-crater-backend"} | json | level="error"`

**Row E — Alert Status** (if SLO defined)
- Burn rate (if consuming SLO budget)
- Example: "30-day availability SLO = 99.5%; current burn rate = 0.2x"

**Row F — Related Dashboards**
- Link to Kafka dashboard (queue depth)
- Link to PostgreSQL dashboard (story_crater DB)
- Link to Temporal dashboard (workflow execution times)

## Adding Hardware to the Cluster

### Step 1 — Get the Talos image

The image must include the same extensions as the existing nodes (`iscsi-tools` + `util-linux-tools`).
Download from the Image Factory using the cluster's schematic ID:

| Format | Use case | URL |
|--------|----------|-----|
| ISO | USB boot (recommended for bare metal) | `https://factory.talos.dev/image/613e1592.../v1.13.3/metal-amd64.iso` |
| RAW disk image | Write directly to drive via another machine | `https://factory.talos.dev/image/613e1592.../v1.13.3/metal-amd64.raw.xz` |
| PXE / iPXE | Network boot — no USB needed | `https://factory.talos.dev/image/613e1592.../v1.13.3/kernel-amd64` |

> Full schematic ID: `613e1592b2da41ae5e265e8789429f22e121aab91cb4deb6bc3c0b6262961245`

---

### Option A — USB Bootable (recommended)

```bash
curl -Lo talos-worker.iso \
  "https://factory.talos.dev/image/613e1592b2da41ae5e265e8789429f22e121aab91cb4deb6bc3c0b6262961245/v1.13.3/metal-amd64.iso"
sudo dd if=talos-worker.iso of=/dev/sdX bs=4M status=progress && sync
```

---

### Option B — In-Memory (diskless / RAM boot via PXE)

Talos runs entirely from RAM. Useful for temporary nodes or hardware where you don't want to touch the existing OS.

```bash
# Boot via PXE pointing to:
# Kernel:  https://factory.talos.dev/image/613e1592.../v1.13.3/kernel-amd64
# Initrd:  https://factory.talos.dev/image/613e1592.../v1.13.3/initramfs-amd64.xz
# Cmdline: talos.platform=metal
```

> Note: in-memory nodes lose state on reboot. Not suitable for Longhorn storage nodes.

---

### Option C — Direct Disk Image (headless / remote)

```bash
xz -d talos-worker.raw.xz
sudo dd if=talos-worker.raw of=/dev/sda bs=4M status=progress && sync
```

---

### Step 2 — Discover hardware in maintenance mode

```bash
# Scan your LAN for the new node in maintenance mode
nmap -sn 192.168.1.0/24
# Note: Replace with your actual subnet (e.g., 10.0.1.0/24)

# Discover available disks on the node
talosctl --nodes <maintenance-ip> --talosconfig cluster-config/talosconfig disks --insecure
```

---

### Step 3 — Prepare the worker config

```bash
cp cluster-config/worker-1.yaml cluster-config/worker-N.yaml
```

Edit exactly these four fields:

| Field | Value |
|-------|-------|
| `machine.network.hostname` | `talos-worker-N` |
| `machine.network.interfaces[0].addresses` | `192.168.1.16N/24` |
| `machine.install.disk` | disk path from Step 2 |
| `machine.nodeLabels.topology.kubernetes.io/zone` | `az-N` |

---

### Step 4 — Apply config

```bash
make apply-worker-new N=<num> WN_IP=<maintenance-ip>
```

---

### Step 5 — Persist the worker IP

```bash
# Substitute <WN_MAINTENANCE_IP> with the actual IP discovered in Step 2
echo 'export W<N>_IP=192.168.1.<last-octet>' >> ~/.zshrc && source ~/.zshrc
```

---

### Step 6 — Set the node-role label

```bash
kubectl label node talos-worker-N node-role.kubernetes.io/worker=
```

---

### Step 7 — Verify

```bash
kubectl get nodes -w
kubectl describe node talos-worker-N | grep -A10 Labels
```

---

### Step 8 — What automatically extends to the new node

| Service | Behaviour |
|---------|-----------|
| Cilium | New pod scheduled automatically |
| Promtail | DaemonSet — starts immediately |
| Nginx Ingress | DaemonSet — starts immediately, port 80/443 available on new node |
| Longhorn | Detects new node, available for replica scheduling |
| MinIO / Loki / Grafana | Stay on existing node (single-replica Deployments) |
