Cách phát triển bộ lưu trữ CSI (Container Storage Interface) cho k8s cùng ví dụ

Giới thiệu

Container storage interface hay CSI là một khái niệm khá mới mẻ và không quá phổ biến ở mức độ người sử dụng nền tảng Cloud thông thường do đó để hiểu được khái niệm này cần hiểu trước một số kiến thức nền tảng. Do đó ở phần mở đầu, bài blog này sẽ giới thiệu sơ qua các khái niệm cơ bản để người đọc có thể dễ tiếp cận hơn

Container:

Truyền thống:

  • Các ứng dụng truyền thống được chạy trên các máy server vật lý do đó việc phân bổ tài nguyên cho các ứng dụng khó có thể thực hiện được.
  • Ngoài ra, trên mỗi máy server đều cần phải cài đặt môi trường và bộ thư viện để chạy được tất cả các ứng dụng cần thiết
    • Điều này dẫn tới vấn đề các ứng dụng này hoàn toàn không có tính portable, có nghĩa là để chạy được các ứng dụng này trên một máy khác, máy đó phải được cài toàn bộ hệ điều hành và thư viện yêu cầu bởi các ứng dụng
    • Nếu muốn tối ưu tài nguyên bằng cách cân bằng tải cho các máy server, các máy server phải được cài cùng một bộ thư viện và môi trường duy nhất để chạy được tất cả các ứng dụng, điều này tốn rất nhiều công sức để có thể đạt được nếu số lượng ứng dụng nhiều và đa dạng

Ảo hóa:

  • Ảo hóa là giải pháp cho các vấn đề của kiến trúc truyền thống bằng cách tạo ra các máy ảo (Guest) với hệ điều hành và môi trường được cài đặt sẵn thông qua các Hypervisor như kvm, virtual box, vmware,… trên máy vật lý (Host)
  • Tuy nhiên các máy ảo tuy đóng gói được các ứng dụng nhưng mỗi máy ảo có dung lượng rất lớn vì bao gồm cả hệ điều hành

Container:

  • Kiến trúc container là giải pháp cho vấn đề về kích thước của máy ảo
  • Các ứng dụng và bộ thư viện cần thiết được đóng gói trong các container image, các container image này không bao gồm hệ điều hành nên kích thước rất nhỏ vì các container có thể chia sẻ hệ điều hành của máy Host thông qua Container Runtime (vd: Docker)
  • Trong kiến trúc này các ứng dụng được đóng gói gọn nhẹ và có tính portable rất cao, tăng thêm tính linh hoạt cho hệ thống

Kubernetes:

  • Container là giải pháp để đóng gói các ứng dụng
  • Tuy nhiên, để các ứng dụng vận hành trơn tru trên nền tảng Cloud, cần có một giải pháp để triển khai các container lên hệ thống cũng như quản lý, giám sát và đảm bảo tính ổn định
  • Các giải pháp như vậy được gọi là Container Orchestration và một ví dụ điển hình là Kubernetes hay K8s 
  • Trong kiến trúc của Kubernetes, đơn vị nhỏ nhất trong cụm không phải là các Container mà là các Pod.
    • Một Pod có thể bao gồm 1 hoặc nhiều container liên quan đến nhau chạy cùng nhau, có thể hiểu 1 pod sẽ tương ứng với 1 máy ảo trong kiến trúc ảo hóa hoặc 1 máy vật lý trong kiến trúc truyền thống
  • Một trong  các nhiệm vụ chính của K8s là phân phối tài nguyên cho các tác vụ và 2 loại tài nguyên chính là tài nguyên xử lý (computing) và tài nguyên lưu trữ (storage)
    • Computing: được thực hiện bằng việc lập lịch và gán các pod cho các máy trong cụm (node)
    • Storage: được thực hiện bằng việc gửi yêu cầu đến các nguồn tài nguyên lưu trữ (storage pools) để tạo ra các Volume (đơn vị cho một khối tài nguyên lưu trữ trong K8s) và cung cấp Volume cho các Pod. Việc cung cấp tài nguyên lưu trữ có thể được thực hiện bằng 2 cách:
      • Static provision: các volume được tạo sẵn bởi system admin, khi một pod nào đó yêu cầu tài nguyên lưu trữ (persistent volume claim – PVC), K8s sẽ tìm trong số các volume tạo sẵn và gán cho yêu cầu của Pod nếu có volume đáp ứng yếu cầu
      • Dynamic provision: K8s tương tác với nguồn tài nguyên lưu trữ để tự động tạo volume theo đúng yêu cầu của PVC và gán volume đó cho PVC

