kubernetes (k8s) csi 插件開發簡介

請勿轉載

  1. kubernetes (k8s) csi 插件開發簡介 http://www.lxweimin.com/p/88ec8cba7507

  2. kubernetes(k8s) csi 插件attach-detach流程 http://www.lxweimin.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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容