Cloud-Provider-Openstack 介绍——CSI 块存储部分

概览

随着 Kubernetes 逐步成为容器 orchestrator 领域的事实标准,标准化成为了非常重要的目标之一,现在有 CSI(Container Storage Interface)、CRI(Container Runtime Interface)、CNI(Container Network Interface) 等,在不同的云提供商(Cloud provider)中有各自不同的实现。

利用 OpenStack 的 cindermanila 分别作为块存储和文件存储的后端,提供 Kubernetes API 可以使用的存储后端。由于篇幅有限,本文主要讨论了 OpenStack Cinder 作为后端的实现和使用简介。

概念介绍

Kubernetes Cloud Provider 为了更好地让 Kubernetes 在公有云平台上运行以及提供容器云服务,云厂商(例如 IBM Cloud、AWS、OpenStack 等)需要通过实现该接口来提供自己的 Cloud Provider,它是 Kubernetes 中开放给各个云厂商的通用接口,便于 Kubernetes 创建/管理/利用各个云服务商提供的资源,这些资源包括但不限于虚拟机/裸金属机器资源、网络资源(VPC),负载均衡服务(Load balancer)、弹性公网 IP (Floating IP)、存储服务(块,对象,文件存储)等。

随着 Kubernetes 不断地成熟,OpenStack 社区也在和 Kubernetes 不断地加强合作,通过 SIG (special interest group)来进行合作,主要项目是 Cloud Provider OpenStack,该项目是 Cloud Provider 在 OpenStack 上的实现,这样通过这一层抽象,我们可以在 PaaS (Kubernetes) 和 IaaS (OpenStack) 之间加入一层粘合剂,从而所有的针对云提供商的 API 调用可以通过特定的 OpenStack 一个或者多个子项目来完成,Cloud Provider OpenStack 包含了若干个子项目:

Kubernetes 中有卷和持久卷的概念,代表临时的或者持久的磁盘文件,分别用 Volume 和 PersistentVolume 这两个对象来表示。临时的卷没有办法解决内容的持久化问题,所以 Kubernetes 引入了 PersistentVolume(PV)这个对象,而如何提供这些卷,就是存储提供者(例如云提供商)要考虑的问题了。

CSI 是 Container Storage Interface 的缩写, 在容器编排引擎(例如 Kubernetes)和存储之间定义了一套标准的接口额(概念上与 CRI 和 CNI 类似),这样把 Kubernetes 和存储提供者彻底解耦。CSI 只是一套接口,因此不同的存储提供商都提供了自己的实现,在 Openstack 上有三种存储服务分别是 swift (对象存储),cinder(块存储),manila(文件存储)。通过这样的方式,无论是虚拟机和容器的 workload,都可以通过 openstack 来管理块设备,从而达到统一管理的目的。

例如我们可以假设我们有 2 个应用负载,一部分需要在虚拟机中运行,另一部分在容器中,他们都有对于 persistent 块存储有一定的需求,我们可以通过 IaaS (OpenStack)来管理所有的该类型存储,而如何使用则由 Cloud Provider OpenStack(针对容器)或者 cinder(针对虚拟机)来决定。

创建 Kubernetes 运行环境

由于使用 CSI 需要 Kubernetes 运行环境,为了简单起见,笔者直接使用了源码的开发模式,读者可以直接使用现有的 Kubernetes 环境或者通过 Kubeadm 等工具创建环境;本文假设采用了源码的开发模式。

首先,由于需要编译,所以需要确保 go 编译环境创建完成,可以参考创建 golang 环境;接着需要通过 git 下载 kubernetes,并且运行如下命令从而创建一个 Kubernetes 运行环境:

ALLOW_PRIVILEGED=true RUNTIME_CONFIG="storage.k8s.io/v1=true" LOG_LEVEL=5 hack/l
ocal-up-cluster.sh
WARNING : The kubelet is configured to not fail even if swap is enabled; production deployments should disable swap.
make: Entering directory '/root/go/src/github.com/kubernetes'
make[1]: Entering directory '/root/go/src/github.com/kubernetes'
…..
To start using your cluster, you can open up another terminal/tab and run:

  export KUBECONFIG=/var/run/kubernetes/admin.kubeconfig
  cluster/kubectl.sh

Alternatively, you can write to the default kubeconfig:

export KUBERNETES_PROVIDER=local

  cluster/kubectl.sh config set-cluster local --server=https://localhost:6443 --certificate-authority=/var/run/kubernetes/server-ca.crt
  cluster/kubectl.sh config set-credentials myself --client-key=/var/run/kubernetes/client-admin.key --client-certificate=/var/run/kubernetes/client-admin.crt
  cluster/kubectl.sh config set-context local --cluster=local --user=myself
  cluster/kubectl.sh config use-context local
