请勿转载
kubernetes (k8s) csi 插件开发简介 https://www.jianshu.com/p/88ec8cba7507
kubernetes(k8s) csi 插件attach-detach流程 https://www.jianshu.com/p/5c6e78b6b320
CSI介绍
CSI是Container Storage Interface(容器存储接口)的简写.
1.CSI规范:
https://github.com/container-storage-interface/spec
CSI的目的是定义行业标准“容器存储接口”,使存储供应商(SP)能够开发一个符合CSI标准的插件并使其可以在多个容器编排(CO)系统中工作。CO包括Cloud Foundry, Kubernetes, Mesos等.
2.CSI规范详细描述
CSI文档中详细描述了一些基本定义,以及CSI的相关组件和工作流程.
https://github.com/container-storage-interface/spec/blob/master/spec.md
- 术语
Term | Definition |
---|---|
Volume | A unit of storage that will be made available inside of a CO-managed container, via the CSI. |
Block Volume | A volume that will appear as a block device inside the container. |
Mounted Volume | A volume that will be mounted using the specified file system and appear as a directory inside the container. |
CO | Container Orchestration system, communicates with Plugins using CSI service RPCs. |
SP | Storage Provider, the vendor of a CSI plugin implementation. |
RPC | Remote Procedure Call. |
Node | A host where the user workload will be running, uniquely identifiable from the perspective of a Plugin by a node ID. |
Plugin | Aka “plugin implementation”, a gRPC endpoint that implements the CSI Services. |
Plugin Supervisor | Process that governs the lifecycle of a Plugin, MAY be the CO. |
Workload | The atomic unit of "work" scheduled by a CO. This MAY be a container or a collection of containers. |
-
RPC接口: CO通过RPC与插件交互, 每个SP必须提供两类插件:
- Node plugin:在每个节点上运行, 作为一个grpc端点服务于CSI的RPCs,执行具体的挂卷操作。
- Controller Plugin:同样为CSI RPCs服务,可以在任何地方运行,一般执行全局性的操作,比如创建/删除网络卷。
-
CSI有三种RPC:
- 身份服务:Node Plugin和Controller Plugin都必须实现这些RPC集。
- 控制器服务:Controller Plugin必须实现这些RPC集。
- 节点服务:Node Plugin必须实现这些RPC集。
service Identity { rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {} rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {} rpc Probe (ProbeRequest) returns (ProbeResponse) {} } service Controller { rpc CreateVolume (CreateVolumeRequest) returns (CreateVolumeResponse) {} rpc DeleteVolume (DeleteVolumeRequest) returns (DeleteVolumeResponse) {} rpc ControllerPublishVolume (ControllerPublishVolumeRequest) returns (ControllerPublishVolumeResponse) {} rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest) returns (ControllerUnpublishVolumeResponse) {} rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest) returns (ValidateVolumeCapabilitiesResponse) {} rpc ListVolumes (ListVolumesRequest) returns (ListVolumesResponse) {} ... } service Node { ... rpc NodePublishVolume (NodePublishVolumeRequest) returns (NodePublishVolumeResponse) {} rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest) returns (NodeUnpublishVolumeResponse) {} rpc NodeExpandVolume(NodeExpandVolumeRequest) returns (NodeExpandVolumeResponse) {} rpc NodeGetCapabilities (NodeGetCapabilitiesRequest) returns (NodeGetCapabilitiesResponse) {} rpc NodeGetInfo (NodeGetInfoRequest) returns (NodeGetInfoResponse) {} ... }
架构举例
CSI包括很多种架构,这里只举一个例子看一下CSI是如何工作的:
CO "Master" Host
+-------------------------------------------+
| |
| +------------+ +------------+ |
| | CO | gRPC | Controller | |
| | +-----------> Plugin | |
| +------------+ +------------+ |
| |
+-------------------------------------------+
CO "Node" Host(s)
+-------------------------------------------+
| |
| +------------+ +------------+ |
| | CO | gRPC | Node | |
| | +-----------> Plugin | |
| +------------+ +------------+ |
| |
+-------------------------------------------+
Figure 1: The Plugin runs on all nodes in the cluster: a centralized
Controller Plugin is available on the CO master host and the Node
Plugin is available on all of the CO Nodes.
这里的CO就是k8s之类的容器编排工具,然后Controller Plugin和Node plugin是自己实现的plugin,一般Controller plugin负责一些全局性的方法调用,比如网络卷的创建和删除,而node plugin主要负责卷在要挂载的容器所在主机的一些工作,比如卷的绑定和解绑.当然,具体情况要看plugin的工作原理而定.
- 卷的生命周期
卷的生命周期也有好几种方式,这里只举个例子看一下:
CreateVolume +------------+ DeleteVolume
+------------->| CREATED +--------------+
| +---+----+---+ |
| Controller | | Controller v
+++ Publish | | Unpublish +++
|X| Volume | | Volume | |
+-+ +---v----+---+ +-+
| NODE_READY |
+---+----^---+
Node | | Node
Publish | | Unpublish
Volume | | Volume
+---v----+---+
| PUBLISHED |
+------------+
Figure 5: The lifecycle of a dynamically provisioned volume, from
creation to destruction.
通过调用不同的plugin组件,就可以完成一个如上图的卷生命周期.比如在Control plugin中创建卷,然后完成一些不依赖节点的工作(ControllerPublishVolume),然后再调用Node plugin中的NodePublishVolume来进行具体的挂卷工作,卸载卷则先执行NodeUnpublishVolume,然后再调用ControllerUnpublishVolume,最后删除卷.其中ControllerPublishVolume和ControllerUnpublishVolume也可以什么都不做,直接返回对应的reponse即可.具体要看你实现的plugin的工作流程.
CSI在Kubernetes 中的应用
CSI规范体现在Kubernetes 中就是支持CSI标准的k8s csi plugin.
https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/container-storage-interface.md
在这个设计文档中,kubernetes CSI的设计者讲述了一些为什么要开发CSI插件的原因,大概就是:
- Kubernetes卷插件目前是“in-tree”,意味着它们与核心kubernetes二进制文件链接,编译,构建和一起发布。有不利于核心代码的发布,增加了工作量,并且卷插件的权限太高等缺点.
- 现有的Flex Volume插件需要访问节点和主机的根文件系统才能部署第三方驱动程序文件,并且对主机的依赖性强.
容器存储接口(CSI)是由来自各个CO的社区成员(包括Kubernetes,Mesos,Cloud Foundry和Docker)之间的合作产生的规范。此接口的目标是为CO建立标准化机制,以将任意存储系统暴露给其容器化工作负载。
kubernetes csi drivers
- 官方开发文档
https://kubernetes-csi.github.io/docs/
官方文档中讲解了k8s csi 插件的开发/测试/部署等.
- 示例
k8s-csi官方实现的一些dirvers,包括范例和公共部分代码:
https://github.com/kubernetes-csi/drivers
- 版本
在动手开发之前,先要确定一下版本,以下是各个k8s版本支持的csi版本:
Kubernetes | CSI spec | Status |
---|---|---|
v1.9 | v0.1 | Alpha |
v1.10 | v0.2 | Beta |
v1.11 | v0.3 | Beta |
v1.12 | v0.3 | Beta |
v1.13 | v1.0.0 | GA |
目前最新的版本是V1.0.0,只有k8s v1.13支持,所以建议初学者就开始从1.13版本的k8s开始学习,版本一致可以少走一些弯路.
- 公共部分
https://github.com/kubernetes-csi/drivers/tree/master/pkg/csi-common
k8s实现了一个官方的公共代码,公共代码实现了CSI要求的RPC方法,我们自己开发的插件可以继承官方的公共代码,然后把自己要实现的部分方法进行覆盖即可.
Kubernetes csi driver开发
type driver struct {
csiDriver *csicommon.CSIDriver
endpoint string
ids *csicommon.DefaultIdentityServer
cs *controllerServer
ns *nodeServer
}
首先我们需要定义一个driver结构体,基本包含了plugin启动的所需信息(除了以上信息还可以添加其他参数):
- csicommon.CSIDriver :
k8s自定义代表插件的结构体, 初始化的时候需要指定插件的RPC功能和支持的读写模式.
func NewCSIDriver(nodeID string) *csicommon.CSIDriver {
csiDriver := csicommon.NewCSIDriver(driverName, version, nodeID)
csiDriver.AddControllerServiceCapabilities(
[]csi.ControllerServiceCapability_RPC_Type{
csi.ControllerServiceCapability_RPC_LIST_VOLUMES,
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
})
csiDriver.AddVolumeCapabilityAccessModes([]csi.VolumeCapability_AccessMode_Mode{csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER})
return csiDriver
}
- endpoint:
插件的监听地址,一般的,我们测试的时候可以用tcp方式进行,比如tcp://127.0.0.1:10000
,最后在k8s中部署的时候一般使用unix方式:/csi/csi.sock
- csicommon.DefaultIdentityServer :
认证服务一般不需要特别实现,使用k8s公共部分的即可.
- controllerServer:
实现CSI中的controller服务的RPC功能,继承后可以选择性覆盖部分方法.
type controllerServer struct {
*csicommon.DefaultControllerServer
}
- nodeServer:
实现CSI中的node服务的RPC功能,继承后可以选择性覆盖部分方法.
type nodeServer struct {
*csicommon.DefaultNodeServer
}
- driver的Run方法:
该方法中调用csicommon的公共方法启动socket监听,RunControllerandNodePublishServer方法会同时启动controller和node.还可以单独启动controller和node,需要写两个入口main函数.
func (d *driver) Run(nodeID, endpoint string) {
d.endpoint = endpoint
d.cloudconfig = cloudConfig
csiDriver := NewCSIDriver(nodeID)
d.csiDriver = csiDriver
// Create GRPC servers
ns, err := NewNodeServer(d, nodeID, containerized)
if err != nil {
glog.Fatalln("failed to create node server, err:", err.Error())
}
glog.V(3).Infof("Running endpoint [%s]", d.endpoint)
csicommon.RunControllerandNodePublishServer(d.endpoint, d.csiDriver, NewControllerServer(d), ns)
}
然后写一个main函数来创建driver并调用driver.Run()方法来运行自定义的plugin.
代码结构可以参考k8s官方提供的一些driver.
测试
csc功能测试命令
- 安装
go get github.com/rexray/gocsi/csc
- 命令
NAME
csc -- a command line container storage interface (CSI) client
SYNOPSIS
csc [flags] CMD
AVAILABLE COMMANDS
controller
identity
node
OPTIONS
-e, --endpoint
The CSI endpoint may also be specified by the environment variable
CSI_ENDPOINT. The endpoint should adhere to Go's network address
pattern:
* tcp://host:port
* unix:///path/to/file.sock.
If the network type is omitted then the value is assumed to be an
absolute or relative filesystem path to a UNIX socket file
子命令可以继续使用 --help查看
- 一些例子
1.直接运行main函数
go run main.go --endpoint tcp://127.0.0.1:10000 --nodeid deploy-node -v 5
2. 测试创建卷
csc controller create-volume --endpoint tcp://127.0.0.1:10000 test1
3. 测试挂载卷
csc node publish --endpoint tcp://127.0.0.1:10000 --target-path "/tmp/test1" --cap MULTI_NODE_MULTI_WRITER,mount,xfs,uid=0,gid=0 $volumeID --attrib $VolumeContext
ps: --attrib 代表NodePublishVolumeRequest中的VolumeContext,代表map结构,示例如下:
--attrib pool=rbd,clusterName=test,userid=cinder
当plugin运行的时候,这个VolumeContext是controllerServer中CreateVolume方法的返回值
*csi.CreateVolumeResponse的Volume的VolumeContext.
csi-sanity单元测试命令
csi-sanity是官方提供的单元测试工具
- 安装
go get github.com/kubernetes-csi/csi-test/cmd/csi-sanity
这个命令我用go get安装后没有发现可执行文件,只能进入目录下重新编译一个:
cd $GOPATH/src/github.com/kubernetes-csi/csi-test/cmd/csi-sanity
make
然后就可以执行可执行文件了:
./csi-sanity --help
- 命令
Usage of ./csi-sanity:
-csi.endpoint string
CSI endpoint
-csi.mountdir string
Mount point for NodePublish (default "/tmp/csi")
-csi.secrets string
CSI secrets file
-csi.stagingdir string
Mount point for NodeStage if staging is supported (default "/tmp/csi")
-csi.testvolumeparameters string
YAML file of volume parameters for provisioned volumes
-csi.testvolumesize int
Base volume size used for provisioned volumes (default 10737418240)
-csi.version
Version of this program
...
- 示例
1.直接运行main函数
go run main.go --endpoint tcp://127.0.0.1:10000 --nodeid deploy-node -v 5
2.运行单元测试
./csi-sanity --csi.endpoint=127.0.0.1:10000 -csi.testvolumeparameters config.yaml -ginkgo.v 5
docker镜像
dockerfile很简单,如下:
FROM centos:7
LABEL maintainers="Kubernetes Authors"
LABEL description="CSI XXX Plugin"
...
COPY csi-test /bin/csi-test
RUN chmod +x /bin/csi-test
ENTRYPOINT ["/bin/csi-test"]
部署
- 需要添加以下配置在对应的k8s服务中:
kube-apiserver:
--allow-privileged=true
--feature-gates=BlockVolume=true,CSIBlockVolume=true,CSIPersistentVolume=true,MountPropagation=true,VolumeSnapshotDataSource=true,KubeletPluginsWatcher=true,CSINodeInfo=true,CSIDriverRegistry=true
kubelet:
--allow-privileged=true
--feature-gates=BlockVolume=true,CSIBlockVolume=true,CSIPersistentVolume=true,MountPropagation=true,VolumeSnapshotDataSource=true,KubeletPluginsWatcher=true,CSINodeInfo=true,CSIDriverRegistry=true
controller-manager:
--feature-gates=BlockVolume=true,CSIBlockVolume=true
- 创建自定义资源CSIDriver和CSINodeInfo
kubectl create -f https://raw.githubusercontent.com/kubernetes/csi-api/master/pkg/crd/manifests/csidriver.yaml --validate=false
kubectl create -f https://raw.githubusercontent.com/kubernetes/csi-api/master/pkg/crd/manifests/csinodeinfo.yaml --validate=false
这两个资源用来列出集群中的csi driver信息和node信息.(但是不知道我配置不正确还是功能没实现,plugin可以正常运行,但是driver和node信息并没有自动注册)
- RBAC规则,定义对应服务的权限规则,k8s提供了例子,直接拿来创建即可:
provisioner:
kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/external-provisioner/1cd1c20a6d4b2fcd25c98a008385b436d61d46a4/deploy/kubernetes/rbac.yaml
attacher:
kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/external-attacher/9da8c6d20d58750ee33d61d0faf0946641f50770/deploy/kubernetes/rbac.yaml
node-driver:
kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/driver-registrar/87d0059110a8b4a90a6d2b5a8702dd7f3f270b80/deploy/kubernetes/rbac.yaml
snapshotter:
kubectl create -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/01bd7f356e6718dee87914232d287631655bef1d/deploy/kubernetes/rbac.yaml
- sidecar container
由于CSI plugin的代码在k8s中被认为是不可信的,因此不允许在master服务器上运行。因此,Kube controller manager(负责创建,删除,附加和分离)无法通过Unix Socket与 CSI plugin容器进行通信。
为了能够在Kubernetes上轻松部署容器化的CSI plugin程序, Kubernetes提供一些辅助的代理容器,它会观察Kubernetes API并触发针对CSI plugin程序的相应操作。
主要有三个:
- csi-provisioner
与controller server结合处理卷的创建和删除操作。
csi-provisioner会在StorageClass中指定,而StorageClass又在pvc的定义中指定,当创建PVC的时候会自动调用controller中的CreateVolum方法来创建卷,同时可以在定义StorageClass的时候传递创建卷需要的参数。
- csi-attacher
attacher代表CSI plugin 程序监视Kubernetes API以获取新VolumeAttachment对象,并触发针对CSI plugin 程序的调用来附加卷。
当挂载卷的时候,k8s会创建一个VolumeAttachment来记录卷的attach/detach情况,也就是说k8s会记录卷的使用情况并在调度pod的时候进行对应的操作.
- csi-driver-registrar
node-driver-registrar是一个辅助容器,它使用kubelet插件注册机制向Kubelet注册CSI驱动程序.
- sidecar container 和driver结合部署
首先确定自定义csi driver的发布方式, 是controller和node一同发布,还是分别发布.
假设一同发布, 即调用csicommon.RunControllerandNodePublishServer
方法来发布server.
- node driver
首先, 需要用DaemonSet的方式在每个kubelet节点运行node driver:
kind: DaemonSet
apiVersion: apps/v1
metadata:
name: csi-test-node
spec:
selector:
matchLabels:
app: csi-test-node
template:
metadata:
labels:
app: csi-test-node
spec:
serviceAccountName: csi-driver-registrar
hostNetwork: true
containers:
- name: csi-driver-registrar
image: quay.io/k8scsi/csi-node-driver-registrar:v1.0.2
args:
- "--v=5"
- "--csi-address=$(ADDRESS)"
- "--kubelet-registration-path=/var/lib/kubelet/plugins/csi-test/csi.sock"
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "rm -rf /registration/csi-test /registration/csi-test-reg.sock"]
env:
- name: ADDRESS
value: /csi/csi.sock
volumeMounts:
- name: plugin-dir
mountPath: /csi
- name: registration-dir
mountPath: /registration
- name: cinder-test
securityContext:
privileged: true
capabilities:
add: ["SYS_ADMIN"]
allowPrivilegeEscalation: true
image: ${your docker registry}/csi-test:v1.0.0
terminationMessagePath: "/tmp/termination-log"
args :
- /bin/csi-test
- "--nodeid=$(NODE_ID)"
- "--endpoint=$(CSI_ENDPOINT)"
- "--v=5"
env:
- name: NODE_ID
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: CSI_ENDPOINT
value: unix://csi/csi.sock
imagePullPolicy: "IfNotPresent"
volumeMounts:
- name: plugin-dir
mountPath: /csi
- name: mountpoint-dir
mountPath: /var/lib/kubelet/pods
mountPropagation: "Bidirectional"
volumes:
- name: registration-dir
hostPath:
path: /var/lib/kubelet/plugins_registry/
type: Directory
- name: plugin-dir
hostPath:
path: /var/lib/kubelet/plugins/csi-test/
type: DirectoryOrCreate
- name: mountpoint-dir
hostPath:
path: /var/lib/kubelet/pods
type: DirectoryOrCreate
- provisioner
因为controller是和node rpc服务一起发布的,所以provisioner不用再启动driver了,直接把sock文件挂载到provisioner容器中就行.
kind: Service
apiVersion: v1
metadata:
name: csi-test-provisioner
labels:
app: csi-test-provisioner
spec:
selector:
app: csi-test-provisioner
ports:
- name: dummy
port: 12345
---
kind: StatefulSet
apiVersion: apps/v1
metadata:
name: csi-test-provisioner
spec:
serviceName: "csi-test-provisioner"
replicas: 1
selector:
matchLabels:
app: csi-test-provisioner
template:
metadata:
labels:
app: csi-test-provisioner
spec:
serviceAccountName: csi-provisioner
hostNetwork: true
containers:
- name: csi-provisioner
image: quay.io/k8scsi/csi-provisioner:v1.0.0
args:
- "--provisioner=csi-test"
- "--csi-address=$(ADDRESS)"
- "--connection-timeout=15s"
env:
- name: ADDRESS
value: /csi/csi.sock
imagePullPolicy: "IfNotPresent"
volumeMounts:
- mountPath: /csi
name: plugin-dir
volumes:
- name: plugin-dir
hostPath:
path: /var/lib/kubelet/plugins/csi-test
type: DirectoryOrCreate
- attacher
attach也是同样:
kind: Service
apiVersion: v1
metadata:
name: csi-test-attacher
labels:
app: csi-test-attacher
spec:
selector:
app: csi-test-attacher
ports:
- name: dummy
port: 12345
---
kind: StatefulSet
apiVersion: apps/v1
metadata:
name: csi-test-attacher
spec:
serviceName: "csi-test-attacher"
replicas: 1
selector:
matchLabels:
app: csi-test-attacher
template:
metadata:
labels:
app: csi-test-attacher
spec:
serviceAccountName: csi-attacher
hostNetwork: true
containers:
- name: csi-attacher
image: quay.io/k8scsi/csi-attacher:v1.0.0
args:
- "--v=5"
- "--csi-address=$(ADDRESS)"
- "--connection-timeout=10m"
env:
- name: ADDRESS
value: /csi/csi.sock
imagePullPolicy: "IfNotPresent"
volumeMounts:
- mountPath: /csi
name: plugin-dir
volumes:
- name: plugin-dir
hostPath:
path: /var/lib/kubelet/plugins/csi-test
type: DirectoryOrCreate
使用示例
- 定义StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-test-sc
provisioner: csi-test
parameters:
pool: "rbd"
clusterName: "ceph"
type: "test"
fsType: "xfs"
...
reclaimPolicy: Delete
volumeBindingMode: Immediate
这里的parameters对应*csi.CreateVolumeRequest
的Parameters. 可用req.GetParameters()
获取.
而fsType则可以在*csi.NodePublishVolumeRequest
中获取:
fsType := req.GetVolumeCapability().GetMount().GetFsType()
- 定义pvc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: csi-test-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: csi-test-sc
对应的pv会自动创建
- 应用
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- image: nginx
imagePullPolicy: IfNotPresent
name: nginx
ports:
- containerPort: 80
protocol: TCP
volumeMounts:
- mountPath: /var/lib/www/html
name: csi-data-test
volumes:
- name: csi-data-test
persistentVolumeClaim:
claimName: csi-test-pvc
readOnly: false