k8s容器运行时

k8s容器运行时的发展脉络

早期

首先看一下docker 如何启动容器的

早期的k8s+docker架构

那时候k8s还不是容器的老大,需要兼容各个容器产品的接口,每次那些容器产品升级了,k8s也得跟着升级。

CRI

后面为了兼容性,kubernetes从1.5版本之后加入了容器运行时插件,即 Container Runtime Interface 简称 CRI。用来充当kubelet和容器运行时的桥梁。

CRI本质是一个规范、标准,怎么做是由各个厂商自己实现的。

就变成了这样的一个调用过程:

由于历史原因,docker-shim 还是由k8s项目组维护。可以看到,kubelet创建出容器 需要通过很多链路,比较复杂。所以在1.20之后,逐步分离出了docker,就变成这样了:

这样 整个流程就比之前简单很多了。

OCI规范介绍

OCI标准

包含两个协议:

镜像标准(Image Spec )和 运行时标准(Runtime Spec),这两个标准通过OCI运行时文件系统包(OCI runtime filesystem bundle)的标准格式链接在一起,OCI镜像可以通过工具转换成文件系统包,OCI Runtime也可以识别该文件系统包并运行容器

  • 镜像标准,规范了以layer 保存的文件系统,每个层保存了和上层之间的变化,如 用manifest、config和index文件找出镜像的具体信息
  • 运行时标准,定义了容器的创建、删除、查看等操作,规范了容器的状态描述。 runC就是OCI运行时标准的一个参考实现。

OCI Runtime ( Open Container Initiative Runtime Specification )规范了容器的配置、执行环境和生命周期管理。容器的配置信息由config.json配置文件来管理。规范容器的执行环境可以保证容器内运行的应用在生命周期内拥有一致的运行环境。

设计的考虑因素:

  • 操作标准化:容器的标准化操作包括使用标准流程创建、启动和停止容器,使用标准文件系统工具复制和创建容器快照,使用标准化网络工具进行下载和上传
  • 内容无关:不关系容器内的具体应用内容是什么,都能通过容器标准操作来运行
  • 基础设施无关
  • 工业级交付

文档:https://github.com/opencontainers/runtime-spec/blob/main/spec.md

相关文章:浅析容器运行时奥秘——OCI标准 - 腾讯云开发者社区-腾讯云

RunC 是 Open Container Initivate Runtime Specification(指定容器的配置、执行环境和生命周期) 的基本实现

runC

了解了k8s kubelet创建容器的流程,发现 都有 runC这个工具。那就先来看看runC到底是什么

介绍

它是用来运行容器的一个轻量级工具,被称为运行容器的运行时,它负责利用符合标准的文件OCI(Open Container Initiative)标准等资源运行容器。

搭建环境

首先下载runc,本次使用的是最新版本的runc:https://github.com/opencontainers/runc/releases/tag/v1.1.10

因为我的环境中有docker,下载下来的runc和docker中的有冲突,所以可以重命名为rc,放入/usr/local/bin 下。

准备一个镜像

1
2
3
4
5
6
7
docker pull alpine:3.18

# 准备文件夹
mkdir -p alpine/rootfs

# 解压并导出alpine镜像文件
docker export $(docker create alpine:3.18) | tar -C alpine/rootfs -xvf -

执行 rc spec 会得到一个配置文件:config.json

