[K8S] 认识 CRI (含代码)

27,330 阅读4分钟

1. 什么是CRI?

CRI 全称是 Container Runtime Interface(容器运行时接口),k8s发展了这么多年相信大家也不陌生,那么容器运行时我们通常指的就是 runc传送门)。容器运行时就是我们最终启动容器进程的一个工具,帮我们组装好一个进程空间。

容器运行时(Container Runtime)了解完后,那么接口谁去统一实现呢?根据k8s官方介绍有以下几种:

PS : Kubernetes 1.24 dockershim 被弃用,代替者为 cri-dockerd,所以使用docker engine 还是能正常使用。

我们现在都知道 K8S 推荐使用Containerd作为启动容器的媒介,而Containerd最终会通过containerd-shim-runc 调用 runc 启动容器。早期使用 Docker Engine 时用dockershim 来实现了 CRI,然后Docker Engine 调用 Containerd 相当于同时依赖了 Docker Engine 和 Containerd ,那么如果 Docker 本身出现 bug 会影响 K8S 对容器的把控。

如图所示,kubelet 是通过CRI的grpc接口与容器运行时的实现产品(Containerd)进行交互;

image.png

由于容器运行时这个概念可能部分读者会有歧义,容器运行时可以指 Containerd 也可以指 RunC,这是看你所在的角度;实际上 Containerd 是实现了CRI的整体功能如pod概念,cni的调用方式image的拉取等,而实际拉起容器的RunC; 为了消除歧义后面指容器运行时就是指实现了CRI的产品;

2. CRI 控制工具

Containerd 的安装和使用 传送门

github.com/kubernetes-… cri-tools 实现了通过命令行方式去操控所有实现了CRI的容器运行时产品,如本文提到的Containerd。下面我们看看 cri-tools 的常用命令

2.1 安装

VERSION="v1.23.0"
wget https://github.com/kubernetes-sigs/cri-> tools/releases/download/$VERSION/crictl-$VERSION-linux-amd64.tar.gz
sudo tar zxvf crictl-$VERSION-linux-amd64.tar.gz -C /usr/local/bin
rm -f crictl-$VERSION-linux-amd64.tar.gz