Container Storage Interface (CSI):

Như vậy để thực hiện được nhiệm vụ cung cấp tài nguyên lưu trữ, K8s cần một phương pháp để giao tiếp với các nguồn tài nguyên. Trên thực tế K8s đã từng làm các plugin phục vụ mục đích này, tuy nhiên giải pháp này sớm cho thấy các vấn đề sau:

  • Với việc K8s ngày càng trở nên phổ biến và sự đa dạng của các nhà cung cấp thiết bị lưu trữ cũng như các nguồn lưu trữ qua Cloud, plugin của K8s khó có thể đảm bảo hỗ trợ toàn bộ
  • Việc plugin này là một phần của mã nguồn của K8s tạo một mối liên kết chặt chẽ giữa K8s và driver của các hãng cung cấp mà đáng ra phải độc lập với nhau:
    • Mỗi khi các hãng cập nhật driver để hỗ trợ cho phần cứng mới hoặc thêm tính năng mới,mã nguồn plugin của K8s có thể sẽ phải thay đổi theo
    • Các hãng phải chờ K8s cập nhật phiên bản plugin thì các cập nhật mới được hỗ trợ trên K8s
    • K8s rất khó để kiểm soát tính ổn định và bảo mật của các phần mã nguồn trong plugin K8s được cộng tác bởi các nhà cung cấp phần cứng  

CSI chính là giải pháp cho những vấn đề nêu trên. CSI định nghĩa ra một interface chuẩn chung cho tất cả các plugin cung cấp tài nguyên lưu trữ nhờ đó các nhà cung cấp phần cứng cũng như cung cấp dịch vụ lưu trữ qua cloud có thể phát triển các plugin của riêng họ và triển khai lên nền tảng Kubernetes. CSI có bản chất giống với interface trong lập trình hướng đối tượng (OOP) nhằm đạt được tính đa hình (polymorphism) giữa plugin của các nhà cung cấp

Kiến trúc

CSI plugin được chia làm 2 thành phần chính là Controller Plugin và Node Plugin:

  • Các chức năng cơ bản của Controller Plugin:
    • Chịu trách nhiệm tương tác với nguồn tài nguyên lưu trữ tương ứng của nhà cung cấp để tạo ra các Volume
    • Cung cấp volume cho các node chỉ định bởi K8s
    • Trong 1 K8s cluster, mỗi CSI plugin chỉ được phép chạy duy nhất 1 controller plugin
  • Các chức năng cơ bản của Node Plugin:
    • Khai báo node plugin đang chạy trên node nào để K8s cung cấp volume cho node tương ứng
    • Cung cấp volume từ node đến các pod chỉ định bởi K8s
    • Trong 1 K8s cluster, mỗi CSI plugin phải triển khai node plugin trên tất cả các worker node để volume có thể cung cấp được cho Pod chạy ở một node bất kỳ

Controller Plugin và Node Plugin là các gRPC server và giao tiếp với K8s thông qua UNIX domain socket 

Phương thức triển khai

Cách thức triển khai CSI plugin khá linh hoạt, nhà cung cấp plugin có thể chọn một cách thức bất kỳ trong 4 cách thức sau:

Các RPC Interfaces:

Identity Interface:

Indentity interface cần phải được implement trong cả Node Plugin và Controller Plugin