文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
{
"ociVersion":"1.0.2-dev",
"process":{
"terminal":true,
"user":{
"uid":0,
"gid":0
},
"args":[
"sh"
],
"env":[
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd":"/",
"capabilities":{
"bounding":[
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"effective":[
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"permitted":[
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
],
"ambient":[
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
]
},
"rlimits":[
{
"type":"RLIMIT_NOFILE",
"hard":1024,
"soft":1024
}
],
"noNewPrivileges":true
},
"root":{
"path":"rootfs",
"readonly":true
},
"hostname":"runc",
"mounts":[
{
"destination":"/proc",
"type":"proc",
"source":"proc"
},
{
"destination":"/dev",
"type":"tmpfs",
"source":"tmpfs",
"options":[
"nosuid",
"strictatime",
"mode=755",
"size=65536k"
]
},
{
"destination":"/dev/pts",
"type":"devpts",
"source":"devpts",
"options":[
"nosuid",
"noexec",
"newinstance",
"ptmxmode=0666",
"mode=0620",
"gid=5"
]
},
{
"destination":"/dev/shm",
"type":"tmpfs",
"source":"shm",
"options":[
"nosuid",
"noexec",
"nodev",
"mode=1777",
"size=65536k"
]
},
{
"destination":"/dev/mqueue",
"type":"mqueue",
"source":"mqueue",
"options":[
"nosuid",
"noexec",
"nodev"
]
},
{
"destination":"/sys",
"type":"sysfs",
"source":"sysfs",
"options":[
"nosuid",
"noexec",
"nodev",
"ro"
]
},
{
"destination":"/sys/fs/cgroup",
"type":"cgroup",
"source":"cgroup",
"options":[
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
]
}
],
"linux":{
"resources":{
"devices":[
{
"allow":false,
"access":"rwm"
}
]
},
"namespaces":[
{
"type":"pid"
},
{
"type":"network"
},
{
"type":"ipc"
},
{
"type":"uts"
},
{
"type":"mount"
},
{
"type":"cgroup"
}
],
"maskedPaths":[
"/proc/acpi",
"/proc/asound",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/sys/firmware",
"/proc/scsi"
],
"readonlyPaths":[
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
}
}

简单使用

使用runc运行容器

修改config.json文件:

1
2
3
4
5
6
7
8
9
10
{
...
"root": {
"path": "rootfs",
"readonly": true
},
"hostname": "just", // 修改主机名为just
"mounts": {...}
...
}
1
rc run abc  # 启动一个容器abc

这样直接rc run 是前台运行的,退出容器之后 就停了,我们希望使用detach模式在后台运行。

随便写个go程序:

myhttp.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"log"
"net/http"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
})
log.Println("开始启动http服务~")
http.ListenAndServe(":80", nil)
}

编译之后放到 alpine/rootfs/app 目录下,并设置可执行权限

修改config.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
{
"process": {
"terminal": false, // 修改
"args": [
"/app/myhttp" // 修改如何启动程序
],
},
"root": {
"path": "rootfs",
"readonly": false // readonly也改为false
},
}

基本命令:

1
2
3
4
5
runc run -d abc   // 后台运行程序
runc list // 列出运行容器列表
runc kill abc // 停止容器
runc delete abc // 删除容器

挂载文件夹

可以把可执行程序放到 /root/app 文件夹下

同样执行:runc run -d abc > abc.out 2>&1

exec 命令:

1
rc exec -t abc sh   # 进入容器

给容器配置网络

之前我们随便run了一个容器abc,但是没有设置网络

可以看到只有一个回环地址

现在我们要设置这个容器的虚拟网卡,使得能和联通宿主机的网络

虚拟网卡设置

可以先看一下之前的文章:

Linux的namespace基础

上图是大概的结构

首先安装一个网桥管理的工具

1
2
3
4
5
6
7
8
9
yum install -y bridge-utils

brctl show # 可以看到本机有哪些网桥


# 创建网桥
brctl addbr just0
ip link set just0 up
ip addr add 10.12.0.1/24 dev just0

1
2
3
4
5
6
7
8
# 创建veth设备
ip link add name veth0-host type veth peer name veth0-ns
ip link set veth0-host up
brctl addif just0 veth0-host # 把veth0-host搭在网桥上

# 创建一个网络命名空间
ip netns add mycontainer
ip link set veth0-ns netns mycontainer # 把veth0-ns 移动到 mycontainer这个命名空间下

1
2
3
4
5
6
7
# 设置ns里面的网卡名称和启动
ip netns exec mycontainer ip link set veth0-ns name eth0 # 把veth0-ns 改为eth0 设置名字
ip netns exec mycontainer ip addr add 10.12.0.2/24 dev eth0
ip netns exec mycontainer ip link set eth0 up
ip netns exec mycontainer ip addr add 127.0.0.1 dev lo
ip netns exec mycontainer ip link set lo up
ip netns exec mycontainer ip route add default via 10.12.0.1 # 设置ip路由 指向外部host veth设备

这样 网络就算通了

访问容器服务

我们手动创建的命名空间在/var/run/netns

所以我们在config文件中指定网络命名空间,让容器使用我们之前创建的网络命名空间mycontianer

可以看到已经可以在宿主机 访问容器的http服务了!

端口映射,使得外部可以访问容器服务

1
2
3
4
5
# 端口映射
iptables -t nat -I PREROUTING -p tcp -m tcp --dport 9090 -j DNAT --to-destination 10.12.0.2:80

# 删除
iptables -t nat -D PREROUTING -p tcp -m tcp --dport 9090 -j DNAT --to-destination 10.12.0.2:80

1
2
3
# 开启内核数据包转发
sysctl -w net.ipv4.ip_forward=1 # 临时开启

这样就可以外部访问容器服务了

k8s中的sandbox

  • 容器:一个隔离(Linux Namespace)的应用运行时环境

  • POD 沙箱:一组共同被Pod约束的容器就叫做 Pod Sandbox,各个容器共享底层资源,其中扮演沙箱的角色就是pause

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"flag"
"log"
"net/http"
)

