17.9 CSI: Container Storage Interface and Third-Party Drivers
Right, so you’ve got your PersistentVolume and PersistentVolumeClaim objects all figured out. You can dynamically provision a volume with a StorageClass and feel pretty good about yourself. But let’s be honest: the built-in drivers for your cloud provider’s block storage are… fine. They get the job done. But what if your job is weirder? What if you need to talk to a storage system that isn’t AWS EBS, GCP PD, or Azure Disk? You know, something like Ceph, GlusterFS, MinIO, or that legacy NAS box in the corner that the storage team swears is “perfectly reliable”?
This is where the Container Storage Interface (CSI) comes in, and it’s one of the best ideas Kubernetes has ever had. Before CSI, if you wanted to add a new storage system, you had to literally fork and modify the core Kubernetes code. I’m not joking. It was a mess. The CSI standard fixed this by saying, “Look, the core Kubernetes code doesn’t need to know anything about your fancy storage hardware. It just needs to know how to talk to a standard interface. You write a driver that speaks that interface and knows how to talk to your specific storage backend.”
Think of it like a printer driver. Your laptop’s OS knows how to say “print a document.” The driver’s job is to translate that generic command into the bizarre, proprietary screeching that your specific brand of printer understands. CSI drivers are exactly that, but for storage in Kubernetes.
How CSI Drivers Actually Work (It’s Not Magic)
A CSI driver isn’t just a simple pod; it’s a full-blown distributed application deployed onto your cluster, typically as a DaemonSet and a StatefulSet. It has two main components:
- Node Plugin: Runs on every node (
DaemonSet). Its job is to handle the nitty-gritty of mounting and unmounting the storage volume to the actual host machine. This is the part that needs direct access to the host’s filesystem and is why it often requires elevated privileges. - Controller Plugin: Runs in a few highly available pods (
StatefulSet). This is the brain. It handles centralized operations like actually creating, deleting, and snapshotting volumes. It talks to the external storage system’s API.
When you create a PersistentVolumeClaim that references a StorageClass with a provisioner: csi.example.com, the CSI controller sees it, makes an API call to your storage system (e.g., “create a 10Gi volume”), and then creates a PersistentVolume object representing that new volume. The scheduler then places a pod that uses it on a node, and the CSI node plugin on that specific node gets the call to mount it.
Installing a CSI Driver: The Reality Check
You don’t just kubectl apply a simple YAML file. Well, you do, but that YAML is usually a massive bundle of manifests from the vendor that includes everything: the DaemonSet, StatefulSet, RBAC roles, StorageClass definitions, and CSIDriver objects. The installation process is the first hint that you’re dealing with something more complex.
Let’s say we’re installing the excellent CSI driver for the S3-compatible object storage MinIO. The manifests might look something like this (heavily simplified):
# This is the CustomResourceDefinition for the driver's specific parameters.
# Every driver has its own special sauce.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: buckets.storage.min.io
spec:
group: storage.min.io
names:
kind: Bucket
plural: buckets
scope: Namespaced
---
# This is the actual Deployment for the CSI controller
apiVersion: apps/v1
kind: Deployment
metadata:
name: minio-csi-controller
spec:
replicas: 2
selector:
matchLabels:
app: minio-csi-controller
template:
metadata:
labels:
app: minio-csi-controller
spec:
serviceAccountName: minio-csi-controller-sa
containers:
- name: csi-driver
image: minio/minio-csi:latest
args:
- --endpoint=$(CSI_ENDPOINT)
- --nodeid=$(NODE_ID)
- --type=controller
env:
- name: CSI_ENDPOINT
value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock
- name: MINIO_ENDPOINT # The critical part: telling it where your storage is
value: "https://minio-api.mycompany.com"
The key takeaway here is the env section. This is where you, the operator, have to provide the connection details for your actual storage backend. The driver is generic; this configuration is what makes it specific to your deployment.
Using a CSI Driver: It’s Just a StorageClass
The beautiful part is that for an end-user (a developer writing a Pod spec), nothing changes. The complexity is abstracted away behind a StorageClass. Once the driver is installed, you’ll have one or more new StorageClasses available.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: minio-csi-bucket
provisioner: minio.storage.min.io # This must match the driver's name
parameters:
# These parameters are 100% specific to the MinIO CSI driver.
# This is where the driver's custom CRD comes into play.
mounter: s3fs # How to mount the bucket (e.g., s3fs-fuse)
capacity: "5Gi" # A fun fiction for object storage, but Kubernetes expects a size
# Other secrets like accessKey/secretKey are usually handled via a Secret reference
And then the developer uses it exactly like always:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-app-bucket-claim
spec:
accessModes:
- ReadWriteMany # A good CSI driver can make object storage look like RWX
storageClassName: minio-csi-bucket
resources:
requests:
storage: 5Gi
The Rough Edges and Pitfalls
This is where my “brilliant friend” voice gets a little cynical. CSI is amazing, but it’s not perfect.
- The Quality Varies Wildly: Some CSI drivers are built and maintained by massive companies with dedicated teams. Others are a labor of love by one maintainer. The stability, performance, and feature set will reflect that. Do your homework.
- The Permissions Nightmare: The Node Plugin needs host privileges to mount things. This is a security team’s worst nightmare. You’ll need to use a Pod Security Admission policy or a Pod Security Context that carefully grants only the necessary capabilities (
CAP_SYS_ADMIN, I’m looking at you). Never run these pods asprivileged: trueif you can avoid it. - It’s a Translation Layer: Remember, it’s translating Kubernetes semantics into your storage system’s semantics. Sometimes that translation is lossy. If your storage system can’t do ReadWriteMany, the CSI driver can’t magically add that capability. It might just fail silently, or worse, lie and let your pod start only to have writes fail from other nodes.
- Debugging is a Deep, Dark Hole: When something goes wrong, you’re now debugging a three-way conversation between Kubernetes, the CSI driver pod, and the external storage API. Your
kubectl describe podoutput will only get you so far. You’ll live in the logs of the CSI driver pods.
The CSI model is fundamentally the right way to do this. It outsources complexity to experts who understand their specific storage system best. It lets Kubernetes be a generic orchestrator, which is what it’s good at. Just go in with your eyes open: you’re not just adding storage, you’re adding a whole new, complex service to your cluster. Treat it with the respect (and monitoring) it deserves.