Longhorn Storage Module¶
Terraform module for deploying Longhorn distributed block storage to Kubernetes. Provides persistent volumes with replication, snapshots, and backups for stateful workloads.
Architecture¶
flowchart TB
subgraph k8s [Kubernetes Cluster]
subgraph ns [Namespace: longhorn-system]
Manager[Longhorn Manager]
UI[Longhorn UI]
Driver[CSI Driver]
end
subgraph workloads [Workloads]
GitLab[(GitLab PVC)]
Prometheus[(Prometheus PVC)]
MinIO[(MinIO PVCs)]
end
subgraph node [Node Storage]
Disk1["Data Path: /var/lib/longhorn"]
Replica1[Volume Replicas]
end
subgraph ingress [Ingress Layer]
Nginx[NGINX Ingress]
OAuth[OAuth2 Proxy]
end
end
User[User Browser] --> Nginx
Nginx --> OAuth
OAuth --> UI
Manager --> Driver
Driver --> GitLab
Driver --> Prometheus
Driver --> MinIO
Manager --> Replica1
Replica1 --> Disk1
Volume Provisioning Flow¶
sequenceDiagram
participant App as Application
participant K8s as Kubernetes API
participant CSI as Longhorn CSI
participant Manager as Longhorn Manager
participant Node as Node Storage
App->>K8s: Create PVC (storageClass: longhorn)
K8s->>CSI: Provision volume
CSI->>Manager: Create volume request
Manager->>Node: Allocate storage on disk
Manager->>Manager: Create replica(s)
Manager-->>CSI: Volume ready
CSI-->>K8s: PV created and bound
K8s-->>App: PVC bound, ready to use
Node Topology¶
Storage nodes and compute nodes are separated. Only nodes labeled node.longhorn.io/create-default-disk=true get a Longhorn disk, so replicas stay on the control plane. Tainted workers run the CSI stack so their pods can mount volumes, but hold no data.
flowchart TB
PodApp["Pod with Longhorn PVC<br/>scheduled on a worker"]
CSIWorker["Worker: longhorn-manager + csi-plugin<br/>tolerates node.homelab/class<br/>no disk, no replicas"]
StorageCP["Control plane: labeled create-default-disk=true<br/>holds the disk and every replica"]
PodApp --> CSIWorker
CSIWorker -->|"volume attached over the network"| StorageCP
The label is applied by kubernetes_labels.longhorn_default_disk, not by hand. Longhorn evaluates createDefaultDiskLabeledNodes only the first time it detects a node, so the label must exist before helm_release.longhorn runs. It is a hard precondition: without it a rebuilt control plane comes up with no disk and every PVC stays Pending, and nothing fails at apply time to tell you why.
Enabling the setting does not remove disks from nodes Longhorn already knows about. A worker that joined while the setting was disabled keeps the default disk it was given; remove that disk in the Longhorn UI if you want it compute-only.
Applying toleration changes¶
defaultSettings.taintToleration is a Longhorn Danger Zone setting. Longhorn will not reconcile it into a component that has running engines or replicas, so on a cluster with attached volumes the Setting CR can sit at APPLIED=false indefinitely:
kubectl -n longhorn-system get setting taint-toleration
# NAME VALUE APPLIED
# taint-toleration node.homelab/class:NoSchedule false
APPLIED=false does not mean the toleration is missing everywhere. A newly joined node's instance-manager has no running engines, so it receives the toleration immediately. What lags is the control plane's own instance-manager, which is harmless here because the control plane is untainted. Check the component that actually matters rather than the flag:
kubectl -n longhorn-system get ds longhorn-csi-plugin \
-o jsonpath='{.spec.template.spec.tolerations}'
kubectl -n longhorn-system get pods -o wide \
--field-selector spec.nodeName=<worker-node>
To force full reconciliation, detach every volume first, then re-apply:
kubectl scale deploy/<app> --replicas=0 -n <namespace> # detach the workloads
kubectl -n longhorn-system get volumes.longhorn.io \
-o custom-columns=NAME:.metadata.name,STATE:.status.state # all must read "detached"
# terraform apply, then scale back up
Resources Created¶
kubernetes_namespace.longhorn- Dedicated namespace (longhorn-system)kubernetes_secret.frontend_basic_auth- Basic auth for UIdata.kubernetes_nodes.control_plane- Looks up the control-plane node to labelkubernetes_labels.longhorn_default_disk- Marks the control plane as the storage nodehelm_release.longhorn- Longhorn Helm chartnull_resource.remove_default- Removes default StorageClass from local-path
Variables¶
| Name | Description | Default |
|---|---|---|
nginx_frontend_basic_auth_base64 |
Base64 encoded basic auth | (required, sensitive) |
longhorn_default_settings_default_data_path |
Data storage path on nodes | /var/lib/longhorn |
longhorn_ingress_class_name |
Ingress class for UI | (required) |
longhorn_ingress_host |
Hostname for UI access | (required) |
longhorn_ingress_enable_tls |
Enable TLS for UI | true |
auth_oauth2_proxy_host |
OAuth2 proxy for authentication | auth.chrislee.local |
Usage¶
Configure Storage Path¶
Set custom data path if needed:
Configure UI Access¶
Access Dashboard¶
Navigate to https://longhorn.chrislee.local (OAuth2 protected).
Helm Chart¶
| Property | Value |
|---|---|
| Repository | https://charts.longhorn.io |
| Chart | longhorn |
StorageClass¶
After deployment, longhorn becomes the default StorageClass:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 10Gi
Features¶
| Feature | Description |
|---|---|
| Replication | Configurable replica count for data redundancy |
| Snapshots | Point-in-time volume snapshots |
| Backups | Backup to S3-compatible storage (MinIO) |
| Expansion | Online volume expansion |
| Encryption | Volume encryption at rest |
| DR | Disaster recovery with cross-cluster replication |