# Traefik LXC → K8s Migration (2026-07-14)

## CT99999 Inventory (Source)

- **Location:** proxmox7, VLAN 60, IP `10.0.60.10/24`
- **Traefik version:** 3.4.1
- **Static config:** `/etc/traefik/traefik.yaml`
- **Dynamic config:** `/etc/traefik/conf.d/` (file provider, watch=true)
- **TLS:** Let's Encrypt via TLS challenge, `acme.json` at `/etc/traefik/ssl/acme.json`
- **Entry Points:** web (:80 → redirect to :443), websecure (:443), traefik (:8080 dashboard)

## Services on CT99999

| Router | Host | Backend | Port | Notes |
|--------|------|---------|------|-------|
| grafana | grafana.familie-schoen.com | 10.0.30.141 | 3000 | CT141 monitoring |
| librechat | chat.familie-schoen.com, chat.schoen.eu | 10.0.30.102 | 8080 | LibreChat VM |
| seafile_old | seafile.familie-schoen.com | 10.0.30.100 | 8081 | Seafile old |
| imkerei | imkerei.familie-schoen.com | 10.0.30.100 | 80 | Catchall forwarder |
| immich | immich.familie-schoen.com | 10.0.30.99 | 2283 | Immich |
| paperless | dokumente.familie-schoen.com | 10.0.30.101 | 8000 | CT104 paperless |
| homeassistant | homeassistant.familie-schoen.com | 10.0.30.10 | 8123 | VM106 HA |
| seafile | cloud.familie-schoen.com | 10.0.30.99 | 80 | Seafile |
| seadoc | cloud.familie-schoen.com + /sdoc-server | 10.0.30.99 | 8888 | SeaDoc, stripPrefix middleware |
| authelia | auth.familie-schoen.com | 192.168.100.11 | 9091 | CT112 authelia |
| authelia-admin | auth-admin.familie-schoen.com | 192.168.100.11 | 9092 | CT112 authelia admin |
| gitea | git.familie-schoen.com | 10.0.30.105 | 3000 | CT108 (being replaced by K8s) |
| catchall-tcp | HostSNI(`*`) passthrough | 10.0.30.100 | 80 | TCP catchall |

## K8s Traefik (Target)

- **Type:** DaemonSet `rke2-traefik` in `kube-system`
- **Ports:** hostPort 80 + 443 on all 6 nodes (10.0.30.51-53, .61-63)
- **IngressClass:** `traefik`
- **Existing Ingresses:** argocd-server (argocd ns), gitea (gitea ns)
- **Needs:** Let's Encrypt certResolver (not configured by default)

## HA Failover VIP via Cilium L2 (Implemented 2026-07-14)

### Architecture

Instead of pointing the firewall NAT at a single K8s node (SPOF), a
`LoadBalancer` Service requests a VIP from the existing Cilium LB IPAM
pool. Cilium announces the VIP via ARP on one node; auto-failover to
another node on failure (~1-2s).

```
Firewall NAT → 10.0.30.203 (Cilium VIP)
                    ↓ L2 Announcement (ARP)
              [aktiver K8s Node — Traefik DaemonSet]
              Automatisches Failover bei Node-Ausfall (~1-2s)
```

### Components Deployed

| Component | File | Purpose |
|-----------|------|---------|
| `Service` (LoadBalancer) | `clusters/main/proxy/traefik-lb-service.yaml` | VIP `.203`, selects Traefik pods |
| `Namespace` | `clusters/main/proxy/namespace.yaml` | Shared proxy namespace |
| ArgoCD App | `clusters/main/apps/proxy.yaml` | GitOps for proxy namespace |

### traefik-lb-service.yaml

```yaml
apiVersion: v1
kind: Service
metadata:
  name: traefik-lb
  namespace: kube-system
spec:
  type: LoadBalancer
  loadBalancerIP: 10.0.30.203
  selector:
    app.kubernetes.io/instance: rke2-traefik-kube-system
    app.kubernetes.io/name: rke2-traefik
  ports:
    - name: web
      port: 80
      targetPort: web
    - name: websecure
      port: 443
      targetPort: websecure
```

### Why Cilium L2 (not MetalLB/keepalived)

- Cilium L2 LB IPAM already running (3 services use it: .200, .201, .202)
- No additional software needed
- Automatic failover — no manual VIP management
- Pool: `10.0.30.200–250` (CiliumLoadBalancerIPPool `default-pool`)
- L2 policy: `default-l2-policy` announces LB IPs via ARP on `^eth[0-9]+`

### Verification

After deploying, tested from mgmt-runner (.124):
- `10.0.30.203:80/443` → 404 (no default route — correct)
- `git.schoen.codes` via VIP → Gitea 1.26.1 ✅
- `argocd` via VIP → ArgoCD UI ✅

### Firewall Cutover

Change NAT target: `10.0.60.10` → `10.0.30.203` (one rule, instant cutover).
Rollback: revert NAT to `10.0.60.10`. CT99999 stays running throughout.

## Pattern for External Backends (not in K8s)

Use `ExternalName` Service + `IngressRoute` CRD:

```yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: grafana
  namespace: proxy
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`grafana.familie-schoen.com`)
      kind: Rule
      services:
        - name: grafana-external
          port: 3000
  tls:
    certResolver: letsencrypt
---
apiVersion: v1
kind: Service
metadata:
  name: grafana-external
  namespace: proxy
spec:
  type: ExternalName
  externalName: 10.0.30.141
```

## SeaDoc StripPath Middleware

```yaml
middlewares:
  seadoc-strippath:
    stripPrefix:
      prefixes:
        - /sdoc-server
```

## Accessing CT99999

```bash
# CT99999 is on proxmox7 (10.0.20.70)
ssh -i ~/.ssh/id_ed25519_proxmox root@10.0.20.70 "pct exec 99999 -- ..."
```

## Remaining Steps

1. Add Let's Encrypt certResolver via HelmChartConfig for `rke2-traefik`
2. Create IngressRoute + ExternalName Service for each of the 11 external backends
3. Handle catchall-TCP (make specific, not HostSNI(`*`) — see pitfall in SKILL.md)
4. Cutover firewall NAT: `10.0.60.10` → `10.0.30.203`
5. Verify each service, then decommission CT99999 after 1 week

## Reference

Full migration plan: `docs/plans/2026-07-14-traefik-ct99999-k8s-migration.md` in iac-homelab repo.