cluster/kubectl.sh

当我们看到如上的界面的时候,我们的本地 cluster 已经可以使用了。

为了简便起见,笔者创建了一个 alias,这样可以直接使用 kubectl 来完成后续命令:

alias 'kubectl=/root/go/src/github.com/kubernetes/cluster/kubectl.sh'

通过如下命令确定 cluster 已经准备完毕, 如果该命令出现错误,请检查上述命令的 log:

# kubectl get pods
No resources found in default namespace.

创建 CSI 运行环境

接着,我们需要创建 CSI 运行环境,所需的定义都已经在 manifests/cinder-csi-plugin 这个目录。通过如下命令创建:

# kubectl apply -f manifests/cinder-csi-plugin/
serviceaccount/csi-cinder-controller-sa created
clusterrole.rbac.authorization.k8s.io/csi-attacher-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-attacher-binding created
clusterrole.rbac.authorization.k8s.io/csi-provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-provisioner-binding created
clusterrole.rbac.authorization.k8s.io/csi-snapshotter-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-snapshotter-binding created
clusterrole.rbac.authorization.k8s.io/csi-resizer-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-resizer-binding created
role.rbac.authorization.k8s.io/external-resizer-cfg created
rolebinding.rbac.authorization.k8s.io/csi-resizer-role-cfg created
service/csi-cinder-controller-service created
statefulset.apps/csi-cinder-controllerplugin created
serviceaccount/csi-cinder-node-sa created
clusterrole.rbac.authorization.k8s.io/csi-nodeplugin-role created
clusterrolebinding.rbac.authorization.k8s.io/csi-nodeplugin-binding created
daemonset.apps/csi-cinder-nodeplugin created
csidriver.storage.k8s.io/cinder.csi.openstack.org created
secret/cloud-config created

