searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

快速开发containerd插件

2023-08-15 10:25:33
231
0

containerd 是一种容器运行时,且是一个CNCF 毕业项目。

从 Kubernetes1.24 开始,dockershim已从 Kubernetes 项目移出,标志着Kubernetes统一按照 CRI 支持容器运行时。而 containerd 作为广泛使用的容器运行时,流行必定有其过人之处。

containerd 的特点之一:插件式拓展

containerd采用插件式架构,方便拓展。从架构图和配置文件可看出,Content、Snapshot 组件都是 以 plugin 方式接入。

/etc/containerd/cofnig.toml
[plugins]
  [plugins."io.containerd.metadata.v1.bolt"]
    content_sharing_policy = "shared"

  [plugins."io.containerd.monitor.v1.cgroups"]
    no_prometheus = false

  [plugins."io.containerd.runtime.v1.linux"]
    no_shim = false
    runtime = "runc"
    runtime_root = ""
    shim = "containerd-shim"
    shim_debug = false

  [plugins."io.containerd.runtime.v2.task"]
    platforms = ["linux/amd64"]
    sched_core = false

  [plugins."io.containerd.snapshotter.v1.native"]
    root_path = ""

  [plugins."io.containerd.snapshotter.v1.overlayfs"]
    root_path = ""
    upperdir_label = false

所有的插件通过 containerd/plugin.Reginster() 注册,并在 containerd 启动时进行初始化。

下面演示如何通过对 containerd 二次开发加入定制插件demo,提供 gRPC 访问和containerd 内部 cri 插件使用。

定义接口

containerd 所有对外暴露的 gRPC 接口定义在 api 目录下。在 api/services 新建 demo/v1 目录,并新建 doc.go 文件,声明 package,并新建文件夹 v1,新建文件 demo.proto。目录如下:

❯ tree demo
demo
└── v1
    ├── demo.pb.go
    ├── demo.proto
    └── doc.go

2 directories, 3 files
demo.proto
syntax = "proto3";

package containerd.services.demo.v1;

option go_package = "github.com/containerd/containerd/api/services/demo/v1;demo";

service Demo {
  rpc Ping(PingRequest) returns (PongResponse);
}

message PingRequest {
  string msg = 1;
}

message PongResponse {
  string msg = 2;
}

在 containerd 项目根目录,执行 make protos 即可生成 demo.pb.go 文件。 make protos 使用了protobuild 项目,深入研究可参考 protobuild 项目说明。

实现接口

containerd 的所有 services 放在 services 目录下,进入目录可看到content、snapshots 等目录,对应是 containerd 的插件。

新建 demo 目录,并新建 local.go 和 service.go 文件。代码如下。local.go 注册 ServicePlugin,对外提供 demo 的所有逻辑接口。service.go 注册 GRPCPlugin,对外提供 grpc 访问。

local.go
package demo

import (
	"context"
	demoapi "github.com/containerd/containerd/api/services/demo/v1"
	"github.com/containerd/containerd/plugin"
	"github.com/containerd/containerd/services"
	"google.golang.org/grpc"
)

func init() {
	plugin.Register(&plugin.Registration{
		Type: plugin.ServicePlugin,
		ID:   services.DemoService,
		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
			return &local{}, nil
		},
	})
}

type local struct{}

var _ demoapi.DemoClient = &local{}

func (l *local) Ping(ctx context.Context, req *demoapi.PingRequest, _ ...grpc.CallOption) (*demoapi.PongResponse, error) {
	resp := &demoapi.PongResponse{Msg: "hello " + req.Msg}
	return resp, nil
}
service.go
package demo

import (
	demoapi "github.com/containerd/containerd/api/services/demo/v1"
	"github.com/containerd/containerd/plugin"
	"github.com/containerd/containerd/services"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

func init() {
	plugin.Register(&plugin.Registration{
		Type: plugin.GRPCPlugin,
		ID:   "demo",
		Requires: []plugin.Type{
			plugin.ServicePlugin,
		},
		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
			p, err := ic.GetByID(plugin.ServicePlugin, services.DemoService)
			if err != nil {
				return nil, err
			}
			return &service{local: p.(demoapi.DemoClient)}, nil
		},
	})
}

type service struct {
	local demoapi.DemoClient
}

var _ demoapi.DemoServer = &service{}

func (s *service) Register(server *grpc.Server) error {
	demoapi.RegisterDemoServer(server, s)
	return nil
}

