Architecture
Blockstor is built like a Kubernetes operator. The desired state of every node, pool and volume is a custom resource, and controllers keep working until the cluster matches it. Nothing important lives only in a process’s memory, so any blockstor pod can restart without losing state.
Components
flowchart TB
subgraph clients["Clients"]
csi["linstor-csi"]
other["piraeus-operator, ha-controller, golinstor"]
cli["blockstor CLI"]
end
subgraph k8s["Kubernetes API"]
crds[("blockstor custom resources")]
end
subgraph cp["blockstor-system"]
api["blockstor-apiserver<br/>LINSTOR-compatible REST, mTLS"]
ctrl["blockstor-controller<br/>placement and allocation"]
end
subgraph nodes["Storage nodes"]
sat1["satellite on node 1"]
sat2["satellite on node 2"]
sat3["satellite on node 3"]
end
csi -->|REST| api
other -->|REST| api
cli -->|kubeconfig| crds
api <--> crds
ctrl <--> crds
crds <--> sat1
crds <--> sat2
crds <--> sat3- blockstor-apiserver serves the LINSTOR REST API over mutual TLS. It does not keep state of its own: every request becomes a read or a write of custom resources. It runs as several replicas behind one Service.
- blockstor-controller watches the custom resources and fills in what users do not choose: which nodes get replicas, the DRBD port, minor numbers and node IDs, and tiebreakers and quorum settings.
- blockstor-satellite runs on every storage node. It creates the logical volumes or zvols, sets up LUKS and DRBD, and reports what it sees back into the custom resources.
- The
blockstorCLI reads and writes the custom resources directly with your kubeconfig, without going through the REST API.
There is no direct connection between the controller and the satellites. Everything goes through the Kubernetes API.
Custom resources are the source of truth
Every object you know from LINSTOR is a cluster-scoped custom resource in the blockstor.cozystack.io group:
flowchart LR
RG["ResourceGroup<br/>policy: place count, pool, layers"]
RD["ResourceDefinition<br/>one volume set, its size and layers"]
R1["Resource<br/>replica on node 1"]
R2["Resource<br/>replica on node 2"]
R3["Resource<br/>tiebreaker on node 3"]
SP["StoragePool<br/>per node"]
N["Node"]
S["Snapshot"]
RG --> RD
RD --> R1
RD --> R2
RD --> R3
R1 -. lives in .-> SP
SP -. on .-> N
RD --> SEach resource has two halves:
- spec is what you, the controller or the API asked for.
- status is what the satellites observed: disk states, connection states, free capacity.
The split is a rule of thumb with a few exceptions: a satellite writes a replica’s spec.flags while it toggles a disk and records the storage it discovered in the node’s spec.props, and the API clears a replica’s DRBD port in status when you ask it to reallocate the port. Because the state is ordinary Kubernetes objects, you can read it with kubectl get resources, back it up with your usual tools, and drive it from GitOps.
How a volume is created
This is the path from a PersistentVolumeClaim to replicas on disk:
sequenceDiagram
autonumber
participant PVC as PVC
participant CSI as linstor-csi
participant API as blockstor-apiserver
participant K as Kubernetes API
participant C as blockstor-controller
participant S as satellites
PVC->>CSI: provision a volume
CSI->>API: create the resource definition and place it
API->>K: write ResourceDefinition and Resources
K-->>C: change seen
C->>K: pick nodes, allocate DRBD port, minors and node IDs, add a tiebreaker
K-->>S: each satellite sees the Resources for its node
S->>S: create LV or zvol, set up LUKS and DRBD
S->>K: report disk and connection state
API-->>CSI: volume is ready
CSI-->>PVC: bound, device /dev/drbdNEach satellite watches only its own node
A satellite reacts only to the resources assigned to its node: replicas and pools whose node name is its own. Adding nodes adds satellites, and none of them has to talk to a central process.
flowchart LR
subgraph K["Kubernetes API"]
r1["Resource pvc-1 on node-1"]
r2["Resource pvc-1 on node-2"]
r3["Resource pvc-2 on node-1"]
end
s1["satellite node-1"]
s2["satellite node-2"]
r1 --> s1
r3 --> s1
r2 --> s2Layers on a node
Every replica is a stack of layers. The satellite builds it from the bottom up when it creates a replica and takes it apart from the top down when it removes one.
flowchart TB
pod["Pod sees /dev/drbdN"]
drbd["DRBD: replication to the other nodes"]
luks["LUKS: encryption at rest, optional"]
storage["STORAGE: LVM LV, LVM-thin LV, zvol or file"]
pod --> drbd --> luks --> storageThe default stack is DRBD on top of STORAGE. See Layer stack for the other combinations.
Replicas, tiebreakers and quorum
With an even number of data replicas, the controller adds a diskless tiebreaker on another node, so DRBD always has a majority to decide which side keeps writing after a network split. A replica without a disk can also serve I/O over the network to a pod on a node that holds no copy of the data.