普通视图

发现新文章,点击刷新页面。
昨天以前木子

使用 BuildKit on Kubernetes 构建多架构容器镜像

作者 Reimu
2023年4月20日 00:00

去年曾写过一篇介绍如何使用 docker in pod 的方式在 Kubernetes 集群上构建容器镜像的博客 ➡️《流水线中使用 docker in pod 方式构建容器镜像》。自己负责的项目中稳定使用了一年多没啥问题,用着还是挺香的。虽然说众多 Kubernetes PaaS 平台都逐渐抛弃了 docker 作为容器运行时,但 docker 在镜像构建领域还是占据着统治地位滴。不过最近的一些项目需要构建多 CPU 架构的容器镜像,docker in pod 的方式就不太行了。于是就调研了一下 BuildKit,折腾出来 BuildKit on Kubernetes 构建镜像的新玩法分享给大家。

qemu VS native

默认情况下,docker build 只能构建出与 docker 主机相同 CPU 架构的容器镜像。如果要在同一台主机上构建多 CPU 架构的镜像,需要配置 qemu 或 binfmt。例如,在 amd64 主机上构建 arm64 架构的镜像,可以使用 tonistiigi/binfmt 项目,在主机上运行 docker run --privileged --rm tonistiigi/binfmt --install arm64 命令来安装一个 CPU 指令集的模拟器,以处理不同 CPU 架构之间的指令集翻译问题。同样我们在 GitHub 上通过 GitHub Action 提供的 runner 来构建多 CPU 架构的容器镜像,也是采用类似的方式。

- name: Set up QEMU  uses: docker/setup-qemu-action@v2- name: Set up Docker Buildx  uses: docker/setup-buildx-action@v2- name: Build open-vm-tool rpms to local  uses: docker/build-push-action@v2  with:    context: .    file: Dockerfile    platforms: linux/${{ matrix.arch }}    outputs: type=local,dest=artifacts

然而,这种方式构建多 CPU 架构的镜像存在着比较严重的性能问题。尤其是在编译构建一些 C/C++ 项目时,由于 CPU 指令需要翻译的问题,会导致编译速度十分慢缓慢。例如,使用 GitHub 官方提供的机器上构建 open-vm-tools 这个 RPM 包,构建相同 CPU 架构的 amd64 镜像只需要不到 10 分钟就能完成,而构建异构的 arm64 镜像则接近一个小时,构建速度相差 6 倍之多。如果将 arm64 的镜像放到相同 CPU 架构的主机上来构建,构建时间和 amd64 差不太多。

由此可见,在同一台机器上构建异构的容器镜像有着比较严重的性能问题。因此构建多 CPU 架构的容器镜像性能最好的方案就是在对应 CPU 架构的机器上来构建,这种原生的构建方式由于没有 CPU 指令翻译这一开销性能当然是最棒滴,这种方式也被称之为 native nodes provide

BuildKit

BuildKit 是一个将 source code 通过自定义的构建语法转换为 build artifacts 的开源构建工具,被称为下一代镜像构建工具。同时它也是 docker 的一部分,负责容器镜像的构建。我们平时使用 docker build 命令时就是它负责后端容器镜像的构建。BuildKit 它支持四种不同的驱动来执行镜像的构建:

  • docker:使用内嵌在 Docker 守护程序中的 BuildKit 库。默认情况下 docker build 就是这种方式;
  • docker-container:创建一个专门的 BuildKit 容器,将 BuildKit 运行在容器中,有点类似于 docker in docker;
  • kubernetes:在 kubernetes 集群中创建 BuildKit pod,类似于我之前提到的 docker in pod 的方式;
  • remote:通过 TCP 或 SSH 等方式连接一个远端的 BuildKit 守护进程;

不同的驱动所支持的特性也不太一样:

Featuredockerdocker-containerkubernetesremote
Automatically load image
Cache exportInline only
Tarball output
Multi-arch images
BuildKit configurationManaged externally

如果想要使用原生方式构建多 CPU 架构的容器镜像,则需要为 BuildKit 创建多个不同的 driver。同时,由于该构建方案运行在 Kubernetes 集群上,我们当然是采用 Kubernetes 这个 driver 啦。然而,这要求 Kubernetes 集群必须是一个异构集群,即集群中的 node 节点必须同时包含对应 CPU 架构的机器。然而,这也引出了另一个尴尬难题:目前主流的 Kubernetes 部署工具对异构 Kubernetes 集群的支持并不是十分完善,因为异构的 kubernetes 集群有点奇葩需求不多的缘故吧。在此,咱推荐使用 k3skubekey 来部署异构 Kubernetes 集群。

BuildKit on Kubernetes

其实在 kubernetes 集群中部署 buildkit 官方是提供了一些 manifest,不过并不适合我们现在的这个场景,因此我们使用 buildx 来部署。Buildx 是一个 Docker CLI 插件,它扩展了 docker build 命令的镜像构建功能,完全支持 BuildKit builder 工具包提供的特性。它提供了与 docker build 相似的操作体验,并增加了许多新的构建特性,例如多架构镜像构建和并发构建。

在部署 BuildKit 前我们需要先把异构的 kubernetes 集群部署好,部署的方式和流程本文就不在赘述了,可以参考 k3s 或 kubekey 的官方文档。部署好之后我们将 kubeconfig 文件复制到本机并配置好 kubectl 连接这个 kubernetes 集群。

$ kubectl get node -o wide --show-labelsNAME                             STATUS   ROLES                  AGE   VERSION        INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME          LABELSproduct-builder-ci-arm-node-02   Ready    <none>                 11d   v1.26.3+k3s1   192.168.26.20    <none>        Ubuntu 22.04.1 LTS   5.15.0-69-generic   containerd://1.6.19-k3s1   beta.kubernetes.io/arch=arm64,beta.kubernetes.io/os=linux,kubernetes.io/arch=arm64,kubernetes.io/hostname=product-builder-ci-arm-node-02,kubernetes.io/os=linuxcluster-installer                Ready    control-plane,master   11d   v1.26.3+k3s1   192.168.28.253   <none>        Ubuntu 20.04.2 LTS   5.4.0-146-generic   containerd://1.6.19-k3s1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=cluster-installer,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=true,node-role.kubernetes.io/master=true

准备好 kubernetes 集群后我们我们还需要安装 docker-cli 以及 buildx 插件

# 安装 docker,如果已经安装可以跳过该步骤$ curl -fsSL https://get.docker.com -o get-docker.shsudo sh get-docker.sh# 安装 buildx docker-cli 插件$ BUILDX_VERSION=v0.10.4$ mkdir -p $HOME/.docker/cli-plugins$ wget https://github.com/docker/buildx/releases/download/$BUILDX_VERSION/buildx-$BUILDX_VERSION.linux-amd64$ mv buildx-$BUILDX_VERSION.linux-amd64 $HOME/.docker/cli-plugins/docker-buildx$ chmod +x $HOME/.docker/cli-plugins/docker-buildx$ docker buildx versiongithub.com/docker/buildx v0.10.4 c513d34049e499c53468deac6c4267ee72948f02

接着我们参考 docker buildx createKubernetes driver 文档在 kubernetes 集群中部署 amd64 和 arm64 CPU 架构对应的 builder。

# 创建一个单独的 namespace 来运行 buildkit$ kubectl create namespace buildkit --dry-run=client -o yaml | kubectl apply -f -# 创建 linux/amd64 CPU 架构的 builder$ docker buildx create \  --bootstrap \  --name=kube \  --driver=kubernetes \  --platform=linux/amd64 \  --node=builder-amd64 \  --driver-opt=namespace=buildkit,replicas=2,nodeselector="kubernetes.io/arch=amd64"# 创建 linux/arm64 CPU 架构的 builder$ docker buildx create \  --append \  --bootstrap \  --name=kube \  --driver=kubernetes \  --platform=linux/arm64 \  --node=builder-arm64 \  --driver-opt=namespace=buildkit,replicas=2,nodeselector="kubernetes.io/arch=arm64"# 查看 builder 的 deployment 是否正常运行$ kubectl get deploy -n buildkitNAME            READY   UP-TO-DATE   AVAILABLE   AGEbuilder-amd64   2/2     2            2           60sbuilder-arm64   2/2     2            2           30s# 最后将 docker 默认的的 builder 设置为我们创建的这个$ docker buildx use kube

docker buildx create 参数

名称描述
--append追加一个构建节点到 builder 实例中
--bootstrapbuilder 实例创建后进行初始化启动
--buildkitd-flags配置 buildkitd 进程的参数
--config指定 BuildKit 配置文件
--driver指定驱动 (支持: docker, docker-container, kubernetes)
--driver-opt驱动选项
--leave从 builder 实例中移除一个构建节点
--name指定 Builder 实例的名称
--node创建或修改一个构建节点
--platform强制指定节点的平台信息
--use创建成功后,自动切换到该 builder 实例

--driver-opt kubernetes driver 参数

ParameterDescription
imagebuildkit 的容器镜像
namespacebuildkit 部署在哪个 namespace
replicasdeployment 的副本数
requests.cpupod 的资源限额配置,如果并发构建的任务比较多建议多给点或者不配置
requests.memory同上
limits.cpu同上
limits.memory同上
nodeselectornode 标签选择器,这里我们给对应 CPU 架构的 builder 添加上 kubernetes.io/arch=$arch 这个 node 标签选择器来限制运行在指定节点上。
tolerations污点容忍配置
rootless是否选择 rootless 模式。不过要求 kubernetes 版本在 1.19 以上并推荐使用 Ubuntu 内核 Using Ubuntu host kernel is recommended。个人感觉 rootless 模式限制比较多而且也有一堆问题,不建议使用。
loadbalance负载均衡模式,无特殊要求使用默认值即可。
qemu.install是否安装 qemu 以支持在同一台机器上构建多架构的镜像,这种方式就倒车回去了,违背了我们这个方案的初衷,不建议使用
qemu.imageqemu 模拟器的镜像,不建议使用

部署好之后我们运行 docker buildx inspect 就可以查看到 builder 的详细信息

$ docker buildx inspect kubeName:          kubeDriver:        kubernetesLast Activity: 2023-04-19 00:27:57 +0000 UTCNodes:Name:           builder-amd64Endpoint:       kubernetes:///kube?deployment=builder-amd64&kubeconfig=Driver Options: nodeselector="kubernetes.io/arch=amd64" replicas="2" namespace="buildkit"Status:         runningBuildkit:       v0.11.5Platforms:      linux/amd64*, linux/amd64/v2, linux/amd64/v3, linux/386Name:           builder-amd64Endpoint:       kubernetes:///kube?deployment=builder-amd64&kubeconfig=Driver Options: replicas="2" namespace="buildkit" nodeselector="kubernetes.io/arch=amd64"Status:         runningBuildkit:       v0.11.5Platforms:      linux/amd64*, linux/amd64/v2, linux/amd64/v3, linux/386Name:           builder-arm64Endpoint:       kubernetes:///kube?deployment=builder-arm64&kubeconfig=Driver Options: image="docker.io/moby/buildkit:v0.11.5" namespace="buildkit" nodeselector="kubernetes.io/arch=arm64" replicas="2"Status:         runningBuildkit:       v0.11.5Platforms:      linux/arm64*Name:           builder-arm64Endpoint:       kubernetes:///kube?deployment=builder-arm64&kubeconfig=Driver Options: nodeselector="kubernetes.io/arch=arm64" replicas="2" image="docker.io/moby/buildkit:v0.11.5" namespace="buildkit"Status:         runningBuildkit:       v0.11.5Platforms:      linux/arm64*

同时 buildx 会在当前用户的 ~/.docker/buildx/instances/kube 路径下 生成一个 json 格式的配置文件,通过这个配置文件再加上 kubeconfig 文件就可以使用 buildx 来连接 buildkit 构建镜像啦。

{  "Name": "kube",  "Driver": "kubernetes",  "Nodes": [    {      "Name": "builder-amd64",      "Endpoint": "kubernetes:///kube?deployment=builder-amd64&kubeconfig=",      "Platforms": [        {          "architecture": "amd64",          "os": "linux"        }      ],      "Flags": null,      "DriverOpts": {        "namespace": "buildkit",        "nodeselector": "kubernetes.io/arch=amd64",        "replicas": "2"      },      "Files": null    },    {      "Name": "builder-arm64",      "Endpoint": "kubernetes:///kube?deployment=builder-arm64&kubeconfig=",      "Platforms": [        {          "architecture": "arm64",          "os": "linux"        }      ],      "Flags": null,      "DriverOpts": {        "image": "docker.io/moby/buildkit:v0.11.5",        "namespace": "buildkit",        "nodeselector": "kubernetes.io/arch=arm64",        "replicas": "2"      },      "Files": null    }  ],  "Dynamic": false}

我们将 buildx 生成的配置文件创建为 configmap 保存在 kubernetes 集群中,后面我们需要将这个 configmap 挂载到 pod 里。

$ kubectl create cm buildx.config --from-file=data=$HOME/.docker/buildx/instances/kube

构建测试

是骡子是马拉出来遛遛,我们就以构建 open-vm-tools-oe2003 RPM 为例来验证一下咱的这个方案究竟靠不靠谱 🤣。这个项目是给某为的 openEuler 2003 构建 open-vm-tools rpm 包用的 Dockerfile 如下。

