diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md new file mode 100644 index 0000000..4fb532c --- /dev/null +++ b/deploy/kubernetes/README.md @@ -0,0 +1,88 @@ +# Deploying Rackpad on Kubernetes + +Plain, dependency-free manifests for running Rackpad on any Kubernetes cluster. +They mirror [`docker-compose.yml`](../../docker-compose.yml): a single replica, +a non-root container, a read-only root filesystem, and the SQLite database on a +PersistentVolume. + +## Quick start + +```bash +# Apply everything (namespace, PVC, deployment, service): +kubectl apply -k deploy/kubernetes/ + +# ...or apply the files individually: +kubectl apply -f deploy/kubernetes/namespace.yaml +kubectl apply -f deploy/kubernetes/pvc.yaml +kubectl apply -f deploy/kubernetes/deployment.yaml +kubectl apply -f deploy/kubernetes/service.yaml +``` + +Reach the UI with a port-forward: + +```bash +kubectl -n rackpad port-forward svc/rackpad 3000:3000 +# open http://localhost:3000 +``` + +To expose it permanently, edit and apply [`ingress.yaml`](./ingress.yaml) +(set the host, `ingressClassName`, and TLS), or use a LoadBalancer service. + +## What's in here + +| File | Purpose | +| --- | --- | +| `namespace.yaml` | `rackpad` namespace | +| `pvc.yaml` | 2Gi RWO volume for the SQLite DB at `/data` | +| `deployment.yaml` | Single-replica, non-root, read-only-rootfs deployment | +| `service.yaml` | ClusterIP service on port 3000 | +| `ingress.yaml` | Optional ingress example (commented defaults) | +| `kustomization.yaml` | Bundles the core resources for `kubectl apply -k` | + +## Notes + +- **Image tag** — the manifests use `:latest`. Pin to a + [released tag](https://github.com/Kobii-git/rackpad/releases) for production. +- **Storage** — the PVC uses the cluster's default StorageClass. SQLite relies + on file locking, so prefer block storage (iSCSI, local-path, Ceph RBD) over + NFS. Never run more than one replica (`strategy: Recreate`, `replicas: 1`). +- **Configuration** — every environment variable from the Compose files is + supported. The deployment sets the common ones; uncomment `APP_URL` / + `TRUST_PROXY` when running behind an ingress, and add the `OIDC_*` variables + to enable SSO (see [`docs/OIDC.md`](../../docs/OIDC.md)). + +## Network discovery on Kubernetes + +Discovery and SNMP have networking requirements that the minimal-privilege +deployment above deliberately does **not** grant. See +[`docs/DISCOVERY_DEPLOYMENT.md`](../../docs/DISCOVERY_DEPLOYMENT.md) for the full +background; the short version for Kubernetes: + +- **Layer-3 scans (ICMP / TCP / HTTP reachability) and SNMP discovery** work + from a normal pod — they only need routing to your device subnets plus a raw + socket. Grant `NET_RAW` (and `NET_BIND_SERVICE` for the SNMP trap listener). + Because Kubernetes can't give ambient capabilities to a non-root uid, run the + container as root with everything else dropped: + + ```yaml + # deployment.yaml — pod securityContext: + securityContext: + runAsUser: 0 + runAsGroup: 0 + fsGroup: 0 + # ...and the container securityContext: + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + add: ["NET_RAW", "NET_BIND_SERVICE"] + drop: ["ALL"] + ``` + +- **Layer-2 scans (ARP / MAC capture)** require the pod to share a broadcast + domain with the targets — a normal pod on a routed CNI cannot see client MACs + and will return empty results. To capture MACs on the node's own subnet, also + set `hostNetwork: true` and add `NET_ADMIN` (the equivalent of + [`docker-compose.host-discovery.yml`](../../docker-compose.host-discovery.yml)'s + `network_mode: host`). For devices on other VLANs, prefer SNMP against your + switches instead. diff --git a/deploy/kubernetes/deployment.yaml b/deploy/kubernetes/deployment.yaml new file mode 100644 index 0000000..a29cf44 --- /dev/null +++ b/deploy/kubernetes/deployment.yaml @@ -0,0 +1,98 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rackpad + namespace: rackpad + labels: + app.kubernetes.io/name: rackpad +spec: + replicas: 1 + # SQLite is single-writer; never scale this beyond one replica. + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: rackpad + template: + metadata: + labels: + app.kubernetes.io/name: rackpad + spec: + # Image runs as the non-root "rackpad" user (uid/gid 10001). + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + fsGroup: 10001 + seccompProfile: + type: RuntimeDefault + containers: + - name: rackpad + # Pin to a released tag for production: https://github.com/Kobii-git/rackpad/releases + image: ghcr.io/kobii-git/rackpad:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 3000 + env: + - name: NODE_ENV + value: production + - name: HOST + value: 0.0.0.0 + - name: PORT + value: "3000" + - name: DATABASE_PATH + value: /data/rackpad.db + # Periodically re-checks existing device monitors (up/down/latency). + # Set to 0 to disable. This is NOT subnet discovery. + - name: MONITOR_INTERVAL_MS + value: "300000" + # Set this to your external URL when behind an ingress/reverse proxy, + # e.g. https://rackpad.example.com — needed for OIDC and CSRF origins. + # - name: APP_URL + # value: "" + # - name: TRUST_PROXY + # value: "1" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data + - name: tmp + mountPath: /tmp + livenessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /api/health + port: http + periodSeconds: 10 + timeoutSeconds: 5 + startupProbe: + httpGet: + path: /api/health + port: http + periodSeconds: 5 + failureThreshold: 30 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 512Mi + volumes: + - name: data + persistentVolumeClaim: + claimName: rackpad-data + # rootfs is read-only; the app needs a writable /tmp. + - name: tmp + emptyDir: {} diff --git a/deploy/kubernetes/ingress.yaml b/deploy/kubernetes/ingress.yaml new file mode 100644 index 0000000..546ca04 --- /dev/null +++ b/deploy/kubernetes/ingress.yaml @@ -0,0 +1,26 @@ +# Optional. Exposes rackpad over HTTP(S). Adjust the host, ingressClassName, and +# TLS to match your cluster. If you use cert-manager, add the issuer annotation. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: rackpad + namespace: rackpad + # annotations: + # cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + # ingressClassName: nginx + rules: + - host: rackpad.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: rackpad + port: + number: 3000 + # tls: + # - hosts: + # - rackpad.example.com + # secretName: rackpad-tls diff --git a/deploy/kubernetes/kustomization.yaml b/deploy/kubernetes/kustomization.yaml new file mode 100644 index 0000000..bfaf78d --- /dev/null +++ b/deploy/kubernetes/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - pvc.yaml + - deployment.yaml + - service.yaml + # Optional — review the host/class/TLS first, then uncomment: + # - ingress.yaml diff --git a/deploy/kubernetes/namespace.yaml b/deploy/kubernetes/namespace.yaml new file mode 100644 index 0000000..ffc1d88 --- /dev/null +++ b/deploy/kubernetes/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: rackpad diff --git a/deploy/kubernetes/pvc.yaml b/deploy/kubernetes/pvc.yaml new file mode 100644 index 0000000..6af2aa9 --- /dev/null +++ b/deploy/kubernetes/pvc.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rackpad-data + namespace: rackpad +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + # Holds the SQLite database (DATABASE_PATH=/data/rackpad.db). A few GiB + # is plenty for a homelab inventory. + storage: 2Gi + # Uses the cluster default StorageClass. Uncomment and set this if you want a + # specific one. SQLite relies on file locking, so prefer block storage + # (e.g. iSCSI / local-path / Ceph RBD) over NFS, where lock semantics vary. + # storageClassName: "" diff --git a/deploy/kubernetes/service.yaml b/deploy/kubernetes/service.yaml new file mode 100644 index 0000000..056f77f --- /dev/null +++ b/deploy/kubernetes/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: rackpad + namespace: rackpad + labels: + app.kubernetes.io/name: rackpad +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: rackpad + ports: + - name: http + port: 3000 + targetPort: http