func main() {
var port string
flag.StringVar(&port, "p", "80", "-p 80")
flag.Parse()
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("hello world!\n"))
})
log.Println("开始启动http服务...")
log.Println("启动端口是: ", port)
http.ListenAndServe(":"+port, nil)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 启动两个容器
# 修改启动参数 args ip:8081
rc run -d web1 > web1.out 2>&1


# 修改启动参数 args ip:8082
rc run -d web2 > web2.out 2>&1

# 这样两个容器就共享了网络命名空间

[root@just alpine]# curl 10.12.0.2:8081
hello world!
[root@just alpine]# curl 10.12.0.2:8082
hello world!
[root@just alpine]# ip netns
mycontainer (id: 0)
[root@just alpine]# ip netns exec mycontainer curl localhost:8081
hello world!
[root@just alpine]# ip netns exec mycontainer curl localhost:8082
hello world!

模拟pod多容器网络共享

目标:运行pause容器,把web1、web2两个容器纳入到pause中

runC 运行pause容器

使用pasue镜像来进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
# 下载镜像-导出文件夹
docker pull mirrorgooglecontainers/pause-amd64:3.1

docker tag mirrorgooglecontainers/pause-amd64:3.1 pause:3.1

mkdir -p pause/rootfs

docker export $(docker create pause:3.1) | tar -C pause/rootfs -xvf -


cd pause

rc spec

测试:

1
2
3
4
5
6
7
8
# 修改启动参数 args /pause
# terminal 改为false
rc run -d pause > pause.out 2>&1


cd /proc/{pid}/ns
# 这里面的就是namespace文件
# 两个进程的某个namespace文件指向同一个链接文件,说明其相关资源在同一个namespace中

命令行操作

先使用runc创建一个pasue容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 设置软连接
# ip netns 所在的目录在 /var/run/netns
# 而我们通过runc创建(或普通进程) 的 ns 在/proc/pid/ns/net 里

ln -s /proc/15609/ns/net /var/run/netns/proc15609

ip link add name veth0-pause type veth peer name veth0-pause-ns
ip link set veth0-pause up
brctl addif just0 veth0-pause

ip link set veth0-pause-ns netns proc15609

ip netns exec proc15609 ip link set veth0-pause-ns name eth0
ip netns exec proc15609 ip addr add 10.12.0.4/24 dev eth0
ip netns exec proc15609 ip link set eth0 up
ip netns exec proc15609 ip route add default via 10.12.0.1


# 清理脚本
for i in $(ip netns | grep ^proc | grep -v id);do
rm -rf /var/run/netns/${i}
done

将web容器纳入到pause容器

首先改web容器的config配置

然后启动web1、web2容器

1
2
rc run -d web1 > web1.out 2>&1
rc run -d web2 > web2.out 2>&1

补充一个命令:nsenter 是一个 可以在指定进程的命名空间下运行指定程序的命令

  • -t 、—target pid:指定被进入命名空间的目标进程的pid
  • -m、—mount[=file]:进入mount命名空间。如果指定了file,则进入file的命名空间
  • -u、—uts[=file]:进入uts命名空间
  • -i、—ipc[=file]:进入ipc命名空间
  • -n、—net[=file]:进入net命名空间
  • -p、—pid[=file]:进入pid命名空间
  • —user[=file]:进入user命名空间
  • -G、—setgid gid:设置运行程序的gid
  • -S、—setuid uid:设置运行程序的uid
  • -r、—root[=directory]: 设置根目录
  • -w、—wd[=directory]:设置工作目录