FROM openeuler/openeuler:20.03 as builderRUN sed -i "s#repo.openeuler.org#repo.huaweicloud.com/openeuler#g" /etc/yum.repos.d/openEuler.repo && \    dnf install rpmdevtools* dnf-utils -y && \    rpmdev-setuptree# clone open-vm-tools source code and update spec file for fixes oe2003 build errorARG COMMIT_ID=8a7f961ARG GIT_REPO=https://gitee.com/src-openeuler/open-vm-tools.gitWORKDIR /root/rpmbuild/SOURCESRUN git clone $GIT_REPO . && \    git reset --hard $COMMIT_ID && \    sed -i 's#^%{_bindir}/vmhgfs-fuse$##g' open-vm-tools.spec && \    sed -i 's#^%{_bindir}/vmware-vmblock-fuse$##g' open-vm-tools.spec && \    sed -i 's#gdk-pixbuf-xlib#gdk-pixbuf2-xlib#g' open-vm-tools.spec# install open-vm-tools rpm build dependenciesRUN yum-builddep -y open-vm-tools.specRUN rpmbuild --define "dist .oe1" -ba open-vm-tools.spec --quiet# download rpm runtime dependenciesFROM openeuler/openeuler:20.03 as depCOPY --from=builder /root/rpmbuild/RPMS/ /root/rpmbuild/RPMS/RUN sed -i "s#repo.openeuler.org#repo.huaweicloud.com/openeuler#g" /etc/yum.repos.d/openEuler.repo && \    dnf install -y --downloadonly --downloaddir=/root/rpmbuild/RPMS/$(arch) /root/rpmbuild/RPMS/$(arch)/*.rpm# copy rpms to localFROM scratchCOPY --from=dep /root/rpmbuild/RPMS/ /COPY --from=builder /root/rpmbuild/RPMS/ /

其中 RUN rpmbuild --define "dist .oe1" -ba open-vm-tools.spec --quiet 这个步骤是构建和编译 RPM 里的二进制文件因此十分耗费 CPU 资源,也是整个镜像构建最耗时的一部分。

# copy rpms to localFROM scratchCOPY --from=dep /root/rpmbuild/RPMS/ /COPY --from=builder /root/rpmbuild/RPMS/ /

因为我们构建的目标产物是 RPM 包文件并不需要把镜像 push 到镜像仓库中,所以 Dockerfile 最后面这一段是为了将构建产物捞出来输出到我们本地的目录上,buildx 对应的参数就是 --output type=local,dest=path。同时为了排除 cache 的影响,我们再加上 --no-cache 参数构建过程中不使用缓存。接着我们运行 docker build 命令进行构建,看一下构建的用时是多久 🤓

DOCKER_BUILDKIT=1 docker buildx build \--no-cache \--ulimit nofile=1024:1024 \--platform linux/amd64,linux/arm64 \-f /root/usr/src/github.com/muzi502/open-vm-tools-oe2003/Dockerfile \--output type=local,dest=/root/usr/src/github.com/muzi502/open-vm-tools-oe2003/output \/root/usr/src/github.com/muzi502/open-vm-tools-oe2003[+] Building 364.6s (30/30) FINISHED => [internal] load .dockerignore                                                                                                         0.0s => => transferring context: 2B                                                                                                           0.0s => [internal] load build definition from Dockerfile                                                                                      0.0s => => transferring dockerfile: 1.35kB                                                                                                    0.0s => [internal] load .dockerignore                                                                                                         0.0s => => transferring context: 2B                                                                                                           0.0s => [internal] load build definition from Dockerfile                                                                                      0.0s => => transferring dockerfile: 1.35kB                                                                                                    0.0s => [linux/amd64 internal] load metadata for docker.io/openeuler/openeuler:20.03                                                          2.1s => [linux/arm64 internal] load metadata for docker.io/openeuler/openeuler:20.03                                                          2.1s => [auth] openeuler/openeuler:pull token for registry-1.docker.io                                                                        0.0s => [auth] openeuler/openeuler:pull token for registry-1.docker.io                                                                        0.0s => CACHED [linux/arm64 builder 1/6] FROM docker.io/openeuler/openeuler:20.03@sha256:4aef44f5d6af7b07b02a9a3b29cbac5f1f109779209d7649a2e  0.0s => => resolve docker.io/openeuler/openeuler:20.03@sha256:4aef44f5d6af7b07b02a9a3b29cbac5f1f109779209d7649a2ea196a681a52ee                0.0s => [linux/arm64 builder 2/6] RUN sed -i "s#repo.openeuler.org#repo.huaweicloud.com/openeuler#g" /etc/yum.repos.d/openEuler.repo &&      54.6s => CACHED [linux/amd64 builder 1/6] FROM docker.io/openeuler/openeuler:20.03@sha256:4aef44f5d6af7b07b02a9a3b29cbac5f1f109779209d7649a2e  0.0s => => resolve docker.io/openeuler/openeuler:20.03@sha256:4aef44f5d6af7b07b02a9a3b29cbac5f1f109779209d7649a2ea196a681a52ee                0.0s => [linux/amd64 builder 2/6] RUN sed -i "s#repo.openeuler.org#repo.huaweicloud.com/openeuler#g" /etc/yum.repos.d/openEuler.repo &&      65.1s => [linux/arm64 builder 3/6] WORKDIR /root/rpmbuild/SOURCES                                                                              0.3s => [linux/arm64 builder 4/6] RUN git clone https://gitee.com/src-openeuler/open-vm-tools.git . &&     git reset --hard 8a7f961 &&     s  1.8s => [linux/arm64 builder 5/6] RUN yum-builddep -y open-vm-tools.spec                                                                     58.8s => [linux/amd64 builder 3/6] WORKDIR /root/rpmbuild/SOURCES                                                                              0.3s => [linux/amd64 builder 4/6] RUN git clone https://gitee.com/src-openeuler/open-vm-tools.git . &&     git reset --hard 8a7f961 &&     s  2.1s => [linux/amd64 builder 5/6] RUN yum-builddep -y open-vm-tools.spec                                                                     71.9s => [linux/arm64 builder 6/6] RUN rpmbuild --define "dist .oe1" -ba open-vm-tools.spec --quiet                                          175.2s => [linux/amd64 builder 6/6] RUN rpmbuild --define "dist .oe1" -ba open-vm-tools.spec --quiet                                          181.4s => [linux/arm64 dep 2/3] COPY --from=builder /root/rpmbuild/RPMS/ /root/rpmbuild/RPMS/                                                   0.1s => [linux/arm64 dep 3/3] RUN sed -i "s#repo.openeuler.org#repo.huaweicloud.com/openeuler#g" /etc/yum.repos.d/openEuler.repo &&     dnf  31.6s => [linux/amd64 dep 2/3] COPY --from=builder /root/rpmbuild/RPMS/ /root/rpmbuild/RPMS/                                                   0.1s => [linux/amd64 dep 3/3] RUN sed -i "s#repo.openeuler.org#repo.huaweicloud.com/openeuler#g" /etc/yum.repos.d/openEuler.repo &&     dnf  39.2s => [linux/arm64 stage-2 1/2] COPY --from=dep /root/rpmbuild/RPMS/ /                                                                      0.1s => [linux/arm64 stage-2 2/2] COPY --from=builder /root/rpmbuild/RPMS/ /                                                                  0.2s => exporting to client directory                                                                                                         2.4s => => copying files linux/arm64 35.93MB                                                                                                  2.3s => [linux/amd64 stage-2 1/2] COPY --from=dep /root/rpmbuild/RPMS/ /                                                                      0.1s => [linux/amd64 stage-2 2/2] COPY --from=builder /root/rpmbuild/RPMS/ /                                                                  0.2s => exporting to client directory                                                                                                         1.6s => => copying files linux/amd64 36.59MB                                                                                                  1.6stree rpms
用时对比amd64 (Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz)arm64(HUAWEI Kunpeng 920 5250 2.6 GHz)
yum-builddep71.9s58.8s
rpmbuild181.4s175.2s

通过上面的构建用时对比可以看到 arm64 的机器上构建比 amd64 要快一点,是由于 Kunpeng 920 5250 CPU 主频比 Intel Xeon 4110 高的缘故,如果主频拉齐的话二者的构建速度应该是差不多的。可惜我们 IDC 内部的机器 CPU 大多是十几块钱包邮还送硅脂的钥匙串(某宝上搜 E5 v3/v4)找不到合适的机器进行 PK 对比,大家自己脑补一下吧🥹,要不汝给咱点 CPU 😂。

总之我们这套方案实现的效果还是蛮不错滴,比用 qemu 模拟多架构的方式不知道高到哪里去了 🤓。

Jenkins 流水线

首先,我们需要定制自己的 Jenkins slave pod 的基础镜像,将 docker 和 buildx 这两个二进制工具添加进来。需要注意的是,这里的 docker 命令行只是作为客户端使用,因此我们可以直接从 docker 的官方镜像中提取此二进制文件。不同的项目需要不同的工具集,可以参考我的 Dockerfile

FROM python:3.10-slimARG BUILDER_NAME=kubeCOPY --from=docker.io/library/docker:20.10.12-dind-rootless /usr/local/bin/docker /usr/local/bin/dockerCOPY --from=docker.io/docker/buildx-bin:v0.10 /buildx /usr/libexec/docker/cli-plugins/docker-buildx

这里还有一个冷门的 Dockerfile 的小技巧:通过 COPY --from= 的方式来下载一些二进制工具。基本上我写的 Dockerfile 都会用它,可谓是屡试不爽 身经百战了😎。别再用 wget/curl 这种方式傻乎乎地安装这些二进制工具啦,一句 COPY --from= 不知道高到哪里去了。

分享一个比较冷门的 Dockerfile 的小技巧:
当你要安装一个 binary 工具时(比如 jq、yq、kubectl、helm、docker 等等),可以考虑直接从它们的镜像里 COPY 过来,替代使用 wget/curl 下载安装的方式,比如:
COPY --from=docker:20.10.12-dind-rootless /usr/local/bin/docker /usr/local/bin/docker pic.twitter.com/4ZWFqk5EEv

— Reimu (@muzi_ii) May 6, 2022

接下来,我们需要自定义 Jenkins Kubernetes 插件的 Pod 模板,将我们上面创建的 buildx 配置文件的 configMap 通过 volume 挂载到 Pod 中。这个 Jenkins slave Pod 就可以在 k8s 中通过 Service Accounts 加上 buildx 配置文件来连接 buildkit 了。可以参考我这个 Jenkinsfile

// Kubernetes pod template to run.podTemplate(    cloud: JENKINS_CLOUD,    namespace: POD_NAMESPACE,    name: POD_NAME,    label: POD_NAME,    yaml: """apiVersion: v1kind: Podmetadata: annotations:    kubectl.kubernetes.io/default-container: runnerspec:  nodeSelector:    kubernetes.io/arch: amd64  containers:  - name: runner    image: ${POD_IMAGE}    imagePullPolicy: Always    tty: true    volumeMounts:    # 将 buildx 配置文件挂载到当前用户的 /root/.docker/buildx/instances/kube 目录下    - name: buildx-config      mountPath: /root/.docker/buildx/instances/kube      readOnly: true      subPath: kube    env:    - name: HOST_IP      valueFrom:        fieldRef:          fieldPath: status.hostIP  - name: jnlp    args: ["\$(JENKINS_SECRET)", "\$(JENKINS_NAME)"]    image: "docker.io/jenkins/inbound-agent:4.11.2-4-alpine"    imagePullPolicy: IfNotPresent  volumes:    # 配置 configmap 挂载    - name: buildx-config      configMap:        name: buildx.config        items:          - key: data            path: kube"""

当 Jenkins slave pod 创建好之后,我们还需要进行一些初始化配置,例如设置 buildx 和登录镜像仓库等。我们可以在 Jenkins pipeline 中增加一个 Init 的 stage 来完成这些操作。

stage("Init") {    withCredentials([usernamePassword(credentialsId: "${REGISTRY_CREDENTIALS_ID}", passwordVariable: "REGISTRY_PASSWORD", usernameVariable: "REGISTRY_USERNAME")]) {        sh """        # 将 docker buildx build 重命名为 docker build        docker buildx install        # 设置 buildx 使用的 builder,不然会默认使用 unix:///var/run/docker.sock        docker buildx use kube        # 登录镜像仓库        docker login ${REGISTRY} -u '${REGISTRY_USERNAME}' -p '${REGISTRY_PASSWORD}'        """    }}

其他

构建镜像时,我们可以在 buildkit 部署节点上运行 pstree 命令,来查看构建的过程。

root@product-builder-master:~# pstree -l -c -a -p -h -A 2637buildkitd,2637  |-buildkit-runc,989505 --log /var/lib/buildkit/runc-overlayfs/executor/runc-log.json --log-format json run --bundle /var/lib/buildkit/runc-overlayfs/executor/82zvcfesf5g19t2682g3j9hrr 82zvcfesf5g19t2682g3j9hrr  |   |-rpmbuild,989519 --define dist .oe1 -ba open-vm-tools.spec --quiet  |   |   `-sh,989562 -e /var/tmp/rpm-tmp.xKly7N  |   |       `-make,995708 -O -j64 V=1 VERBOSE=1