Identity interface định nghĩa các API sau:

  • GetPluginInfo: trả về các thông tin của CSI plugin (vd: tên plugin, phiên bản,…)
  • GetPluginCapabilities: trả về các tính năng mà CSI plugin hỗ trợ. Ví dụ:
    • CSI plugin có triển khai controller plugin không
    • Khi volume được chia sẻ với nhiều pod, CSI plugin có hỗ trợ phân quyền cho từng pod không
  • Probe: API được gọi bởi K8s để liên tục giám sát CSI plugin có đang hoạt động hay không, do đó API này có thể trả về một response rỗng là đủ
service Identity {
  rpc GetPluginInfo(GetPluginInfoRequest)
    returns (GetPluginInfoResponse) {}
 
  rpc GetPluginCapabilities(GetPluginCapabilitiesRequest)
    returns (GetPluginCapabilitiesResponse) {}
 
  rpc Probe (ProbeRequest)
    returns (ProbeResponse) {}
}

Controller Interface:

Controller interface định nghĩa các API cần được implement trong controller plugin:


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) {}
 
  rpc GetCapacity (GetCapacityRequest)
    returns (GetCapacityResponse) {}
 
  rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest)
    returns (ControllerGetCapabilitiesResponse) {}
 
  rpc CreateSnapshot (CreateSnapshotRequest)
    returns (CreateSnapshotResponse) {}
 
  rpc DeleteSnapshot (DeleteSnapshotRequest)
    returns (DeleteSnapshotResponse) {}
 
  rpc ListSnapshots (ListSnapshotsRequest)
    returns (ListSnapshotsResponse) {}
 
  rpc ControllerExpandVolume (ControllerExpandVolumeRequest)
    returns (ControllerExpandVolumeResponse) {}
}

Node Interface

Node interface định nghĩa các API cần được implement trong node plugin:


service Node {
  rpc NodeStageVolume (NodeStageVolumeRequest)
    returns (NodeStageVolumeResponse) {}
 
  rpc NodeUnstageVolume (NodeUnstageVolumeRequest)
    returns (NodeUnstageVolumeResponse) {}
 
  rpc NodePublishVolume (NodePublishVolumeRequest)
    returns (NodePublishVolumeResponse) {}
 
  rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest)
    returns (NodeUnpublishVolumeResponse) {}
 
  rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest)
    returns (NodeGetVolumeStatsResponse) {}
 
 
  rpc NodeExpandVolume(NodeExpandVolumeRequest)
    returns (NodeExpandVolumeResponse) {}
 
 
  rpc NodeGetCapabilities (NodeGetCapabilitiesRequest)
    returns (NodeGetCapabilitiesResponse) {}
 
  rpc NodeGetInfo (NodeGetInfoRequest)
    returns (NodeGetInfoResponse) {}
}

Kịch bản vận hành cơ bản

Luồng vận hành với kịch bản dynamic provisioning:

  • CreateVolume: K8s gửi yêu cầu tạo volume → CSI plugin giải quyết yêu cầu của K8s bằng cách tạo 1 volume và trả về thông tin của volume đó
  • ControllerPublishVolume: Sau khi K8s nhận được phản hồi rằng volume đã được tạo, K8s sẽ yêu cầu CSI plugin gán volume này vào node đã được K8s lập lịch để chạy Pod
  • NodePublishVolume: Sau khi volume được gán vào node thành công, K8s sẽ tiếp tục gửi yêu cầu để CSI plugin gán volume từ node vào Pod đang yêu cầu

Luồng vận hành với kịch bản dynamic provisioning khi plugin có capability STAGE_UNSTAGE_VOLUME

  • Tương tự kịch bản dynamic provisioning cơ bản, tuy nhiên volume được mount vào staging folder trước khi được mount vào trong Pod

