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/rootfsdocker 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" , "mounts" : { ...} ... }
这样直接rc run 是前台运行的,退出容器之后 就停了,我们希望使用detach模式在后台运行。
随便写个go程序:
myhttp.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "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 } , }
基本命令:
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 命令:
给容器配置网络 之前我们随便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 ip link add name veth0-host type veth peer name veth0-ns ip link set veth0-host up brctl addif just0 veth0-host ip netns add mycontainer ip link set veth0-ns netns mycontainer
1 2 3 4 5 6 7 ip netns exec mycontainer ip link set veth0-ns name 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
这样 网络就算通了
访问容器服务 我们手动创建的命名空间在/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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "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 rc run -d web1 > web1.out 2>&1 rc run -d web2 > web2.out 2>&1 [root@just alpine] hello world! [root@just alpine] hello world! [root@just alpine] mycontainer (id : 0) [root@just alpine] hello world! [root@just alpine] 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/rootfsdocker export $(docker create pause:3.1) | tar -C pause/rootfs -xvf - cd pauserc spec
测试:
1 2 3 4 5 6 7 8 rc run -d pause > pause.out 2>&1 cd /proc/{pid}/ns
命令行操作 先使用runc创建一个pasue容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ln -s /proc/15609/ns/net /var/run/netns/proc15609ip 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-imgtar -xf alpine-img.tar -C alpine-img
把镜像导出来看看是啥样的
大概的目录结构:
1 2 3 4 5 6 7 8 ├── b541f2080109ab7b6bf2c06b28184fb750cdd17836c809211127717f48809858.json ├── c4a8dbca6271e3b1737cc978e30b84cd80bcff117b5eb0ecd01b526de36a5e7c │ ├── json │ ├── layer.tar │ └── VERSION ├── manifest.json └── 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 myalpinetar -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 umoci repack --image myimage:v1 bundle umoci stat --image myimage:v1
这里的mediaType 不同的规范所对应的文件格式是不一样的
将umoci制作的镜像发布到阿里云镜像仓库 使用工具:skopeo
https://github.com/containers/skopeo
skopeo是用来对Register(镜像服务)上的image操作的工具,功能主要包括:
查看Register上的镜像信息
在Register之间或Register与本地之间复制镜像、删除Register上的镜像
安装:
1 2 3 4 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 skopeo copy docker://alpine:3.16 oci:alpine:v1 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
现在开始玩一把:
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 mainimport ( "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) } } } func main () { 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 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 [plugins."io.containerd.grpc.v1.cri" .containerd.runtimes.runc.options] SystemdCgroup = true
安装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 ~] 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 cmdsimport ( "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 cmdsimport ( "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
具体配置文件查看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/binmkdir -p /etc/cni/net.dtar -zxvf cni-plugins-linux-amd64-v1.4.0.tgz -C /opt/cni/bin/ 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 cmdsimport ( "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 cmdsimport ( "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" , 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() pConfig := &v1.PodSandboxConfig{} err = utils.YamlFile2Struct(podConfig, pConfig) if err != nil { log.Fatalln(err) } req := &v1.CreateContainerRequest{ PodSandboxId: podId, Config: config, SandboxConfig: pConfig, } rsp, err := NewRuntimeService(). CreateContainer(ctx, req) if err != nil { log.Fatalln(err) } fmt.Println(rsp.ContainerId) }, }
实现容器列表加载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 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: 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) } }, }