通过 buildkitd 的进程树,我们可以看到 buildkitd 进程中有一个 buildkit-runc 的子进程。它会在一个 runc 容器中运行 Dockerfile 中对应的命令。因此,我们可以得知 buildkit on kubernetes 和之前的 docker in pod 实现原理是类似的,只不过这里的 buildkit 只用于构建镜像而已。

参考

写在上海封城一年之后

作者 Reimu
2023年4月16日 00:00

时代的一粒灰尘落在个人身上就是一座山,不幸的是:我们偏偏却活在一个尘土飞扬的年代。如果要对三年防疫做一句总结的话,我愿意称之为一场政治运动型的防疫闹剧

防疫闹剧

2022 依旧是人类社会倒车和加速灭亡的一年。不过有幸的是,在经济下行压力、白纸运动抗议、国际共存舆论等众多因素的影响下,年末当权者终于叫停了这场政治运动型的防疫闹剧,底层的屁民韭菜们终于有了口喘气活下去的机会。回想起去年的现在,还被封在家里、还在抢菜、还在为明天吃什么发愁、还在担心这场荒诞至极的防疫闹剧什么时候能结束、还在担心这场闹剧的现实会一直持续下去。回想起那段时间唯独两天一次的支性检测不敢半点耽搁、老老实实做核酸、戴口罩、出示健康码、被训得服服帖帖,可谓是奴(zhi)性十足。

即便是去年六月初上海解封后,依旧没有从那种恐惧中解脱出来,反而变得越来越自闭,直到现在此刻的心态和上海封城那段时间没有太大区别。思想审查、文字狱、集中营、白色恐怖、谎言欺骗,这些并没有因为疫情结束而消失,每天的感触就像是在历史与现实的夹缝中苟活,对未来充满着无限的恐惧。

或许我们已经早已经听惯了你不关心政治政治会来关心你这句废话,但当经历了这场灾难(防疫闹剧)之后,我们或多或少地能感触到一个国家的政治气候的变化是如何深刻地影响和限制人类生存的状态和生存的可能。在某些性命攸关的时刻,政治它直接或间接地决定了我们还能不能在这个世界生存下去。

从去年上海封城到现在,在这一年多的时间里,博客已经很少再更新了,也慢慢地淡出了推特。除了正常的工作生活外,大部分时间和精力都是在研究和思考这个国家和社会为什么会上演着一场场荒诞至极的防疫闹剧。结合这三年防疫闹剧期间所发生、暴露出的一切,以及自己的生活感受,我越来越有一种想探究这场防疫闹剧的政治基础是如何一步步建立起来的想法。于是近期就整理了一些最近一年多的时间里所读的书以及一些个人的想法。

读书笔记

孔飞力《叫魂:1768 年中國妖術大恐慌》

第一次读这本书是在 2021 年五一假期期间,那时我正在 2021 五一假期环太湖骑行之旅中。2022 年上海封城后,我又重新读了一遍。再次读这本书的动机,已经想不太清了。我记得当时看到了一些令人匪夷所思的防疫闹剧,比如某个小区的防疫工作人员把居民团购买的菜扔进垃圾桶里,这种防疫闹剧令我十分费解。我不能理解大白和红袖章这些群体以防疫为名拥有了一点权力后就会上演着一场场令人匪夷所思的防疫闹剧。

还有就是当时在推特上看到了一个以防疫为名殴打村民的短视频,让我再一次想起了我的另一段切身经历:在 2020 年武汉肺炎刚爆发的时候,我外婆在大年初六那天不幸离开了这个世界。依稀记得那天去我外婆家的路也被封得严严实实,没办法开车去,只能骑电动车。在村与村交界的路上被当地的红袖章拦住不让过,给他说明了原因也死活不让过。真是欺人太甚,气得我直接踹开挡板骑车电车硬闯了过去。随后红袖章拨打了当地派出所的电话,两辆警车十几号人来抓捕我。当时一直就想不清为什么一个普通的村民戴上红袖章之后就如此膨胀,直到读了这本书后我慢慢地想通了。

这本书向人们展示了中国社会普遍存在的一个现象:社会上到处表现出冤冤相报的敌意。中国的专制制度从公元前 221 年秦始皇统一中国开始,沿袭了两千多年,有着丰厚的历史积淀。即便是在今天,权力仍垄断在专制统治阶层手中,让普通民众享有权力十分困难,而当以叫魂或防疫为名的“幻觉权力”进入社会之后,普通民众就拥有了互相报复的武器。他们所能爆发出来的威力正如书中所描写的那样恐怖。

一旦官府认真发起对妖术的清剿,普通人就有了很好的机会来清算宿怨或谋取私利。这是扔在大街上的上了膛的武器,每个人——无论恶棍或良善——都可以取而用之。在这个权力对普通民众来说向来稀缺的社会里,以“叫魂”罪名来恶意中伤他人成了普通人的一种突然可得的权力。对任何受到横暴的族人或贪婪的债主逼迫的人来说,这一权力为他们提供了某种解脱;对害怕受到迫害的人,它提供了一块盾牌;对想得到好处的人,它提供了奖赏;对妒嫉者,它是一种补偿;对恶棍,它是一种力量;对虐待狂,它则是一种乐趣。

施行妖术和提出妖术指控所折射反映出来的是人们的无权无势状态。对一些无权无势的普通民众来说,弘历的清剿给他们带来了慷慨的机会。即使在今天,让普通民众享有权力仍是一个还未实现的许诺。毫不奇怪,冤冤相报(这是“受困扰社会”中最为普遍的社会进攻方式)仍然是中国社会生活的一个显著特点。

没有人会哀悼旧中国的官僚制度。即使按照当时的标准,它所造成的社会伤害也已超出了仅仅压碎几个无依无助的游民踝骨的程度。但不论是好事还是坏事,它的特性却可以阻挡任何一种狂热。没有这样一个应急的锚碇,中国就会在风暴中急剧偏航。在缺乏一种可行的替代制度的情况下,统治者就可以利用操纵民众的恐惧,将之转变为可怕的力量。生活于我们时代的那些异见人士和因社会背景或怪异信仰而易受指控的替罪羊,便会成为这种力量的攻击目标。

当代中国的历史中充满了这种幻觉权力进入社会的例子。我还记得 1982 年在北京与一个老红卫兵的谈话。他当时是一个低收入的服务工。他感慨地说,毛泽东的文化革命对于像他这样没有正式资格循常规途径在社会上进身的人来说是一个黄金时代,毛号召年轻人起来革命造反,这一来自顶端的突然可得的权力使他的野心得到了满足。他抱怨说,现在的社会样样都要通过考试,他再也没有希望从现在这个最底层的位置爬上去了。

高华《历史笔记》

最近在读高华的《历史笔记》,感触最深的就是:六十年前大饥荒时基层干部对死亡人数谎报瞒报、中央政府拒不承认事实、新闻媒体跟着愚民洗脑。再看看我们所处的时代,从武汉肺炎谎报瞒报到西安的掩耳到“零”。六十年过去了,还是熟悉的味道。

所以我十分相信“对未来充满希望的人往往对历史一无所知”。 pic.twitter.com/QNhH5AfxPs

— Reimu (@muzi_ii) January 14, 2022

《历史笔记》是我在 2022 年读的第一本书,从元旦开始阅读,花费了相当长的时间和精力才读完。这本书是高华教授的遗作,共分为上下两卷四编,繁体中文。

第一编《⾰命、内战与⺠族主义》分论国⺠党共产党两党 1949 年前各⾃的历史。作为国共内战胜利方的中共是本章的论述重点,所选文章不仅反映其革命夺权历程,还映射出 1949 年后政治实践的某些雏形。

民族主義與民主主義是一對雙胞胎,區別在於:民族主義,強調集體認同和國家認同;民主主義,強調個人本位,個人權利,個人自由。從理論上講,當國家、民族面臨嚴重的危機時,國民應讓渡出自己的一部分個人權利,以服從於國家利益,支持國家戰勝危機,而國家的最終目的是保護個人自由。但是在近代以來,民族主義經常吞噬民主主義,這主要是由中國近代的政治和大的環境造成的。也和人們認識的誤區,統治階級的狹隘和自私有關。

第⼆编《断裂与延续》主要论及⽑泽东时代,内容涵盖了三反五反、大跃进运动、四清运动、林彪事件等多个历史事件。其中十分推荐大家去仔细地去读一下《大躍進運動與國家權力的擴張》这个章节,从某种程度上我觉着这场防疫闹剧和大跃进运动背后的政治逻辑极其相似。

1958 年由毛澤東親自發動、席捲全國的大躍進運動,是一場具有空想烏托邦性質的政治運動。今天人們憶及當年的大躍進,馬上會聯想到「高產衛星」、「全民煉鋼」、「公社食堂」等帶有荒誕色彩的景象。然而大躍進並非僅僅是一場烏托邦運動,在大躍進期間,國家權力借着這場運動的推動,以前所未有的規模急速地向社會各個領域擴張。大躍進運動使國家權威得以擴大和強化,不僅深刻地改變了中國社會的面貌,也大大加強了民眾對國家權威的認知。

在大躍進期間,國家意志透過強有力的政治動員和組織措施得以全力貫徹,國家權力在這個過程中急速擴張。

與以往歷次政治運動相比,大躍進是一場規模更大的群眾性運動,這場運動不僅促使國家權威向城鄉全面滲透,而且在社會生活所有領域都建立、鞏固和強化了國家權力。

令人驚奇的是,即使到了這一步,一些領導幹部仍在繼續隱瞞饑荒的真相。周恩來以後回憶道,在 I960 年夏天召開的北戴河會議上,他本人「已經意識到糧食有問題,但大家不承認,結果把真實情況給掩蓋起來了」。

直至 1960 年 10 月,《人民日報》在國慶社論中才對形勢作出了新的解釋。社論稱,「兩年來,全國大部分地區連續遭受嚴重的自然災害,造成糧食嚴重減產」。社論並宣稱,「人民公社已使我國農民永遠擺脱了那種每遭自然災害必然有成百萬、成千萬人饑餓、逃荒和死亡的歷史命運」。社論作者當然知道,就在這篇社論發表之時,全國各地農村正在發生大面積餓死人的情況,但事實歸事實,宣傳歸宣傳,他們選擇採取了「硬着頭皮頂住」的方針。

还有《1949-1965 年中國社會的政治分層》章节里提到的向党交心运动十分有有意思,大跃进时期都是要把心交给党的,比现在安个反诈 app 把隐私交给党高到不知道哪里去了 😂。

全體教師聯合舉行改造促進大會,他們抬着「大紅心」的標誌上街遊行。4 月 4 日,南京市各高校師生與科研機關的民主人士共三千餘人,高舉「把心交給黨」、「把知識交給人民」的旗幟在南京市舉行大遊行,之後,又舉行了社會主義自我改造促進大會。4 月 21 日,南京市工商界三千多人召開大會,宣佈「立即開展向黨交心運動」,民建中央主席黃炎培親臨會場予以鼓勵。4 月 22 日,南京市工商界和民主黨派提出向黨「交心」要「快、透、深、真」的口號,表示要把「接受黨的領導和走社會主義道路的三心二意,躍進到一心一意」。江蘇省宗教界人士也開展了「交心」運動,天主教界通過「自選」、「自聖」主教,「使全省天主教出現了一個新的局面」。在「交心」運動中,全省 11 個城市民主黨派和工商界人士 4,106 人,共交心 47 萬條。據當時的記載稱,這次交心「大量暴露了他們長期隱瞞的腐朽思想和反動行為」12()。對於工商界和民主人士的「交心」,組織上規定的原則是「自梳自理,求醫會診」。先讓他們對照要求、自我批判,然後引導他們懇請黨員和領導對他們的「壞思想」有針對性地進行批評,並鼓勵他們打破庸俗的情面觀,「比先進,比幹勁」,互相展開批評和思想鬥爭,以使「交心」落在實處,防止「交心」走過場。

經過對「二十二個文件」的逐字逐句的精讀,和反復對照檢查,個人原來的小資產階級的自我意識開始分裂。隨着「發掘本心」的逐步深入,學習者普遍對自己的缺點錯誤產生了羞愧意識,出身剝削階級家庭的知識分子黨員更自慚形穢,認為自己確實如毛澤東所言,除了讀了一些如同「狗屎」般無用的書之外,對共產黨和人民的價值無多,尤其嚴重的是,剝削階級的家庭背景,甚至還會使自己在革命的關鍵時刻動搖革命立場,在客觀上危害革命!這樣的自我壓力有如大山般沉重,使許多知識分子黨員原有的沾沾自喜、驕傲自滿等不良習氣一掃而空。

按照毛澤東的看法,一個人的階級立場必然決定了他的觀點和態度。例如:你是不是在心裏還欣賞資產階級個性自由、個性解放的錯誤思想?你是否心悦誠服地把一切都獻給黨?你是否真正同意你所出身的剝削階級家庭是骯髒和反動的?你對沒有文化的工農群眾是滿心鄙夷,還是甘心做他們的小學生?你對黨的考驗是真心接受,還是抱冤叫屈?