Luồng vận hành với kịch bản static provisioning:

  • ValidateVolumeCapabilities: K8s gửi yêu cầu đến plugin để kiểm tra volume có đáp ứng được yêu cầu về lưu trữ của Pod không
  • Controller/Node PublishVolume: nếu có volume đáp ứng yêu cầu, plugin sẽ thực hiện mount volume vào node và mount từ node vào Pod

Luồng vận hành với kịch bản static provision tối giản:

  • Luồng vận hành tối giản khi plugin không có controller (phương thức triển khai 4)
  • Volume được tạo sẵn sẽ được mount trực tiếp vào Pod

Ví dụ về CSI plugin:

Ở mục này bài blog sẽ đưa ra các ví dụ cụ thể để người đọc có thể hình dung được cách lập trình một CSI plugin đơn giản nhất có thể. Lưu ý rằng các ví dụ bên dưới đã được tối giản để dễ tiếp cận cho người mới bắt đầu và không nên được sử dụng trong thực tế vì thiếu rất nhiều các bước kiểm tra đầu vào/đầu ra.

Scope:

  • CSI plugin đơn giản này sẽ có chức năng tạo volume (từ nguồn là một NFS server) theo yêu cầu của pod và mount volume này cho pod sử dụng
  • Các interface được implement bao gồm:
    • Identity: GetPluginInfo, GetPluginCapabilities, Probe
    • Controller: CreateVolume, DeleteVolume, ControllerGetCapabilities
    • Node: NodePublishVolume, NodeUnpublishVolume, NodeGetCapabilities
  • Controller Plugin và Node plugin sẽ được đóng gói chung trong 1 file chạy
    • Tuy nhiên khi deploy, file chạy này sẽ được deploy theo 2 phương thức đồng thời (chi tiết ở phần Deploy).
    • Lưu ý rằng việc này khác với việc deploy duy nhất một gRPC server chạy chung cả Node và Controller plugin
  • Cấu trúc mã nguồn:
    • main.go: chạy gRPC server
    • pkg
      • driver.go: định nghĩa gRPC server để chạy các service controller, node và identity
      • identity.go: implement các identity interface 
      • node.go: implement các node interface 
      • controller.go: implement các controller interface
  • Nguồn lưu trữ được lấy từ một NFS server trong mạng local

main.go:

package main
 
import (
    "flag"
    "fmt"
    "driver"
)
 
var (
    endpoint = flag.String("endpoint", "unix:///csi/csi.sock", "default endpoint to run gRPC")     
    nodeid = flag.String("nodeid", "", "node id")
)
 
func main() {
    flag.Parse()
    fmt.Println(*endpoint)
    fmt.Println(*nodeid)
    drv := driver.NewDriver("demo-csi", *endpoint, *nodeid)
 
    if err := drv.Start(); err != nil {
        fmt.Printf("Error %s, running the driver", err.Error())
    }
}

pkg/driver.go:

package driver
 
import (
    "context"
    "github.com/container-storage-interface/spec/lib/go/csi"
)
 
func (d *Driver) GetPluginInfo(context.Context, *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
    return &csi.GetPluginInfoResponse{
        Name: d.name,
    }, nil
}
 
func (d *Driver) GetPluginCapabilities(context.Context, *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
    return &csi.GetPluginCapabilitiesResponse{
        Capabilities: []*csi.PluginCapability{
            {
                Type: &csi.PluginCapability_Service_{
                    Service: &csi.PluginCapability_Service{
                        Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
                    },
                },
            },
        },
    }, nil
}
 
func (d *Driver) Probe(context.Context, *csi.ProbeRequest) (*csi.ProbeResponse, error) {
    return &csi.ProbeResponse{}, nil
}

pkg/controller.go:
Thay thế <NFS_SERVER_IP>, <NFS_SERVER_DIR> với thông tin của server NFS cụ thể và <CONTROLLER_DIR> là một folder được mount vào trong container chạy controller plugin để thực hiện các thay đổi như tạo volume

package driver
 