func (s *service) Ping(ctx context.Context, req *demoapi.PingRequest) (*demoapi.PongResponse, error) {
	return s.local.Ping(ctx, req)
}

简单的 demo plugin 已经实现,接下来部署运行 containerd。

通过 make bin/containerd 重新编译 containerd可执行文件,重新部署后,通过 journalctl 查看日志,可发现以下 log。说明 demo-service 和 demo GRPC plugin 已经在 containerd 启动时成功注册了。

Aug 15 15:06:02 0000000g-1skzsvv7ro containerd[4125]: time="2023-08-15T15:06:02.899951562+08:00" level=info msg="loading plugin \"io.containerd.service.v1.demo-service\"..." type=io.containerd.service.v1

Aug 15 15:06:02 0000000g-1skzsvv7ro containerd[4125]: time="2023-08-15T15:06:02.902695763+08:00" level=info msg="loading plugin \"io.containerd.grpc.v1.demo\"..." type=io.containerd.grpc.v1

再进一步探索,查看Registration定义,发现 Requires字段,此字段声明当前 plugin 依赖哪些 plugin,contaienrd 启动时会分析依赖关系调整 plugin 启动顺序。如 demo GRPC plugin 声明依赖 ServicePlugin,通过 log 可确认 demo GRPCPlugin 在 service 启动后启动。实现逻辑在 LoadPlugins 函数。

 

验证

编写一个简单的 client,验证 containerd 是否已经对外暴露 demo GRPC 接口。如果对 Golang GRPC 实现不了解,可参考官方 helloword 例子,了解 server 和 client 端使用。

在 cmd 新建 demo 目录,新建 client.go 文件。demo如下,通过socket 连接 containerd,再访问 Ping API。

package main

import (
	"context"
	demoapi "github.com/containerd/containerd/api/services/demo/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
	"time"
)