Pod共享进程命名空间和通信

没有共享进程命名空间之前的:

修改web容器的config 配置

效果:

使用runC配置Cgroups资源限制 cpu

OCI规范之Image Spec规范

OCI规范分为 Image Spec和Runtime Spec ,Runtime Spec是上面的内容(runc是其基本实现)

文档:https://github.com/opencontainers/image-spec/blob/main/spec.md

镜像规范定义了如何创建一个符合OCI规范的镜像,规定了镜像需要输出的内容和格式。

镜像分层说明

准备配置

1
2
3
4
docker pull alpine:3.18
docker save alpine:3.18 -o alpine-img.tar
mkdir alpine-img
tar -xf alpine-img.tar -C alpine-img

把镜像导出来看看是啥样的

大概的目录结构:

1
2
3
4
5
6
7
8
├── b541f2080109ab7b6bf2c06b28184fb750cdd17836c809211127717f48809858.json
├── c4a8dbca6271e3b1737cc978e30b84cd80bcff117b5eb0ecd01b526de36a5e7c
│   ├── json
│   ├── layer.tar
│   └── VERSION
├── manifest.json # 记录config文件和层信息、镜像名称和tag
└── repositories

manifest.json:

1
2
3
4
5
6
7
8
9
10
11
[
{
"Config": "b541f2080109ab7b6bf2c06b28184fb750cdd17836c809211127717f48809858.json",
"RepoTags": [
"alpine:3.18"
],
"Layers": [
"c4a8dbca6271e3b1737cc978e30b84cd80bcff117b5eb0ecd01b526de36a5e7c/layer.tar"
]
}
]

其中Layers列表中的tar包共同组成了生产容器的rootfs

使用Dockerfile 说明镜像分层

准备一个Dockerfile文件:

1
2
3
4
FROM alpine:3.18
RUN mkdir /app
EXPOSE 80

然后执行:docker build -t myalpine:v1 .

然后解压这个镜像:

1
2
3
docker save myalpine:v1 -o myalpine.tar
mkdir myalpine
tar -xf myalpine.tar -C myalpine

多了一层layer

使用umoci制作镜像文件

文档:https://github.com/opencontainers/umoci

OCI镜像规范的参考实现,为用户提供创建、操作容器镜像以及与容器镜像交互的能力

首先准备一个alpine文件夹,里面是下载下来的alpine

1
2
3
4
5
6
7
8
9
10
11
12
umoci init --layout myimage  # 初始化一个布局文件夹

umoci new --image myimage:v1 # 创建出一个新的镜像

umoci unpack --image myimage:v1 bundle # 将一个镜像提取到一个文件夹中


# 进入bundle中,把之前的rootfs拷贝进去

umoci repack --image myimage:v1 bundle # 重新pack

umoci stat --image myimage:v1 # 查看状态

这里的mediaType 不同的规范所对应的文件格式是不一样的

将umoci制作的镜像发布到阿里云镜像仓库

使用工具:skopeo

https://github.com/containers/skopeo

skopeo是用来对Register(镜像服务)上的image操作的工具,功能主要包括:

  • 查看Register上的镜像信息
  • 在Register之间或Register与本地之间复制镜像、删除Register上的镜像

安装:

1
2
3
4
# 按照文档上安装,有各个linux发行版的安装方式,我这个是centos7,yum 安装的skopeo版本很低,我们使用编译安装的方式
# 安装依赖
yum install gpgme-devel device-mapper-devel btrfs-progs-devel glib2-devel libassuan-devel go-md2man -y

使用源码构建始终不成功,所以就使用镜像来构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git clone https://github.com/containers/skopeo.git && cd skopeo

# 构建
docker run --name skopeo-build \
-v $PWD:/src \
-v /usr/bin/go-md2man:/go/bin/go-md2man \
-w /src \
-e CGO_ENABLED=0 \
-e GOPROXY=https://goproxy.cn,direct \
golang:1.21 \
sh -c 'make BUILDTAGS=containers_image_openpgp && \
CGO_CFLAGS="" CGO_LDFLAGS="" GO111MODULE=on go build -mod=vendor \
-tags "containers_image_openpgp" -o bin/skopeo ./cmd/skopeo'