从上面的输出可以看出为了运行 CSI 我们创建的资源主要分为几类:

  • clusterrole/role

    由于 kubernetes 对于权限的控制和要求,需要定义一系列的 role 和 clusterrole,以 csi-nodeplugin-role 为例,定义了该 clusterrole 可以针对 events 这个资源进行 [“get”, “list”, “watch”, “create”, “update”, “patch”] 等操作。

    kind: ClusterRole
    apiVersion: rbac.authorization.k8s.io/v1
    metadata:
      name: csi-nodeplugin-role
    rules:
      - apiGroups: [""]
        resources: ["events"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
    
  • serviceaccount

    serviceaccount 针对运行在 Pod 的进程进行授权,以如下 csi-cinder-node-sa 为例,定义了一个 serviceaccount 被 clusterrolebinding 使用。

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: csi-cinder-node-sa
    namespace: kube-system
    
  • clusterrolebinding

    有了 clusterrole 和 serviceaccount 之后,需要通过 clusterrolebinding 把二者结合在一起,以如下 csi-nodeplugin-binding 为例,该 clusterrolebinding 使得拥有 csi-cinder-node-sa serviceaccount 的 pod 拥有 csi-nodeplugin-role 这个 role 来进行 API 操作。

    kind: ClusterRoleBinding
    apiVersion: rbac.authorization.k8s.io/v1
    metadata:
      name: csi-nodeplugin-binding
    subjects:
      - kind: ServiceAccount
        name: csi-cinder-node-sa
        namespace: kube-system
    roleRef:
      kind: ClusterRole
      name: csi-nodeplugin-role
    apiGroup: rbac.authorization.k8s.io
    
  • csidriver

    定义以 cinder.csi.openstack.org 为名字的 CSIDriver。

    apiVersion: storage.k8s.io/v1beta1
    kind: CSIDriver
    metadata:
      name: cinder.csi.openstack.org
    spec:
      attachRequired: true
    podInfoOnMount: false
    
  • secret

    cloud-config 这个 secret 里存放了 Openstack cloud 的信息,所有的后续的通过 openstack API 的调用操作都需要该文件完成认证,授权和服务发现;里是通过 base64 转码之后的 cloud 配置内容。

    apiVersion: v1
    metadata:
      name: cloud-config
      namespace: kube-system
    data:
      cloud.conf: <conf>
    statefulset
    

    csi-cinder-controllerplugin 定义了 CSI controller,也就是运行了一系列的 CSI 容器来完成 CSI 的控制工作,具体定义请参考这里

  • daemonset

    csi-cinder-nodeplugin 定义了每一个计算节点(node)上要运行的服务,具体定义请参考这里

创建完成之后,可以通过如下命令来查看已经创建的 pod 以及相关 container 的定义:

# kubectl get pods --all-namespaces
NAMESPACE     NAME                            READY   STATUS    RESTARTS   AGE
kube-system   csi-cinder-controllerplugin-0   5/5     Running   0          24s
kube-system   csi-cinder-nodeplugin-ssmc6     2/2     Running   0          24s
kube-system   kube-dns-547db76c8f-ckwrp       3/3     Running   0          2m50s

获取容器所用的镜像名字。

# kubectl get pods --all-namespaces -o=jsonpath='{range .items[*]}{"\n"}{.metadata.name}{":\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}'

csi-cinder-controllerplugin-0:  quay.io/k8scsi/csi-attacher:v1.2.1, quay.io/k8scsi/csi-provisioner:v1.3.0, quay.io/k8scsi/csi-snapshotter:v1.2.0, quay.io/k8scsi/csi-resizer:v0.2.0, docker.io/k8scloudprovider/cinder-csi-plugin:latest,
csi-cinder-nodeplugin-ssmc6:    quay.io/k8scsi/csi-node-driver-registrar:v1.1.0, docker.io/k8scloudprovider/cinder-csi-plugin:latest

我们可以看出创建了两个 pod,分别是 controller 和 node(笔者的环境是 local 所以 controller 和 node 是在一起的), controller 这个 pod 运行了 5 个容器而 node 这个 pod 运行了两个容器。为了使得开发 CSI driver 简便,Kubernetes CSI 社区提供了一系列的 sidecar 容器,这样开发者只需要关注支持自己存储的逻辑, 除非有自己的定制需求,可以直接使用社区提供的容器。

  • attacher 这是一个边车(sidecar)容器,主要是 watch VolumeAttachment 这个对象并且针对 CSI 对象调用 PublishVolume 和 UnpublisVolume 这个动作,具体可以参考这里
  • provisioner 这是一个边车(sidecar)容器,主要是 watch PersistentVolumeClaim 这个对象并且针对 CSI 对象调用 CreateVolume 这个动作,具体可以参考这里
  • snapshotter 这是一个边车(sidecar)容器,主要是 VolumeSnapshot 和 VolumeSnapshotContent 这两个对象,具体可以参考这里
  • resizer 这是一个边车(sidecar)容器,主要是 watch PersistentVolumeClaim 这个对象并且针对 CSI 对象调用 ControllerExpandVolume 这个动作,具体可以参考这里
  • registrar 这是一个边车(sidecar)容器,主要是从 CSI 获取 driver 的信息并向 kubelet 注册,具体可以参考这里
  • cinder-csi-plugin 是主要的服务容器,运行 CSI driver,针对 controller 和 node 有不同的配置,具体可以参考上文 csi-cinder-controllerplugin 和 csi-cinder-nodeplugin 的定义。

使用 CSI

通过上面的准备工作,我们已经准备好所需要的环境,下面我们开始创建工作容器来通过 CSI 来分配和挂载 openstack cinder 的卷,在执行如下操作之前,请确定上面的 CSI 的 pods 都正常运行。 接着,我们通过命令创建如下定义:

# kubectl apply -f examples/cinder-csi-plugin/nginx.yaml
storageclass.storage.k8s.io/csi-sc-cinderplugin created
persistentvolumeclaim/csi-pvc-cinderplugin created
pod/nginx created

创建的资源主要分为如下几类:

  • StorageClass

    首先需要创建一个名为 csi-sc-cinderplugin 的 storageclass, 并且它的后端是 cinder.csi.openstack.org 这个 driver, 这和上述的 csidriver 是对应的,如果不指定或者指定错了该 driver 名字,会导致后续错误。

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: csi-sc-cinderplugin
    provisioner: cinder.csi.openstack.org
    
  • PersistentVolumeClaim

    接着,我们需要创建一个名为 csi-pvc-cinderpluginPersistentVolumeClaim,从如下定义我们可以看出,该 PersistentVolumeClaim 是 RWO(ReadWriteOnce),每一个 PVC 都有一个访问模式,csi-pvc-cinderplugin 的访问模式就是 RWO,代表该 volume 只能被一个 node 挂载成读写模式,这个 volume 的大小为 1G。当该 PersistentVolumeClaim 被创建之后,Cloud-Provider-Openstack 会通过 cinder 接口来在 OpenStack 中创建 volume 来满足这个需求。

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: csi-pvc-cinderplugin
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
    storageClassName: csi-sc-cinderplugin
    
  • Pod

    最后,创建一个 Pod 来使用创建的 volume,也就是该 Pod 使用了我们创建的 csi-data-cinderplugin 这个 PersistentVolumeClaim,从 yaml 定义,我们也可以看出 csi-data-cinderplugin 这个 volume 会被挂载到 /var/lib/www/html 这个位置。

    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-cinderplugin
      volumes:
      - name: csi-data-cinderplugin
        persistentVolumeClaim:
          claimName: csi-pvc-cinderplugin
          readOnly: false
    

现在我们可以通过 openstack 和 Kubernetes 命令分别查看,有一个 1G 的卷已经被分配并且挂载到相应的容器之上了,并且该 volume 已经是 in-use(挂载状态)了。

通过 openstack 命令,这个 volume 大小为 1G,并且 attach 为 jj-cloud-provider 这个机器的 /dev/vdb 设备了。

# openstack volume list
+--------------------------------------+------------------------------------------+--------+------+-----------------------------------------------------+
| ID                                   | Display Name                             | Status | Size | Attached to                                         |
+--------------------------------------+------------------------------------------+--------+------+-----------------------------------------------------+
| c78334f9-b296-436e-bde8-b8e51e1de458 | pvc-631cef46-a56c-46ad-89ce-60ec23acdf04 | in-use |    1 | Attached to jj-cloud-provider on /dev/vdb           |

接着我们查看 PersistentVolumeClaim 的状态:

# kubectl get pvc
NAME                   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
csi-pvc-cinderplugin   Bound    pvc-631cef46-a56c-46ad-89ce-60ec23acdf04   1Gi        RWO            csi-sc-cinderplugin   21s

从操作系统层面,我们也可以在 worker node 之上我们可以看到有一块盘已经创建:

# ls /dev/vdb
/dev/vdb

 # mount | grep vdb
/dev/vdb on /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-631cef46-a56c-46ad-89ce-60ec23acdf04/globalmount type ext4 (rw,relatime,data=ordered)
/dev/vdb on /var/lib/kubelet/pods/c9097d48-3021-42e6-a5b5-15bc354bbf2b/volumes/kubernetes.io~csi/pvc-631cef46-a56c-46ad-89ce-60ec23acdf04/mount type ext4 (rw,relatime,data=ordered)

删除卷

最后,我们清除之前的 Pod 和 PersistentVolumeClaim,可以看出,当 pod 被删除之后,之前的创建的 volume 已经恢复到 available 状态(也就是非挂载状态)。 首先,我们删除创建的工作 Pod:

# kubectl delete pod nginx
pod "nginx" deleted

当容器删除之后,该 Volume 已经不再挂载在工作节点上了:

# ls /dev/vdb
ls: cannot access '/dev/vdb': No such file or directory

同时 OpenStack 的 Volume 已经变为 available 状态:

# openstack volume list
+--------------------------------------+------------------------------------------+-----------+------+-----------------------------------------------------+
| ID                                   | Display Name                             | Status    | Size | Attached to                                         |
+--------------------------------------+------------------------------------------+-----------+------+-----------------------------------------------------+
| c78334f9-b296-436e-bde8-b8e51e1de458 | pvc-631cef46-a56c-46ad-89ce-60ec23acdf04 | available |    1 |

删除 PersistentVolumeClaim 并检查系统使用资源状态:

# kubectl get pvc
NAME                   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
csi-pvc-cinderplugin   Bound    pvc-631cef46-a56c-46ad-89ce-60ec23acdf04   1Gi        RWO            csi-sc-cinderplugin   13m

# kubectl delete pvc csi-pvc-cinderplugin
persistentvolumeclaim "csi-pvc-cinderplugin" deleted

# kubectl get pv
No resources found in default namespace.
# kubectl get pvc
No resources found in default namespace.

进阶功能

上面我们介绍了最简单的通过 cinder 来创建和删除块设备功能,除此之外,我们还可以使用多种进阶功能。

  • 块镜像:通过 Cinder 的镜像(snapshot)功能,可以创建 snapshot 作为 volumesnapshot.snapshot.storage.k8s.io 这个资源在 OpenStack 上的实现。
  • 块扩展:通过 Cinder 的 Expand 功能,可以在线扩展(不能缩小)某块 Volume。
  • 块克隆:通过 Cinder 的 Clone 功能,完成对某快 Volume 的克隆操作。

结束语

本文首先简介了 CSI 定义以及其使用场景,包括所有的 side car 的容器定义以及如何部署这些容器环境,然后以 OpenStack cinder 为例介绍了如何使用 Kubernetes 通过 CSI 来从 Cloud Provider OpenStack 申请和管理卷块设备资源。