func main() {
	opts := []grpc.DialOption{
		grpc.WithBlock(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	}
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()
	conn, err := grpc.DialContext(ctx, "unix:///run/containerd/containerd.sock", opts...)
	if err != nil {
		log.Fatal("grpc dial error", err)
	}
	demoClient := demoapi.NewDemoClient(conn)
	resp, err := demoClient.Ping(ctx, &demoapi.PingRequest{Msg: "container"})
	if err != nil {
		log.Fatal("failed to ping", err)
	}
	log.Println("ping pong", resp.Msg)
}

通过 make bin/demo 编译 demo 执行文件,执行测试。

❯ sudo ./bin/demo
2023/08/15 15:06:18 ping pong hello container

正常打印出log,说明正常访问。

CRI访问

部署在 k8s 集群节点的 containerd,kebelet 通过 cri接口访问 containerd,官方对此有说明。

访问方式如下:

那怎样能让 CRI-plugin 能访问自定义 plugin 呢?不妨也查看源代码寻找答案。

其实 cri 也是通过 GRPC plugin 方式注册的,且 containerd 在 CRI-pulgin 里是一个 client。 通过 contaienrd.New() 实例化的一个 client,提供 WithServices ClientOpt注入更多依赖 service。

这样只需要额外提供实例化 DemoService 的 ServiceOpt 即可。

// pkg/cri/cri.go

// Register CRI service plugin
func init() {
	config := criconfig.DefaultConfig()
	plugin.Register(&plugin.Registration{
		Type:   plugin.GRPCPlugin,
		ID:     "cri",
		Config: &config,
		Requires: []plugin.Type{
			plugin.EventPlugin,
			plugin.ServicePlugin,
		},
		InitFn: initCRIService,
	})
}

func initCRIService(ic *plugin.InitContext) (interface{}, error) {
    ...
    serviceOpts, err := getServicesOpts(ic)
    ...
    client, err := containerd.New("", containerd.WithServices(serviceOpts...))
    ...
}


func getServicesOpts(ic *plugin.InitContext) ([]containerd.ServicesOpt, error) {
    ...
    for s, fn := range map[string]func(interface{}) containerd.ServicesOpt{
    ...
		services.DemoService: func(s interface{}) containerd.ServicesOpt {
			return containerd.WithDemoService(s.(demoapi.DemoClient))
		},
    ...
}

WithDemoService 可自行探索实现,思路是观察 plugins 类型和 ServiceOpt 定义注入 DemoService。

至此,一个可供 gRPC 和 CRI-Plugin 访问的插件已完成。

0条评论
0 / 1000
大瘾
1文章数
0粉丝数
大瘾
1 文章 | 0 粉丝
大瘾
1文章数
0粉丝数
大瘾
1 文章 | 0 粉丝
原创

快速开发containerd插件

2023-08-15 10:25:33
231
0

containerd 是一种容器运行时,且是一个CNCF 毕业项目。

从 Kubernetes1.24 开始,dockershim已从 Kubernetes 项目移出,标志着Kubernetes统一按照 CRI 支持容器运行时。而 containerd 作为广泛使用的容器运行时,流行必定有其过人之处。

containerd 的特点之一:插件式拓展

containerd采用插件式架构,方便拓展。从架构图和配置文件可看出,Content、Snapshot 组件都是 以 plugin 方式接入。

/etc/containerd/cofnig.toml
[plugins]
  [plugins."io.containerd.metadata.v1.bolt"]
    content_sharing_policy = "shared"

  [plugins."io.containerd.monitor.v1.cgroups"]
    no_prometheus = false

  [plugins."io.containerd.runtime.v1.linux"]
    no_shim = false
    runtime = "runc"
    runtime_root = ""
    shim = "containerd-shim"
    shim_debug = false

  [plugins."io.containerd.runtime.v2.task"]
    platforms = ["linux/amd64"]
    sched_core = false

  [plugins."io.containerd.snapshotter.v1.native"]
    root_path = ""

  [plugins."io.containerd.snapshotter.v1.overlayfs"]
    root_path = ""
    upperdir_label = false

所有的插件通过 containerd/plugin.Reginster() 注册,并在 containerd 启动时进行初始化。

下面演示如何通过对 containerd 二次开发加入定制插件demo,提供 gRPC 访问和containerd 内部 cri 插件使用。

定义接口

containerd 所有对外暴露的 gRPC 接口定义在 api 目录下。在 api/services 新建 demo/v1 目录,并新建 doc.go 文件,声明 package,并新建文件夹 v1,新建文件 demo.proto。目录如下:

❯ tree demo
demo
└── v1
    ├── demo.pb.go
    ├── demo.proto
    └── doc.go

2 directories, 3 files
demo.proto
syntax = "proto3";

package containerd.services.demo.v1;

option go_package = "github.com/containerd/containerd/api/services/demo/v1;demo";

service Demo {
  rpc Ping(PingRequest) returns (PongResponse);
}

message PingRequest {
  string msg = 1;
}

message PongResponse {
  string msg = 2;
}

在 containerd 项目根目录,执行 make protos 即可生成 demo.pb.go 文件。 make protos 使用了protobuild 项目,深入研究可参考 protobuild 项目说明。

实现接口

containerd 的所有 services 放在 services 目录下,进入目录可看到content、snapshots 等目录,对应是 containerd 的插件。

新建 demo 目录,并新建 local.go 和 service.go 文件。代码如下。local.go 注册 ServicePlugin,对外提供 demo 的所有逻辑接口。service.go 注册 GRPCPlugin,对外提供 grpc 访问。

local.go
package demo

import (
	"context"
	demoapi "github.com/containerd/containerd/api/services/demo/v1"
	"github.com/containerd/containerd/plugin"
	"github.com/containerd/containerd/services"
	"google.golang.org/grpc"
)

func init() {
	plugin.Register(&plugin.Registration{
		Type: plugin.ServicePlugin,
		ID:   services.DemoService,
		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
			return &local{}, nil
		},
	})
}

type local struct{}

var _ demoapi.DemoClient = &local{}

func (l *local) Ping(ctx context.Context, req *demoapi.PingRequest, _ ...grpc.CallOption) (*demoapi.PongResponse, error) {
	resp := &demoapi.PongResponse{Msg: "hello " + req.Msg}
	return resp, nil
}
service.go
package demo

import (
	demoapi "github.com/containerd/containerd/api/services/demo/v1"
	"github.com/containerd/containerd/plugin"
	"github.com/containerd/containerd/services"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

func init() {
	plugin.Register(&plugin.Registration{
		Type: plugin.GRPCPlugin,
		ID:   "demo",
		Requires: []plugin.Type{
			plugin.ServicePlugin,
		},
		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
			p, err := ic.GetByID(plugin.ServicePlugin, services.DemoService)
			if err != nil {
				return nil, err
			}
			return &service{local: p.(demoapi.DemoClient)}, nil
		},
	})
}

type service struct {
	local demoapi.DemoClient
}

var _ demoapi.DemoServer = &service{}

func (s *service) Register(server *grpc.Server) error {
	demoapi.RegisterDemoServer(server, s)
	return nil
}

func (s *service) Ping(ctx context.Context, req *demoapi.PingRequest) (*demoapi.PongResponse, error) {
	return s.local.Ping(ctx, req)
}

简单的 demo plugin 已经实现,接下来部署运行 containerd。

通过 make bin/containerd 重新编译 containerd可执行文件,重新部署后,通过 journalctl 查看日志,可发现以下 log。说明 demo-service 和 demo GRPC plugin 已经在 containerd 启动时成功注册了。

Aug 15 15:06:02 0000000g-1skzsvv7ro containerd[4125]: time="2023-08-15T15:06:02.899951562+08:00" level=info msg="loading plugin \"io.containerd.service.v1.demo-service\"..." type=io.containerd.service.v1

Aug 15 15:06:02 0000000g-1skzsvv7ro containerd[4125]: time="2023-08-15T15:06:02.902695763+08:00" level=info msg="loading plugin \"io.containerd.grpc.v1.demo\"..." type=io.containerd.grpc.v1

再进一步探索,查看Registration定义,发现 Requires字段,此字段声明当前 plugin 依赖哪些 plugin,contaienrd 启动时会分析依赖关系调整 plugin 启动顺序。如 demo GRPC plugin 声明依赖 ServicePlugin,通过 log 可确认 demo GRPCPlugin 在 service 启动后启动。实现逻辑在 LoadPlugins 函数。

 

验证

编写一个简单的 client,验证 containerd 是否已经对外暴露 demo GRPC 接口。如果对 Golang GRPC 实现不了解,可参考官方 helloword 例子,了解 server 和 client 端使用。

在 cmd 新建 demo 目录,新建 client.go 文件。demo如下,通过socket 连接 containerd,再访问 Ping API。

package main

import (
	"context"
	demoapi "github.com/containerd/containerd/api/services/demo/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
	"time"
)

func main() {
	opts := []grpc.DialOption{
		grpc.WithBlock(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	}
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()
	conn, err := grpc.DialContext(ctx, "unix:///run/containerd/containerd.sock", opts...)
	if err != nil {
		log.Fatal("grpc dial error", err)
	}
	demoClient := demoapi.NewDemoClient(conn)
	resp, err := demoClient.Ping(ctx, &demoapi.PingRequest{Msg: "container"})
	if err != nil {
		log.Fatal("failed to ping", err)
	}
	log.Println("ping pong", resp.Msg)
}

通过 make bin/demo 编译 demo 执行文件,执行测试。

❯ sudo ./bin/demo
2023/08/15 15:06:18 ping pong hello container

正常打印出log,说明正常访问。

CRI访问

部署在 k8s 集群节点的 containerd,kebelet 通过 cri接口访问 containerd,官方对此有说明。

访问方式如下:

那怎样能让 CRI-plugin 能访问自定义 plugin 呢?不妨也查看源代码寻找答案。

其实 cri 也是通过 GRPC plugin 方式注册的,且 containerd 在 CRI-pulgin 里是一个 client。 通过 contaienrd.New() 实例化的一个 client,提供 WithServices ClientOpt注入更多依赖 service。

这样只需要额外提供实例化 DemoService 的 ServiceOpt 即可。

// pkg/cri/cri.go

// Register CRI service plugin
func init() {
	config := criconfig.DefaultConfig()
	plugin.Register(&plugin.Registration{
		Type:   plugin.GRPCPlugin,
		ID:     "cri",
		Config: &config,
		Requires: []plugin.Type{
			plugin.EventPlugin,
			plugin.ServicePlugin,
		},
		InitFn: initCRIService,
	})
}

func initCRIService(ic *plugin.InitContext) (interface{}, error) {
    ...
    serviceOpts, err := getServicesOpts(ic)
    ...
    client, err := containerd.New("", containerd.WithServices(serviceOpts...))
    ...
}


func getServicesOpts(ic *plugin.InitContext) ([]containerd.ServicesOpt, error) {
    ...
    for s, fn := range map[string]func(interface{}) containerd.ServicesOpt{
    ...
		services.DemoService: func(s interface{}) containerd.ServicesOpt {
			return containerd.WithDemoService(s.(demoapi.DemoClient))
		},
    ...
}

WithDemoService 可自行探索实现,思路是观察 plugins 类型和 ServiceOpt 定义注入 DemoService。

至此,一个可供 gRPC 和 CRI-Plugin 访问的插件已完成。

文章来自个人专栏
containerd
1 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0