cp ./bin/skopeo /usr/local/bin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 为了与docker兼容,先从网上直接下载一个alpine
skopeo copy docker://alpine:3.16 oci:alpine:v1 # 会生成一个alpine的文件夹

umoci unpack --image alpine:v1 bundle # 解包

umoci repack --image alpine:v1 bundle

skopeo inspect docker://docker.io/alpine:3.16 # 查看镜像信息

# 登录
skopeo login --username=你的用户名 registry.cn-hangzhou.aliyuncs.com

# 发布镜像
skopeo copy oci:alpine:v1 docker://registry.cn-hangzhou.aliyuncs.com/chengwz/alpine:v1

OCI规范之分发规范

官方:https://github.com/opencontainers/distribution-spec

  • OCI分发规范项目定义了一系列API协议来促进、标准化内容的分发
  • 最早实现该规范的是Docker Distribution(可以用来搭建本地私有镜像仓库),后来捐给了CNCF。现在地址是:https://github.com/distribution/distribution

现在开始玩一把:

1
2
3
4
docker run -d -p 5000:5000 --name registry registry:2

docker tag alpine:3.18 localhost:5000/alpine:3.18
docker push localhost:5000/alpine:3.18

安装 https://github.com/opencontainers/distribution-spec/blob/main/spec.md 分发规范的api endpoint,肯定是可以请求的,现在试一试。

1
2
curl localhost:5000/v2/alpine/manifests/3.18
# 得到镜像的基本信息

使用代码获取镜像信息

使用第三方库:https://github.com/google/go-containerregistry

关于代码中镜像清单类型可以参考文档:

image和index模式 仅仅只是格式不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"log"
)

func parseImage(image string, options ...name.Option) {
ref, err := name.ParseReference(image, options...)
if err != nil {
log.Fatalln(err)
}

des, err := remote.Get(ref) // 镜像描述信息
if err != nil {
log.Fatalln(err)
}
fmt.Println(des.MediaType)

if des.MediaType.IsImage() {
img, _ := des.Image()
conf, _ := img.ConfigFile()
fmt.Println(conf.OS, conf.Architecture, conf.Config.Entrypoint, conf.Config.Cmd)
} else if des.MediaType.IsIndex() {
index, err := des.ImageIndex()
if err != nil {
return
}
mf, err := index.IndexManifest()
if err != nil {
return
}
for _, d := range mf.Manifests {
img, err := index.Image(d.Digest)
if err != nil {
return
}
conf, err := img.ConfigFile()
if err != nil {
return
}
fmt.Println(conf.OS, "/", conf.Architecture, ":", conf.Config.Entrypoint, conf.Config.Cmd) // linux amd64 [] [/bin/sh]

}
}

}

func main() {
// application/vnd.docker.distribution.manifest.v2+json
//img := "192.168.34.172:5000/alpine:3.18"
//parseImage(imag, name.Insecure)

// application/vnd.docker.distribution.manifest.list.v2+json
img := "docker.io/alpine:3.18"
parseImage(img)
}

containerd和cri功能模拟开发

准备工作:

  • 准备一个centos7系统,安装containerd和 go1.20+环境,不要安装docker

安装containerd和crictl客户端工具

https://github.com/containerd/containerd/releases,选择当前最新版本 1.7.11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
wget https://github.com/containerd/containerd/releases/download/v1.7.11/containerd-1.7.11-linux-amd64.tar.gz

# 解压并将文件夹内所有文件放入环境变量中

# 交给systemd纳管
vim /etc/systemd/system/containerd.service

[Unit]
Description=containerd container runtime

[Service]
ExecStart=/usr/local/containerd/containerd

Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSpec=5
LimitNPROC=infinity
LimitNOFILE=infinity
TaskMax=infinity
OOMScoreAdjust=-999

[Install]
WantedBy=muti-user.target


# 启动
systemctl daemon-reload && systemctl start containerd

# 配置文件
containerd config default > /etc/containerd/config.toml

# 修改文件
# cgroup
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true


# 重启containerd

安装crictl工具:

1
2
3
4
5
6
7
8
9
10
11
wget https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.29.0/crictl-v1.29.0-linux-amd64.tar.gz

# 解压
tar -zxvf crictl-v1.29.0-linux-amd64.tar.gz -C /usr/local/bin

# 配置
cat > /etc/crictl.yaml <<EOF
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
EOF

cri接口初步调用

