前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何实现一个 Kubernetes CSI Driver

如何实现一个 Kubernetes CSI Driver

作者头像
CS实验室
发布2022-12-28 18:57:20
1.1K0
发布2022-12-28 18:57:20
举报
文章被收录于专栏:CS实验室

维护了一个 CSI Driver 有一年半的时间了,期间也被一些朋友询问 CSI 相关的问题以及如何开发自己的 CSI Driver。本篇文章就来介绍如何快速开发自己的 Kubernetes CSI Driver,本篇也是继上一篇 《浅析 CSI 工作原理》 的 CSI 系列第二篇。

本文展示的完整的项目代码可见:https://github.com/zwwhdls/csi-hdls

CSIbuilder

其实 CSI Driver 无非就是实现一些接口,实现第三方存储的逻辑。短短一句话包含的工作是很大的,也比较繁琐,需要理解清楚 CSI 的工作原理,但好在工作是有迹可循的。

近期开发了一个脚手架工具 CSIbuilder (代码仓库地址:https://github.com/zwwhdls/csibuilder),其原理类似于 kubebuilder,用户只需要输入几行命令,就可以搭建一个 CSI Driver 的代码框架,然后再填入自己的逻辑即可。

使用过程很简单,首先下载二进制包:

代码语言:javascript
复制
weiwei@hdls-mbp $ curl -L -o csibuilder.tar https://github.com/zwwhdls/csibuilder/releases/download/v0.1.0/csibuilder-darwin-amd64.tar
weiwei@hdls-mbp $ tar -zxvf csibuilder.tar  && chmod +x csibuilder && mv csibuilder /usr/local/bin/

新建一个 golang 项目的工作目录:

代码语言:javascript
复制
weiwei@hdls-mbp $ export GO111MODULE=on
weiwei@hdls-mbp $ mkdir $GOPATH/src/csi-hdls
weiwei@hdls-mbp $ cd $GOPATH/src/csi-hdls

使用 csibuilder 进行 repo 初始化:

代码语言:javascript
复制
weiwei@hdls-mbp $ csibuilder init --repo hdls --owner "zwwhdls"
Init CSI Driver Project for you...
Update dependencies:
$ go mod tidy
go: warning: "all" matched no packages
Next: define a csi driver with:
$ csibuilder create api

创建一个名为 hdls 的 CSI Driver:

代码语言:javascript
复制
weiwei@hdls-mbp $ csibuilder create --csi hdls
Writing scaffold for you to edit...
Update dependencies:
$ go mod tidy
go: finding module for package google.golang.org/grpc/status
go: finding module for package github.com/container-storage-interface/spec/lib/go/csi
go: finding module for package google.golang.org/grpc
go: finding module for package google.golang.org/grpc/codes
go: finding module for package k8s.io/klog
go: found k8s.io/klog in k8s.io/klog v1.0.0
go: found github.com/container-storage-interface/spec/lib/go/csi in github.com/container-storage-interface/spec v1.7.0
go: found google.golang.org/grpc in google.golang.org/grpc v1.50.1
go: found google.golang.org/grpc/codes in google.golang.org/grpc v1.50.1
go: found google.golang.org/grpc/status in google.golang.org/grpc v1.50.1
Scaffolding complete. Enjoy your new project!

默认使用 go 1.18,也可以在 init 阶段通过参数 --goversion=1.19 来指定使用的 go 版本。

然后就能看到项目里已经初始化好了 CSI Driver 的代码文件和部署 yaml:

代码语言:javascript
复制
weiwei@hdls-mbp $ tree
.
├── Dockerfile
├── Makefile
├── PROJECT
├── deploy
│   ├── clusterrole.yaml
│   ├── clusterrolebinding.yaml
│   ├── csidriver.yaml
│   ├── daemonset.yaml
│   ├── serviceaccount.yaml
│   └── statefulset.yaml
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
├── main.go
└── pkg
    └── csi
        ├── controller.go
        ├── driver.go
        ├── identity.go
        ├── node.go
        └── version.go

4 directories, 18 files

实现 CSI Node 接口

Pod 挂载的过程在上一篇文章《浅析 CSI 工作原理》 中已经详细介绍过了,我们知道 CSI 工作最重要的组件是 CSI Node,也就是其 NodePublishVolumeNodeUnpublishVolume 两个接口。

kubelet 调用接口时传进来的参数都可以在 request 中找到,volumeID 为 Pod 所使用的 PV 的 id(即 pv.spec.csi.volumeHandle);target 为 Pod 内所需要挂载 volume 的路径;options 为本次挂载的挂载参数。

我们所需要做的就是将我们的存储挂载到 Pod 内的 target 路径。这里以 nfs 为例:

代码语言:javascript
复制
func (n *nodeService) NodePublishVolume(ctx context.Context, request *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
   ...
   volCtx := request.GetVolumeContext()
 klog.Infof("NodePublishVolume: volume context: %v", volCtx)

 hostPath := volCtx["hostPath"]
 subPath := volCtx["subPath"]
 sourcePath := hostPath
 if subPath != "" {
  sourcePath = path.Join(hostPath, subPath)
  exists, err := mount.PathExists(sourcePath)
  if err != nil {
   return nil, status.Errorf(codes.Internal, "Could not check volume path %q exists: %v", sourcePath, err)
  }
  if !exists {
   klog.Infof("volume not existed")
   err := os.MkdirAll(sourcePath, 0755)
   if err != nil {
    return nil, status.Errorf(codes.Internal, "Could not make directory for meta %q", sourcePath)
   }
  }
 }
 klog.Infof("NodePublishVolume: binding %s at %s", hostPath, target)
 if err := n.Mount(sourcePath, target, "none", mountOptions); err != nil {
  os.Remove(target)
  return nil, status.Errorf(codes.Internal, "Could not bind %q at %q: %v", hostPath, target, err)
 }
 return &csi.NodePublishVolumeResponse{}, nil
}

我们可以在 PV.spec.parameter 中传入 nfs server 的一些参数,从 request.GetVolumeContext() 中可以获取到,然后再以 mount bind 的方式挂载到 target。为了逻辑简单,这里我们假定参数 hostPath 为宿主机上已经挂载好的 nfs 路径。

NodeUnpublishVolume 接口就是上面 NodePublishVolume 接口的反向操作。我们只需要将 target 路径 umount 掉即可,代码如下:

代码语言:javascript
复制
// NodeUnpublishVolume unmount the volume from the target path
func (n *nodeService) NodeUnpublishVolume(ctx context.Context, request *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) {
 target := request.GetTargetPath()
 if len(target) == 0 {
  return nil, status.Error(codes.InvalidArgument, "Target path not provided")
 }

 // TODO modify your volume umount logic here
 ...

 klog.Infof("NodeUnpublishVolume: unmounting %s", target)
 if err := n.Unmount(target); err != nil {
  return nil, status.Errorf(codes.Internal, "Could not unmount %q: %v", target, err)
 }

 return &csi.NodeUnpublishVolumeResponse{}, nil
}

实现 CSI Controller 接口

CSI Controller 接口主要是配合实现 PV 的自动创建,只有在使用 StorageClass 时才会工作。这里的逻辑通常为在文件系统下创建一个子目录,并将其放在 PV 的 spec 中。在本文展示的 demo 中,创建子目录这一步也是在 CSI Node 的接口中实现,所以在 CSI Controller 的接口只需要将子目录传在 PV 的参数中即可。

CreateVolume 的代码如下:

代码语言:javascript
复制
func (d *controllerService) CreateVolume(ctx context.Context, request *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
 if len(request.Name) == 0 {
  return nil, status.Error(codes.InvalidArgument, "Volume Name cannot be empty")
 }
 if request.VolumeCapabilities == nil {
  return nil, status.Error(codes.InvalidArgument, "Volume Capabilities cannot be empty")
 }

 requiredCap := request.CapacityRange.GetRequiredBytes()

 volCtx := make(map[string]string)
 for k, v := range request.Parameters {
  volCtx[k] = v
 }

 volCtx["subPath"] = request.Name

 volume := csi.Volume{
  VolumeId:      request.Name,
  CapacityBytes: requiredCap,
  VolumeContext: volCtx,
 }

 return &csi.CreateVolumeResponse{Volume: &volume}, nil
}

返回值中的 VolumeContext 会放在自动创建的 PV 的 spec.csi.volumeAttributes 中。

总结

以上就是实现一个 CSI Driver 最简单可用功能的完整展示,有了 csibuilder 之后,CSI Driver 的代码编写也变得异常简单。不过 CSI 的功能远不止这些,还包括 attach、stage、expand、snapshot 等。csibuilder 目前还是一个刚刚完工的状态,在后续的迭代中会陆续支持这些功能。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CS实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CSIbuilder
  • 实现 CSI Node 接口
  • 实现 CSI Controller 接口
  • 总结
相关产品与服务
专用宿主机
专用宿主机(CVM Dedicated Host,CDH)提供用户独享的物理服务器资源,满足您资源独享、资源物理隔离、安全、合规需求。专用宿主机搭载了腾讯云虚拟化系统,购买之后,您可在其上灵活创建、管理多个自定义规格的云服务器实例,自主规划物理资源的使用。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档