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:
- Sau khi chạy “go build” ở thư mục mã nguồn, ta sẽ thu được file chạy chung cho cả Identity, Controller và Node
- Tạo Dockerfile để đóng gói container image cho CSI plugin (nguồn: https://github.com/kubernetes-csi/csi-driver-nfs/blob/master/Dockerfile)
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]/
- Thư mục
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:
Hay