cri相关接口的定义:https://github.com/kubernetes/cri-api

自己可以写grpc代码调用即可

1
2
3
4
5
[root@just ~]# crictl version
Version: 0.1.0
RuntimeName: containerd
RuntimeVersion: v1.7.11
RuntimeApiVersion: v1

版本是v1,看https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto

安装两个依赖:

  • go get google.golang.org/grpc
  • go get k8s.io/cri-api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"context"
"fmt"
"log"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
v1 "k8s.io/cri-api/pkg/apis/runtime/v1"
)

func main() {
gopts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
addr := "unix:///run/containerd/containerd.sock"
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
conn, err := grpc.DialContext(ctx, addr, gopts...)
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

req := &v1.VersionRequest{}
rsp := &v1.VersionResponse{}
err = conn.Invoke(ctx, "/runtime.v1.RuntimeService/Version", req, rsp)
if err != nil {
log.Fatalln(err)
}
fmt.Println(rsp)

}

crictl客户端开发

version接口

部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package cmds

import (
"context"
"fmt"
"log"
"time"

"github.com/spf13/cobra"
v1 "k8s.io/cri-api/pkg/apis/runtime/v1"
)

var versionCmd = &cobra.Command{
Use: "version",
Run: func(c *cobra.Command, args []string) {
req := &v1.VersionRequest{}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
runtimeService := v1.NewRuntimeServiceClient(grpcClient)
rsp, err := runtimeService.Version(ctx, req)
if err != nil {
log.Fatalln(err)
}
fmt.Println("Version:", rsp.Version)
fmt.Println("RuntimeName:", rsp.RuntimeName)
fmt.Println("RuntimeVersion:", rsp.RuntimeVersion)
fmt.Println("RuntimeApiVersion:", rsp.RuntimeApiVersion)
},
}

实现了类似的效果

打印镜像列表

部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package cmds

import (
"context"
"log"
"os"
"time"

"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
v1 "k8s.io/cri-api/pkg/apis/runtime/v1"

"gorunc/utils"
)

// 镜像相关的处理
var imagesCmd = &cobra.Command{
Use: "images",
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
req := &v1.ListImagesRequest{}
rsp, err := NewImageService().ListImages(ctx, req)
if err != nil {
log.Fatalln(err)
}

table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"镜像", "标签", "ID", "大小"})
for _, img := range rsp.GetImages() {
imageName, _ := utils.ParseRepoDigest(img.RepoDigests)
repoTag := utils.ParseRepoTag(img.RepoTags, imageName)[0] // 取到镜像名和标签
row := []string{imageName, repoTag[1], utils.ParseImageID(img.Id), utils.ParseSize(img.Size_)}
table.Append(row)
}
utils.SetTable(table)
table.Render()
},
}

创建pod

使用命令行创建

使用crictl工具创建,crictl 时面向k8s接口的,并非面向普通容器用户(如docker),所以crictl工具使用的是配置文件的方式来创建POD

1
2
3
4
crictl run container-config.json pod-config.json

# contianer-config.json是容器配置文件
# pod-config.json 是pod沙箱配置文件

具体配置文件查看api定义:https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.pb.go#L1343

  • PodSandboxConfig
  • ContainerConfig

Sandbox.yaml

1
2
3
4
5
6
7
metadata:
name: mysandbox
namespace: default
log_directory: "/root/temp"
port_mappings:
- protocol: 0
container_port: 80

Container.yaml

1
2
3
4
5
metadata:
name: myngx
image:
image: docker.io/nginx:1.18-alpine
log_path: ngx.log

想要启动pod需要cni插件的

直接下载cni plugins https://github.com/containernetworking/plugins

1
2
3
4
5
6
7
8
mkdir -p /opt/cni/bin
mkdir -p /etc/cni/net.d

tar -zxvf cni-plugins-linux-amd64-v1.4.0.tgz -C /opt/cni/bin/
# 默认cni的bin路径指向的就是这个目录

# 查看containerd的配置。SystemdCgroup 需要改为false
systemctl restart containerd

需要一个cni的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cat >/etc/cni/net.d/10-mynet.conf <<EOF
{
"cniVersion": "0.2.0",
"name": "mynet",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.22.0.0/16",
"routes": [
{ "dst": "0.0.0.0/0" }
]
}
}
EOF

systemctl restart containerd

