kubernetes (k8s) csi 插件开发简介

请勿转载

  1. kubernetes (k8s) csi 插件开发简介 https://www.jianshu.com/p/88ec8cba7507

  2. 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"]

部署

  1. 需要添加以下配置在对应的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
  1. 创建自定义资源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信息并没有自动注册)

  1. 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
  1. 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驱动程序.
  1. 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

使用示例

  1. 定义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()

  1. 定义pvc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: csi-test-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: csi-test-sc

对应的pv会自动创建

  1. 应用
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

你可能感兴趣的:(kubernetes (k8s) csi 插件开发简介)