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)进行交互;
由于容器运行时这个概念可能部分读者会有歧义,容器运行时可以指 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接口后,除了加深对容器技术的了解外。我们还可以去实现自己的虚拟节点。
都看到这了,点个 赞 👈👍❤️再走吧。