第三编《「從『大破』走向『大立』」:文革中的「新生事物」》是高華教授生前承擔了香港中文大學中國文化研究所《中華人民共和國史》第七卷的寫作任務。他已列出該卷寫作綱要,惜乎天不假年,只完成了十餘萬字的文稿。

毛為什麼要發動「文革」?「文革」是如何發動起來的?我認為毛澤東發動「文革」有兩方面的動因,第一個因素:「文革」集中體現了毛對他所理想的社會主義的追求;第二個因素:他認為自己已大權旁落,而急於追回,這兩方面的因素互相纏繞,緊密的交融在一起。

國家的領導者為了快速建立起一個強大的社會主義的國家,他們一直在謀求一種「最好的」治理中國的制度或管理形式,他們有許多創造,建構了一種新意識形態敍述,中國傳統的思想及制度資源,革命年代的經驗與蘇聯因素融為一體,都被運用其中,被用來統合社會大眾的意識。他們也非常重視做動員、組織民眾的工作,使社會的組織化、軍事化程度不斷增強

第四編《讀書有感》包含多篇書評,論及對象既有風雲人物,也有平頭百姓,既有追隨國民黨政權遷台的作家,也有大陸人所皆知的左翼文人。本章通過對他們回憶的評議展現出多角度的時代變遷與個體感受。

我認為,學歷史、讀歷史,記住余英時先生的一段話是很重要的。他説:學歷史的好處不是光看歷史教訓,歷史教訓也是很少人接受,前面犯多少錯誤,到後面還是繼續。因為人性就是大權在握或利益在手,但難以捨棄,權力和利益的關口,有人過得去,也有人過不去。所以我認為讀歷史的最大好處是使我們懂得人性。

在大學讀書的那幾年,我知道,雖然毛澤東晚年的錯誤已被批評,但毛的極左的一套仍根深蒂固,它已滲透到當代人思想意識的深處,成為某種習慣性思維,表現在中國現代史、中共黨史研究領域,就是官學甚行,為聖人避諱,或研究為某種權威著述作注腳,幾乎成為一種流行的風尚。

杨继绳《墓碑:一九五八—一九六二年中國大饑荒紀實》

親身經歷了上海封城之後再讀楊繼繩的著作《墓碑:中國六十年代大饑荒紀實》,又一次對這個社會陷入了深深的絕望之中😭。

任何災難都可以被用來塑造成正確的集體記憶,然後成為政權合法性的組成部分。正是這種對民族記憶的大清洗和對罪惡的強制遺忘,遂使得相同的歷史悲劇一次次不斷地重演。 pic.twitter.com/DfcE6V28Vw

— Reimu (@muzi_ii) June 28, 2022

我是去年上海解封后才开始读这本书的。经历了封城,我的心态已经麻木了,无论发生什么荒唐的事情,我已经习以为常了。读这本书的时候,有好几次我都想大哭一场,眼泪和鼻涕都止不住。因为我们现在所经历的悲剧在六十多年前已经发生过一次,而官僚体制应对灾难的方式和六十年前相比没有多少改变。

从武汉肺炎刚爆发时的谎报瞒报、训诫李文亮医生再到后来西安的”掩耳到零”等招式在六十年前已经使用过了。六十年前怎么应对大饥荒的,我们现在就是怎么应对防疫的,没有任何改变。

各级政府千方百计地对外封锁饥饿的消息。公安局控制了所有的邮局,向外面发出的信件一律扣留。中共信阳地委让邮局扣了 12000 多封向外求助的信。为了不让外出逃荒的饥民走漏消息,在村口封锁,不准外逃。对已经外逃的饥民则以“盲流”的罪名游街、拷打或其它惩罚。

1960 年 3 月 12 日卫生所的干部王启云写信给党中央,反映饿死人的严重问题,要求中央仿照“包文丞陈州放粮”,公安局侦破后,对王启云进行残酷的批判斗争。

伞陂公社第一次向上报的死亡人数 523 人,第二次报的是 3889 人(后又改为 2907 人),后来省委工作组调查结果是 6668 人。

用空洞的“全国形势一派大好”淡化人们实实在在的饥饿,压制人们对饥饿的不满。

就在信阳大量饿死人、人相食普遍发生的时候,《河南日报》还宣传形势一派大好,连续发表七篇“向共产主义进军”的文章。

在饿殍遍地的情况下,1960 年《河南日报》的元旦社论却以“开门红 春意浓”为题,继续粉饰太平,仍坚持全面跃进。

农民明明是饿死了,还不能说是因饥饿而死的。县委领导人赵玉书和董安春到武店公社考城大队检查浮肿病情况,问医师王善良:“为什么浮肿病总是治不好,少什么药?”王医生回答说:“少一味粮食!”赵、董二人立即决定,将王医生交大会批斗后逮捕。

在大批农民饿死的时刻,1960 年 2 月 16 日到 18 日,贵州省委召开了三天地、州、市委第一书记会议,主要讨论农村公共食堂问题。这个会不是解决食堂缺粮的问题,而是闭眼不看现实,向中共中央写了一个假报告――《关于农村公共食堂的报告》

强大的政治思想工作使人们驯服,新闻封锁使人愚昧。饿死上百万人的“信阳事件”、饿死三分之一人口的“通渭问题”,不仅当时邻近地区不知有其事,甚到几十年后还严加保密。

死人明明是饿死的,而说成是年老死的,疾病死的,把非正常死亡说成是正常死亡。有些地方还不允许死者家属哭丧带孝,不准埋坟,对反映死人情况的来信加以扣压,甚至对来信者进行打击;有的干部因为如实向组织反映了死人的情况还挨了斗争。”“因为怕犯错误,怕受处分,怕摘掉乌纱帽,而不敢暴露真实情况;越不敢暴露,问题就发展越大;问题越大,就越不敢暴露。”

上海养老院将活人装尸袋要求殡仪馆火化 这种荒唐事历史上已经发生过不止一次了

省委副秘书长周颐在雅安考察时看到不少肿得很严重的病人,问他们为什么不去医院治疗,他们说:医院条件很坏,在那里死得更快些。金堂县五星管理区的肿病医院是牛棚改的,清洁卫生没有搞彻底,臭气难闻。病房没有门,四周没有墙,90% 的病人睡地铺,铺草很薄。有的病人没被子,白天还喊冷。广汉县金鱼公社医院院长黄某,把活人装进棺材埋掉。

温江清平公社社员李方平饿得奄奄一息,县委检查团下来检查生活,管区干部怕他走漏风声,便把他关进保管室关了三天,生产队长报告说李已死,管区干部下令“死了把他埋了算球”。社员张绍春薅油菜饿倒在田头,队长以为他死了,赶快挖了个坑想把他埋了,埋到一半,张醒过来,大叫“活埋人了……”,吓得队长扔掉锄头就跑。

一些地区规定死人后“四不准”:一不准浅埋,要深埋三尺,上面种上庄稼;二不准哭;三不准埋在路旁;四不准戴孝。更恶劣的是黄湾公社张湾小队规定死了人不仅不准戴白布,还叫人披红!

二十多岁的民工任文厚被打死后,水库派人直接将尸体拉到该家坟地埋葬,父母想看上一眼都不允许。

同样为了应付上级的视察,官僚组织如何上演共谋闹剧的也是如出一辙。

为了应付上级检查,把大部分人力、畜力、肥料,都调到公路铁路两旁,调到社与社、县与县的交界处,做出样子,而里面却是大片土地抛荒。

在外宾所到之处,完全布置了一派丰饶、富裕的景象:湖里有穿着漂亮的女子悠闲地划船唱歌,在路旁的小店里食品丰富。省委所划定了外宾活动的地方,不让老百姓进入,特意布置假象欺骗外宾。

1959 年 12 月 9 日,我下放到和政县苏集公社。这里群众没有粮吃,饿得干瘦、浮肿,有的冻饿而死。榆树皮都被剥光吃掉了!有一天县上来电话,说张鹏图副省长要到康乐视察,命令我们连夜组织人把公路两边被剥光皮的榆树,统统砍掉,运到隐蔽的地方去。人都快饿死了,哪有力量去砍树、抬树?我们办不到,留下榆树正好让张鹏图副省长看看。

还有被集中隔离关进方舱的方式也是熟悉的味道

有的地方把社员自留地里的南瓜苗拔出来栽到集休的地里,结果全部死光。强占房屋,逼人搬家,不搬就强行把东西扔到外面。强行收走各家做饭的锅,甚至当着社员的面把锅砸烂了,老人要求留下一口锅烧水也不行。大通桥大队为了办农场,乘社员下地生产之机,将大通桥东头一个小庄子社员家的东西全部抛了出来,房屋由大队占领。社员无家可归,痛哭流涕。

乔山大队 31 个村庄,1960 年 6 月,总支书记梅某强迫群众在半天之内并成 6 个庄子,拆掉房子 300 多间,党员不干开除党籍,团员不干开除团籍,社员不干不给饭吃。说是建新村,实际上旧房子拆了新房子没有建,社员无家可归,100 多人被迫集中居住,有 14 户 40 人住在 3 间通连的房子里,晚上大门上锁,民兵持棍把门,尿尿拉屎都在一起。

食堂缺柴也是一个普遍问题。解决缺柴的办法一是砍树,二是拆房。全县树木被砍达 80% 以上,全县房屋倒塌和被扒 10 万间以上。有的地方挖坟劈棺当柴烧。在田野劈棺后剩下片片白骨,令人胆寒。

对大饥荒的反思也值得我们认真思考这场防疫闹剧

这是一场人类历史上空前的悲剧。在气候正常的年景,没有战争,没有瘟疫,却有几千万人死于饥饿,却有大范围的“人相食”,这是人类历史上绝无仅有的异数。

总路线,大跃进,人民公社,当时合称为“三面红旗”。这是 1958 年令中国人狂热的政治旗帜,是造成三年大饥荒的直接原因,也就是大饥荒的祸根。

的确,造成中国几千万人饿死的根本原因是极权制度。当然,我不是说极权制度必然造成如此大规模的死亡,而是说极权制度最容易造成重大政策失误,一旦出现重大政策失误又很难纠正。更重要的是,在这种制度下,政府垄断了一切生产和生活资源,出现灾难以后,普通百姓没有自救能力,只能坐以待毙。

为什么没有纠错机制?这是专制制度固有的缺陷。1958 年指导思想的错误,不仅仅是领袖和领导集团的错误,而是制度性错误。

权制度就是这样使民族性堕落。大跃进和文化大革命中,人们表现的那样疯狂,那样的残忍,正是民族性堕落的结果,也正是极权制度的“政绩”。

中国有句古话:“上有好者,下必甚焉”。这是在专制制度下,下级官员迎合上级的情形。1958 年的情况也是如此。处在一层一层的权力阶梯上的官员们,总是把最高层的意志一步一步地推向极端。

中共中央和毛泽东没有从制度、政策、指导思想方面寻找大饥荒的原因,而把大量死人的原因归罪于早已被打入十八层地狱的地、富、反、坏、右。说是因“民主革命不彻底”,而使阶级敌人篡夺基层领导权。这显然是违背事实。

书单

以下是我个人推荐的历史书籍。在经历了三年的防疫闹剧之后,结合自己的切身经历再次阅读这些历史书籍,就会有种亲临历史的错觉,仿佛活在历史与现实的夹缝之中。这些历史事件并不遥远,就像昨天一样清晰地铭刻在我们的脑海中。

作者书名
徐贲人以什么理由来记忆
周雪光中国国家治理的制度逻辑:一个组织学研究
谢岳维稳的政治逻辑
笑蜀历史的先声:半个世纪前的承诺
赵紫阳改革历程
徐中约中国近代史
高华身份和差异:1949-1965 年中国社会的政治分层
高华在历史的风陵渡口
高华历史笔记
杨继绳天地翻覆:中国文化大革命史
杨继绳中国改革年代的政治斗争
杨继绳中国当代社会阶层分析
杨继绳墓碑: 中国六十年代大饥荒纪实
橙实 山川 等文革笑料集
【美】傅高义邓小平时代
【美】亨利·基辛格世界秩序
【美】亨利·基辛格论中国
【英】乔治·奥威尔1984
【美】孔飞力叫魂:1768 年中国妖术大恐慌
【美】芭芭拉·德米克我们最幸福:北韩人民的真实生活
【捷克】哈维尔哈维尔文集
【捷克】伊凡·克里玛布拉格精神
【白俄】S·A·阿列克谢耶维奇切尔诺贝利的悲鸣

使用 Redfish 自动化安装 ESXi OS

作者 Reimu
2022年4月30日 00:00

从去年十一月底到现在一直在做在 VMware ESXi 上部署 超融合集群 的产品化工具,也是在最近完成了前后端的联调,五一节后开始进入测试阶段。为了测试不同的 VMware ESXi 版本和我们产品的兼容性,需要很频繁地在一些物理服务器(如戴尔、联想、惠普、浪潮、超微等)上安装 VMware ESXi OS。

之前一直都是登录 IPMI 管理页面,挂载远程的 ISO 文件手动安装。安装完成之后还需要配置 ESXi 管理网络的 IP 地址。整体的安装流程比较繁琐,而且物理服务器每次重启和开机都十分耗时,对经常要安装 ESXi 的 QE 小伙伴来讲十分痛苦。