import (
    "context"
    "os"
    "fmt"
    "os/exec"
    "path/filepath"
 
    "github.com/container-storage-interface/spec/lib/go/csi"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)
 
const (
    nfsServerIpAddr = "<NFS_SERVER_IP>"
    baseNfsServerDir = "<NFS_SERVER_DIR>"
    controllerDir = "<CONTROLLER_DIR>"
)
 
func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
    volumeName := req.GetName()
 
    sourcePath := fmt.Sprintf("%s:%s", nfsServerIpAddr, baseNfsServerDir)
    targetPath := controllerDir
 
    if _, err := os.Stat(controllerDir); os.IsNotExist(err) {
        err := os.Mkdir(controllerDir, 777)
        if err != nil {
            fmt.Println("error mkdir:", err)
        }
    }
 
    mountCmd := exec.Command("mount.nfs", sourcePath, targetPath)
    mountErr := mountCmd.Run()
    if mountErr != nil {
        fmt.Println("error mount:", mountErr)
    }
 
    newVolumePath := filepath.Join(targetPath, volumeName)
    mkdirErr := os.MkdirAll(newVolumePath, 777);
    if mkdirErr != nil {
        fmt.Println("error mkdir:", mkdirErr)
    }
 
    cmd := exec.Command("chmod", "777", newVolumePath)
    cmd.Run()
 
    return &csi.CreateVolumeResponse{
        Volume: &csi.Volume{
            VolumeId: filepath.Join(sourcePath, volumeName),
        },
    }, nil
}
 
func (d *Driver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
    volumeName := req.GetVolumeId()  
 
    sourcePath := fmt.Sprintf("%s:%s", nfsServerIpAddr, baseNfsServerDir)  
    targetPath := baseNfsClientDir
 
    mountCmd := exec.Command("mount.nfs", sourcePath, targetPath)
    mountErr := mountCmd.Run()
    if mountErr != nil {
        fmt.Println("error :", mountErr)
    }
 
    volumePath := filepath.Join(targetPath, volumeName)
    mkdirErr := os.RemoveAll(volumePath);
    if mkdirErr != nil {
        fmt.Println("error :", mkdirErr)
    }
 
    return &csi.DeleteVolumeResponse{}, nil
}
 
func (d *Driver) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) {
    capList := []csi.ControllerServiceCapability_RPC_Type{
        csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
    }
 
    var controllerCap []*csi.ControllerServiceCapability
 
    for _, cap := range capList {
        controllerCap = append(controllerCap, &csi.ControllerServiceCapability{
            Type: &csi.ControllerServiceCapability_Rpc{
                Rpc: &csi.ControllerServiceCapability_RPC{
                    Type: cap,
                },
            },
        })
    }
 
    return &csi.ControllerGetCapabilitiesResponse{
        Capabilities: controllerCap,
    }, nil
}

Ví dụ của một API không được implement, API đó cần trả về non-ok code, cụ thể hơn là unimplemented code

func (d *Driver) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
    fmt.Println("req :", req.VolumeId)
    return nil, status.Error(codes.Unimplemented, "")
}

pkg/node.go:

package driver
 
import (
    "context"
    "fmt"
    "os/exec"
 
    "github.com/container-storage-interface/spec/lib/go/csi"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)
 
func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (* csi.NodePublishVolumeResponse, error) {
    volumeId := req.GetVolumeId()
 
    targetPath := req.GetTargetPath()
    sourcePath := volumeId
 
    if _, err := os.Stat(targetPath); os.IsNotExist(err) {
        err := os.Mkdir(targetPath, 777)
        if err != nil {
            fmt.Println("error mkdir:", err)
        }
    }
 
    mountCmd := exec.Command("mount.nfs", sourcePath, targetPath)
    mountErr := mountCmd.Run()
    if mountErr != nil {
        fmt.Println("error :", mountErr)
    }
 
    return &csi.NodePublishVolumeResponse{}, nil
}
 
