6.4 CSI
PVC를 만들면 볼륨이 생기고, Pod를 만들면 그 볼륨이 컨테이너 경로에 나타난다. 앞의 장들이 다룬 이 흐름에서 Kubernetes 본체가 직접 하는 일은 생각보다 적다. 스토리지 시스템에 볼륨을 만들고, 노드에 디스크를 연결하고, 파일시스템을 마운트하는 실제 작업은 전부 드라이버의 몫이다. 그 드라이버가 Kubernetes와 만나는 표준 접점이 CSI(Container Storage Interface)다.
in-tree의 문제와 CSI 이관
초기 Kubernetes는 스토리지 드라이버를 본체 안에 포함하고 있었다. AWS EBS, GCE PD, Azure Disk 같은 볼륨 플러그인의 코드가 kubernetes/kubernetes 저장소 안에 있어서 in-tree 플러그인이라 부른다. 이 구조의 문제는 결합이다. 드라이버 버그 수정이 Kubernetes 릴리스 일정에 묶이고, 스토리지 벤더는 자기 코드를 Kubernetes 프로젝트의 리뷰와 릴리스 절차에 맞춰야 하며, 드라이버 코드의 결함이 kubelet이나 kube-controller-manager 같은 코어 컴포넌트의 안정성까지 위협한다. 새 스토리지를 지원할 때마다 코어 코드가 불어나는 방향이 지속 가능하지 않다는 것도 분명했다.
CSI는 이 결합을 끊는 표준 인터페이스다. 드라이버는 Kubernetes 바깥(out-of-tree)에서 별도의 컨테이너로 배포되고, 정해진 gRPC 인터페이스만 구현하면 어떤 스토리지든 연동된다. 드라이버의 릴리스 주기가 Kubernetes에서 독립하고, 드라이버의 장애가 코어 컴포넌트와 격리된다.
기존 in-tree 플러그인은 CSIMigration 메커니즘으로 이관됐다. in-tree 볼륨 타입을 참조하는 기존 매니페스트의 처리를 대응하는 CSI 드라이버로 투명하게 전달하는 방식이라, awsElasticBlockStore 같은 필드를 쓰는 오래된 PV도 실제 동작은 CSI 드라이버가 담당한다. 코어 CSIMigration 메커니즘은 1.25에서 GA가 됐고, 주요 클라우드 볼륨 플러그인의 in-tree 코드는 이후 릴리스에서 순차적으로 제거됐다. 지금 클러스터에 스토리지를 연동한다는 말은 곧 CSI 드라이버를 설치한다는 말이다.
controller 플러그인과 node 플러그인
CSI 드라이버 하나는 역할이 다른 두 부분으로 나뉘어 배포된다.
controller 플러그인은 스토리지 시스템의 관리 API와 대화하는 쪽이다. 볼륨 생성과 삭제(CreateVolume, DeleteVolume), 볼륨을 노드에 연결하고 해제하는 attach와 detach(ControllerPublishVolume, ControllerUnpublishVolume), 볼륨 확장과 스냅샷 생성이 여기에 속한다. 어느 노드에서 실행해도 되는 작업이라 보통 Deployment로 배포된다.
node 플러그인은 노드 로컬 작업을 담당한다. 노드에 연결된 디스크를 포맷해 스테이징 경로에 마운트하고(NodeStageVolume), 그 경로를 Pod의 볼륨 디렉터리에 bind mount한다(NodePublishVolume). 마운트는 해당 노드의 커널에서 일어나야 하므로 DaemonSet으로 모든 노드에 배포되고, kubelet이 노드 안의 unix 소켓으로 이 플러그인을 직접 호출한다.
| 구분 | controller 플러그인 | node 플러그인 |
|---|---|---|
| 배포 형태 | Deployment | DaemonSet |
| 실행 위치 | 임의 노드 | 볼륨을 쓸 모든 노드 |
| 담당 호출 | CreateVolume, ControllerPublishVolume | NodeStageVolume, NodePublishVolume |
| 대화 상대 | 스토리지 관리 API | 노드 커널, kubelet |
사이드카 컨테이너 군단
드라이버가 구현하는 것은 gRPC 서비스뿐이다. Kubernetes API를 watch하고 오브젝트를 만드는 일은 커뮤니티가 표준으로 제공하는 사이드카 컨테이너들이 담당한다. 드라이버 Pod 안에 드라이버 컨테이너와 나란히 배치되어, Kubernetes API에서 벌어지는 사건을 드라이버에 대한 gRPC 호출로 변환한다.
external-provisioner는 controller 플러그인 옆에서 PVC를 watch한다. 자신의 드라이버가 담당할 PVC가 생기면 CreateVolume을 호출하고, 성공하면 PV 오브젝트를 만들어 바인딩으로 이어지게 한다. 6.3의 동적 프로비저닝은 이 사이드카가 실행하는 동작이다.
external-attacher는 VolumeAttachment 오브젝트를 watch해 ControllerPublishVolume을 호출한다. attach라는 단계 자체가 없는 NFS 계열 스토리지는 CSIDriver 오브젝트에 attachRequired: false를 선언해 이 단계를 통째로 생략한다.
node-driver-registrar는 node 플러그인 옆에 붙어 드라이버의 소켓을 kubelet의 플러그인 등록 메커니즘에 등록한다. 등록이 끝난 뒤로는 개입하지 않고, kubelet이 드라이버를 직접 호출한다.
이 밖에 볼륨 확장을 처리하는 external-resizer, 스냅샷을 처리하는 external-snapshotter가 같은 패턴으로 동작한다. 사이드카 방식의 이득은 재사용이다. API watch, 재시도, leader election 같은 공통 로직을 드라이버마다 다시 만들지 않고, 드라이버는 스토리지 시스템과의 대화에만 집중한다.
볼륨이 Pod에 연결되기까지
PVC 생성부터 컨테이너가 파일시스템을 보기까지는 provision, attach, mount 세 단계를 서로 다른 주체가 이어서 진행한다.
flowchart TD
A["PVC 생성"] --> B["external-provisioner<br/>CreateVolume 호출"]
B --> C["PV 생성·바인딩"]
C --> D["Pod 노드 확정"]
D --> E["VolumeAttachment 생성"]
E --> F["external-attacher<br/>ControllerPublishVolume"]
F --> G["디스크가 노드에 연결됨"]
G --> H["NodeStageVolume<br/>포맷·스테이징 마운트"]
H --> I["NodePublishVolume<br/>Pod 경로 bind mount"]
I --> J["컨테이너 기동"]
WaitForFirstConsumer 클래스라면 provision이 Pod의 노드 확정 이후로 미뤄진다는 것이 6.3의 내용이었다. attach 단계는 kube-controller-manager 안의 attach/detach controller가 VolumeAttachment 오브젝트를 만드는 것으로 시작되고, external-attacher가 이를 받아 스토리지 API를 호출해 디스크를 노드에 장치로 연결한다. mount 단계는 kubelet 주도다. NodeStageVolume은 노드당 한 번 수행되어 필요하면 포맷까지 담당하고, NodePublishVolume은 그 스테이징 경로를 Pod마다 bind mount한다. 같은 볼륨을 같은 노드의 Pod 여러 개가 쓸 때 스테이징을 반복하지 않기 위한 이단 구조다.
이 분업은 장애 진단의 지도이기도 하다. kubectl describe pod의 이벤트에 FailedAttachVolume이 보이면 attach 단계, 즉 controller 쪽과 스토리지 API를 의심하고, FailedMount면 mount 단계, 즉 해당 노드의 kubelet과 node 플러그인을 검사한다.
드라이버 확인
클러스터에 설치된 드라이버는 CSIDriver 오브젝트로 조회한다.
kubectl get csidrivers
NAME ATTACHREQUIRED PODINFOONMOUNT MODES AGE
ebs.csi.aws.com true false Persistent 210d
efs.csi.aws.com false false Persistent 210dATTACHREQUIRED 열이 위에서 본 attach 단계의 유무다. 블록 스토리지인 EBS는 attach를 거치고, 네트워크 파일시스템인 EFS는 provision에서 바로 mount로 간다. 노드별 등록 상태는 CSINode 오브젝트가 담는다. kubectl get csinode로 노드마다 어떤 드라이버가 등록됐는지 확인하고, 특정 노드에서만 마운트가 실패한다면 그 노드의 CSINode에서 드라이버가 빠져 있지 않은지부터 확인한다.
정리
- 스토리지 드라이버는 in-tree 결합에서 out-of-tree CSI로 이관이 끝났다. 코어 CSIMigration은 1.25에서 GA가 됐다.
- controller 플러그인(Deployment)은 스토리지 관리 API를, node 플러그인(DaemonSet)은 노드 로컬 마운트를 담당한다.
- external-provisioner, external-attacher, node-driver-registrar 같은 사이드카가 Kubernetes API와 드라이버 gRPC 사이를 변환한다.
- 볼륨은 provision, attach, mount 순서로 Pod에 도달하고, FailedAttachVolume과 FailedMount라는 이벤트 이름이 실패한 단계를 가리킨다.