为了后续测试起来爽快一点,不用再为安装 ESXi OS 而烦恼,于是就基于 Redfish 快速实现了一套自动化安装 ESXi OS 的工具 redfish-esxi-os-installer。通过它我们内部的戴尔、联想、HPE 服务器安装 ESXi OS 只需要填写一个配置文件并选择需要安装的 ESXi ISO,运行一下 Jenkins Job 等待十几分钟就能自动安装好。原本需要一个多小时的工作量,现在只需要运行一下 Jenkins Job 帮助我们自动安装好 ESXi OS 啦 😂,真是爽歪歪。

五一假期刚开始,正好有时间抽空整理一下最近学到的东西,和大家分享一下这套自动化安装 ESXi OS 工具。

需求分析

  • 支持服务器:联想/戴尔/HPE(超微和浪潮优先级不高,暂时不支持);
  • 一键自动化安装/重装 ESXi OS,最好能配置好 Jenkins Job;
  • 指定 ESXi OS 安装的物理盘:由于物理服务器有多块硬盘,ESXi OS 需要安装在指定的硬盘上。一般为 SATA DOM 盘。比如戴尔的 DELLBOSS,联想的 ThinkSystem M.2 。这类 DOM 盘的好处就在于不占用多余的 HBA 卡或 PCI 插槽,有点类似于家用台式机主板上的 M.2 硬盘位插槽;
  • 指定网卡并配置静态 IP 地址:由于我们的物理服务器上有多块网卡,且不同的网卡有不同的网络用途,因此需要指定某块物理网卡为 ESXi 管理网络所使用的网卡。
  • 为 ESXi 管理网路配置静态 IP、子网掩码、网关,便于部署好之后直接就能通过该 IP 访问 ESXi。而不是通过 DHCP 分配一个 IP,然后再登录 IPMI 管理页面手动查看 ESXi 的 IP;

技术调研

目前市面上主流的裸金属服务器自动化安装 OS 的工具有 PXE 和 IPMI/Redfish 两种。

PXE

虽然内部也有 PXE 服务可用,但重启服务器和设置服务器的引导项为 PXE 启动仍然需要手动登录 IPMI 管理页面进行操作,无法做到自动重启和自动重装,仍有一定的工作量。而且 PXE 安装 OS 无法解决为每台服务器配置各自的安装盘和管理网络网卡及静态 IP 地址的问题,遂放弃。

IPMI/Redfish

Redfish 的概念和原理什么的就懒得介绍了,下面就直接剽窃一下官方的文档吧 😅:

DMTFRedfish® 是一个标准 API,旨在为融合、混合 IT 和软件定义数据中心(SDDC)提供简单和安全管理。

Redfish 出现之前,现代数据中心环境中缺乏互操作管理标准。随着机构越来越青睐于大规模的解决方案,传统标准不足以成功管理大量简单的多节点服务器或混合基础设施。IPMI 是一种较早的带外管理标准,仅限于“最小公共集”命令集(例如,开机/关机/重启、温度值、文本控制台等),由于供应商扩展在所有平台上并不常见,导致了客户常用的功能集减少。许多用户开发了自己的紧密集成工具,但是也不得不依赖带内管理软件。

而对于企业级用户来说,设备都是上千台,其需要统一的管理界面,就要对接不同供应商的 API。当基本 IPMI 功能已经不太好满足大规模 Scale-out 环境时,如何以更便捷的方式调用服务器高级管理功能就是一个新的需求。

为了寻求一个基于广泛使用的工具来加快发展的现代接口,现如今,客户需要一个使用互联网和 web 服务环境中常见的协议、结构和安全模型定义的 API

Redfish 可扩展平台管理 APIThe Redfish Scalable Platforms Management API)是一种新的规范,其使用 RESTful 接口语义来访问定义在模型格式中的数据,用于执行带外系统管理 (out of band systems management)。其适用于大规模的服务器,从独立的服务器到机架式和刀片式的服务器环境,而且也同样适用于大规模的云环境。

Redfish 的第 1 版侧重于服务器,为 IPMI-over-LAN 提供了一个安全、多节点的替代品。随后的 Redfish 版本增加了对网络接口(例如 NICCNAFC HBA)、PCIe 交换、本地存储、NVDIMM、多功能适配器和可组合性以及固件更新服务、软件更新推送方法和安全特权映射的管理。此外,Redfish 主机接口规范允许在操作系统上运行应用程序和工具,包括在启动前(固件)阶段-与 Redfish 管理服务沟通。

在定义 Redfish 标准时,协议与数据模型可分开并允许独立地修改。以模式为基础的数据模型是可伸缩和可扩展的,并且随着行业的发展,它将越来越具有人类可读性定义。

通过 Redfish 我们可以对服务器进行挂载/卸载 ISO、设置 BIOS 启动项、开机/关机/重启等操作。只需要使用一些特定的 ansible 模块,将它们缝合起来就能将整个流程跑通。

内部的服务器戴尔、联想、HPE 的较多,这三家厂商对 Redfish 支持的也比较完善。于是这个 ESXi OS 自动化安装工具 redfish-esxi-os-installer 就基于 Redfish 并结合 Jenkins 实现了一套自动化安装 ESXi OS 的方案,下面就详细介绍一下这套方案的安装流程和技术实现细节。

安装流程

  1. 获取硬盘和网卡硬件设备信息
  2. 根据硬件设备信息填写配置文件
  3. 根据配置文件生成 ansible inventory 文件
  4. 根据配置文件为每台主机生成 kickstart 文件
  5. 将生成好的 kickstart 文件打包放到 ESXi ISO 当中
  6. 为每台主机重新构建一个 ESXi ISO 文件
  7. 通过 redfish 弹出已有的 ISO 镜像
  8. 通过 redfish 插入远程的 ISO 镜像
  9. 设置 one-boot 启动引导项为虚拟光驱
  10. 重启服务器到 ESXI ISO
  11. ESXi installer 调用 Kickstart 脚本安装 OS
  12. 等待 ESXi OS 安装完成

获取硬件信息

该步骤主要是获取 ESXi OS 所要安装的硬盘和管理网络网卡设备信息。

获取硬盘型号/序列号

要指定 ESXi OS 安装的硬盘,可以通过硬盘型号或序列号的方式。如果当前服务器已经安装了 ESXi,登录到 ESXi 则可以查看到所安装硬盘的型号:

  • 比如这台戴尔的服务器 ESXi OS 安装的硬盘型号是 DELLBOSS VD(注意中间的空格不要省略);

img

  • 比如这台联想服务器的 SATA DOM 盘型号为 ThinkSystem M.2

img

  • 如果安装的是 Linux,可以通过 smartctl 工具查看所要安装硬盘的型号即 Device Model,比如:
╭─root@esxi-debian-nas ~╰─# smartctl -x /dev/sdbsmartctl 6.6 2017-11-05 r4594 [x86_64-linux-4.19.0-18-amd64] (local build)Copyright (C) 2002-17, Bruce Allen, Christian Franke, www.smartmontools.org=== START OF INFORMATION SECTION ===Device Model:     HGST HUH721212ALE604Serial Number:    5PJAMUHDLU WWN Device Id: 5 000cca 291e10521

img

如果有多块型号相同的硬盘,ESXi 会默认选择第一块,如果要指定某一块硬盘则使用 WWN 号的方式,获取 WWN ID 的命令如下:

╭─root@esxi-debian-nas ~╰─# smartctl -x /dev/sdb | sed -n "s/LU WWN Device Id:/naa./p" | tr -d ' 'naa.5000cca291e10521

获取网卡设备名/MAC 地址

  • 如果当前物理服务器已经安装了 ESXi,则登录 ESXi 主机查看 ESXi 默认的管理网络 vSwitch0 虚拟交换机所连接的物理网卡设备名,比如这台服务器网卡设备名为 vmnic4

img

  • 另一种方式则是登录服务器的 IPMI 管理页面,查看对应网卡的 MAC 地址

img

填写配置文件

通过以上方式确定好 ESXi OS 所安装的硬盘型号或序列号,以及 ESXi 默认管理网络 vSwitch0 所关联的物理网卡设备名或 MAC 地址之后,我们就将这些配置参数填入到该配置文件当中。后面的工具会使用该配置为每台机器生成不同的 kickstart 文件,在 kickstart 文件中指定 ESXi OS 安装的硬盘,ESXi 管理网络所使用的网卡,以及设置静态 IP、子网掩码、网关、主机名等参数。

hosts:- ipmi:    vendor: lenovo                  # 服务器厂商名 [dell, lenovo, hpe]    address: 10.172.70.186          # IPMI IP 地址    username: username              # IPMI 用户名    password: password              # IPMI 密码  esxi:    esxi_disk: ThinkSystem M.2      # ESXi OS 所安装硬盘的型号或序列号    password: password              # ESXi 的 root 用户密码    address: 10.172.69.86           # ESXi 管理网络 IP 地址    gateway: 10.172.64.1            # ESXi 管理网络网关    netmask: 255.255.240.0          # ESXi 管理网络子网掩码    hostname: esxi-69-86            # ESXi 主机名(可选)    mgtnic: vmnic4                  # ESXi 管理网络网卡名称或MAC 地址- ipmi:    vendor: dell    address: 10.172.18.191    username: username    password: password  esxi:    esxi_disk: DELLBOSS VD    password: password    address: 10.172.18.95    gateway: 10.172.16.1    netmask: 255.255.240.0    mgtnic: B4:96:91:A7:3F:D6

生成 inventory 文件

tools.sh 脚本中通过 yq 命令行工具解析 config.yaml 配置文件,得到每台主机的配置信息,并根据该信息生成一个 ansible 的 inventory 文件

function rendder_host_info(){    local index=$1    vendor=$(yq -e eval ".hosts.[$index].ipmi.vendor" ${CONFIG})    os_disk="$(yq -e eval ".hosts.[$index].esxi.esxi_disk" ${CONFIG})"    esxi_mgtnic=$(yq -e eval ".hosts.[$index].esxi.mgtnic" ${CONFIG})    esxi_address=$(yq -e eval ".hosts.[$index].esxi.address" ${CONFIG})    esxi_gateway=$(yq -e eval ".hosts.[$index].esxi.gateway" ${CONFIG})    esxi_netmask=$(yq -e eval ".hosts.[$index].esxi.netmask" ${CONFIG})    esxi_password=$(yq -e eval ".hosts.[$index].esxi.password" ${CONFIG})    ipmi_address=$(yq -e eval ".hosts.[$index].ipmi.address" ${CONFIG})    ipmi_username=$(yq -e eval ".hosts.[$index].ipmi.username" ${CONFIG})    ipmi_password=$(yq -e eval ".hosts.[$index].ipmi.password" ${CONFIG})    esxi_hostname="$(yq -e eval ".hosts.[$index].esxi.hostname" ${CONFIG} 2> /dev/null || true)"}function gen_inventory(){    cat << EOF > ${INVENTORY}_hpe__dell__lenovo_[all:children]hpedelllenovoEOF    for i in $(seq 0 `expr ${nums} - 1`); do        rendder_host_info ${i}        host_info="${ipmi_address} username=${ipmi_username} password=${ipmi_password} esxi_address=${esxi_address} esxi_password=${esxi_password}"        sed -i "/_${vendor}_/a ${host_info}" ${INVENTORY}    done    sed -i "s#^_dell_#[dell]#g;s#^_lenovo_#[lenovo]#g;s#_hpe_#[hpe]#g" ${INVENTORY}    echo "gen inventory success"}

生成后的 inventory 文件内容如下,根据不同的厂商名称进行分组

[hpe]10.172.18.191 username=username password=password esxi_address=10.172.18.95 esxi_password=password[dell]10.172.18.192 username=username password=password esxi_address=10.172.18.96 esxi_password=password[lenovo]10.172.18.193 username=username password=password esxi_address=10.172.18.97 esxi_password=password[all:children]hpedelllenovo

检查 Redfish 登录是否正常

通过 Redfish 的 GetSystemInventory 命令获取服务器的 inventory 清单来检查登录 Redfish 是否正常,用户名或密码是否正确。

- name: Getting system inventory  community.general.redfish_info:    category: Systems    command: GetSystemInventory    baseuri: "{{ baseuri }}"    username: "{{ username }}"    password: "{{ password }}"

生成 kickstart 文件

tools.sh 同样使用 yq 命令行工具渲染配置文件,得到每台主机的配置信息,为每台主机生成一个特定的 kickstart 文件。

在 kickstart 文件中我们我们可以通过 install --overwritevmfs --firstdisk="${ESXI_DISK}" 配置 ESXi OS 安装在哪一块硬盘上;

通过 network --bootproto=static 为 ESXi 管理网络配置静态 IP、子网掩码、网关、主机名、物理网卡等参数。需要注意的是,如果使用 MAC 地址指定网卡,MAC 地址必须为大写,因此需要使用 tr 进行了一下大小写转换;

通过 clearpart --alldrives --overwritevmfs 可以清除所有硬盘上的分区,我们安装时一般是将它们全部清理掉,方便进行测试;