func (d *Driver) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (* csi.NodeUnpublishVolumeResponse, error) {
    targetPath := req.GetTargetPath()
 
    umountCmd := exec.Command("umount", targetPath)
    umountErr := umountCmd.Run()
    if umountErr != nil {
        fmt.Println("error :", umountErr)
    }
 
    return &csi.NodeUnpublishVolumeResponse{}, nil
}
 
func (d *Driver) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (* csi.NodeGetCapabilitiesResponse, error) {
    capList := []csi.NodeServiceCapability_RPC_Type{
        csi.NodeServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME,
    }
 
    var nodeCap []*csi.NodeServiceCapability
 
    for _, cap := range capList {
        nodeCap = append(nodeCap, &csi.NodeServiceCapability{
            Type: &csi.NodeServiceCapability_Rpc{
                Rpc: &csi.NodeServiceCapability_RPC{
                    Type: cap,
                },
            },
        })
    }
 
    return &csi.NodeGetCapabilitiesResponse{
        Capabilities: nodeCap,
    }, nil
}
 
func (d *Driver) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (* csi.NodeGetInfoResponse, error) {
    return &csi.NodeGetInfoResponse{
        NodeId: d.nodeid,
    }, nil
}

Ví dụ của một API không được implement, API đó cần trả về non-ok code, cụ thể hơn là unimplemented code

func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (* csi.NodeStageVolumeResponse, error) {
    fmt.Println("NodeStageVolume")
    return nil, status.Error(codes.Unimplemented, "NodeStageVolume")
}

Deploy CSI plugin lên K8s:

Đóng gói CSI plugin:

FROM registry.k8s.io/build-image/debian-base:bullseye-v1.4.2
RUN apt update && apt upgrade -y && apt-mark unhold libcap2 && clean-install ca-certificates mount nfs-common netbase
ADD csi /usr/local/bin/csi
ENTRYPOINT ["/usr/local/bin/csi"]

Deploy Controller:

Để deploy CSI plugin chạy Controller gRPC server trên K8s cần tuân thủ các quy tắc sau:

  • Controller cần được đóng gói cùng với các helper của K8s như external-attacher và externel-provisioner để giao tiếp với K8s master thông qua API server
  • Controller cùng các helpers của K8s sẽ là các container thành phần của một Pod và Pod đó sẽ được triển khai dưới dạng Deploment hoặc Statefulset
  • Controller và tất cả các helpers của K8s sẽ cùng được mount volume emptyDir (một volume được tạo ra khi Pod được tạo và có thể được truy cập bởi tất cả các container trong Pod)
    • Controller sẽ tạo một UNIX domain socket trong thư mục emptyDir này và chờ yêu cầu từ K8s
    • K8s helpers cũng sẽ giao tiếp và gửi yêu cầu đến Controller thông qua socket này

