Add k3s Skills.md

This commit is contained in:
2026-03-17 10:27:32 +11:00
parent e271d82abe
commit a892c26ca6
+344
View File
@@ -0,0 +1,344 @@
---
name: home-k3s-deploy
description: Deploy and manage apps on junv's home K3s cluster at 192.168.1.2 (server-3). Use this skill when creating new Kubernetes manifests, adding services, configuring ingress/TLS, managing storage, or modifying existing deployments in this homelab cluster. Covers namespace conventions, storage patterns, ingress annotations, TLS, OAuth2 auth, node affinity, and apply workflow.
compatibility: Requires kubectl configured for 192.168.1.2; manifests live in /Users/junv/code/home-docker
metadata:
author: junv
cluster: server-3 (192.168.1.2)
domain: "*.junv.cc"
---
# Home K3s Deployment Skill
## Cluster Overview
- **Node**: `server-3` at `192.168.1.2` (single-node K3s cluster)
- **K3s version**: v1.33.3+k3s1, Ubuntu 24.04.3
- **SSH**: `ssh -p 22422 junv@192.168.1.2` (Ed25519 key)
- **Primary domain**: `*.junv.cc` (Cloudflare DNS + cert-manager Let's Encrypt)
- **Local network**: `192.168.1.0/24` (trusted, never block)
- **Kubeconfig**: `scp -P 22422 junv@192.168.1.2:/etc/rancher/k3s/k3s.yaml ~/.kube/config && sed -i '' 's/127.0.0.1/192.168.1.2/g' ~/.kube/config`
## Namespace Organization
| Namespace | Purpose |
|-----------|---------|
| `home-apps` | Home automation, productivity, personal tools |
| `media` | Media servers (alist, static files) |
| `db` | Databases: Redis (`db` ns), Qdrant vector DB |
| `ai` | AI/ML workloads |
| `argocd` | GitOps deployment (ArgoCD) |
| `ingress-nginx` | NGINX Ingress Controller |
| `cert-manager` | TLS certificate management |
| `crowdsec` | Security monitoring |
| `default` | Prometheus/Grafana monitoring |
## Repository File Layout
New manifests go in the directory matching the app's namespace:
```
home-docker/
├── home-apps/ → namespace: home-apps
├── media/ → namespace: media
├── db/ → namespace: db
├── ai/ → namespace: ai
├── adhoc-config/ → one-off PVs, config patches
├── terraform/ → infra-as-code (prefer for cluster-level changes)
└── archive/ → deprecated configs (do not edit)
```
## Adding a New App: Step-by-Step
1. Choose the correct directory and namespace (see table above).
2. Create a single YAML file `{app-name}.yaml` with all resources separated by `---`.
3. Resource order in the file: PV → PVC → Deployment → Service → Ingress.
4. If local storage is needed, also create `adhoc-config/{app-name}-local-pv.yaml`.
5. Apply: `kubectl apply -f {file}.yaml` (or `kubectl apply -f adhoc-config/{app-name}-local-pv.yaml` first).
## Storage Patterns
### Local Storage (most common for new apps)
Use `local-storage` storageClass with node affinity pinned to `server-3`. Data path: `/mnt/k8s/{app-name}/`.
Create the directory first on the node: `ssh -p 22422 junv@192.168.1.2 "sudo mkdir -p /mnt/k8s/{app-name}"`
```yaml
# adhoc-config/{app-name}-local-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: {app-name}-local-pv
labels:
type: local
name: {app-name}
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/k8s/{app-name}
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- server-3
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
namespace: {namespace}
name: {app-name}-local-pvc
spec:
volumeName: {app-name}-local-pv
storageClassName: "local-storage"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
```
### NFS Storage (for large or shared data)
NFS server: `192.168.1.5`, base path: `/fs/1000/nfs/k8s/`. Use `storageClassName: ""` (empty) with no volumeName binding.
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: {app-name}-nfs-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
nfs:
server: 192.168.1.5
path: "/fs/1000/nfs/k8s/{app-name}"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
namespace: {namespace}
name: {app-name}-nfs-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: ""
resources:
requests:
storage: 10Gi
```
## Deployment Pattern
Always set `revisionHistoryLimit: 2`. Pin the timezone. For images from Docker Hub that may be rate-limited, use `mirror.gcr.io/` prefix (e.g., `mirror.gcr.io/library/nginx:latest`).
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {app-name}
namespace: {namespace}
labels:
app: {app-name}
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app: {app-name}
template:
metadata:
labels:
app: {app-name}
spec:
nodeSelector:
kubernetes.io/hostname: server-3
# If pulling from GitHub Container Registry:
imagePullSecrets:
- name: github-image-pull-secret
containers:
- name: {app-name}
image: mirror.gcr.io/{org}/{image}:{tag}
imagePullPolicy: IfNotPresent
ports:
- containerPort: {port}
env:
- name: TZ
value: Australia/Melbourne
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
volumes:
- name: data
persistentVolumeClaim:
claimName: {app-name}-local-pvc
```
## Service Pattern
```yaml
apiVersion: v1
kind: Service
metadata:
name: {app-name}
namespace: {namespace}
spec:
type: ClusterIP
selector:
app: {app-name}
ports:
- name: web
port: 80
targetPort: {container-port}
protocol: TCP
```
For direct LAN access (no ingress), use `LoadBalancer` type with a static MetalLB IP:
```yaml
spec:
type: LoadBalancer
annotations:
metallb.universe.tf/loadBalancerIPs: 192.168.1.2XX
```
## Ingress Patterns
Always use `ingressClassName: nginx` and `cert-manager.io/cluster-issuer: "letsencrypt-prod"` for TLS.
### Public HTTPS (anyone on the internet)
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {app-name}-ingress
namespace: {namespace}
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- {app-name}.junv.cc
secretName: {app-name}-tls
rules:
- host: {app-name}.junv.cc
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {app-name}
port:
number: 80
```
### LAN-Only (IP whitelisted to home network)
Add the whitelist annotation — no OAuth needed:
```yaml
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/whitelist-source-range: "192.168.1.0/24"
```
### OAuth2-Protected (via Pocket ID / passkey-auth)
Add these two annotations to require login via `pass.junv.cc`:
```yaml
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/auth-url: "https://pass.junv.cc/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://pass.junv.cc/oauth2/start?rd=https://$host$escaped_request_uri"
```
## Existing Secrets (reference, do not recreate)
| Secret Name | Namespace | Keys | Purpose |
|-------------|-----------|------|---------|
| `postgres-credential` | home-apps | `username`, `password` | PostgreSQL at 192.168.1.2:5432 |
| `qdrant-api-key` | db | `key` | Qdrant vector DB API key |
| `github-image-pull-secret` | home-apps | — | Pull from ghcr.io |
Use secrets via `secretKeyRef`:
```yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credential
key: password
```
## Infrastructure Services (in-cluster)
| Service | Address | Notes |
|---------|---------|-------|
| PostgreSQL | `192.168.1.2:5432` | Host-level install |
| Redis | `192.168.1.2:6379` | Bitnami Helm chart, `db` namespace |
| Qdrant | `qdrant.db.svc.cluster.local:6333` | Vector DB, `db` namespace |
| ArgoCD | `https://argo.junv.cc` | GitOps; add apps via ArgoCD UI or Application CR |
## Common Conventions
- **Timezone**: Always set `TZ: Australia/Melbourne`
- **Node pin**: `nodeSelector: kubernetes.io/hostname: server-3` for stateful workloads
- **Image mirror**: Prefix Docker Hub images with `mirror.gcr.io/` to avoid rate limits
- **Ingress class**: `ingressClassName: nginx` (not the legacy annotation)
- **TLS issuer**: `letsencrypt-prod` ClusterIssuer (email: me@junv.cc)
- **Namespace on PVC/Ingress**: Always specify `namespace:` — it's easy to forget
- **Namespace on PV**: PersistentVolumes are cluster-scoped — do NOT add `namespace:`
- **revisionHistoryLimit: 2**: Keep rollout history small
## Apply Workflow
```bash
# Apply a new manifest
kubectl apply -f home-apps/{app-name}.yaml
# Apply a local-PV config first (before the main manifest)
kubectl apply -f adhoc-config/{app-name}-local-pv.yaml
kubectl apply -f home-apps/{app-name}.yaml
# Check status
kubectl get pods -n {namespace}
kubectl describe pod -n {namespace} {pod-name}
kubectl logs -n {namespace} -l app={app-name} --tail=50
# Restart a deployment
kubectl rollout restart deployment/{app-name} -n {namespace}
# Get events (useful for debugging PVC bind issues)
kubectl get events -n {namespace} --sort-by='.lastTimestamp'
```
## Do NOT
- Delete files without user confirmation
- Use `iptables` integration in CrowdSec (breaks kubectl access to port 6443)
- Use inotify for container log acquisition in CrowdSec (use `poll_without_inotify: true`)
- Hardcode secrets in manifests — always use `secretKeyRef`
- Block `192.168.1.0/24` — it's the trusted local network
- Use SSH port 22 — the correct port is `22422`