crictl run ngx.yaml mysandbox.yaml

代码创建pod

创建单pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package cmds

import (
"context"
"fmt"
"log"
"time"

"github.com/spf13/cobra"
"gorunc/utils"
v1 "k8s.io/cri-api/pkg/apis/runtime/v1"
)

var podsCmd = &cobra.Command{
Use: "runp",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
log.Fatalln("请指定POD配置文件")
}
config := &v1.PodSandboxConfig{}
err := utils.YamlFile2Struct(args[0], config)
if err != nil {
log.Fatalln(err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()

req := &v1.RunPodSandboxRequest{Config: config}
rsp, err := NewRuntimeService().RunPodSandbox(ctx, req)
if err != nil {
log.Fatalln(err)
}
fmt.Println(rsp.PodSandboxId)
},
}

创建容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package cmds

import (
"context"
"fmt"
"log"
"time"

"github.com/spf13/cobra"
"gorunc/utils"
v1 "k8s.io/cri-api/pkg/apis/runtime/v1"
)

var containersCmd = &cobra.Command{
Use: "run", //单创建 pod
Example: "run podid container-config.yaml pod-config.yaml",
Run: func(c *cobra.Command, args []string) {
if len(args) < 3 {
log.Fatalln("参数不完整")
}
podId, containConfig, podConfig := "", "", ""
// 一共三个参数。
podId = args[0]
containConfig = args[1]
podConfig = args[2]

config := &v1.ContainerConfig{}
err := utils.YamlFile2Struct(containConfig, config)
if err != nil {
log.Fatalln(err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

//POD sandbox对应的配置对象
pConfig := &v1.PodSandboxConfig{}
err = utils.YamlFile2Struct(podConfig, pConfig)
if err != nil {
log.Fatalln(err)
}
req := &v1.CreateContainerRequest{
PodSandboxId: podId, //必须要传
Config: config, //容器配置
SandboxConfig: pConfig, //pod配置 。必须要传
}

rsp, err := NewRuntimeService().
CreateContainer(ctx, req)
if err != nil {
log.Fatalln(err)
}

fmt.Println(rsp.ContainerId) //打印容器ID
},
}

实现容器列表加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 容器列表   类似docker  ps
var containersListCmd = &cobra.Command{
Use: "ps", //打印容器
Example: "ps",
Run: func(c *cobra.Command, args []string) {

listReq := &v1.ListContainersRequest{}
rsp, err := NewRuntimeService().ListContainers(context.Background(), listReq)
if err != nil {
log.Fatalln(err)
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "名称", "镜像", "状态"})
for _, c := range rsp.GetContainers() {
row := []string{utils.ParseContainerID(c.Id), c.Metadata.Name, c.Image.GetImage(),
strings.Replace(c.State.String(), "CONTAINER_", "", -1)}
table.Append(row)
}
utils.SetTable(table)
table.Render()
},
}

实现容器exec功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 容器 exec
var containersExecCmd = &cobra.Command{
Use: "exec", //打印容器
Example: "exec",
Run: func(c *cobra.Command, args []string) {
if len(args) < 2 {
log.Fatalln("error params")
}
execReq := &v1.ExecRequest{
Cmd: args[1:],
Stdin: true,
Stdout: true,
Stderr: !TTY, // TTY的时候 ,这个值必须是 false
Tty: TTY,
ContainerId: args[0],
}

execRsp, err := NewRuntimeService().Exec(context.Background(), execReq)
if err != nil {
log.Fatalln(err)
}
URL, err := url.Parse(execRsp.Url)
if err != nil {
log.Fatalln(err)
}
exec, err := remoteclient.NewSPDYExecutor(&restclient.Config{TLSClientConfig: restclient.TLSClientConfig{Insecure: true}}, "POST", URL)

if !TTY { //非 终端模式
streamOptions := remoteclient.StreamOptions{
Stdout: os.Stdout,
Stderr: os.Stderr,
Stdin: os.Stdin,
}
err = exec.Stream(streamOptions)
if err != nil {
log.Fatalln(err)
}
return
}

//下面是终端模式
stdin, stdout, stderr := mobyterm.StdStreams()
streamOptions := remoteclient.StreamOptions{
Stdout: stdout,
Stderr: stderr,
Stdin: stdin,
Tty: TTY,
}

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)
}
},
}