Trong file controller.yaml ví dụ bên dưới:

  • Một deployment được tạo với tên “demo-csi-controller” với 3 container:
  • volume emptyDir được mount vào thư mục /csi trên cả 3 container
  • 2 K8s helpers (external-provisioner, external-attacher, 2 helpers này cần argument –csi-address để biết vị trí của socket mà controller server sẽ chờ yêu cầu
  • Controller (container image này dùng chung cho cả controller và node plugin, khi được deploy kèm với K8s helpers dành cho controller, K8s sẽ hiểu đây là controller plugin
  • Controller cần privilege và cap SYS_ADMIN để có  thể mount Volume từ nguồn NFS server để tạo volume dưới dạng một folder
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: demo-csi-controller
  name: demo-csi-controller
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-csi-controller
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: demo-csi-controller
    spec:
      serviceAccountName: demo-csi-sa
      containers:
      - name: external-provisioner
        image: k8s.gcr.io/sig-storage/csi-provisioner:v3.1.0
        args:
        - "--csi-address=$(CSI_ENDPOINT)"
        env:
        - name: CSI_ENDPOINT
          value: /csi/csi.sock
        volumeMounts:
        - mountPath: /csi
          name: domain-socket
      - name: external-attacher
        image: k8s.gcr.io/sig-storage/csi-attacher:v3.5.0
        args:
        - "--csi-address=$(CSI_ENDPOINT)"
        - "-v=5"
        env:
        - name: CSI_ENDPOINT
          value: /csi/csi.sock
        volumeMounts:
        - name: domain-socket
          mountPath: /csi
      - name: demo-csi-controller
        securityContext:
          privileged: true
          capabilities:
            add: ["SYS_ADMIN"]
          allowPrivilegeEscalation: true
        image: ltson1/demo-csi-img
        env:
          - name: CSI_ENDPOINT
            value: unix:///csi/csi.sock
        imagePullPolicy: "Always"
        volumeMounts:
        - mountPath: /csi
          name: domain-socket
      volumes:
      - name: domain-socket
        emptyDir: {}
status: {}

Deploy Node:

Để deploy CSI plugin chạy Node gRPC server cần tuân thủ các quy tắc sau:

  • Node server cần được đóng gói cùng node-driver-registrar (cũng là một helper container của K8s)
  • Pod gồm container của CSI plugin và node-driver-registrar sẽ được deploy dưới dạng DaemonSet (mỗi node bắt buộc sẽ phải chạy một Pod)
  • Các volume sẽ được cung cấp cho các container theo quy tắc sau:
    • Thư mục /var/lib/kubelet/plugins_registry của mỗi node sẽ được mount vào /registration của Pod chạy trên node tương ứng
      • node-driver-registrar sẽ tạo một socket ở thư mục này để giao tiếp với K8s để đăng ký node plugin với K8s
    • Thư mục /var/lib/kubelet/plugins/[SanitizedCSIDriverName]/ sẽ được tạo ra nếu chưa tồn tại và mount vào contaienr chạy Node plugin tại thư mục mà UNIX socket được tạo ra để chờ yêu cầu từ K8s
    • Thư mục /var/lib/kubelet/ sẽ được mount vào thư mục /var/lib/kubelet/ trong container chạy Node plugin với option Bidirectional để các thay đổi sau khi mount vào container sẽ được cập nhật lên máy chạy node plugin. Ví dụ như việc tạo socket trong thư mục /var/lib/kubelet/plugins/[SanitizedCSIDriverName]/

Trong file node.yaml ví dụ bên dưới:

  • Một daemonset được tạo với tên demo-csi-node với 2 container Node plugin và node-driver-registrar
  • Thư mục /var/lib/kubelet/plugins_registry/ được mount vào trong container node-driver-registrar tại /registration
  • Thư mục /var/lib/kubelet/plugins/demo-csi/ được mount vào trong container chạy Node plugin tại /csi
  • Thư mục /var/lib/kubelet được mount vào trong container chạy Node plugin tại /var/lib/kubelet với mountPropagation option là Bidirectional
  • Node plugin cần 2 arguments:
    • –csi-endpoint: endpoint để chạy node server – unix:///csi/csi.sock (chính là socket được tạo trong thư mục /csi)
    • –nodeid: sử dụng để khai báo với K8s node plugin đang chạy trên node nào khi K8s gọi API GetNodeInfo
  • node-driver-registrar cần 2 arguments:
    • –csi-address: đường dẫn của UNIX socket để giao tiếp với Node server
    • –kubelet-registration-path: đường dẫn của UNIX socket nhưng ở trên node để node-driver-registrar đăng ký node plugin với K8s
kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: demo-csi-node
spec:
  selector:
    matchLabels:
      app: demo-csi-node
  template:
    metadata:
      labels:
        app: demo-csi-node
    spec:
      serviceAccountName: demo-csi-sa
      hostNetwork: true
      dnsPolicy: Default
      containers:
        - name: node-driver-registrar
          image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.6.2
          args:
            - --v=2
            - --csi-address=/csi/csi.sock
            - --kubelet-registration-path=/var/lib/kubelet/plugins/demo-csi/csi.sock
          volumeMounts:
            - name: socket-dir
              mountPath: /csi
            - name: registration-dir
              mountPath: /registration
        - name: demo-csi-node
          securityContext:
            privileged: true
            capabilities:
              add: ["SYS_ADMIN"]
            allowPrivilegeEscalation: true
          image: ltson1/demo-csi-img
          args:
            - "--endpoint=$(CSI_ENDPOINT)"
            - "--nodeid=$(NODE_ID)"
          env:
            - name: NODE_ID
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: CSI_ENDPOINT
              value: unix:///csi/csi.sock
          imagePullPolicy: "Always"
          volumeMounts:
            - name: socket-dir
              mountPath: /csi
            - name: pods-mount-dir
              mountPath: /var/lib/kubelet/
              mountPropagation: "Bidirectional"
      volumes:
        - name: socket-dir
          hostPath:
            path: /var/lib/kubelet/plugins/demo-csi
            type: DirectoryOrCreate
        - name: pods-mount-dir
          hostPath:
            path: /var/lib/kubelet/
            type: Directory
        - hostPath:
            path: /var/lib/kubelet/plugins_registry
            type: Directory
          name: registration-dir

Storageclass:

Cuối cùng chúng ta cần tạo một storage class, các yêu cầu tài nguyên (persistent volume claim – PVC) tới storage class này sẽ được cung cấp bởi plugin trên

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: demo-csi-sc
provisioner: demo-csi
volumeBindingMode: Immediate

Demo

Deploy Controller Plugin: kubectl create -f controller.yaml

Deploy Node Plugin: kubectl create -f node.yaml

Create Storageclass: kubectl create -f storageclass.yaml

Kiểm tra node và controller plugin được chạy thành công: kubectl get pod -o wide

Kiểm tra controller plugin đã giao tiếp được với K8s qua socket: kubectl logs <tên_controller_plugin_pod>

kiểm tra log có dòng thông báo “Started provisioner controller …”

Kiểm tra node plugin được đăng ký thành công với K8s: kubectl logs <tên_node_plugin_pod>
Kiểm tra log có dòng thông báo “Received NotifyRegistrationStatus call: &RegistrationStatus{PluginRegistered:true,Error:,}” là plugin được đăng ký thành công

Kiểm tra storageclass được deploy thành công: kubectl get storage class

Sau khi kiểm tra thấy các thành phần chạy thành công, ta tạo một pod sử dụng volume để kiểm tra tính năng:

testpod.yaml:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: demo-csi-sc
---
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: test-pvc
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - name: data
      mountPath: /test_data

Kiểm tra testpod chạy thành công:

Kiểm tra PVC được gán với một PV được tạo ra bởi CSI plugin:

Kiểm tra volume dưới dạng một thư mục với tên là ID của volume được tạo trong thư mục chạy NFS server:

Vào trong pod và tạo thử file trong volume được mount (theo file testpod.yaml volume sẽ được mount vào thư mục :

kubectl exec -it <tên_test_pod> — /bin/bash

Kiểm tra file được tạo trong pod được cập nhật trong folder tại server NFS:

Reference:

https://github.com/container-storage-interface/spec/blob/master/spec.md
https://github.com/kubernetes/design-proposals-archive/blob/main/storage/container-storage-interface.md
https://github.com/container-storage-interface/spec/blob/master/lib/go/csi/csi.pb.go
https://arslan.io/2018/06/21/how-to-write-a-container-storage-interface-csi-plugin/
https://github.com/kubernetes-csi/csi-driver-nfs

You may also like...

5 2 đánh giá
Đánh giá bài viết
Theo dõi
Thông báo của
guest
0 Góp ý
Phản hồi nội tuyến
Xem tất cả bình luận
0
Rất thích suy nghĩ của bạn, hãy bình luận.x