2.2 配置

  • 需要去配置 runtime-endpoint 和 image-endpoint 这两个ep代表你需要操控的容器运行时;若不配置ep就会按改顺序进行查找 [unix:///var/run/dockershim.sock unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock],所以我们可以改成以下配置使用 Containerd
    cat /etc/crictl.yaml
    runtime-endpoint: "unix:///run/containerd/containerd.sock"
    image-endpoint: ""
    timeout: 0
    debug: false
    pull-image-on-create: false
    disable-pull-on-run: false
    

2.3 常用命令

  • 查看容器

    crictl ps -a
    
  • 查看Pod

    crictl pods
    
  • 查看镜像

    crictl img
    
  • 创建一个pod,但不创建容器,因为容器和pod本身是两个概念(linux只有进程空间概念,并没有pod概念,不再展开聊)。

    pod.yaml

    metadata:
       name: mysanbox
       namespace: default
    log_directory: "/root/temp"
    port_mappings:
       - protocol: 0
         container_port: 80
    
    crictl runp pod.yaml
    
  • 创建一个pod,同时创建容器。

    container.yaml

    metadata:
      name: test-ngx
    image:
      image: docker.io/nginx:1.18-alpine
    log_path: ngx.log
    

    pod.yaml

    metadata:
       name: mysanbox
       namespace: default
    log_directory: "/root/temp"
    port_mappings:
       - protocol: 0
         container_port: 80
    
    crictl run container.yaml pod.yaml
    
  • 剩下的命令和 Docker 大同小异,详细请参考 传送门

3. Golang 代码实现

通过代码调用CRI的grpc接口

3.1 代码结构

tree k8s-native/
k8s-native/
├── containers
│   ├── exec.go
│   ├── main.go
│   └── run.go
├── deploy
│   ├── container.yaml
│   └── sandbox.yaml
├── go.mod
├── go.sum
├── helper.go
├── images
│   └── main.go
├── pods
│   └── main.go
└── version
    └── main.go
  • helper.go 协助方法
package k8s_native

import (
        "context"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
        "log"
)

//const RuntimeEntryPoint = "unix:///var/run/dockershim.sock"

const RuntimeEntryPoint = "/var/run/containerd/containerd.sock"

func InitCRIConnection(ctx context.Context) *grpc.ClientConn {
        // 不使用证书验证
        grpcOpts := []grpc.DialOption{
                grpc.WithTransportCredentials(insecure.NewCredentials()),
        }

        // grpc 调用
        conn, err := grpc.DialContext(ctx, RuntimeEntryPoint, grpcOpts...)
        if err != nil {
                log.Panic(err)
        }
        return conn

}

func NewRuntimeServiceClient(ctx context.Context) v1alpha2.RuntimeServiceClient {
        return v1alpha2.NewRuntimeServiceClient(InitCRIConnection(ctx))
}

func NewImageServiceClient(ctx context.Context) v1alpha2.ImageServiceClient {
        return v1alpha2.NewImageServiceClient(InitCRIConnection(ctx))
}

func WrapE(err error) {
        if err != nil {
                log.Panic(err)
        }
}

3.2 version/main.go (打印version)

package main

// print containerd version
import (
        "context"
        k8s_native "k8s-native"
        "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
        "log"
        "time"
)

func main() {
        // timeout
        ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
        defer cancel()

        req := &v1alpha2.VersionRequest{}
        rsc := k8s_native.NewRuntimeServiceClient(ctx)
        resp, err := rsc.Version(ctx, req)
        k8s_native.WrapE(err)
        log.Println(resp)
}

3.3 images/main.go (打印images)

package main

import (
        "context"
        k8s_native "k8s-native"
        "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
        "log"
        "time"
)

func main() {

        ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
        defer cancel()
        cli := k8s_native.NewImageServiceClient(ctx)
        req := &v1alpha2.ListImagesRequest{}
        resp, err := cli.ListImages(ctx, req)

        k8s_native.WrapE(err)

        for _, img := range resp.GetImages() {
                log.Println("====================")
                log.Println(img.RepoTags)
                log.Println(img.Id)
                log.Println(img.Size())
        }
}

3.4 pods/main.go (创建pod,但不创建container)

package main

import (
        "context"
        "gopkg.in/yaml.v2"
        k8s_native "k8s-native"
        "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
        "log"
        "time"
)

const (
        sandboxYaml = `
metadata:
   name: mysanbox
   namespace: default
log_directory: "/root/temp"
port_mappings:
   - protocol: 0
     container_port: 80`
)

// 创建一个pod,但并未创建 container
// crictl runp sandbox.yaml
func main() {
        ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
        defer cancel()

        config := &v1alpha2.PodSandboxConfig{}
        k8s_native.WrapE(yaml.Unmarshal([]byte(sandboxYaml), config))
        req := &v1alpha2.RunPodSandboxRequest{Config: config}
        cli := k8s_native.NewRuntimeServiceClient(ctx)
        resp, err := cli.RunPodSandbox(ctx, req)
        k8s_native.WrapE(err)

        // 打印pod id
        log.Println(resp.PodSandboxId)

}

3.5 Pod/Containers

3.5.1 run(创建pod并创建containers)

package main

import (
	"context"
	"gopkg.in/yaml.v2"
	k8s_native "k8s-native"
	"k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
	"log"
	"time"
)

const (
	SandboxYaml = `
metadata:
   name: mysanbox
   namespace: default
log_directory: "/root/temp"
port_mappings:
   - protocol: 0
     container_port: 80`

	ContainerYaml = `
metadata:
  name: test-ngx
image:
  image: docker.io/nginx:1.18-alpine
log_path: ngx.log
`
)

// crictl run container.yaml sandbox.yaml
func main() {

	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()

	// container config
	config := &v1alpha2.ContainerConfig{}
	k8s_native.WrapE(yaml.Unmarshal([]byte(ContainerYaml), config))
	// pod config
	pConfig := &v1alpha2.PodSandboxConfig{}
	k8s_native.WrapE(yaml.Unmarshal([]byte(SandboxYaml), pConfig))
	cli := k8s_native.NewRuntimeServiceClient(ctx)

	// 获取pod
	pReq := &v1alpha2.RunPodSandboxRequest{Config: pConfig}
	pResp, err := cli.RunPodSandbox(ctx, pReq)
	k8s_native.WrapE(err)

	cReq := &v1alpha2.CreateContainerRequest{
		PodSandboxId:  pResp.PodSandboxId,
		Config:        config,
		SandboxConfig: pConfig, //pod配置 。必须要传
	}

	// 创建容器
	cResp, err := cli.CreateContainer(ctx, cReq)
	k8s_native.WrapE(err)
	log.Println("Container ID: ", cResp.ContainerId)

	// 启动容器
	sReq := &v1alpha2.StartContainerRequest{ContainerId: cResp.ContainerId}
	_, err = cli.StartContainer(ctx, sReq)
	k8s_native.WrapE(err)
	log.Println("Container ID: ", cResp.ContainerId, "start succeed.")

}

3.5.2 exec(进行容器tty链接)

package main

import (
	"context"
	dockerterm "github.com/docker/docker/pkg/term"
	k8s_native "k8s-native"
	restclient "k8s.io/client-go/rest"
	remoteclient "k8s.io/client-go/tools/remotecommand"
	"k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
	"k8s.io/kubectl/pkg/util/term"
	"log"
	"net/url"
)

const ContainerID = "xx"

// 交互式终端
// crictl exec -i -t <ContainerID> sh
func main() {
	execReq := &v1alpha2.ExecRequest{
		Cmd:         []string{"sh"},
		Stdin:       true,
		Stdout:      true,
		Stderr:      true, // TTY的时候 ,这个值必须是  false
		Tty:         true,
		ContainerId: ContainerID,
	}
	ctx := context.Background()
	execRsp, err := k8s_native.NewRuntimeServiceClient(ctx).Exec(ctx, execReq)
	k8s_native.WrapE(err)
	URL, err := url.Parse(execRsp.Url)
	k8s_native.WrapE(err)
	exec, err := remoteclient.NewSPDYExecutor(
		&restclient.Config{
			TLSClientConfig: restclient.TLSClientConfig{
				Insecure: true,
			},
		}, "POST", URL)

	stdin, stdout, stderr := dockerterm.StdStreams()
	streamOptions := remoteclient.StreamOptions{
		Stdout: stdout,
		Stderr: stderr,
		Stdin:  stdin,
		Tty:    true,
	}

	t := term.TTY{
		In:  stdin,
		Out: stdout,
		Raw: true,
	}
	streamOptions.TerminalSizeQueue = t.MonitorSize(t.GetSize())
	err = t.Safe(func() error {
		return exec.Stream(streamOptions)
	})
	if err != nil {
		log.Fatalln(err)
	}
}

4. 写在最后

了解CRI接口后,除了加深对容器技术的了解外。我们还可以去实现自己的虚拟节点

都看到这了,点个 👈👍❤️再走吧。

4.1 参考