最后再开启 SSH 服务并开启 sshServer 的防火墙,方便后续测试使用;

function gen_iso_ks(){    local ISO_KS=$1    local ESXI_DISK=${os_disk}    local IP_ADDRESS=${esxi_address}    local NETMASK=${esxi_netmask}    local GATEWAY=${esxi_gateway}    local DNS_SERVER="${GATEWAY}"    local PASSWORD=${esxi_password}    local HOSTNAME="$(echo ${esxi_hostname} | sed "s/null/esxi-${esxi_address//./-}/")"    local MGTNIC=$(echo ${esxi_mgtnic} | tr '[a-z]' '[A-Z]' | sed 's/VMNIC/vmnic/g')    cat << EOF > ${ISO_KS}vmaccepteula# Set the root password for the DCUI and Tech Support Moderootpw ${PASSWORD}# Set the keyboardkeyboard 'US Default'# wipe exisiting VMFS store # CAREFUL!clearpart --alldrives --overwritevmfs# Install on the first local disk available on machineinstall --overwritevmfs --firstdisk="${ESXI_DISK}"# Set the network to DHCP on the first network adapternetwork --bootproto=static --hostname=${HOSTNAME} --ip=${IP_ADDRESS} --gateway=${GATEWAY} --nameserver=${DNS_SERVER} --netmask=${NETMASK} --device="${MGTNIC}"reboot%firstboot --interpreter=busybox# Enable SSHvim-cmd hostsvc/enable_sshvim-cmd hostsvc/start_sshesxcli network firewall ruleset set --enabled=false --ruleset-id=sshServerEOF}

重新构建 ESXi ISO

这一步的操作主要是修改 ESXi ISO 的启动项配置,配置 ks 文件的路径,主要是修改 ISO 文件里的 boot.cfgefi/boot/boot.cfg 文件。在启动参数中加入 ks=cdrom:/KS.CFG 用于指定 ESXi OS 安装通过读取 kickstart 脚本的方式来完成。

sed -i -e 's#cdromBoot#ks=cdrom:/KS.CFG systemMediaSize=small#g' boot.cfgsed -i -e 's#cdromBoot#ks=cdrom:/KS.CFG systemMediaSize=small#g' efi/boot/boot.cfg

另外在 VMware 的 KB Boot option to configure the size of ESXi system partitions (81166) 中,提到过可以设置 systemMediaSize=small 来调整 VMFS-L 分区的大小。ESXi 7.0 版本之后会默认创建一个 VMFS-L 分区,如果 SATA DOM 盘比较小的话比如只有 128G,建议设置此参数。不然可能会导致安装完 ESXi OS 之后磁盘剩余的空间都被 VMFS-L 分区给占用,导致没有一个本地的数据存储可以使用。

修改好 ESXi 的启动配置之后,我们再使用 genisoimage 命令重新构建一个 ESXi ISO 文件,将构建好的 ISO 文件放到一个 http 文件服务的目录下,如 nginx 的 /usr/share/nginx/html/iso。后面将会通过 http 的方式将 ISO 挂载到服务器的虚拟光驱上。

function rebuild_esxi_iso() {    local dest_iso_mount_dir=$1    local dest_iso_path=$2    pushd ${dest_iso_mount_dir} > /dev/null    sed -i -e 's#cdromBoot#ks=cdrom:/KS.CFG systemMediaSize=small#g' boot.cfg    sed -i -e 's#cdromBoot#ks=cdrom:/KS.CFG systemMediaSize=small#g' efi/boot/boot.cfg    genisoimage -J \                -R  \                -o ${dest_iso_path} \                -relaxed-filenames \                -b isolinux.bin \                -c boot.cat \                -no-emul-boot \                -boot-load-size 4 \                -boot-info-table \                -eltorito-alt-boot \                -eltorito-boot efiboot.img \                -quiet --no-emul-boot \                . > /dev/null  popd > /dev/null}

重新构建好 ESXi ISO 之后的 nginx 目录结构如下:

# tree /usr/share/nginx/html/iso//usr/share/nginx/html/iso/├── redfish│   ├── 172.20.18.191│   │   └── VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.iso # 重新构建的 ISO│   ├── 172.20.18.192│   │   └── VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.iso # 重新构建的 ISO│   ├── 172.20.18.193│   │   └── VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.iso # 重新构建的 ISO│   └── 172.20.70.186│       └── VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.iso # 重新构建的 ISO├── VMware-VMvisor-Installer-6.7.0.update03-14320388.x86_64.iso # 原 ISO├── VMware-VMvisor-Installer-7.0U2a-17867351.x86_64.iso         # 原 ISO└── VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.iso         # 原 ISO

通过 redfish 弹出已有的 Virtual Media

redfish 插入/弹出 ISO 操作有现成可用的 ansible 模块可以使用,不必重复造轮子。不同的服务器厂商调用的模块可能会有所不同,不过参数基本上是相同的。

如果当前服务器上已经挂载了一些其他的 ISO,要将他们全部弹出才行,不然在挂载 ISO 的时候会失败退出,并且也能避免多个 ISO 重启启动的时候引起冲突启动到另一个 ISO 中。

  • 联想服务器的 VirtualMediaEject 命令可以弹出所有的 ISO
- name: Lenovo | Eject all Virtual Media  community.general.xcc_redfish_command:    category: Manager    command: VirtualMediaEject    baseuri: "{{ baseuri }}"    username: "{{ username }}"    password: "{{ password }}"    resource_id: "1"  when:  - inventory_hostname in groups['lenovo']  tags:  - mount-iso  - umount-iso
  • 戴尔和 HPE 服务器在弹出 ISO 的时候需要先知道原有 ISO 的 URL。因此先通过 GetVirtualMedia 命令获取到一个 ISO 的 URL 列表,然后再根据这个列表一一弹出。
- name: Get virtual media details  community.general.redfish_info:    baseuri: "{{ baseuri }}"    username: "{{ username }}"    password: "{{ password }}"    category: "Manager"    command: "GetVirtualMedia"  register: result  tags:  - mount-iso  - umount-iso  when:  - inventory_hostname not in groups['lenovo']- name: Eject virtual media  community.general.redfish_command:    baseuri: "{{ baseuri }}"    username: "{{ username }}"    password: "{{ password }}"    category: "Manager"    command: "VirtualMediaEject"    virtual_media:      image_url: "{{ item }}"  with_items: "{{ result.redfish_facts.virtual_media.entries[0][1] | selectattr('ConnectedVia', 'equalto','URI') | map(attribute='Image') | list }}"  when:  - inventory_hostname not in groups['lenovo']  tags:  - mount-iso  - umount-iso

在弹出一个 ISO 的时候需要先知道 ISO 的 URL,感觉有点奇葩 😂。更合理的应该是需要一个挂载点的标识,就像比 Linux 上的挂载点。在 umount 挂载的设备时,只需要知道挂载点即可,不需要知道挂载的设备是什么。在 ISSUE VirtualMediaEject should not require image_url 中有大佬反馈过在弹出 ISO 的时候不应该需要 image url,不过被 maintainer 给否决了 😅。

Yes, at least with the behavior we’ve implemented today the image URL is needed since the expectation is the user is specifying the image URL for the ISO to eject. I think we need to consider some things first before making changes.

If the image URL is not given, then what exactly should be ejected? All virtual media your example indicates? This seems a bit heavy handed in my opinion, but others might like this behavior. Redfish itself doesn’t support an “eject all” type of operation, and I suspect the script you’re referencing is either using OEM actions or is just looping on all slots and ejecting everything.

Should a user be allowed specify an alternative identifier (such as the “Id” of the virtual media instance) in order to control what slot is ejected?

Certainly would like opinions from others for desired behavior. I do like the idea of keeping the mandatory argument list as minimal as possible, but would like to agree upon the desired behavior first.

通过 Redfish 插入 ISO

  • 联想服务器使用的是 community.general.xcc_redfish_command 模块,redfish 的 command 为 VirtualMediaInsert;
- name: Lenovo | Insert {{ image_url }} Virtual Media  community.general.xcc_redfish_command:    category: Manager    command: VirtualMediaInsert    baseuri: "{{ baseuri }}"    username: "{{ username }}"    password: "{{ password }}"    virtual_media:      image_url: "{{ image_url }}"      media_types:        - CD        - DVD    resource_id: "1"  when:  - inventory_hostname in groups['lenovo']  tags:  - mount-iso
  • 戴尔和 HPE 服务器挂载 ISO 使用的则是 community.general.redfish_command 模块,command 和联想的相同;
- name: Insert {{ image_url }} ISO as virtual media device community.general.redfish_command:   baseuri: "{{ baseuri }}"   username: "{{ username }}"   password: "{{ password }}"   category: "Manager"   command: "VirtualMediaInsert"   virtual_media:     image_url: "{{ image_url }}"     media_types:       - CD       - DVD when: - inventory_hostname not in groups['lenovo'] tags: - mount-iso

需要注意的是:如果使用 community.general.redfish_command 模块为联想的服务器挂载 ISO 会提示 4xx 错误,必须使用 community.general.xcc_redfish_command 模块才行。

设置启动项为虚拟光驱

此过程是将服务器的启动项设置为虚拟光驱,不同厂商的服务器调用的 ansible 模块可能也会有所不同。

  • 联想和 HPE 服务器
- name: Set one-time boot device to {{ bootdevice }}  community.general.redfish_command:    category: Systems    command: SetOneTimeBoot    bootdevice: "{{ bootdevice }}"    baseuri: "{{ baseuri }}"    username: "{{ username }}"    password: "{{ password }}"    timeout: 20  when:  - inventory_hostname not in groups['dell']
  • 戴尔服务器
- name:  Dell | set iDRAC attribute for one-time boot from virtual CD  community.general.idrac_redfish_config:    baseuri: "{{ baseuri }}"    username: "{{ username }}"    password: "{{ password }}"    category: "Manager"    command: "SetManagerAttributes"    manager_attributes:      ServerBoot.1.BootOnce: "Enabled"      ServerBoot.1.FirstBootDevice: "VCD-DVD"  when:  - inventory_hostname in groups['dell']

重启服务器

重启服务器直接调用 community.general.redfish_command 模块就可以。不过需要注意的是,重启服务器之前要保证服务器当前状态为开启状态,因此调用一下 redfish 的 PowerOn 命令对服务器进行开机,如果已处于开机状态则无影响,然后再调用 PowerForceRestart 命令重启服务器。

- hosts: all  name: Power Force Restart the host  gather_facts: false  tasks:  - name: Turn system power on    community.general.redfish_command:      category: Systems      command: PowerOn      baseuri: "{{ baseuri }}"      username: "{{ username }}"      password: "{{ password }}"  - name: Reboot system    community.general.redfish_command:      category: Systems      command: PowerForceRestart      baseuri: "{{ baseuri }}"      username: "{{ username }}"      password: "{{ password }}"      timeout: 20  tags:  - reboot

这里还有优化的空间,就是根据电源的状态决定是重启还是开机,不过有点麻烦懒得弄了 😅

等待 ESXi OS 安装完成

服务器重启之后,我们通过 govc 命令不断尝试连接 ESXi 主机,如果能够正常连接则说明 ESXi OS 已经安装完成了。一般情况下等待 15 分钟左右就能安装完成,期间需要重启服务器两次,每次重启大概需要 5 分钟左右,实际上 ESXi 进入安装页面到安装完成只需要 5 分钟左右,服务器开机自检占用的时间会稍微长一点。

image-20220428210819057

- hosts: all  name: Wait for the ESXi OS installation to complete  gather_facts: false  vars:    esxi_username: "root"    govc_url: "https://{{ esxi_username }}:{{ esxi_password }}@{{ esxi_address }}"  tasks:  - name: "Wait for {{ inventory_hostname }} install ESXi {{ esxi_address }} host to be complete"    shell: "govc about -k=true -u={{ govc_url}}"    retries: 60    delay: 30    register: result    until: result.rc == 0  tags:  - post-check

Makefile 封装

为了方便操作,将上述流程使用 Makefile 进行封装一下,如果不配置 Jenkins Job 的话,可以在本地填写好 config.yaml 配置文件,然后运行 make 命令来进行相关操作。

vars

SRC_ISO_DIR     ?= /usr/share/nginx/html/isoHTTP_DIR        ?= /usr/share/nginx/html/iso/redfishHTTP_URL        ?= http://172.20.17.20/iso/redfishESXI_ISO        ?= VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.isoSRC_ISO_DIR   # 原 ESXi ISO 的存放目录ESXI_ISO      # ESXi ISO 的文件名,如 VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.isoHTTP_DIR      # HTTP 服务器的静态文件存放目录,比如 /usr/share/nginx/html 或 /var/www/html              # 重新构建好的 ISO 文件将存放到这个目录当中HTTP_URL      # HTTP 服务器的 URL 地址,比如 http://172.20.29.171/iso/redfish

target

make docke-run  # 在 docker 容器里运行所有操作,好处就是不用再安装一堆 ansible 等工具的依赖make inventory  # 根据 config.yaml 配置文件生成 ansible 的 inventory 文件make pre-check  # 检查生成的 inventory 文件是否正确,连接 redfish 是否正常make build-iso  # 为每台主机生成 kickstart 文件并重新构建 ESXi OS ISO 文件make mount-iso  # 将构建好的 ISO 文件通过 redfish 挂载到物理服务器的虚拟光驱,并设备启动项make reboot     # 重启服务器,进入到虚拟光驱启动 ESXi inatllermake post-check # 等待 ESXi OS 安装完成make install-os # 运行 pre-check, mount-iso, reboot, post-check

Jenkins Job

虽然在 Makefile 里封装了比较方便的命令操作,但是对于不太熟悉这套流程的使用人员来讲还是不够便捷。对于使用人员来讲不需要知道具体的流程是什么,因此还需要提供一个更为便捷的入口来使用这套工具,对外屏蔽掉技术实现的细节。

在我们内部,老牌 CI 工具 Jenkins 大叔十分受欢迎,使用的十分普遍。之前同事也常调侃:我们内部的 Jenkins 虽然达不到人手一个的数量,但每个团队有两三个自己的 Jenkins 再正常不过了🤣。因此提供了一个 Jenkins Job 来运行这套安装工具再完美不过了。这样使用人员就不用再 clone repo 代码,傻乎乎地运行一些 make 命令了,毕竟一个 Jenkins build 的按钮比 make 命令好好用得太多。

我们组的 Jenkins 比较特殊,是使用 kubernetes Pod 作为动态 Jenkins slave 节点,即每运行一个 Jenkins Job 就会根据定义的 Pod 模版创建一个 Pod 到指定的 Kubernetes 集群中,然后 Jenkinsfile 中定义的 stage 都会运行在这个 Pod 容器内。这些内容可以参考一下我之前写的 Jenkins 大叔与 kubernetes 船长手牵手 🧑‍🤝‍🧑

Jenkinsfile

如果你熟悉 Jenkins 的话,可以创建一个 Jenkins Job ,并在 Job 中设置好如下几个参数,并将这个 Jenkinsfile 中的内容复制到 Jenkins Job 的配置中。

参数名参数类型说明
esxi_isoArrayListESXi ISO 文件名列表
http_serverStringHTTP 服务器的 IP 地址
http_dirStringHTTP 服务器的文件目录路径
config_yamlTextconfig.yaml 配置文件内容
// params of jenkins jobdef ESXI_ISO = params.esxi_isodef CONFIG_YAML = params.config_yamldef HTTP_SERVER = params.http_server// default params for the jobdef HTTP_DIR  = params.http_dir ?: "/usr/share/nginx/html"def SRC_ISO_DIR = params.src_iso_dir ?: "${HTTP_DIR}/iso"def DEST_ISO_DIR = params.dest_iso_dir ?: "${HTTP_DIR}/iso/redfish"def WORKSPACE = env.WORKSPACEdef JOB_NAME = "${env.JOB_BASE_NAME}"def BUILD_NUMBER = "${env.BUILD_NUMBER}"def POD_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"def POD_IMAGE = params.pod_image ?: "ghcr.io/muzi502/redfish-esxi-os-installer:v0.1.0-alpha.1"// Kubernetes pod template to run.podTemplate(    cloud: "kubernetes",    namespace: "default",    name: POD_NAME,    label: POD_NAME,    yaml: """apiVersion: v1kind: Podspec:  containers:  - name: runner    image: ${POD_IMAGE}    imagePullPolicy: Always    tty: true    volumeMounts:    - name: http-dir      mountPath: ${HTTP_DIR}    securityContext:      privileged: true    env:    - name: ESXI_ISO      value: ${ESXI_ISO}    - name: SRC_ISO_DIR      value: ${SRC_ISO_DIR}    - name: HTTP_DIR      value: ${DEST_ISO_DIR}    - name: HTTP_URL      value: http://${HTTP_SERVER}/iso/redfish  - name: jnlp    args: ["\$(JENKINS_SECRET)", "\$(JENKINS_NAME)"]    image: "jenkins/inbound-agent:4.11.2-4-alpine"    imagePullPolicy: IfNotPresent  volumes:  - name: http-dir    nfs:      server: ${HTTP_SERVER}      path: ${HTTP_DIR}""",) {    node(POD_NAME) {        try {            container("runner") {                writeFile file: 'config.yaml', text: "${CONFIG_YAML}"                stage("Inventory") {                    sh """                    cp -rf /ansible/* .                    make inventory                    """                }                stage("Precheck") {                    sh """                    make pre-check                    """                }                if (params.build_iso) {                    stage("Build-iso") {                        sh """                        make build-iso                        """                    }                }                stage("Mount-iso") {                    sh """                    make mount-iso                    """                }                stage("Reboot") {                    sh """                    make reboot                    sleep 60                    """                }                stage("Postcheck") {                    sh """                    make post-check                    """                }            }            stage("Success"){                MESSAGE = "【Succeed】Jenkins Job ${JOB_NAME}-${BUILD_NUMBER} Link: ${BUILD_URL}"                // slackSend(channel: '${SLACK_CHANNE}', color: 'good', message: "${MESSAGE}")            }        } catch (Exception e) {            MESSAGE = "【Failed】Jenkins Job ${JOB_NAME}-${BUILD_NUMBER} Link: ${BUILD_URL}"            // slackSend(channel: '${SLACK_CHANNE}', color: 'warning', message: "${MESSAGE}")            throw e        }    }}

或者参考 Export/import jobs in Jenkins 将这个 Job 的配置导入到 Jenkins 当中,并设置好上面提到的几个参数。

image-20220429201859704

常见问题

硬件信息收集

整体上该方案有一点不足的就是需要人为地确认 ESXi OS 安装硬盘的型号/序列号,以及 ESXi 管理网络所使用的物理网卡。其实是可以通过 redfish 的 API 来统一地获取,然后再根据这些硬件设备信息进行选择,这样就不用登录到每一台物理服务器上进行查看了。

但考虑到实现成本,工作量会翻倍,而且我们的服务器都是固定的,只要人为确认一次就可以,下一次重装 ESXi OS 的时候只需要复制粘贴上一次的硬件配置即可,所以目前并没有打算做获取硬件信息的功能。

而且即便是将硬件信息获取出来,如果没有一个可视化的 Web UI 展示这些设备信息,也很难从一堆硬件数据中找出特定的设备,对这些数据进行 UI 展示工作量也会翻倍,因此暂时不再考虑这个功能了。

挂载 ISO 之前先确保 ISO 存在

有些服务器比如 HPE 在挂载一个不存在的 ISO 时并不会报错,当时我排查了好久才发现 😂,我一直以为是启动项设置的问题。因此在挂载 ISO 之前我们可以通过 curl 的方式检查一下 ISO 的 URL 是否正确,如果 404 不存在的话就报错退出。

- hosts: all  name: Mount  {{ image_url }} ISO  gather_facts: false  tasks:  - name: Check {{ image_url }} ISO file exists    shell: "curl -sI {{ image_url }}"    register: response    failed_when: "'200 OK' not in response.stdout or '404 Not Found' in response.stdout"    tags:    - mount-iso

单独构建 Kickstart ISO

目前的方案是为将 ESXi 的 kickstart 文件 KS.CFG 放到了 ESXi OS ISO 镜像里,由于每台主机的 kickstart 文件都不相同,这就需要为每台服务器构建一个 ISO 文件,如果机器数量比较多的话,可能会占用大量的磁盘存储空间,效率上会有些问题。也尝试过将 kickstart 文件单独放到一个 ISO 中,大体的思路如下:

  • 构建 kickstart ISO 文件,-V 参数指定 ISO 的 label 名称为 KS
$ genisoimage -o /tmp/ks.iso -V KS ks.cfg
  • 修改 ESXi 启动配置,将 ks 文件路径通过 label 的方式指向刚才构建的 ISO
$ sed -i -e 's#cdromBoot#ks=hd:KS:/ks.cfg systemMediaSize=small#g' boot.cfg$ sed -i -e 's#cdromBoot#ks=hd:KS:/ks.cfg systemMediaSize=small#g' efi/boot/boot.cfg
  • 修改一下 playbook,插入两个 ISO
- name: Insert {{ item }} ISO as virtual media device  community.general.redfish_command:    baseuri: "{{ baseuri }}"    username: "{{ username }}"    password: "{{ password }}"    category: "Manager"    command: "VirtualMediaInsert"    virtual_media:      image_url: "{{ item }}"      media_types:        - CD        - DVD  with_items:  - "{{ esxi_iso_url }}"  - "{{ ks_iso_url }}"  when:  - inventory_hostname not in groups['lenovo']  tags:  - mount-iso

等这些都修改好之后我满怀期待地运行了 make mount-iso 命令等到奇迹的发生,没想到直接翻车了!不支持挂载两个 ISO,白白高兴一场,真气人 😡

TASK [Insert {{ item }} ISO as virtual media device] ******************************************************************************************changed: [10.172.18.191] => (item=http://10.172.29.171/iso/redfish/VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.iso)changed: [10.172.18.192] => (item=http://10.172.29.171/iso/redfish/VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.iso)changed: [10.172.18.193] => (item=http://10.172.29.171/iso/redfish/VMware-VMvisor-Installer-7.0U3d-19482537.x86_64.iso)failed: [10.172.18.193] (item=http://10.172.29.171/iso/redfish/10.172.18.193/ks.iso) => {"ansible_loop_var": "item", "changed": false, "item": "http://10.172.29.171/iso/redfish/10.172.18.193/ks.iso", "msg": "Unable to find an available VirtualMedia resource supporting ['CD', 'DVD']"}failed: [10.172.18.192] (item=http://10.172.29.171/iso/redfish/10.172.18.192/ks.iso) => {"ansible_loop_var": "item", "changed": false, "item": "http://10.172.29.171/iso/redfish/10.172.18.192/ks.iso", "msg": "Unable to find an available VirtualMedia resource supporting ['CD', 'DVD']"}failed: [10.172.18.191] (item=http://10.172.29.171/iso/redfish/10.172.18.191/ks.iso) => {"ansible_loop_var": "item", "changed": false, "item": "http://10.172.29.171/iso/redfish/10.172.18.191/ks.iso", "msg": "Unable to find an available VirtualMedia resource supporting ['CD', 'DVD']"}

或许将 ISO 替换成软盘 floppy 的方式可能行得通,不过当我看了 create-a-virtual-floppy-image-without-mount 后直接把我整不会了,没想创建一个软盘文件到这么麻烦,还是直接放弃该方案吧 🌚。

多说一句,之所以想到使用软盘的方式是因为之前在玩 Packer 的时候,研究过它就是将 kickstart 文件制作成一个软盘,插入到虚拟机中。虚拟机开机后通过 vCenter API 发送键盘输入,插入 kickstart 的路径,anaconda 执行自动化安装 OS。

==> vsphere-iso-base: Creating VM...==> vsphere-iso-base: Customizing hardware...==> vsphere-iso-base: Mounting ISO images...==> vsphere-iso-base: Adding configuration parameters...==> vsphere-iso-base: Creating floppy disk...    vsphere-iso-base: Copying files flatly from floppy_files    vsphere-iso-base: Done copying files from floppy_files    vsphere-iso-base: Collecting paths from floppy_dirs    vsphere-iso-base: Resulting paths from floppy_dirs : [./kickstart/centos/http/]    vsphere-iso-base: Recursively copying : ./kickstart/centos/http/    vsphere-iso-base: Done copying paths from floppy_dirs    vsphere-iso-base: Copying files from floppy_content    vsphere-iso-base: Done copying files from floppy_content==> vsphere-iso-base: Uploading created floppy image==> vsphere-iso-base: Adding generated Floppy...==> vsphere-iso-base: Set boot order temporary...==> vsphere-iso-base: Power on VM...==> vsphere-iso-base: Waiting 15s for boot...==> vsphere-iso-base: Typing boot command...==> vsphere-iso-base: Waiting for IP...root@devbox-fedora:/root # scp 192.168.24.43:/vmfs/volumes/Packer/base-os-centos7/packer-tmp-created-floppy.flp .packer-tmp-created-floppy.flp                                                                                100% 1440KB  89.4MB/s   00:00root@devbox-fedora:/root # mount packer-tmp-created-floppy.flp /mntroot@devbox-fedora:/root # readlink /dev/disk/by-label/packer../../loop2root@devbox-fedora:/root # df -h /mntFilesystem      Size  Used Avail Use% Mounted on/dev/loop2      1.4M   16K  1.4M   2% /mntroot@devbox-fedora:/root #root@devbox-fedora:/root # ls /mntHTTProot@devbox-fedora:/root # ls /mnt/HTTP7root@devbox-fedora:/root # ls /mnt/HTTP/7KS.CFG

通过 http 方式读取 kickstart

不一定可行,在通过 http 方式读取 kickstart 文件之前,ESXi OS installer 需要有一个 IP 地址才行。如果服务器如果有多块网卡的话,就很难确定是否分配到一个 IP,使用默认 DHCP 的方式并不一定能获取到正确的 IP 地址。因此读取 kickstart 文件的方式还是建议使用 ISO 的方式,这样在安装 OS 时对网络环境无依赖,更稳定一些。

支持其他 OS 的安装

目前该方案只支持 ESXi OS 的安装,其他 OS 的自动化安装其实原理是一样的。比如 CentOS 同样也是修改 kickstart 文件。如果要指定 OS 所安装的磁盘可以参考一下戴尔官方的一篇文档 Automating Operating System Deployment to Dell BOSS – Techniques for Different Operating Systems

%include /tmp/bootdisk.cfg%pre# Use DELLBOSS device for OS install if present.BOSS_DEV=$(find /dev -name "*DELLBOSS*" -printf %P"\n" | egrep -v -e part -e scsi| head -1)if [ -n "$BOSS_DEV" ]; then    echo ignoredisk --only-use="$BOSS_DEV" > /tmp/bootdisk.cfgfi%end

如果要为某块物理网卡配置 IP 地址,可以根据 MAC 地址找到对应的物理网卡,然后将静态 IP 配置写入到网卡配置文件当中。比如 CentOS 在 kickstart 中为某块物理网卡配置静态 IP,可以采用如下方式:

MAC_ADDRESS 在生成 kickstart 文件的时候根据 config.yaml 动态修改的# MAC_ADDRESS=B4:96:91:A7:3F:D6# 根据 MAC 地址获取到网卡设备的名称NIC=$(grep -l ${MAC_ADDRESS} /sys/class/net/*/address | awk -F'/' '{print $5}')# 将网卡静态 IP 配置写入到文件当中cat << EOF > /etc/sysconfig/network-scripts/ifcfg-${NIC}TYPE=EhternetBOOTPROTO=staticDEFROUTE=yesNAME=${NIC}DEVICE=${NIC}ONBOOT=yesIPADDR=${IP}NETMASK=${NETMASK}GATEWAY=${GATEWAY}EOF

由于时间关系,在这里就不再进行深入讲解了,在这里只是提供一个方法和思路。至于 Debian/Ubuntu 发行版,还是你们自己摸索吧,因为我工作中确实没有在物理服务器上安装这些发行版的场景,毕竟国内企业私有云环境中使用 CentOS/RedHat 系列发行版的占绝大多数。

参考

Redfish 相关

VMware ESXi 相关

使用 overlay2 或 bind 重新构建 ISO 镜像

作者 Reimu
2022年1月25日 00:00

笔者之前在字节跳动的时候是负责 PaaS 容器云平台的私有化部署相关的工作,所以经常会和一些容器镜像打交道,对容器镜像也有一些研究,之前还写过不少博客文章。比如 深入浅出容器镜像的一生 🤔overlay2 在打包发布流水线中的应用 等等。

自从换了新工作之后,则开始负责 超融合产品 集群部署相关工作,因此也会接触很多 镜像,不过这个镜像是操作系统的 ISO 镜像而不是容器镜像 😂。虽然两者都统称为镜像,但两者有着本质的区别。

首先两者构建的方式有本质的很大的区别,ISO 镜像一般使用 mkisofs 或者 genisoimage 等命令将一个包含操作系统安装所有文件目录构建为一个 ISO 镜像;而容器镜像构建则是根据 Dockerfile 文件使用相应的容器镜像构建工具来一层一层构建;

另外 ISO 镜像挂载后是只读的,这就意味着如果想要修改 ISO 镜像中的一个文件(比如 kickstart 文件),则需要先将 ISO 镜像中的所有内容负责到一个可以读写的目录中,在这个读写的目录中进行修改和重新构建 ISO 操作。

╭─root@esxi-debian-devbox ~/build╰─# mount -o loop CentOS-7-x86_64-Minimal-2009.iso /mnt/isomount: /mnt/iso: WARNING: device write-protected, mounted read-only.╭─root@esxi-debian-devbox ~/build╰─# touch /mnt/iso/kickstart.cfgtouch: cannot touch '/mnt/iso/kickstart.cfg': Read-only file system

在日常工作中经常会对一些已有的 ISO 镜像进行重新构建,重新构建 ISO 的效率根据不同的方式也会有所不同,本文就整理了三种不同重新构建 ISO 镜像的方案供大家参考。

常规方式

以下是按照 RedHat 官方文档 WORKING WITH ISO IMAGES 中的操作步骤进行 ISO 重新构建。

╭─root@esxi-debian-devbox ~/build╰─# mount -o loop CentOS-7-x86_64-Minimal-2009.iso /mnt/isomount: /mnt/iso: WARNING: device write-protected, mounted read-only.
  • 将 ISO 里的所有文件复制到另一个目录
╭─root@esxi-debian-devbox ~/build╰─# rsync -avrut --force /mnt/iso/ /mnt/build/
  • 进入到该目录下修改或新增文件,然后重新构建 ISO 镜像
# 使用 genisoimage 命令构建 ISO 镜像,在 CentOS 上可以使用 mkisofs 命令,参数上会有一些差异╭─root@esxi-debian-devbox ~/build╰─# genisoimage -U -r -v -T -J -joliet-long -V "CentOS 7 x86_64" -volset "CentOS 7 x86_64" -A "CentOS 7 x86_64" -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -no-emul-boot -o /mnt/CentOS-7-x86_64-Minimal-2009-dev.iso .Total translation table size: 124658Total rockridge attributes bytes: 55187Total directory bytes: 100352Path table size(bytes): 140Done with: The File(s)                             Block(s)    527985Writing:   Ending Padblock                         Start Block 528101Done with: Ending Padblock                         Block(s)    150Max brk space used a4000528251 extents written (1031 MB)# 给 ISO 镜像生成 md5 校验╭─root@esxi-debian-devbox ~/build╰─# implantisomd5 /mnt/CentOS-7-x86_64-Minimal-2009-dev.isoInserting md5sum into iso image...md5 = 9ddf5277bcb1d8679c367dfa93f9b162Inserting fragment md5sums into iso image...fragmd5 = f39e2822ec1ae832a69ae399ea4bd3e891eeb31e9deb9c536f529c15bbebfrags = 20Setting supported flag to 0

对于 ISO 镜像比较小或者该操作不是很频繁的情况下按照这种方式是最省事儿的,但如果是 ISO 镜像比较大,或者是在 CI/CD 流水线中频繁地重新构建镜像,每次都要 cp 复制原 ISO 镜像的内容确实比较浪费时间。那有没有一个更加高效的方法呢 🤔️

经过一番摸索,折腾出来两种可以避免使用 cp 复制这种占用大量 IO 操作的构建方案,可以根据不同的场景进行选择。

overlay2

熟悉 docker 镜像的应该都知道镜像是只读的,使用镜像的时候则是通过联合挂载的方式将镜像的每一层 layer 挂载为只读层,将容器实际运行的目录挂载为读写层,而容器运行期间在读写层的所有操作不会影响到镜像原有的内容。容器镜像挂载的方式使用最多的是 overlay2 技术,在 overlay2 在打包发布流水线中的应用深入浅出容器镜像的一生 🤔 中咱曾对它进行过比较深入的研究和使用,对 overlay2 技术感兴趣的可以翻看一下这两篇博客,本文就不再详解其中的技术原理了,只对使用 overlay2 技术重新构建 ISO 镜像的可行性进行一下分析。

  • 首先是创建 overlay2 挂载所需要的几个目录
╭─root@esxi-debian-devbox ~╰─# mkdir -p /mnt/overlay2/{lower,upper,work,merged}╭─root@esxi-debian-devbox ~╰─# cd /mnt/overlay2
  • 接着将 ISO 镜像挂载到 overlay2 的只读层 lower 目录
╭─root@esxi-debian-devbox /mnt/overlay2╰─# mount -o loop  /root/build/CentOS-7-x86_64-Minimal-2009.iso lowermount: /mnt/overlay2/lower: WARNING: device write-protected, mounted read-only.
  • 使用 mount 命令挂载 overlay2 文件系统,挂载点为 merged 目录
╭─root@esxi-debian-devbox /mnt/overlay2╰─# mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged╭─root@esxi-debian-devbox /mnt/overlay2╰─# cd merged
  • 新增一个 kickstart.cfg 文件,然后重新构建 ISO 镜像
╭─root@esxi-debian-devbox /mnt/overlay2/merged╰─# echo '# this is a kickstart config file' > kickstart.cfg╭─root@esxi-debian-devbox /mnt/overlay2/merged╰─# genisoimage -U -r -v -T -J -joliet-long -V "CentOS 7 x86_64" -volset "CentOS 7 x86_64" -A "CentOS 7 x86_64" -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -no-emul-boot -o /mnt/CentOS-7-x86_64-Minimal-2009-dev.iso .Total translation table size: 124658Total rockridge attributes bytes: 55187Total directory bytes: 100352Path table size(bytes): 140Done with: The File(s)                             Block(s)    527985Writing:   Ending Padblock                         Start Block 528101Done with: Ending Padblock                         Block(s)    150Max brk space used a4000528251 extents written (1031 MB)
  • 挂载新的 ISO 镜像验证后发现确实可行
╭─root@esxi-debian-devbox /mnt/overlay2/merged╰─# mount -o loop /mnt/CentOS-7-x86_64-Minimal-2009-dev.iso /mnt/newisomount: /mnt/newiso: WARNING: device write-protected, mounted read-only.╭─root@esxi-debian-devbox /mnt/overlay2/merged╰─# cat /mnt/newiso/kickstart.cfg# this is a kickstart config file

mount –bind

前面讲到了使用 overlay2 的方式避免复制原镜像内容进行重新构建镜像的方案,但是 overlay2 对于不是很熟悉的人来讲还是比较复杂,光 lowerdir、upperdir、workdir、mergeddir 这四个文件夹的作用和原理就把人直接给整不会了。那么还有没有更为简单一点的方式呢?

别说还真有,只不过这种方式的用途比较局限。如果仅仅是用于修改 ISO 中的一个文件或者目录,可以将该文件或目录以 bind 挂载的方式将它挂载到 ISO 目录目录对应的文件上。

原理就是虽然 ISO 目录本身是只读的,但它里面的文件和目录是可以作为一个挂载点的。也就是说我把文件 A 挂载到文件 B,并不是在修改文件 B,这就是 Unix/Linux 文件系统十分奇妙的地方。同样运用 bind 挂载的还有 docker 的 volume 以及 pod 的 volume 也是运用同样的原理,以 bind 的方式将宿主机上的目录或文件挂载到容器运行对应的目录上。对于修改只读 ISO 里的文件/目录我们当然也可以这样做。废话不多说来实践验证一下:

  • 首先依旧是将 ISO 镜像挂载到 /mn/iso 目录
╭─root@esxi-debian-devbox ~/build╰─# mount -o loop CentOS-7-x86_64-Minimal-2009.iso /mnt/isomount: /mnt/iso: WARNING: device write-protected, mounted read-only.
  • 接着创建一个 /mnt/files/ks.cfg 文件,并写入我们需要的内容
╭─root@esxi-debian-devbox ~/build╰─# mkdir -p /mnt/files╭─root@esxi-debian-devbox ~/build╰─# echo '# this is a kickstart config file' > /mnt/files/ks.cfg
  • 接着以 mount –bind 的方式挂载新建的文件到 ISO 的 EULA 文件
╭─root@esxi-debian-devbox /mnt/build╰─# mount --bind /mnt/files/ks.cfg /mnt/iso/EULA╭─root@esxi-debian-devbox /mnt/build╰─# cat /mnt/iso/EULA# this is a kickstart config file
  • 可以看到原来 ISO 文件中的 EULA 文件已经被成功替换成了我们修改的文件,然后再重新构建一下该 ISO 镜像
╭─root@esxi-debian-devbox /mnt/iso╰─# genisoimage -U -r -v -T -J -joliet-long -V "CentOS 7 x86_64" -volset "CentOS 7 x86_64" -A "CentOS 7 x86_64" -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -no-emul-boot -o /mnt/CentOS-7-x86_64-Minimal-2009-dev.iso .
  • 然后我们再重新挂载新的 ISO 文件验证一下是否可以
╭─root@esxi-debian-devbox /mnt/iso╰─# mkdir /mnt/newiso╭─root@esxi-debian-devbox /mnt/iso╰─# mount -o loop /mnt/CentOS-7-x86_64-Minimal-2009-dev.iso /mnt/newisomount: /mnt/newiso: WARNING: device write-protected, mounted read-only.╭─root@esxi-debian-devbox /mnt/iso╰─# cat /mnt/newiso/EULA# this is a kickstart config file

验证通过,确实可以!不过这种方式很局限,比较适用于修改单个文件如 kickstart.cfg,如果是要新增文件那还是使用上文提到的 overlay2 的方式更为方便一些。

收获

虽然 ISO 镜像和容器镜像二者有着本质的差别,但对于只读和联合挂载的这些特性二者可以相互借鉴滴。

不止如此 overlay2 这种联合挂载的特性,还可以用在其他地方。比如我有一个公共的 NFS 共享服务器,共享着一些目录,所有人都可以以 root 用户并以读写的权限进行 NFS 挂载。这种情况下很难保障一些重要的文件和数据被误删。这时候就可以使用 overlay2 的方式将一些重要的文件数据挂载为 overlay2 的 lowerdir 只读层,保证这些数据就如容器镜像一样,每次挂载使用的时候都作为一个只读层。所有的读写操作都在 overlay2 的 merged 那一层,不会真正影响到只读层的内容。

草草地水了一篇博客,是不是没有用的知识又增加了 😂

推荐阅读

❌
❌