3 分钟带你搞定 Kubernetes CNI 插件开发

本文介绍 CNI 插件调用的时机、CNI 插件配置的读取,以及 CNI 插件的调用、执行。读完此文,您将清楚 CNI 插件的运行机制、调用细节,并能够自信地写出一个简单的 CNI 插件。CRI:containerdCNI plugin:flannelCNI 的调用时机源码位置:containerd

本文介绍 CNI 插件调用的时机、CNI 插件配置的读取,以及 CNI 插件的调用、执行。读完此文,您将清楚 CNI 插件的运行机制、调用细节,并能够自信地写出一个简单的 CNI 插件。
  • CRI:containerd
  • CNI plugin:flannel

CNI 的调用时机

源码位置:containerd - pkg/cri/server/service.go#L61

CRI 的RunPodSandbox()实现中。

RunPodSandbox的启动流程

  1. 生成 container id,名称;
  2. 确保 Infra 容器用的镜像存在,不存在的话就拉取;
  3. 确定 ociRuntime,比如 runC;
  4. 创建 Infra 容器将要托管的网络
  5. 按照 ociRuntime 规范创建所要求的 spec;
  6. 生成 spec opts;
  7. 创建容器;
  8. 基于容器配置来创建 Task
  9. 启动 Task;

因此,Network Namespace 的创建,早于容器的创建。

CNI 配置文件的读取

源码位置:containerd - pkg/cri/server/service.go#L149

先在 containerd 的配置文件中,配置 CNI 的配置路径,如下。

    [plugins."io.containerd.grpc.v1.cri".cni]
      bin_dir = "/opt/cni/bin"
      conf_dir = "/etc/cni/net.d"

在 containerd 启动时读取该目录下的配置文件,封装成一个网络插件,最终保存在 libcni 里。

type libcni struct {
	config

	cniConfig    cnilibrary.CNI
	networkCount int // minimum network plugin configurations needed to initialize cni
	networks     []*Network
	sync.RWMutex
}

type Network struct {
	cni    cnilibrary.CNI
	config *cnilibrary.NetworkConfigList
	ifName string
}

libcni 中有个 Network 数组,通常这个数组有两个元素,最终分别用来设置 loopbacketh0这两个网卡。

总结一下这里的过程,可以概述为以下 3 点

containerd 会读取 conf_dir  下的内容,生成默认的网络插件(还可以添加其他路径,来生成其他网络插件);

如果 conf_dir 下面有 N 个配置文件,会生成 N + 1Network

Network 会生成相应的网络设备,默认名称为 eth0eth1...。

CNI 配置文件示例 (flannel)

在 K8s 节点上的路径:/etc/cni/net.d/10-flannel.conflist

{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

CNI 插件的调用

源码位置:containerd - pkg/cri/server/sandbox_run.go#L377

总体流程

获取网络插件

调用网络插件(libcni)的 Setup() 方法。

创建 namespace;

创建网络设备;

遍历所有的网络 Network

  • lo
  • eth0

调用 CNI 接口 AddNetworkList

遍历该 Network 中所有的 Plugins

  • flannel
  • portmap
  1. 检查 plugin 是否存在。(默认:/opt/cni/bin/{type}
  2. 校验参数
  3. 执行插件命令。(环境变量 CNI_COMMAND 值为 ADD

整理创建结果。

整理结果;

给 CNI 插件所传环境变量的分类:

CNI 保留字段

  • CNI_COMMAND
  • CNI_CONTAINERID
  • CNI_NETNS
  • CNI_ARGS CNI 自定义参数由 map 转成 string 后的字符串
  • CNI_IFNAME
  • CNI_PATH

CNI 自定义参数

调用 CNI 插件时,详细的传参如下:

exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
  • pluginPath 插件的完整路径;
  • netconf plugin 配置;
  • args.AsEnv() 将所有上述环境变量转换成 key=value 后得到的数组。

CNI 插件的执行

项目地址:github.com/flannel-io/cni-plugin

插件作为二进制直接被调用,执行时,根据 CNI_COMMAND 取值不同,走不同的处理流程(由 switch 分发逻辑)。还有一套对这种操作的封装,只需提供 ADDDELCHECK 的实现函数。

ADD 操作

总结为两个步骤:
  1. 填充 delegate 字段的参数;
  2. 以填充后的 delegate 字段作为参数去调用其他的组件,来完成目标。

此时,插件拿到的网络配置数据

{
    "type": "flannel",
    "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
    }
}

设置 delegate 将要调用的命令为 bridge,插件填充完数据之后,得到的 delegate 字段如下:

{
    "cniVersion": "0.3.1",
    "hairpinMode": true,
    "ipMasq": false,
    "ipam": {
        "ranges": [
            [
                {
                    "subnet": "10.244.1.0/24"
                }
            ]
        ],
        "routes": [
            {
                "dst": "10.244.0.0/16"
            }
        ],
        "type": "host-local"
    },
    "isDefaultGateway": true,
    "isGateway": true,
    "mtu": 1450,
    "name": "cbr0",
    "type": "bridge"
}

调用其他组件来完成配置

ExecPluginWithResult(ctx, pluginPath, netconf, delegateArgs("ADD"), realExec)

Read more

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd