Chapter 2: Set Up the Project
We're building localdir-csi — a driver that provisions volumes as plain
directories on the node's local disk. It's not a driver you'd run in
production (no real attach/detach story, no replication), but every RPC
you implement here has the exact same signature a real EBS or NFS driver
would use. When you're done, the RPC contract carries over — the actual
work of retargeting CreateVolume at a real storage API, with durable
metadata and real attach/detach, does not.
Prerequisites
You'll need:
- Go 1.24+ (tests from Chapter 3 onward use
t.Context(), a method added to thetestingpackage in Go 1.24 — more on why in Chapter 3) - Docker (or another
kind-compatible container runtime) kind,kubectl
Install kind if you don't have it:
go install sigs.k8s.io/kind@latest
The project skeleton
mkdir localdir-csi && cd localdir-csi
go mod init github.com/yourname/localdir-csi
Layout we're building toward — don't create all of this now, just know where things are headed:
localdir-csi/
├── cmd/
│ └── localdir-csi/
│ └── main.go # entrypoint: parse flags, start gRPC server
├── internal/
│ └── driver/
│ ├── driver.go # Driver struct, shared by all three services
│ ├── identity.go # Identity service RPCs
│ ├── controller.go # Controller service RPCs
│ └── node.go # Node service RPCs
├── deploy/
│ ├── csidriver.yaml
│ ├── controller.yaml # Deployment: driver + provisioner + attacher sidecars
│ └── node.yaml # DaemonSet: driver + registrar sidecar
├── go.mod
└── go.sum
Every RPC method you write goes on Driver in internal/driver/. One
struct, three interfaces satisfied (csi.IdentityServer,
csi.ControllerServer, csi.NodeServer) — that's the whole shape of a CSI
driver.
Pulling in the CSI spec package
The CSI project publishes the generated Go types and gRPC service
interfaces so you never write .proto files yourself:
go get github.com/container-storage-interface/spec/lib/go/csi
go get google.golang.org/grpc
Confirm it resolved:
go list -m github.com/container-storage-interface/spec
Everything you implement in this book is satisfying interfaces from that one package. Worth opening it once now to see what's there:
go doc github.com/container-storage-interface/spec/lib/go/csi.IdentityServer
You should see three method signatures — GetPluginInfo,
GetPluginCapabilities, Probe. That's the entire Identity service. We
implement it in the next chapter.
The kind cluster
CSI drivers are the kind of thing that's genuinely painful to develop
against a real cloud cluster — every iteration means pushing an image
somewhere reachable. kind runs Kubernetes in Docker containers on your
laptop, and critically, its nodes are real Linux containers with a real
kubelet, so mount calls in your Node service behave exactly like they
would on a cloud VM.
kind create cluster --name csi-dev
kubectl cluster-info --context kind-csi-dev
One kind-specific thing worth knowing now, because it'll save you
confusion in Chapter 4: kind nodes are containers, so when your Node
service later runs mount --bind, it's bind-mounting inside that
container's filesystem, not your laptop's. That's fine and expected — it's
exactly how a real node's kubelet and CSI node plugin relate — but if you
go looking for the volume directories on your actual machine, you won't
find them. You'll docker exec into the kind node to look, and we'll do
that together in Chapter 6.
Loading local images into kind (you'll use this constantly once we start
building the image in Chapter 4):
docker build -t localdir-csi:dev .
kind load docker-image localdir-csi:dev --name csi-dev
A Makefile for the repeated stuff
You'll run the same handful of commands dozens of times over the next ten
chapters — build the image, load it into kind, apply manifests, tear the
cluster down and stand up a fresh one when state gets weird. Worth wrapping
those now rather than retyping them:
CLUSTER := csi-dev
IMAGE := localdir-csi:dev
.PHONY: kind-up kind-down build load deploy logs clean
kind-up:
kind create cluster --name $(CLUSTER)
kind-down:
kind delete cluster --name $(CLUSTER)
build:
docker build -t $(IMAGE) .
load: build
kind load docker-image $(IMAGE) --name $(CLUSTER)
deploy: load
kubectl apply -f deploy/
kubectl rollout restart deployment/localdir-csi-controller daemonset/localdir-csi-node 2>/dev/null || true
kubectl rollout status deployment/localdir-csi-controller --timeout=60s 2>/dev/null || true
kubectl rollout status daemonset/localdir-csi-node --timeout=60s 2>/dev/null || true
logs:
kubectl logs -l app=localdir-csi -c localdir-csi --tail=100 -f
clean: kind-down
rm -f deploy/*.generated.yaml
Save that as Makefile in the project root. A couple of things worth
noting about it, since they'll matter as the book goes on:
loaddepends onbuild, anddeploydepends onload—make deployalone rebuilds the image, reloads it into the cluster, and applies manifests, in the right order, every time. No more forgetting you editednode.goand re-applying stale YAML against an old image.- Rebuilding and reloading the image is not, by itself, enough to make a
running pod pick up your change. Every rebuild reuses the same tag,
localdir-csi:dev— that's deliberate, so you never have to edit YAML just to bump a tag — but Kubernetes only pulls or restarts a container when something about its spec changes, and an unchanged tag string looks unchanged to it even though the bytes behind it inkind's local image store are new. A pod that was already running keeps running whatever image it already pulled.kubectl rollout restartis the real fix for exactly this: it forces the Deployment/DaemonSet to replace their pods, and the replacements pull the tag fresh fromkind's image store — where the bytes you just loaded are now waiting. The|| trueon all three lines exists because before Chapter 4 there's nodeployment/ordaemonset/for these commands to find yet; once they exist,make deployalways leaves both the Controller and every Node pod running the binary you just built, not whatever was running before. deploy/doesn't exist with real manifests yet — that starts in Chapter 4. The target is here now so the Makefile grows with the book instead of getting rewritten later.logsfilters to the driver container specifically (-c localdir-csi), because once sidecars are in the picture in Chapter 4,kubectl logson a pod with five containers is useless without-c.
From here on, when a chapter says "build and deploy," it means make deploy.
What you should have now
go.modwith thecsiandgrpcpackages resolved- A
kindcluster namedcsi-devup and reachable viakubectl(or runmake kind-up) - A
Makefilewithbuild/load/deploy/logstargets - An empty
cmd/andinternal/driver/ready for Chapter 3
Next chapter we write the first real code: a Driver struct, a gRPC
server listening on a Unix socket, and the three-method Identity service —
small enough to finish in one sitting, and the first point where you can
actually run something and watch it answer a gRPC call.