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 the testing package 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:

  • load depends on build, and deploy depends on loadmake deploy alone rebuilds the image, reloads it into the cluster, and applies manifests, in the right order, every time. No more forgetting you edited node.go and 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 in kind's local image store are new. A pod that was already running keeps running whatever image it already pulled. kubectl rollout restart is the real fix for exactly this: it forces the Deployment/DaemonSet to replace their pods, and the replacements pull the tag fresh from kind's image store — where the bytes you just loaded are now waiting. The || true on all three lines exists because before Chapter 4 there's no deployment/ or daemonset/ for these commands to find yet; once they exist, make deploy always 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.
  • logs filters to the driver container specifically (-c localdir-csi), because once sidecars are in the picture in Chapter 4, kubectl logs on 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.mod with the csi and grpc packages resolved
  • A kind cluster named csi-dev up and reachable via kubectl (or run make kind-up)
  • A Makefile with build / load / deploy / logs targets
  • An empty cmd/ and internal/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.