让我们回顾一下 KubeOS 的架构
本章节,我们讲述 KubeOS 容器 OS 的制作工具——kbimg,kbimg 是 KubeOS 部署和升级所需镜像的制作工具,可以使用 kbimg 制作 KubeOS 容器/虚拟机/物理机 镜像。一个可能带来混淆的概念是:容器 OS是支持容器运行的精简化OS,而不是容器。
kbimg 制作的镜像
我们来看一下这三个镜像的概念以及官方给出的介绍
- upgrade-image OCI镜像:
- 统一标准化容器镜像格式,让标准镜像能够在各容器软件下构建、传递及准备容器镜像运行
- 在 KubeOS 中,制作的 OCI 镜像仅用于后续的虚拟机/物理机镜像制作或升级使用,不支持启动容器
- 使用默认 rpmlist 进行容器OS镜像制作时所需磁盘空间至少为6G,如自已定义 rpmlist 可能会超过6G
- 制作完成后查看制作出来的KubeOS容器镜像
-
docker images
-
- vm-image 虚拟机镜像:
- 如使用 docker 镜像制作请先拉取相应镜像或者先制作docker镜像(也就是在 upgrade-image 选项下制作出的镜像),并保证 docker 镜像的安全性
- 制作结果说明:
- system.qcow2: qcow2 格式的系统镜像,大小默认为 20GiB,支持的根文件系统分区大小 < 2020 MiB,持久化分区 < 16GiB 。
- update.img: 用于升级的根文件系统分区镜像
- pxe-image 物理机安装所需镜像及文件:
- 如使用 docker 镜像制作请先拉取相应镜像或者先制作docker镜像,并保证 docker 镜像的安全性
- 制作结果说明:
- initramfs.img: 用于pxe启动用的 initramfs 镜像
- kubeos.tar: pxe安装所用的 OS 压缩文件
起初,kbimg 作为一个脚本,支持通过命令行参数的方式调用
bash kbimg.sh [ --help | -h ] create [ COMMANDS ] [ OPTIONS ]
经过最新的 feature,kbimg 提供了命令行工具,同时还为 kbimg 提供了用户自定义配置的选项,只需要在 yaml 文档中配置好自己需要的参数即可快捷地进行一键式部署。
在我进行 kbimg 开发难点、创建镜像过程之前,我们先掌握几个重点概念
容器镜像是什么?和容器是什么关系?和容器 OS 镜像又有什么关系?
我们以常见的 docker 由浅入深地了解一下:
镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时、库、环境变量和配置文件。
- docker的镜像是一堆只读层堆叠的文件系统,每一层都有一个指针指向下一层
- 所以当我们运行docker create <image-id>时,docker会为镜像添加一个可读写层,使其构成一个新的容器
- 当我们运行 docker start <container-id> 命令时,docker会为容器文件系统创建一个进程隔离空间
一张图片概述
为什么Docker镜像要采用这种分层结构呢?
最大的一个好处就是 - 共享资源
比如:有多个镜像都从相同的 base 镜像构建而来,那么宿主机只需在磁盘上保存一份base镜像,同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。
容器 OS 镜像,是用于在虚拟机上启动/在物理机上安装的镜像,而非直接通过 Docker 去运行的镜像。
kbimg 的制作流程是什么样子的?
大家可以看到下图所示的制作流程
-
用户编写 yaml 文件,定义想要配置的参数
- 必备参数,如官方的指导手册所示,需要
- repo path,也就是 repo 源的位置,openEuler 的 repo 源一般存放在 /etc/yum.repos.d,用户可以新建一个文件来指定源,repo 源的作用是安装指定的软件包
- vision of image,这一参数可以帮助用户在通过 KubeOS 进行整体升级时,确定需要的版本是哪一个,比如一个业务线上经过调研之后,决定以 v1 作为此次版本镜像,那它们就可以打上一个 v1 的 tag
- binary path,这一参数需要指定组件 os-agent 的位置,因为 os-agent 是容器 OS 生命周期管理的执行单元,在制作镜像时将其传入,可以协助之后进行升级时的任务流程
- password,用户能够在制作镜像时就指定好镜像的密码
- docker image(可选),前文中我们提到,不同制作方式下制作出的镜像结果不同,有的是可以通过 docker images 直接查看的镜像,有的是 .qcow2、.img 格式的文件。
-
选配参数
- 自定义分区大小,在早期版本中,镜像各个分区大小是固定的,如今支持对 Persist 分区进行划分,用户如今可以通过自定义分区来实现更灵活的内存分配。
- 必备参数,如官方的指导手册所示,需要
具体来说,用户可以在 BOOT、ROOT-A、ROOT-B、Persist 分区之外,自己通过 Lable、limit、type 来指定新分区的大小、文件系统类型,如 ext4 类型和 fat32 类型。
2. 自定义传入镜像中的文件/文件夹,用户如今可以通过指定本地文件/文件夹的绝对路径,以及镜像中的地址,来做到传入的目的。比如可以指定一个开机自动开启的 systemd service或是传入一个 sh 脚本文件。
3.自定义用户名、密码、用户组,用户经过这一配置,可以在镜像中提前指定好所需的用户/用户组,避免在开机后再统一通过脚本去新建。
4. 最后是用户方面的配置,分别是 hostname 、grub 密码指定、systemd service自定义,其中的 systemd service 文件可以由新建文件/文件夹功能传入
实现流程
实现流程分为两个主要板块,rootfsCreate 和 imageCreate,两个板块中又包含一些不同的细节。
- 用户在 yaml 文件中,对自己需要的内容进行配置,如果不改动参数,则会按照默认的数值来创建镜像
- 准备yum仓库配置文件,包含OS所需的rpm软件包列表
- 构建根文件系统
- 使用rpm命令初始化rpm数据库
- 挂载proc、dev、sys文件系统
- 初始化yum仓库配置
- 安装所需rpm软件包
- 配置OS版本信息、挂载信息等文件
- 配置引导程序
- 拷贝并配置引导程序文件
- 初始化initramfs
- 制作磁盘分区和文件系统
- 使用parted创建分区
- 使用mkfs创建文件系统
- 制作镜像
- 将根文件系统打包成tar文件
- 解压tar文件到initramfs
- 使用qemu-img将分区写到镜像文件
- 转换为qcow2格式
构建Dockerfile,用于制作Docker镜像(如果有需要的话)
实现细节与问题
kbimg 由 cobra 制作,参考了 kubectl 的实现,预计在不久后加入用户在命令行编辑参数,自动生成 yaml 文件的功能,在实现过程中,需要理解对操作系统的启动过程,处理各个组件的交互细节。
整体来说可以分为两个大模块,主要是制作 os.tar、制作镜像文件,这个会根据用户的需求来,
- 如果是制作物理机安装所需文件的话,就会仅制作 os.tar 文件,然后将其转换为 .img 格式的镜像文件,最后提供给用户两个文件 os.tar 和 .img 镜像
- 如果是制作虚拟机镜像,那就会在制作过程中,根据==选项==来看,是从 repo 源从头构建,还是从 docker 仓库中选取某个版本进行构建。也就是说不管是制作物理机文件还是虚拟机镜像,都会走 repo 源构建的这一过程。区别是制作虚拟机镜像的话会依据构建出的 os.tar(rootfs)进行镜像构建,会将 .img 格式的文件转换为 .qcow2 格式的文件,或者根据需求,创建一个 docker 的空白镜像,将 os.tar 文件打包放进 docker 仓库里面。
- 这里会延申一个常见的误区,就是容器 OS 它是能够支持容器运行的最小化OS,而不是一个支持 docker run 的容器镜像,放进 docker 仓库里面是为了能够让业务在需要对一批机器进行整体升级的时候方便分发,也就是交给 proxy、operator、agent 相互协调达到升级的这一目的。
1. kbimg 读取用户自定义的 yaml 文件,经过(viper)解码,(mapstructure)将各个需要的参数读取出来,并选择对应的镜像制作方法开始制作
1. 这里我对用户配置文件选择 yaml 而不是 json,因为**从需求出发,配置文件面向用户,需要尽量简单易读**,同时使用Go解析yaml文件也不难实现。
接下来进入正式构建的过程,主要分为两个板块:
1. 构建根文件系统 rootfs
1. 准备yum仓库配置文件,包含OS所需的rpm软件包列表(这个 rpm 软件包列表是开发部门经过筛选之后,做到尽量精简的,也符合容器OS的**极简化原则**,当然用户可以酌情在 rpm list 里面添加部分自己需要的软件包,但是有一个大小限制)
2. 因为在构建过程中,会需要对 OS 进行一些配置,像是配置引导程序、下载软件包、配置主机名、设置引导密码等,所以需要模拟出文件系统环境,通过挂载一些必备文件夹像是proc、dev、sys,这些可以通过 mount 来实现。接着就将软件源配置写入rootfs 中
function mount_proc_dev_sys() {
local tmp_root=$1
mount -t proc none "${tmp_root}/proc"
mount --bind /dev "${tmp_root}/dev"
mount --bind /dev/pts "${tmp_root}/dev/pts"
mount -t sysfs none "${tmp_root}/sys"
}
`mount --bind /dev /tmp/dev` 会将已经挂载的 `/dev` 目录再次挂载到 `/tmp/dev` 目录上,这样在 `/tmp/dev` 和 `/dev` 目录下的文件内容会是相同的,因为它们实际上指向了同一个文件系统。
3. 通过软件源来安装一些必备的软件包
4. 拷贝并配置引导程序文件
2. 制作镜像
1. 使用 parted 创建分区,使用 mkfs 创建文件系统
1. 这里会创建四个主分区,分别是 boot、rootA、rootB、persist,boot 分区用来存放grub2文件,用来进行引导配置,rootA、rootB 是进行双分区升级设计,每个分区存放一个版本的 OS image,每次升级时会下载一个版本到另一个分区,下次启动时将目录切换到另一个分区,就**完成了双分区的升级**
2. persist 分区:出于安全性考虑,KubeOS 文件系统是只读的,但是我们还是提供了 persist 分区,供用户存放持久性数据。其中包括
1. Union Path (overlay):采用 overlay 形式,它将一个底层的只读文件系统和一个或多个只读写层(overlay layer)叠加在一起,形成一个新的虚拟文件系统。这样做的好处是可以保护底层的只读文件系统,同时在顶层实现了可写的文件系统。
2. Writable Path (bind mount):使用 bind mount 形式,直接在镜像上增加一个可写层。这个方法直接将一个文件夹挂载到镜像的特定路径上,让这个路径可以进行写操作。
3. 区别在于 overlay 方式可以保护底层的只读文件系统,同时实现了可写层,而 bind mount 直接将一个文件夹挂载到镜像上,可以在特定路径进行写操作,但没有底层保护。(就是说它直接写是写在 Union Path,如果进入 Writable Path 进行写的话就是写在那个文件夹上)
(“出于安全性考虑,文件系统设置为只读的”,如果不是**只读**的,那么在运行时,用户或程序可以对文件系统中的文件进行修改,可能会对关键组件进行修改,导致这个容器OS出现一些问题)
3. 转换为qcow2格式