阅读视图

发现新文章,点击刷新页面。
🔲 ☆

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

去年曾写过一篇介绍如何使用 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 只用于构建镜像而已。

参考

☑️ ☆

使用 Packer 构建虚拟机镜像踩的坑

不久前写过一篇博客《使用 Redfish 自动化安装 ESXi OS》分享了如何使用 Redfish 给物理服务器自动化安装 ESXi OS。虽然在我们内部做到了一键安装/重装 ESXi OS,但想要将这套方案应用在客户的私有云机房环境中还是有很大的难度。

首先这套工具必须运行在 Linux 中才行,对于 Bare Metal 裸服务器来讲还没有安装任何 OS,这就引申出了鸡生蛋蛋生鸡的尴尬难题。虽然可以给其中的一台物理服务器安装上一个 Linux 发行版比如 CentOS,然后再将这套自动化安装 ESXi OS 的工具搭建上去,但这会额外占用一台物理服务器,客户也肯定不愿意接受。

真实的实施场景中,可行的方案就是将这套工具运行在实施人员的笔记本电脑或者客户提供的台式机上。这又引申出了一个另外的难题:实施人员的笔记本电脑或者客户提供的台式机运行的大都是 Windows 系统,在 Windows 上安装 Ansible、Make、Python3 等一堆依赖,想想就不太现实,而且稳定性和兼容性很难得到保障,以及开发环境和运行环境不一致导致一些其他的奇奇怪怪的问题。虽然该工具支持容器化运行能够解决开发环境和运行环境不一致的问题,但在 Windows 上安装 docker 也比较繁琐和麻烦。

这时候就要搬出计算机科学中的至理名言: 计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决

Any problem in computer science can be solved by another layer of indirection.

既然我们这套工具目前只能在 Linux 上稳定运行,那么我们不如就将这套工具和它所运行的环境封装在一个“中间容器”里,比如虚拟机。使用者只需要安装像 VMware Workstation 或者 Oracle VirtualBox 虚拟化管理软件运行这台虚拟机不就行了。一切皆可套娃(🤣

其实原理就像 docker 容器那样,我们将这套工具和它所依赖的运行环境在构建虚拟机的时候将它们全部打包在一起,使用者只需要想办法将这个虚拟机运行起来,就能一键使用我们这个工具,不必再手动安装 Ansible 和 Python3 等一堆依赖了,真正做到开箱即用。

于是本文分享一下如何使用 PackerVMware vSphere 环境上构建虚拟机镜像的方案,以及如何在这个虚拟机中运行一个 k3s 集群,然后通过 argo-workflow 工作流引擎运行 redfish-esxi-os-installer 来对裸金属服务器进行自动化安装 ESXi OS 的操作。

劝退三连 😂

Packer

很早之前玩儿 VMware ESXi 的时候还没有接触到 Packer,那时候只能使用手搓虚拟机模版的方式,费时费力还容易出错,下面就介绍一下这个自动化构建虚拟机镜像的工具。

简介

Packerhashicorp 公司开源的一个虚拟机镜像构建工具,与它类似的工具还有 OpenStack diskimage-builderAWS EC2 Image Builder ,但是这两个只支持自家的平台。Packer 能够支持主流的公有云、私有云以及混合云,比它俩高到不知道哪里去了。可以这么来理解:Packer 在 IaaS 虚拟化领域的地位就像 Docker 在 PaaS 容器虚拟中那样重要,一个是虚拟机镜像的构建,另一个容器镜像的构建,有趣的是两者都是在 2013 年成立的项目。

Kubernetes 社区的 image-builder 项目就是使用 Packer 构建一些公有云及私有云的虚拟机模版提供给 cluster-api 项目使用,十分推荐大家去看下这个项目的代码,刚开始我也是从这个项目熟悉 Packer 的,并从中抄袭借鉴了很多内容 😅。

下面就介绍一下 Packer 的基本使用方法

安装

对于 Linux 发行版,建议直接下载二进制安装包来安装,通过包管理器安装感觉有点麻烦

$ wget https://releases.hashicorp.com/packer/1.8.0/packer_1.8.0_linux_amd64.zip$ unzip packer_1.8.0_linux_amd64.zip$ mv packer /usr/local/bin/packer

如果是 macOS 用户直接 brew install packer 命令一把梭就能安装好

配置

不同于 Docker 有一个 Dockerfile 文件来定义如何构建容器镜像,Packer 构建虚拟机镜像则是由一系列的配置文件缝合而成,主要由 BuildersProvisionersPost-processors 这三部分组成。其中 Builder 主要是与 IaaS Provider 构建器相关的一些参数;Provisioner 用来配置构建过程中需要运行的一些任务;Post-processors 用于配置构建动作完成后的一些后处理操作;下面就依次介绍一下这几个配置的详细使用说明:

另外 Packer 推荐的配置语法是 HCL2,但个人觉着 HCL 的语法风格怪怪的,不如 json 那样整洁好看 😅,因此下面我统一使用 json 来进行配置,其实参数都一样,只是格式不相同而已。

vars/var-file

Packer 的变量配置文件有点类似于 Ansible 中的 vars。一个比较合理的方式就是按照每个参数的作用域进行分类整理,将它们统一放在一个单独的配置文件中,这样维护起来会更方便一些。参考了 image-builder 项目中的 ova 构建后我根据参数的不同作用划分成了如下几个配置文件:

  • vcenter.json:主要用于配置一些与 vCenter 相关的参数,比如 datastore、datacenter、resource_pool、vcenter_server 等;另外像 vcenter 的用户名和密码建议使用环境变量的方式,避免明文编码在文件当中;
{  "folder": "Packer",  "resource_pool": "Packer",  "cluster": "Packer",  "datacenter": "Packer",  "datastore": "Packer",  "convert_to_template": "false",  "create_snapshot": "true",  "linked_clone": "true",  "network": "VM Network",  "password": "password",  "username": "administrator@vsphere.local",  "vcenter_server": "vcenter.k8s.li",  "insecure_connection": "true"}
  • centos7.json:主要用于配置一些通过 ISO 安装 CentOS 的参数,比如 ISO 的下载地址、ISO 的 checksum、kickstart 文件路径、关机命令、isolinux 启动参数等;
{  "boot_command_prefix": "<tab> text ks=hd:fd0:",  "boot_command_suffix": "/7/ks.cfg<enter><wait>",  "boot_media_path": "/HTTP",  "build_name": "centos-7",  "distro_arch": "amd64",  "distro_name": "centos",  "distro_version": "7",  "floppy_dirs": "./kickstart/{{user `distro_name`}}/http/",  "guest_os_type": "centos7-64",  "iso_checksum": "07b94e6b1a0b0260b94c83d6bb76b26bf7a310dc78d7a9c7432809fb9bc6194a",  "iso_checksum_type": "sha256",  "iso_url": "https://mirrors.edge.kernel.org/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Minimal-2009.iso",  "os_display_name": "CentOS 7",  "shutdown_command": "shutdown -h now",  "vsphere_guest_os_type": "centos7_64Guest"}
  • photon3.json:主要用于配置一些通过 ISO 安装 Photon3 OS 的参数,和上面的 centos7.json 作用基本一致;
{  "boot_command_prefix": "<esc><wait> vmlinuz initrd=initrd.img root/dev/ram0 loglevel=3 photon.media=cdrom ks=",  "boot_command_suffix": "/3/ks.json<enter><wait>",  "boot_media_path": "http://{{ .HTTPIP }}:{{ .HTTPPort }}",  "build_name": "photon-3",  "distro_arch": "amd64",  "distro_name": "photon",  "distro_version": "3",  "guest_os_type": "vmware-photon-64",  "http_directory": "./kickstart/{{user `distro_name`}}/http/",  "iso_checksum": "c2883a42e402a2330d9c39b4d1e071cf9b3b5898",  "iso_checksum_type": "sha1",  "iso_url": "https://packages.vmware.com/photon/3.0/Rev3/iso/photon-minimal-3.0-a383732.iso",  "os_display_name": "VMware Photon OS 64-bit",  "shutdown_command": "shutdown now",  "vsphere_guest_os_type": "vmwarePhoton64Guest"}
  • common.json:一些公共参数,比如虚拟机的 ssh 用户名和密码(要和 kickstart 中设置的保持一致)、虚拟机的一些硬件配置如 CPU、内存、硬盘、虚拟机版本、网卡类型、存储控制器类型等;
{  "ssh_username": "root",  "ssh_password": "password",  "boot_wait": "15s",  "disk_controller_type": "lsilogic",  "disk_thin_provisioned": "true",  "disk_type_id": "0",  "firmware": "bios",  "cpu": "2",  "cpu_cores": "1",  "memory": "4096",  "disk_size": "65536",  "network_card": "e1000",  "ssh_timeout": "3m",  "vmx_version": "14",  "base_build_version": "{{user `template`}}",  "build_timestamp": "{{timestamp}}",  "build_name": "k3s",  "build_version": "{{user `ova_name`}}",  "export_manifest": "none",  "output_dir": "./output/{{user `build_version`}}"}

Builder

Builder 就是告诉 Packer 要使用什么类型的构建器构建什么样的虚拟机镜像,主要是与底层 IaaS 资源提供商相关的配置。比如 vSphere Builder 中有如下两种构建器:

  • vsphere-iso 从 ISO 安装 OS 开始构建,通常情况下构建为一个虚拟机或虚拟机模版
  • vsphere-clone 通过 clone 虚拟机的方式进行构建,通常情况下构建产物为导出后的 OVF/OVA 文件

不同类型的 Builder 配置参数也会有所不同,每个参数的详细用途和说明可以参考 Packer 官方的文档,在这里就不一一说明了。因为 Packer 的参数配置是在是太多太复杂了,很难三言两语讲清楚。最佳的方式就是阅读官方的文档和一些其他项目的实现方式,照葫芦画瓢学就行。

builders.json:里面的配置参数大多都是引用的 var-file 中的参数,将这些参数单独抽出来的好处就是不同的 builder 之间可以复用一些公共参数。比如 vsphere-iso 和 vsphere-clone 这两种不同的 builder 与 vCenter 相关的 datacenter、datastore、vcenter_server 等参数都是其实相同的。

  • vsphere-iso :通过 ISO 安装 OS 构建一个虚拟机或虚拟机模版
{  "builders": [    {      "CPUs": "{{user `cpu`}}",      "RAM": "{{user `memory`}}",      "boot_command": [        "{{user `boot_command_prefix`}}",        "{{user `boot_media_path`}}",        "{{user `boot_command_suffix`}}"      ],      "boot_wait": "{{user `boot_wait`}}",      "cluster": "{{user `cluster`}}",      "communicator": "ssh",      "convert_to_template": "{{user `convert_to_template`}}",      "cpu_cores": "{{user `cpu_cores`}}",      "create_snapshot": "{{user `create_snapshot`}}",      "datacenter": "{{user `datacenter`}}",      "datastore": "{{user `datastore`}}",      "disk_controller_type": "{{user `disk_controller_type`}}",      "firmware": "{{user `firmware`}}",      "floppy_dirs": "{{ user `floppy_dirs`}}",      "folder": "{{user `folder`}}",      "guest_os_type": "{{user `vsphere_guest_os_type`}}",      "host": "{{user `host`}}",      "http_directory": "{{ user `http_directory`}}",      "insecure_connection": "{{user `insecure_connection`}}",      "iso_checksum": "{{user `iso_checksum_type`}}:{{user `iso_checksum`}}",      "iso_urls": "{{user `iso_url`}}",      "name": "vsphere-iso-base",      "network_adapters": [        {          "network": "{{user `network`}}",          "network_card": "{{user `network_card`}}"        }      ],      "password": "{{user `password`}}",      "shutdown_command": "echo '{{user `ssh_password`}}' | sudo -S -E sh -c '{{user `shutdown_command`}}'",      "ssh_clear_authorized_keys": "false",      "ssh_password": "{{user `ssh_password`}}",      "ssh_timeout": "4h",      "ssh_username": "{{user `ssh_username`}}",      "storage": [        {          "disk_size": "{{user `disk_size`}}",          "disk_thin_provisioned": "{{user `disk_thin_provisioned`}}"        }      ],      "type": "vsphere-iso",      "username": "{{user `username`}}",      "vcenter_server": "{{user `vcenter_server`}}",      "vm_name": "{{user `base_build_version`}}",      "vm_version": "{{user `vmx_version`}}"    }  ]}
  • vsphere-clone:通过 clone 虚拟机构建一个虚拟机,并导出虚拟机 OVF 模版
{  "builders": [    {      "CPUs": "{{user `cpu`}}",      "RAM": "{{user `memory`}}",      "cluster": "{{user `cluster`}}",      "communicator": "ssh",      "convert_to_template": "{{user `convert_to_template`}}",      "cpu_cores": "{{user `cpu_cores`}}",      "create_snapshot": "{{user `create_snapshot`}}",      "datacenter": "{{user `datacenter`}}",      "datastore": "{{user `datastore`}}",      "export": {        "force": true,        "manifest": "{{ user `export_manifest`}}",        "output_directory": "{{user `output_dir`}}"      },      "folder": "{{user `folder`}}",      "host": "{{user `host`}}",      "insecure_connection": "{{user `insecure_connection`}}",      "linked_clone": "{{user `linked_clone`}}",      "name": "vsphere-clone",      "network": "{{user `network`}}",      "password": "{{user `password`}}",      "shutdown_command": "echo '{{user `ssh_password`}}' | sudo -S -E sh -c '{{user `shutdown_command`}}'",      "ssh_password": "{{user `ssh_password`}}",      "ssh_timeout": "4h",      "ssh_username": "{{user `ssh_username`}}",      "template": "{{user `template`}}",      "type": "vsphere-clone",      "username": "{{user `username`}}",      "vcenter_server": "{{user `vcenter_server`}}",      "vm_name": "{{user `build_version`}}"    }  ]}

Provisioner

Provisioner 就是告诉 Packer 要如何构建镜像,有点类似于 Dockerile 中的 RUN/COPY/ADD 等指令,用于执行一些命令/脚本、往虚拟机里添加一些文件、调用第三方插件执行一些操作等。

在这个配置文件中我先使用 file 模块将一些脚本和依赖文件上传到虚拟机中,然后使用 shell 模块在虚拟机中执行 install.sh 安装脚本。如果构建的 builder 比较多,比如需要支持多个 Linux 发行版,这种场景建议使用 Ansible。由于我在 ISO 安装 OS 的构建流程中已经将一些与 OS 发行版相关的操作完成了,在这里使用 shell 执行的操作不需要区分哪个 Linux 发行版,所以就没有使用 ansible。

{  "provisioners": [    {      "type": "file",      "source": "scripts",      "destination": "/root",      "except": [        "vsphere-iso-base"      ]    },    {      "type": "file",      "source": "resources",      "destination": "/root",      "except": [        "vsphere-iso-base"      ]    },    {      "type": "shell",      "environment_vars": [        "INSECURE_REGISTRY={{user `insecure_registry`}}"      ],      "inline": "bash /root/scripts/install.sh",      "except": [        "vsphere-iso-base"      ]    }  ]}

post-processors

一些构建后的操作, 比如 "type": "manifest" 可以导出一些构建过程中的配置参数,给后续的其他操作来使用。再比如 "type": "shell-local" 就是执行一些 shell 脚本,在这里就是执行一个 Python 脚本将 OVF 转换成 OVA。

{  "post-processors": [    {      "custom_data": {        "release_version": "{{user `release_version`}}",        "build_date": "{{isotime}}",        "build_name": "{{user `build_name`}}",        "build_timestamp": "{{user `build_timestamp`}}",        "build_type": "node",        "cpu": "{{user `cpu`}}",        "memory": "{{user `memory`}}",        "disk_size": "{{user `disk_size`}}",        "distro_arch": "{{ user `distro_arch` }}",        "distro_name": "{{ user `distro_name` }}",        "distro_version": "{{ user `distro_version` }}",        "firmware": "{{user `firmware`}}",        "guest_os_type": "{{user `guest_os_type`}}",        "os_name": "{{user `os_display_name`}}",        "vsphere_guest_os_type": "{{user `vsphere_guest_os_type`}}"      },      "name": "packer-manifest",      "output": "{{user `output_dir`}}/packer-manifest.json",      "strip_path": true,      "type": "manifest",      "except": [        "vsphere-iso-base"      ]    },    {      "inline": [        "python3 ./scripts/ova.py --vmx {{user `vmx_version`}} --ovf_template {{user `ovf_template`}} --build_dir={{user `output_dir`}}"      ],      "except": [        "vsphere-iso-base"      ],      "name": "vsphere",      "type": "shell-local"    }  ]}

构建

packer-vsphere-example 项目的目录结构如下:

../packer-vsphere-example├── kickstart        # kickstart 配置文件存放目录├── Makefile         # makefile,make 命令的操作的入口├── packer           # packer 配置文件│   ├── builder.json # packer builder 配置文件│   ├── centos7.json # centos iso 安装 os 的配置│   ├── common.json  # 一些公共配置参数│   ├── photon3.json # photon3 iso 安装 os 的配置│   └── vcenter.json # vcenter 相关的配置├── resources        # 一些 k8s manifests 文件└── scripts          # 构建过程中需要用到的脚本文件

与 docker 类似,packer 执行构建操作的子命令同样也是 build,即 packer build,不过 packer build 命令支持的选项并没有 docker 那么丰富。最核心选项就是 -except, -only, -var, -var-file 这几个:

$ packer buildOptions:# 控制终端颜色输出  -color=false                  Disable color output. (Default: color)  # debug 模式,类似于断点的方式运行  -debug                        Debug mode enabled for builds.  # 排除一些 builder,有点类似于 ansible 的 --skip-tags  -except=foo,bar,baz           Run all builds and post-processors other than these.  # 指定运行某些 builder,有点类似于 ansible 的 --tags  -only=foo,bar,baz             Build only the specified builds.  # 强制构建,如果构建目标已经存在则强制删除重新构建  -force                        Force a build to continue if artifacts exist, deletes existing artifacts.  -machine-readable             Produce machine-readable output.  # 出现错误之后的动作,cleanup 清理所有操作、abort 中断执行、ask 询问、  -on-error=[cleanup|abort|ask|run-cleanup-provisioner] If the build fails do: clean up (default), abort, ask, or run-cleanup-provisioner.  # 并行运行的 builder 数量,默认没有限制,有点类似于 ansible 中的 --forks 参数  -parallel-builds=1            Number of builds to run in parallel. 1 disables parallelization. 0 means no limit (Default: 0)  # UI 输出的时间戳  -timestamp-ui                 Enable prefixing of each ui output with an RFC3339 timestamp.  # 变量参数,有点类似于 ansible 的 -e 选项  -var 'key=value'              Variable for templates, can be used multiple times.  # 变量文件,有点类似于 ansible 的 -e@ 选项  -var-file=path                JSON or HCL2 file containing user variables.# 指定一些 var 参数以及 var-file 文件,最后一个参数是 builder 的配置文件路径$ packer build  --var ova_name=k3s-photon3-c4ca93f --var release_version=c4ca93f --var ovf_template=/root/usr/src/github.com/muzi502/packer-vsphere-example/scripts/ovf_template.xml --var template=base-os-photon3 --var username=${VCENTER_USERNAME} --var password=${VCENTER_PASSWORD} --var vcenter_server=${VCENTER_SERVER} --var build_name=k3s-photon3 --var output_dir=/root/usr/src/github.com/muzi502/packer-vsphere-example/output/k3s-photon3-c4ca93f -only vsphere-clone -var-file=/root/usr/src/github.com/muzi502/packer-vsphere-example/packer/vcenter.json -var-file=/root/usr/src/github.com/muzi502/packer-vsphere-example/packer/photon3.json -var-file=/root/usr/src/github.com/muzi502/packer-vsphere-example/packer/common.json /root/usr/src/github.com/muzi502/packer-vsphere-example/packer/builder.json

上面那个又长又臭的 packer build 命令我们在 Makefile 里封装一下,那么多的参数选项手动输起来能把人气疯 😂

  • 首先定义一些默认的参数,比如构建版本、构建时间、base 模版名称、导出 ova 文件名称等等。
# Ensure Make is run with bash shell as some syntax below is bash-specificSHELL:=/usr/bin/env bash.DEFAULT_GOAL:=help# Full directory of where the Makefile residesROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))RELEASE_VERSION       ?= $(shell git describe --tags --always --dirty)RELEASE_TIME          ?= $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')PACKER_IMAGE          ?= hashicorp/packer:1.8PACKER_CONFIG_DIR     = $(ROOT_DIR)/packerPACKER_FORCE          ?= falsePACKER_OVA_PREFIX     ?= k3sPACKER_BASE_OS        ?= centos7PACKER_OUTPUT_DIR     ?= $(ROOT_DIR)/outputPACKER_TEMPLATE_NAME  ?= base-os-$(PACKER_BASE_OS)OVF_TEMPLATE          ?= $(ROOT_DIR)/scripts/ovf_template.xmlPACKER_OVA_NAME       ?= $(PACKER_OVA_PREFIX)-$(PACKER_BASE_OS)-$(RELEASE_VERSION)
  • 然后定义 vars 和 var-file 参数
# 是否为强制构建,增加 force 参数ifeq ($(PACKER_FORCE), true)  PACKER_FORCE_ARG = --force=trueendif# 定义 vars 可变参数,比如 vcenter 用户名、密码 等参数PACKER_VARS = $(PACKER_FORCE_ARG) \            # 是否强制构建--var ova_name=$(PACKER_OVA_NAME) \          # OVA 文件名--var release_version=$(RELEASE_VERSION) \   # 发布版本--var ovf_template=$(OVF_TEMPLATE) \         # OVF 模版文件--var template=$(PACKER_TEMPLATE_NAME) \     # OVA 的 base 虚拟机模版名称--var username=$${VCENTER_USERNAME} \        # vCenter 用户名(环境变量)--var password=$${VCENTER_PASSWORD} \        # vCenter 密码(环境变量)--var vcenter_server=$${VCENTER_SERVER} \    # vCenter 访问地址(环境变量)--var build_name=$(PACKER_OVA_PREFIX)-$(PACKER_BASE_OS) \  # 构建名称--var output_dir=$(PACKER_OUTPUT_DIR)/$(PACKER_OVA_NAME)   # OVA 导出的目录# 定义 var-file 参数PACKER_VAR_FILES = -var-file=$(PACKER_CONFIG_DIR)/vcenter.json \ # vCenter 的参数配置-var-file=$(PACKER_CONFIG_DIR)/$(PACKER_BASE_OS).json \        # OS 的参数配置-var-file=$(PACKER_CONFIG_DIR)/common.json                     # 一些公共配置
  • 最后定义 make targrt
.PHONY: build-template# 通过 ISO 安装 OS 构建一个 base 虚拟机build-template: ## build the base os template by isopacker build $(PACKER_VARS) -only vsphere-iso-base $(PACKER_VAR_FILES) $(PACKER_CONFIG_DIR)/builder.json.PHONY: build-ovf# 通过 clone 方式构建并导出 OVF/OVAbuild-ovf: ## build the ovf template by clone the base os templatepacker build $(PACKER_VARS) -only vsphere-clone $(PACKER_VAR_FILES) $(PACKER_CONFIG_DIR)/builder.json
  • 构建 BASE 模版
# 通过 PACKER_BASE_OS 参数设置 base os 是 photon3 还是 centos7$ make build-template PACKER_BASE_OS=photon3
  • 构建 OVF 模版并导出为 OVA
# 通过 PACKER_BASE_OS 参数设置 base os 是 photon3 还是 centos7$ make build-ovf PACKER_BASE_OS=photon3

构建流程

将 Packer 的配置文件以及 Makefile 封装好之后,我们就可以运行 make build-templatemake build-ovf 命令来构建虚拟机模版了,整体的构建流程如下:

  • 先使用 ISO 构建一个与业务无关的 base 虚拟机
  • 在 base 虚拟机之上通过 vsphere-clone 方式构建业务虚拟机
  • 导出 OVF 虚拟机文件,打包成 OVA 格式的虚拟机模版

通过 vsphere-iso 构建 Base 虚拟机

base 虚拟机有点类似于 Dockerfile 中的 FROM base 镜像。在 Packer 中我们可以把一些很少会改动的内容做成一个 base 虚拟机。然后从这个 base 虚拟机克隆出一台新的虚拟机来完成接下来的构建流程,这样能够节省整体的构建耗时,使得构建效率更高一些。

  • centos7 构建输出日志
vsphere-iso-base: output will be in this color.==> vsphere-iso-base: File /root/.cache/packer/e476ea1d3ef3c2e3966a7081ac4239cd5ae5e8a3.iso already uploaded; continuing==> vsphere-iso-base: File [Packer] packer_cache//e476ea1d3ef3c2e3966a7081ac4239cd5ae5e8a3.iso already exists; skipping upload.==> vsphere-iso-base: the vm/template Packer/base-os-centos7 already exists, but deleting it due to -force flag==> 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...==> vsphere-iso-base: IP address: 192.168.29.46==> vsphere-iso-base: Using SSH communicator to connect: 192.168.29.46==> vsphere-iso-base: Waiting for SSH to become available...==> vsphere-iso-base: Connected to SSH!==> vsphere-iso-base: Executing shutdown command...==> vsphere-iso-base: Deleting Floppy drives...==> vsphere-iso-base: Deleting Floppy image...==> vsphere-iso-base: Eject CD-ROM drives...==> vsphere-iso-base: Creating snapshot...==> vsphere-iso-base: Clear boot order...Build 'vsphere-iso-base' finished after 6 minutes 42 seconds.==> Wait completed after 6 minutes 42 seconds==> Builds finished. The artifacts of successful builds are:--> vsphere-iso-base: base-os-centos7[root@localhost:/vmfs/volumes/622aec5b-de94a27c-948e-00505680fb1d] ls packer_cache/51511394170e64707b662ca8db012be4d23e121f.iso  d3e175624fc2d704975ce9a149f8f270e4768727.iso  e476ea1d3ef3c2e3966a7081ac4239cd5ae5e8a3.iso[root@localhost:/vmfs/volumes/622aec5b-de94a27c-948e-00505680fb1d] ls -alh base-os-centos7/total 4281536drwxr-xr-x    1 root     root       72.0K Apr  1 09:17 .drwxr-xr-t    1 root     root       76.0K Apr  1 09:17 ..-rw-------    1 root     root        4.0G Apr  1 09:17 base-os-centos7-3ea6b205.vswp-rw-r--r--    1 root     root         253 Apr  1 09:17 base-os-centos7-65ff34a3.hlog-rw-------    1 root     root       64.0G Apr  1 09:17 base-os-centos7-flat.vmdk-rw-------    1 root     root        8.5K Apr  1 09:17 base-os-centos7.nvram-rw-------    1 root     root         482 Apr  1 09:17 base-os-centos7.vmdk-rw-r--r--    1 root     root           0 Apr  1 09:17 base-os-centos7.vmsd-rwxr-xr-x    1 root     root        2.3K Apr  1 09:17 base-os-centos7.vmx-rw-------    1 root     root           0 Apr  1 09:17 base-os-centos7.vmx.lck-rwxr-xr-x    1 root     root        2.2K Apr  1 09:17 base-os-centos7.vmx~-rw-------    1 root     root        1.4M Apr  1 09:17 packer-tmp-created-floppy.flp-rw-r--r--    1 root     root       96.1K Apr  1 09:17 vmware.logroot@devbox-fedora:/root # scp 192.168.24.43:/vmfs/volumes/Packer/base-os-centos7/packer-tmp-created-floppy.flp .root@devbox-fedora:/root # mount packer-tmp-created-floppy.flp /mntroot@devbox-fedora:/root # readlink /dev/disk/by-label/packer../../loop2root@devbox-fedora:/root # ls /mnt/HTTP/7/KS.CFGKS.CFG
  • Photon3 构建输出日志
vsphere-iso-base: output will be in this color.==> vsphere-iso-base: File /root/.cache/packer/d3e175624fc2d704975ce9a149f8f270e4768727.iso already uploaded; continuing==> vsphere-iso-base: File [Packer] packer_cache//d3e175624fc2d704975ce9a149f8f270e4768727.iso already exists; skipping upload.==> vsphere-iso-base: the vm/template Packer/base-os-photon3 already exists, but deleting it due to -force flag==> 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: Starting HTTP server on port 8674==> vsphere-iso-base: Set boot order temporary...==> vsphere-iso-base: Power on VM...==> vsphere-iso-base: Waiting 15s for boot...==> vsphere-iso-base: HTTP server is working at http://192.168.29.171:8674/==> vsphere-iso-base: Typing boot command...==> vsphere-iso-base: Waiting for IP...==> vsphere-iso-base: IP address: 192.168.29.208==> vsphere-iso-base: Using SSH communicator to connect: 192.168.29.208==> vsphere-iso-base: Waiting for SSH to become available...==> vsphere-iso-base: Connected to SSH!==> vsphere-iso-base: Executing shutdown command...==> vsphere-iso-base: Deleting Floppy drives...==> vsphere-iso-base: Eject CD-ROM drives...==> vsphere-iso-base: Creating snapshot...==> vsphere-iso-base: Clear boot order...Build 'vsphere-iso-base' finished after 5 minutes 24 seconds.==> Wait completed after 5 minutes 24 seconds==> Builds finished. The artifacts of successful builds are:--> vsphere-iso-base: base-os-photon3

通过 packer build 命令的输出我们大致可以推断出通过 vsphere-iso 构建 Base 虚拟机的主要步骤和原理:

  • 下载 ISO 文件到本地的 ${HOME}/.cache/packer 目录,并以 checksum.iso 方式保存,这样的好处就是便于缓存 ISO 文件,避免重复下载;
  • 上传本地 ISO 文件到 vCenter 的 datastore 中,默认保存在 datastore 的 packer_cache 目录下,如果 ISO 文件已经存在了,则会跳过上传的流程;
  • 创建虚拟机,配置虚拟机硬件,挂载上传的 ISO 文件到虚拟机上的 CD/ROM,设置 boot 启动项为 CD/ROM
  • 如果 boot_media_path 是 http 类型的则在本地随机监听一个 TCP 端口来运行一个 http 服务,用于提供 kickstart 文件的 HTTP 下载功能;如果是目录类型的则将 kickstart 文件创建成一个软盘文件,并将该文件上传到 datastore 中,将软盘文件插入到虚拟机中;
  • 虚拟机开机启动到 ISO 引导页面,通过 vCenter API 发送键盘输入,插入 kickstart 文件的路径;
  • 通过 vCenter API 发送回车键盘输入,ISO 中的 OS 安装程序读取 kickstart 进行 OS 安装;
  • 在 kickstart 脚本里安装 open-vm-tools 工具;
  • 等待 OS 安装完成,安装完成重启后进入安装好的 OS,OS 启动后通过 DHCP 获取 IP 地址;
  • 通过 vm-tools 获取到虚拟机的 IP 地址,然后 ssh 连接到虚拟机执行关机命令;
  • 虚拟机关机,卸载 ISO 和软驱等不需要的设备;
  • 创建快照或者将虚拟机转换为模版;

个人觉着这里比较好玩儿就是居然可以通过 vCenter 或 ESXi 的 PutUsbScanCodes API 来给虚拟机发送一些键盘输入的指令,感觉这简直太神奇啦 😂。之前我们的项目是将 kickstart 文件构建成一个 ISO 文件,然后通过重新构建源 ISO 的方式来修改 isolinux 启动参数。后来感觉这种重新构建 ISO 的方式太蠢了,于是就参考 Packer 的思路使用 govc 里内置的 vm.keystrokes 命令来给虚拟机发送键盘指令,完成指定 kickstart 文件路径参数启动的操作。具体的 govc 操作命令可以参考如下:

# 发送 tab 键,进入到 ISO 启动参数编辑页面$ govc vm.keystrokes -vm='centos-vm-192' -c='KEY_TAB'# 发送 Right Control + U 键清空输入框$ govc vm.keystrokes -vm='centos-vm-192' -rc=true -c='KEY_U'# 输入 isolinux 的启动参数配置,通过 ks=hd:LABEL=KS:/ks.cfg 指定 kickstart 路径,LABEL 为构建 ISO 时设置的 lable$ govc vm.keystrokes -vm='centos-vm-192' -s='vmlinuz initrd=initrd.img ks=hd:LABEL=KS:/ks.cfg inst.stage2=hd:LABEL=CentOS\\x207\\x20x86_64 quiet console=ttyS0'# 按下回车键,开始安装 OS$ govc vm.keystrokes -vm='centos-vm-192' -c='KEY_ENTER'

通过 vsphere-clone 构建业务虚拟机并导出 OVF/OVA

通过 vsphere-iso 构建 Base 虚拟机之后,我们就使用这个 base 虚拟机克隆出一台新的虚拟机,用来构建我们的业务虚拟机镜像,将 k3s, argo-workflow, redfish-esxi-os-installer 这一堆工具打包进去;

vsphere-clone: output will be in this color.==> vsphere-clone: Cloning VM...==> vsphere-clone: Customizing hardware...==> vsphere-clone: Power on VM...==> vsphere-clone: Waiting for IP...==> vsphere-clone: IP address: 192.168.30.112==> vsphere-clone: Using SSH communicator to connect: 192.168.30.112==> vsphere-clone: Waiting for SSH to become available...==> vsphere-clone: Connected to SSH!==> vsphere-clone: Uploading scripts => /root==> vsphere-clone: Uploading resources => /root==> vsphere-clone: Provisioning with shell script: /tmp/packer-shell557168976==> vsphere-clone: Executing shutdown command...==> vsphere-clone: Creating snapshot...    vsphere-clone: Starting export...    vsphere-clone: Downloading: k3s-photon3-c4ca93f-disk-0.vmdk    vsphere-clone: Exporting file: k3s-photon3-c4ca93f-disk-0.vmdk    vsphere-clone: Writing ovf...==> vsphere-clone: Running post-processor: packer-manifest (type manifest)==> vsphere-clone: Running post-processor: vsphere (type shell-local)==> vsphere-clone (shell-local): Running local shell script: /tmp/packer-shell2376077966    vsphere-clone (shell-local): image-build-ova: cd /root/usr/src/github.com/muzi502/packer-vsphere-example/output/k3s-photon3-c4ca93f    vsphere-clone (shell-local): image-build-ova: create ovf k3s-photon3-c4ca93f.ovf    vsphere-clone (shell-local): image-build-ova: create ova manifest k3s-photon3-c4ca93f.mf    vsphere-clone (shell-local): image-build-ova: creating OVA using tar    vsphere-clone (shell-local): image-build-ova: ['tar', '-c', '-f', 'k3s-photon3-c4ca93f.ova', 'k3s-photon3-c4ca93f.ovf', 'k3s-photon3-c4ca93f.mf', 'k3s-photon3-c4ca93f-disk-0.vmdk']    vsphere-clone (shell-local): image-build-ova: create ova checksum k3s-photon3-c4ca93f.ova.sha256Build 'vsphere-clone' finished after 14 minutes 16 seconds.==> Wait completed after 14 minutes 16 seconds==> Builds finished. The artifacts of successful builds are:--> vsphere-clone: k3s-photon3-c4ca93f--> vsphere-clone: k3s-photon3-c4ca93f--> vsphere-clone: k3s-photon3-c4ca93f

通过 packer build 命令的输出我们大致可以推断出构建流程:

  • clone 虚拟机,修改虚拟机的硬件配置
  • 虚拟机开机,通过 vm-tools 获取虚拟机的 IP 地址
  • 获取到虚拟机的 IP 地址后等待 ssh 能够正常连接
  • ssh 能够正常连接后,通过 scp 的方式上传文件
  • ssh 远程执行虚拟机里的 install.sh 脚本
  • 执行虚拟机关机命令
  • 创建虚拟机快照
  • 导出虚拟机 OVF 文件
  • 导出构建配置参数的 manifest.json 文件
  • 执行 ova.py 脚本,根据 manifest.json 配置参数将 OVF 格式转换成 OVA

至此,整个的虚拟机模版的构建流程算是完成了,最终我们的到一个 OVA 格式的虚拟机模版。使用的时候只需要在本地机器上安装好 VMware Workstation 或者 Oracle VirtualBox 就能一键导入该虚拟机,开机后就可以使用啦,算是做到了开箱即用的效果。

output└── k3s-photon3-c4ca93f    ├── k3s-photon3-c4ca93f-disk-0.vmdk    ├── k3s-photon3-c4ca93f.mf    ├── k3s-photon3-c4ca93f.ova    ├── k3s-photon3-c4ca93f.ova.sha256    ├── k3s-photon3-c4ca93f.ovf    └── packer-manifest.json

argo-workflow 和 k3s

在虚拟机内使用 redfish-esxi-os-installer 有点特殊,是将它放在 argo-workflow 的 Pod 内来执行的。在 workflow 模版文件 workflow.yaml 中我们定义了若干个 steps 来运行 redfish-esxi-os-installer。

apiVersion: argoproj.io/v1alpha1kind: Workflowmetadata:  generateName: redfish-esxi-os-installer-  namespace: defaultspec:  entrypoint: redfish-esxi-os-installer  templates:  - name: redfish-esxi-os-installer    steps:    - - arguments:          parameters:          - name: command            value: pre-check        name: Precheck        template: installer    - - arguments:          parameters:          - name: command            value: build-iso        name: BuildISO        template: installer    - - arguments:          parameters:          - name: command            value: mount-iso        name: MountISO        template: installer    - - arguments:          parameters:          - name: command            value: reboot        name: Reboot        template: installer    - - arguments:          parameters:          - name: command            value: post-check        name: Postcheck        template: installer    - - arguments:          parameters:          - name: command            value: umount-iso        name: UmountISO        template: installer  - container:      name: installer      image: ghcr.io/muzi502/redfish-esxi-os-installer:v0.1.0-alpha.1      command:      - bash      - -c      - |        make inventory && make {{inputs.parameters.command}}      env:      - name: POD_NAME        valueFrom:          fieldRef:            fieldPath: metadata.name      - name: HOST_IP        valueFrom:          fieldRef:            fieldPath: status.hostIP      - name: SRC_ISO_DIR        value: /data/iso      - name: HTTP_DIR        value: /data/iso/redfish      - name: HTTP_URL        value: http://$(HOST_IP)/files/iso/redfish      - name: ESXI_ISO        valueFrom:          configMapKeyRef:            name: redfish-esxi-os-installer-config            key: esxi_iso      securityContext:        privileged: true      volumeMounts:      - mountPath: /ansible/config.yaml        name: config        readOnly: true        subPath: config.yaml      - mountPath: /data        name: data    inputs:      parameters:      - name: command    name: installer    retryStrategy:      limit: "2"      retryPolicy: OnFailure  volumes:  - configMap:      items:      - key: config        path: config.yaml      name: redfish-esxi-os-installer-config    name: config  - name: data    hostPath:      path: /data      type: DirectoryOrCreate

由于目前没有 Web UI 和后端 Server 所以还是需要手动编辑 /root/resources/workflow/configmap.yaml 配置文件,然后再执行 kubectl create -f /root/resources/workflow 命令创建 workflow 工作流。

workflow 创建了之后,就可以通过 argo 命令查看 workflow 执行的进度和状态

root@localhost [ ~/resources/workflow ]# argo get redfish-esxi-os-installer-tjjqzName:                redfish-esxi-os-installer-tjjqzNamespace:           defaultServiceAccount:      unset (will run with the default ServiceAccount)Status:              SucceededConditions: PodRunning          False Completed           TrueCreated:             Mon May 23 11:07:31 +0000 (16 minutes ago)Started:             Mon May 23 11:07:31 +0000 (16 minutes ago)Finished:            Mon May 23 11:23:38 +0000 (19 seconds ago)Duration:            16 minutes 7 secondsProgress:            6/6ResourcesDuration:   29m45s*(1 cpu),29m45s*(100Mi memory)STEP                                TEMPLATE                   PODNAME                                     DURATION  MESSAGE ✔ redfish-esxi-os-installer-tjjqz  redfish-esxi-os-installer ├───✔ Precheck(0)                  installer                  redfish-esxi-os-installer-tjjqz-647555770   11s ├───✔ BuildISO(0)                  installer                  redfish-esxi-os-installer-tjjqz-3078771217  14s ├───✔ MountISO(0)                  installer                  redfish-esxi-os-installer-tjjqz-4099695623  19s ├───✔ Reboot(0)                    installer                  redfish-esxi-os-installer-tjjqz-413209187   7s ├───✔ Postcheck(0)                 installer                  redfish-esxi-os-installer-tjjqz-2674696793  14m └───✔ UmountISO(0)                 installer                  redfish-esxi-os-installer-tjjqz-430254503   13s

argo-workflow

之所以使用 argo-workflow 而不是使用像 docker、nerdctl 这些命令行工具来运行 redfish-esxi-os-installer ,是因为通过 argo-workflow 来编排我们的安装部署任务能够比较方便地实现多个任务同时运行、获取任务执行的进度及日志、获取任务执行的耗时、停止重试等功能。使用 argo-workflow 来编排我们的安装部署任务,并通过 argo-workflow 的 RESTful API 获取部署任务的进度日志等信息,这样做更云原生一些(🤣

argo-workfloe-apis

在我们内部其实最终目的是准备将该方案做成一个产品化的工具,提供一个 Web UI 用来进行配置部署参数以及展示部署的进度日志等功能。当初设计方案的时候也是参考了一下 VMware Tanzu 社区版 :部署 Tanzu 管理集群的时候需要有一个已经存在的 k8s 集群,或者通过 Tanzu 新部署一个 kind 集群。部署一个 tanzu 管理集群可以通过 tanzu 命令行的方式,也可以通过 Tanzu Web UI 的方式,Tanzu Web UI 的方式其实就是一个偏向于产品化的工具。在 VMware Tanzu kubernetes 发行版部署尝鲜 我曾分享过 Tanzu 的部署方式,感兴趣的话可以去看一下。

tanzu-cluster

该方案主要是面向一些产品化的场景,由于引入了 K8s 这个庞然大物,整体的技术栈会复杂一些,但也有一些好处啦 😅。

k8s and k3s

argo-workflow 需要依赖一个 k8s 集群才能运行,我们内部测试了 kubekey、sealos、kubespray、k3s 几种常见的部署工具。综合评定下来 k3s 集群占用的资源最少。参考 K3s 资源分析 给出的资源要求,最小只需要 768M 内存就能运行。对于硬件资源不太充足的笔记本电脑来讲,k3s 无疑是目前最佳的方案。

另外还有一个十分重要的原因就是 k3s server 更换单 control plan 节点的 IP 地址十分方便,对用户来说是无感知的。这样就可以将安装 k3s 的操作在构建 OVA 的时候完成,而不是在使用的时候手动执行安装脚本来安装。

只要开机运行虚拟机能够通过 DHCP 分配到一个内网 IPv4 地址或者手动配置一个静态 IP,k3s 就能够正常运行起来,能够真正做到开箱即用,而不是像 kubekey、sealos、kubespray 那样傻乎乎地填写一个复杂无比的配置文件,然后再执行一些命令来安装 k8s 集群。这种导入虚拟机开即用的方式,对用户来讲十分友好。

当然在使用 kubekey、sealos、kubespray 在构建虚拟机的时候安装好 k8s 集群也不是不可行,只不过我们构建时候虚拟机的 IP 地址(比如 10.172.20.223)和使用时的 IP 地址(比如 192.168.20.11)基本上是不会相同的。给 k8s control plain 节点更换 IP 的操作 阳明博主 曾在 如何修改 Kubernetes 节点 IP 地址? 文章中分享过他的经历,看完后直接把我整不会了,感觉操作起来实在是太麻烦了,还不如重新部署一套新的 k8s 方便呢 😂

其实构建虚拟机模版的时候安装 k8s 的思路最初我是借鉴的 cluster-api 项目 😂。即将部署 k8s 依赖的一些文件和容器镜像构建在虚拟机模版当中,部署 k8s 的时候不需要再联网下载这些依赖资源了。不同的是,我们通过 k3s 直接提前将 k8s 集群部署好了,也就省去了让用户执行部署的操作。

综上,选用 k3s 作为该方案的 K8s 底座无疑是最佳的啦(

其他

使用感受

使用了一段时间后感觉 Packer 的复杂度和上手难度要比 Docker 构建容器镜像要高出一个数量级。可能是因为虚拟机并不像容器镜像那样有 OCI 这种统一的构建、分发、运行工业标准。虚拟机的创建克隆等操作与底层的 IaaS 供应商耦合的十分紧密,这就导致不同 IaaS 供应商比如 vSphere、kvm/qemu 他们之间能够复用的配置参数并不多。比如 vSphere 里有 datastore、datacenter、resource_pool、folder 等概念,但 kvm/qemu 中缺没有,这就导致很难将它们统一成一个配置。

OVA 格式

使用 OVA 而不是 vagrant.box、vmdk、raw、qcow2 等其他格式是因为 OVA 支持支持一键导入的特性,在 Windows 上使用起来比较方便。毕竟 Windows 上安装 Vagrant 或者 qemu/KVM 也够你折腾的了,VMware Workstation 或者 Oracle VirtualBox 使用得更广泛一些。

另外 Packer 并不支持直接将虚拟机导出为 OVA 的方式,默认情况下只会通过 vCenter 的 API 导出为 ovf。如果需要 OVA 格式,需要将 OVF 打包成 OVA。在 ISSUE Add support for exporting to OVA in vsphere-iso builder #9645 也有人反馈了支持 OVA 导出的需求,但 Packer 至今仍未支持。将 OVF 转换为 OVA 我是参考的 image-builder 项目的 image-build-ova.py 来完成的。

安装 open-vm-tool 失败

由于 ISO 中并不包含 open-vm-tool 软件包,这就需要在 ISO 安装 OS 的过程中联网安装 open-vm-tools。如果安装的时候网络抖动了就可能会导致 open-vm-tools 安装失败。open-vm-tools 安装失败 packer 是无法感知到的,只能一直等到获取虚拟机 IP 超时后退出执行。目前没有很好的办法,只能在 kickstart 里安装 open-vm-tools 的时候进行重试直到 open-vm-tools 安装成功。

减少导出后 vmdk 文件大小

曾经在 手搓虚拟机模板 文章中分析过通过 dd 置零的方式可以大幅减少虚拟机导出后的 vmdk 文件大

464M Aug 28 16:15 Ubuntu1804-2.ova # 置零后的大小1.3G Aug 28 15:48 Ubuntu1804.ova   # 置零前的大小

需要注意的是,在 dd 置零之前要先停止 k3s 服务,不然置零的时候会占满 root 根分区导致 kubelet 启动 GC 将一些镜像给删除掉。之前导出虚拟机后发现少了一些镜像,排查了好久才发现是 kubelet GC 把我的镜像给删掉了,踩了个大坑,可气死我了 😡

另外也可以删除一些不必要的文件,比如 containerd 中 io.containerd.content.v1.content/blobs/sha256 一些镜像 layer 的原始 blob 文件是不需要的,可以将它们给删除掉,这样能够减少部分磁盘空间占用;

function cleanup(){  # stop k3s server for for prevent it starting the garbage collection to delete images  systemctl stop k3s  # Ensure on next boot that network devices get assigned unique IDs.  sed -i '/^\(HWADDR\|UUID\)=/d' /etc/sysconfig/network-scripts/ifcfg-* 2>/dev/null || true  # Clean up network interface persistence  find /var/log -type f -exec truncate --size=0 {} \;  rm -rf /tmp/* /var/tmp/*  # cleanup all blob files of registry download image  find /var/lib/rancher/k3s/agent/containerd/io.containerd.content.v1.content/blobs/sha256 -size +1M -type f -delete  # zero out the rest of the free space using dd, then delete the written file.  dd if=/dev/zero of=/EMPTY bs=4M status=progress || rm -f /EMPTY  dd if=/dev/zero of=/data/EMPTY bs=4M status=progress || rm -f /data/EMPTY  # run sync so Packer doesn't quit too early, before the large file is deleted.  sync  yum clean all}

Photon3

之前在 轻量级容器优化型 Linux 发行版 Photon OS 里分享过 VMware 的 Linux 发行版 Photon。不同于传统的 Linux 发行版 Photon 的系统十分精简,使用它替代 CentOS 能够一定程度上减少系统资源的占用,导出后的 vmdk 文件也要比 CentOS 小一些。

goss

在构建的过程中我们在 k3s 集群上安装了一些其他的组件,比如提供文件上传和下载服务的 filebrowser 以及 workflow 工作流引擎 argo-workflow,为了保证这些服务的正常运行,我们就需要通过不同的方式去检查这些服务是否正常。一般是通过 kubectl get 等命令查看 deployment、pod、daemonset 等服务是否正常运行,或者通过 curl 访问这些这些服务的健康检查 API。

由于检查项比较多且十分繁琐,使用传统的 shell 脚本来做这并不是很方便,需要解析每个命令的退出码以及返回值。因此我们使用 goss 通过 YAML 格式的配置文件来定义一些检查项,让它批量来执行这些检查,而不用在 shell 对每个检查项写一堆的 awk/grep 等命令来 check 了。

  • k3s.yaml:用于检查 k3s 以及它默认自带的服务是否正常运行
# DNS 类型的检查dns:  # 检查 coredns 是否能够正常解析到 kubernetes apiserver 的 service IP 地址  kubernetes.default.svc.cluster.local:    resolvable: true    addrs:      - 10.43.0.1    server: 10.43.0.10    timeout: 600    skip: false# TCP/UDP 端口类型的检查addr:  # 检查 coredns 的 UDP 53 端口是否正常  udp://10.43.0.10:53:    reachable: true    timeout: 500# 检查 cni0 网桥是否存在interface:  cni0:    exists: true    addrs:      - 10.42.0.1/24# 本机端口类型的检查port:  # 检查 ssh 22 端口是否正常  tcp:22:    listening: true    ip:      - 0.0.0.0    skip: false  # 检查 kubernetes apiserver 6443 端口是否正常  tcp6:6443:    listening: true    skip: false# 检查一些 systemd 服务的检查service:  # 默认禁用 firewalld 服务  firewalld:    enabled: false    running: false  # 确保 sshd 服务正常运行  sshd:    enabled: true    running: true    skip: false  # 检查 k3s 服务是否正常运行  k3s:    enabled: true    running: true    skip: false# 定义一些 shell 命令执行的检查command:  # 检查 kubernetes cheduler 组件是否正常  check_k8s_scheduler_health:    exec: curl -k https://127.0.0.1:10259/healthz    # 退出码是否为 0    exit-status: 0    stderr: []    # 标准输出中是否包含正确的输出值    stdout: ["ok"]    skip: false  # 检查 kubernetes controller-manager 是否正常  check_k8s_controller-manager_health:    exec: curl -k https://127.0.0.1:10257/healthz    exit-status: 0    stderr: []    stdout: ["ok"]    skip: false  # 检查 cluster-info  中输出的组件运行状态是否正常  check_cluster_status:    exec: kubectl cluster-info | grep 'is running'    exit-status: 0    stderr: []    timeout: 0    stdout:      - CoreDNS      - Kubernetes control plane    skip: false  # 检查节点是否处于 Ready 状态  check_node_status:    exec: kubectl get node -o jsonpath='{.items[].status}' | jq -r '.conditions[-1].type'    exit-status: 0    stderr: []    timeout: 0    stdout:      - Ready    skip: false  # 检查节点 IP 是否正确  check_node_address:    exec: kubectl get node -o wide -o json | jq -r '.items[0].status.addresses[] | select(.type == "InternalIP") | .address'    exit-status: 0    stderr: []    timeout: 0    stdout:      - {{ .Vars.ip_address }}    skip: false  # 检查 traefik loadBalancer 的 IP 地址是否正确  check_traefik_address:    exec: kubectl -n kube-system get svc traefik -o json | jq -r '.status.loadBalancer.ingress[].ip'    exit-status: 0    stderr: []    timeout: 0    stdout:      - {{ .Vars.ip_address }}    skip: false  # 检查 containerd 容器运行是否正常  check_container_status:    exec: crictl ps --output=json | jq -r '.containers[].metadata.name' | sort -u    exit-status: 0    stderr: []    timeout: 0    stdout:      - coredns      - /lb-.*-443/      - /lb-.*-80/      - traefik    skip: false  # 检查 kube-system namespace 下的 pod 是否正常  check_kube_system_namespace_pod_status:    exec: kubectl get pod -n kube-system -o json | jq -r '.items[] | select((.status.phase != "Running") and (.status.phase != "Succeeded") and (.status.phase != "Completed"))'    exit-status: 0    stderr: []    timeout: 0    stdout: ["!string"]  # 检查 k8s deployment 服务是否都正常  check_k8s_deployment_status:    exec: kubectl get deploy --all-namespaces -o json | jq -r '.items[]| select(.status.replicas == .status.availableReplicas) | .metadata.name' | sort -u    exit-status: 0    stderr: []    timeout: 0    stdout:      - coredns      - traefik    skip: false  # 检查 svclb-traefik daemonset 是否正常  check_k8s_daemonset_status:    exec: kubectl get daemonset --all-namespaces -o json | jq -r '.items[]| select(.status.replicas == .status.availableReplicas) | .metadata.name' | sort -u    exit-status: 0    stderr: []    timeout: 0    stdout:      - svclb-traefik    skip: false
  • goss.yaml:用于检查我们部署的一些服务是否正常
# 通过 include 其他 gossfile 方式将上面定义的 k3s.yaml 检查项也包含进来gossfile:  k3s.yaml: {}dns:  # 检查部署的 filebrowser deployment 的 service IP 是否能正常解析到  filebrowser.default.svc.cluster.local:    resolvable: true    server: 10.43.0.10    timeout: 600    skip: false  # 检查部署的 argo-workflow deployment 的 service IP 是否能正常解析到  argo-workflow-argo-workflows-server.default.svc.cluster.local:    resolvable: true    server: 10.43.0.10    timeout: 600    skip: false# 一些 HTTP 请求方式的检查http:  # 检查 filebrowser 服务是否正常运行,类似于 pod 里的存活探针  http://{{ .Vars.ip_address }}/filebrowser/:    status: 200    timeout: 600    skip: false    method: GET  # 检查 argo-workflow 是否正常运行  http://{{ .Vars.ip_address }}/workflows/api/v1/version:    status: 200    timeout: 600    skip: false    method: GET# 同样也是一些 shell 命令的检查项目command:  # 检查容器镜像是否齐全,避免缺镜像的问题  check_container_images:    exec: crictl images --output=json | jq -r '.images[].repoTags[]' | awk -F '/' '{print $NF}' | awk -F ':' '{print $1}' | sort -u    exit-status: 0    stderr: []    timeout: 0    stdout:      - argocli      - argoexec      - workflow-controller      - filebrowser      - nginx    skip: false  # 检查容器运行的状态是否正常  check_container_status:    exec: crictl ps --output=json | jq -r '.containers[].metadata.name' | sort -u    exit-status: 0    stderr: []    timeout: 0    stdout:      - argo-server      - controller      - nginx      - filebrowser    skip: false  # 检查一些 deployment 的状态是否正常  check_k8s_deployment_status:    exec: kubectl get deploy -n default -o json | jq -r '.items[]| select(.status.replicas == .status.availableReplicas) | .metadata.name' | sort -u    exit-status: 0    stderr: []    timeout: 0    stdout:      - argo-workflow-argo-workflows-server      - argo-workflow-argo-workflows-workflow-controller      - filebrowser    skip: false# 一些硬件参数的检查,比如 CPU 核心数、内存大小、可用内存大小matching:  check_vm_cpu_core:    content: {{ .Vars.cpu_core_number }}    matches:      gt: 1  check_vm_memory_size:    content: {{ .Vars.memory_size }}    matches:      gt: 1880000  check_available_memory_size:    content: {{ .Vars.available_memory_size }}    matches:      gt: 600000

另外 goss 也比较适合做一些巡检的工作。比如在一个 k8s 集群中进行巡检:检查集群内 pod 的状态、kubernetes 组件的状态、CNI 运行状态、节点的网络、磁盘存储空间、CPU 负载、内核参数、daemonset 服务状态等,都可以参照上述方式定义一系列的检查项,使用 goss 来帮我们自动完成巡检。

导入 OVA 虚拟机后 Pod 状态异常

将 OVA 虚拟机在 VMware Workstation 上导入之后,由于虚拟机 IP 的变化可能会导致一些 Pod 处于异常的状态,这时候就需要对这些 Pod 进行强制删除,强制重启一下才能恢复正常。因此需要需要在虚拟机里增加一个 prepare.sh 脚本用来重启这些状态异常的 Pod。当导入 OVA 虚拟机后运行这个脚本让所有的 Pod 都正常运行起来,然后再调用 goss 来检查其他服务是否正常。

#!/bin/bashset -o errexitset -o nounsetset -o pipefailkubectl get pods --no-headers -n kube-system | grep -E '0/2|0/1|Error|Unknown|CreateContainerError|CrashLoopBackOff' | awk '{print $1}' | xargs -t -I {} kubectl delete pod -n kube-system --grace-period=0 --force {} > /dev/null  2>&1 || truekubectl get pods --no-headers -n default | grep -E '0/1|Error|Unknown|CreateContainerError|CrashLoopBackOff' | awk '{print $1}' | xargs -t -I {} kubectl delete pod -n default --grace-period=0 --force {} > /dev/null  2>&1 || truewhile true; do  if kubectl get pods --no-headers --all-namespaces | grep -Ev 'Running|Completed'; then    echo "Waiting for service readiness"    sleep 10  else    break  fidonecd ${HOME}/.gosscat > vars.yaml << EOFip_address: $(ip r get 1 | sed "s/ uid.*//g" | awk '{print $NF}' | head -n1)cpu_core_number: $(grep -c ^processor /proc/cpuinfo)memory_size: $(grep '^MemTotal:' /proc/meminfo | awk '{print $2}')available_memory_size: $(grep '^MemAvailable:' /proc/meminfo | awk '{print $2}')EOFgoss --vars vars.yaml -g goss.yaml validate --retry-timeout=10s

参考

Packer 相关

☑️ ☆

流水线中使用 docker in pod 方式构建容器镜像

上个月参加了 Rancher 社区举办的 《Dockershim 即将被移除,你准备好了么?》直播分享后,得知自 1.24 版本之后,Kubernetes 社区将正式放弃对 docker CRI 的支持,docker CRI 这部分代码则由 cri-dockerd 项目来接盘。目前众多主流的 Kubernetes 私有化部署工具(比如 kubespraykubekeysealos)也逐渐地不再将 docker 作为首选的容器运行时而纷纷转向了 containerd,去 docker 也成为了目前云原生社区热门的讨论话题。

docker 虽然作为一个 CRI 在 Kubernetes 社区一直被人诟病,但我们要知道 CRI 仅仅是 docker 的一部分功能而已。对于本地开发测试或者 CI/CD 流水线镜像构建来讲,依然有很多地方严重地依赖着 docker。比如 GitHub 上容器镜像构建的 Action 里, docker 官方的 build-push-action 是众多项目首选的方式。即便是 docker 的竞争对手 podman + skopeo + buildah 三剑客它们自身的容器镜像也是采用 docker 来构建的 multi-arch-build.yaml

jobs:  multi:    name: multi-arch image build    env:      REPONAME: buildah  # No easy way to parse this out of $GITHUB_REPOSITORY      # Server/namespace value used to format FQIN      REPONAME_QUAY_REGISTRY: quay.io/buildah      CONTAINERS_QUAY_REGISTRY: quay.io/containers      # list of architectures for build      PLATFORMS: linux/amd64,linux/s390x,linux/ppc64le,linux/arm64      # Command to execute in container to obtain project version number      VERSION_CMD: "buildah --version"    # build several images (upstream, testing, stable) in parallel    strategy:      # By default, failure of one matrix item cancels all others      fail-fast: false      matrix:        # Builds are located under contrib/<reponame>image/<source> directory        source:          - upstream          - testing          - stable    runs-on: ubuntu-latest    # internal registry caches build for inspection before push    services:      registry:        image: quay.io/libpod/registry:2        ports:          - 5000:5000    steps:      - name: Checkout        uses: actions/checkout@v2      - name: Set up QEMU        uses: docker/setup-qemu-action@v1      - name: Set up Docker Buildx        uses: docker/setup-buildx-action@v1        with:          driver-opts: network=host          install: true      - name: Build and locally push image        uses: docker/build-push-action@v2        with:          context: contrib/${{ env.REPONAME }}image/${{ matrix.source }}          file: ./contrib/${{ env.REPONAME }}image/${{ matrix.source }}/Dockerfile          platforms: ${{ env.PLATFORMS }}          push: true          tags: localhost:5000/${{ env.REPONAME }}/${{ matrix.source }}

Jenkins 流水线

我们的 CI/CD 流水线是使用 Jenkins + Kubernetes plugin 的方式在 Kubernetes 上动态地创建 Pod 作为 Jenkins Slave。在使用 docker 作为容器时的情况下,Jenkins Slave Pod 将宿主机上的 /var/run/docker.sock 文件通过 hostPath 的方式挂载到 pod 容器内,容器内的 docker CLI 就能通过该 sock 与宿主机的 docker 守护进程进行通信,这样在 pod 容器内就可以无缝地使用 docker build 、push 等命令了。

// Kubernetes pod template to run.podTemplate(    cloud: "kubernetes",    namespace: "default",    name: POD_NAME,    label: POD_NAME,    yaml: """apiVersion: v1kind: Podspec:  containers:  - name: debian    image: "${JENKINS_POD_IMAGE_NAME}"    imagePullPolicy: IfNotPresent    tty: true    volumeMounts:    - name: dockersock      mountPath: /var/run/docker.sock  - name: jnlp    args: ["\$(JENKINS_SECRET)", "\$(JENKINS_NAME)"]    image: "jenkins/inbound-agent:4.3-4-alpine"    imagePullPolicy: IfNotPresent  volumes:    - name: dockersock      hostPath:        path: /var/run/docker.sock""",)

当不再使用 docker 作为 Kubernetes 的容器运行时之后,宿主机上则就没有了 docker 守护进程,挂载 /var/run/docker.sock 的方式也就凉凉了,因此我们需要找到一些替代的方法。

目前能想到的有两种方案:方案一是替代掉 docker 使用其他镜像构建工具比如 podman + skopeo + buildah。陈少文博主在《基于 Kubernetes 的 Jenkins 服务也可以去 Docker 了》详细地讲过该方案。但我们的 Makefile 里缝合了一些 docker buildKit 的特性参数并不能地通过 alias docker=podman 别名的方式简单粗暴地给替换掉 😂。

比如 podman 构建镜像就不支持 --output type=local,dest=path Support custom build outputs #3789 这种特性。目前看来 podman 想要完全取代掉 docker 的老大哥地位仍还有很长的路要走,尤其 podman 还没有解决自身的镜像是由 docker 来构建的这个尴尬难题。

方案二就是继续使用 docker 作为镜像构建工具,虽然集群节点上没有了 docker 守护进程,但这并不意味着在 Kubernetes 集群里就无法使用 docker 了。我们可以换种方式将 docker 作为一个 pod 运行在 kubernetes 集群中,而非以 systemd 的方式部署在节点上。然后通过 service IP 或 Node IP 访问 docker 的 TCP 端口进行通信,这样也能无缝地继续使用 docker 。于是在 dind (docker-in-docker) 的基础上就有了 dinp (docker-in-pod) 的套娃操作,其实二者本质上都是相同的,只不过是部署方式和访问方式不太相同而已。

对比一下这两种方案,方案一通过 alias docker=podman 使用 podman 替代 docker 有点投机取巧,在正式的生产环境流水线中应该很少会被采用,除非你的 Makefile 或者镜像构建脚本中没有依赖 docker 的特性参数,能够完全兼容 podman;方案二比较稳定可靠,它无非就是将之前的宿主机节点上的 docker 守护进程替换成了集群内的 Pod,对于使用者而言只需要修改一下访问 docker 的方式,即 DOCKER_HOST 环境变量即可。因此本文选用方案二来给大家介绍几种在 K8s 集群里部署和使用 dind/dinp 的方式。

docker in pod

不同于 docker in docker,docker in pod 并不关心底层的容器运行时是什么,可以是 docker 也可以是 containerd。在 pod 内运行和使用 docker 个人总结出以下三种比较合适的方式,可以根据不同的场景选择一个合适的:

sidecar

将 dind 容器作为 sidecar 容器 来运行,主容器通过 localhost 的方式访问 docker 的 2375/2376 TCP 端口。这种方案的好处就是如果创建了多个 Pod,各个 Pod 之间是相互独立的,dind 容器不会共享给其他 pod 使用,隔离性比较好。缺点也比较明显,每一个 Pod 都带一个 dind 容器占用的系统资源比较多,有点大材小用的感觉;

apiVersion: v1kind: Podmetadata:  name: dinp-sidecarspec:  containers:  - image: docker:20.10.12    name: debug    command: ["sleep", "3600"]    env:    - name: DOCKER_TLS_VERIFY      value: ""    - name: DOCKER_HOST      value: tcp://localhost:2375  - name: dind    image: docker:20.10.12-dind-rootless    args: ["--insecure-registry=$(REGISTRY)"]    env:    # 如果镜像仓库域名为自签证书,需要在这里配置 insecure-registry    - name: REGISTRY      value: hub.k8s.li    - name: DOCKER_TLS_CERTDIR      value: ""    - name: DOCKER_HOST      value: tcp://localhost:2375    securityContext:      privileged: true    tty: true    # 使用 docker info 命令就绪探针来确保 dind 容器正常启动     readinessProbe:      exec:        command: ["docker", "info"]      initialDelaySeconds: 10      failureThreshold: 6

daemonset

daemonset 则是在集群的每一个 Node 节点上运行一个 dind Pod,并且使用 hostNetwork 方式来暴露 2375/2376 TCP 端口。使用者则通过 status.hostIP 访问宿主机的 2375/2376 TCP 端口来与 docker 进行通信;另外再通过 hostPath 挂载的方式来将 dind 容器内的 /var/lib/docker 数据持久化存储下来,能够缓存一些数据提高镜像构建的效率。

apiVersion: apps/v1kind: DaemonSetmetadata:  name: dinp-daemonset  namespace: defaultspec:  selector:    matchLabels:      name: dinp-daemonset  template:    metadata:      labels:        name: dinp-daemonset    spec:      hostNetwork: true      containers:      - name: dind        image: docker:20.10.12-dind        args: ["--insecure-registry=$(REGISTRY)"]        env:        - name: REGISTRY          value: hub.k8s.li        - name: DOCKER_TLS_CERTDIR          value: ""        securityContext:          privileged: true        tty: true        volumeMounts:        - name: docker-storage          mountPath:  /var/lib/docker        readinessProbe:          exec:            command: ["docker", "info"]          initialDelaySeconds: 10          failureThreshold: 6        livenessProbe:          exec:            command: ["docker", "info"]          initialDelaySeconds: 60          failureThreshold: 10      volumes:      - name: docker-storage        hostPath:          path: /var/lib/docker

deployment

Deployment 方式则是在集群中部署一个或多个 dind Pod,使用者通过 service IP 来访问 docker 的 2375/2376 端口,如果是以非 TLS 方式启动 dind 容器,使用 service IP 来访问 docker 要比前面的 daemonset 使用 host IP 安全性要好一些。

apiVersion: apps/v1kind: Deploymentmetadata:  name: dinp-deployment  namespace: default  labels:    name: dinp-deploymentspec:  replicas: 1  selector:    matchLabels:      name: dinp-deployment  template:    metadata:      labels:        name: dinp-deployment    spec:      containers:      - name: dind        image: docker:20.10.12-dind        args: ["--insecure-registry=$(REGISTRY)"]        env:        - name: REGISTRY          value: hub.k8s.li        - name: DOCKER_TLS_CERTDIR          value: ""        - name: DOCKER_HOST          value: tcp://localhost:2375        securityContext:          privileged: true        tty: true        volumeMounts:        - name: docker-storage          mountPath:  /var/lib/docker        readinessProbe:          exec:            command: ["docker", "info"]          initialDelaySeconds: 10          failureThreshold: 6        livenessProbe:          exec:            command: ["docker", "info"]          initialDelaySeconds: 60          failureThreshold: 10      volumes:      - name: docker-storage        hostPath:          path: /var/lib/docker---kind: ServiceapiVersion: v1metadata:  # 定义 service name,使用者通过它来访问 docker 的 2375 端口  name: dinp-deploymentspec:  selector:    name: dinp-deployment  ports:  - protocol: TCP    port: 2375    targetPort: 2375

Jenkinsfile

在 Jenkins 的 podTemplate 模版里,可以根据 dinp 部署方式的不同选用以下几种不同的模版:

sidecare

Pod 内容器共享同一个网络协议栈,因此可以通过 localhost 来访问 docker 的 TCP 端口,另外最好使用 rootless 模式启动 dind 容器,这样能在同一节点上运行多个这样的 Pod 实例。

def JOB_NAME = "${env.JOB_BASE_NAME}"def BUILD_NUMBER = "${env.BUILD_NUMBER}"def POD_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"def K8S_CLUSTER = params.k8s_cluster ?: kubernetes// Kubernetes pod template to run.podTemplate(    cloud: K8S_CLUSTER,    namespace: "default",    name: POD_NAME,    label: POD_NAME,    yaml: """apiVersion: v1kind: Podspec:  containers:  - name: runner    image: golang:1.17-buster    imagePullPolicy: IfNotPresent    tty: true    env:    - name: DOCKER_HOST      vaule: tcp://localhost:2375    - name: DOCKER_TLS_VERIFY      value: ""  - name: jnlp    args: ["\$(JENKINS_SECRET)", "\$(JENKINS_NAME)"]    image: "jenkins/inbound-agent:4.11.2-4-alpine"    imagePullPolicy: IfNotPresent  - name: dind    image: docker:20.10.12-dind-rootless    args: ["--insecure-registry=$(REGISTRY)"]    env:    - name: REGISTRY      value: hub.k8s.li    - name: DOCKER_TLS_CERTDIR      value: ""    securityContext:      privileged: true    tty: true    readinessProbe:      exec:        command: ["docker", "info"]      initialDelaySeconds: 10      failureThreshold: 6""",) {    node(POD_NAME) {        container("runner") {            stage("Checkout") {                retry(10) {                    checkout([                        $class: 'GitSCM',                        branches: scm.branches,                        doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations,                        extensions: [[$class: 'CloneOption', noTags: false, shallow: false, depth: 0, reference: '']],                        userRemoteConfigs: scm.userRemoteConfigs,                    ])                }            }            stage("Build") {                sh """                # make docker-build                docker build -t app:v1.0.0-alpha.1 .                """            }        }    }}

daemonset

由于使用的是 hostNetwork,因此可以通过 host IP 来访问 docker 的 TCP 端口,当然也可以像 deployment 那样通过 service Name 来访问,在这里就不演示了。

def JOB_NAME = "${env.JOB_BASE_NAME}"def BUILD_NUMBER = "${env.BUILD_NUMBER}"def POD_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"def K8S_CLUSTER = params.k8s_cluster ?: kubernetes// Kubernetes pod template to run.podTemplate(    cloud: K8S_CLUSTER,    namespace: "default",    name: POD_NAME,    label: POD_NAME,    yaml: """apiVersion: v1kind: Podspec:  containers:  - name: runner    image: golang:1.17-buster    imagePullPolicy: IfNotPresent    tty: true    env:    - name: DOCKER_HOST      valueFrom:        fieldRef:          fieldPath: status.hostIP    - name: DOCKER_TLS_VERIFY      value: ""  - name: jnlp    args: ["\$(JENKINS_SECRET)", "\$(JENKINS_NAME)"]    image: "jenkins/inbound-agent:4.11.2-4-alpine"    imagePullPolicy: IfNotPresent""",) {    node(POD_NAME) {        container("runner") {            stage("Checkout") {                retry(10) {                    checkout([                        $class: 'GitSCM',                        branches: scm.branches,                        doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations,                        extensions: [[$class: 'CloneOption', noTags: false, shallow: false, depth: 0, reference: '']],                        userRemoteConfigs: scm.userRemoteConfigs,                    ])                }            }            stage("Build") {                sh """                # make docker-build                docker build -t app:v1.0.0-alpha.1 .                """            }        }    }}

deployment

通过 service name 访问 docker,其他参数和 daemonset 都是相同的

def JOB_NAME = "${env.JOB_BASE_NAME}"def BUILD_NUMBER = "${env.BUILD_NUMBER}"def POD_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"def K8S_CLUSTER = params.k8s_cluster ?: kubernetes// Kubernetes pod template to run.podTemplate(    cloud: K8S_CLUSTER,    namespace: "default",    name: POD_NAME,    label: POD_NAME,    yaml: """apiVersion: v1kind: Podspec:  containers:  - name: runner    image: golang:1.17-buster    imagePullPolicy: IfNotPresent    tty: true    env:    - name: DOCKER_HOST       value: tcp://dinp-deployment:2375    - name: DOCKER_TLS_VERIFY      value: ""  - name: jnlp    args: ["\$(JENKINS_SECRET)", "\$(JENKINS_NAME)"]    image: "jenkins/inbound-agent:4.11.2-4-alpine"    imagePullPolicy: IfNotPresent""",) {    node(POD_NAME) {        container("runner") {            stage("Checkout") {                retry(10) {                    checkout([                        $class: 'GitSCM',                        branches: scm.branches,                        doGenerateSubmoduleConfigurations: scm.doGenerateSubmoduleConfigurations,                        extensions: [[$class: 'CloneOption', noTags: false, shallow: false, depth: 0, reference: '']],                        userRemoteConfigs: scm.userRemoteConfigs,                    ])                }            }            stage("Build") {                sh """                # make docker-build                docker build -t app:v1.0.0-alpha.1 .                """            }        }    }}

其他

readinessProbe

有些时候 dind 无法正常启动,所以一定要设置就绪探针,来确保 diind 容器能够正常启动

readinessProbe:  exec:    command: ["docker", "info"]  initialDelaySeconds: 10  failureThreshold: 6

2375/2376 端口

docker 默认是以 TLS 方式启动,监听端口为 2376,如果设置环境变量 DOCKER_TLS_CERTDIR 为空则就以非 TLS 模式启动,监听端口为 2375,这时就不会校验 TLS 证书。如果使用 2376 端口,则就需要一个持久化存储来将 docker 生成的证书共享给客户端,这点比较麻烦。因此如果不想瞎折腾还是使用 2375 非 TLS 方式吧 😂。

dinp 必须以开启 privileged: true

以 pod 方式运行 docker,无论是否是 rootless 模式,都要在 pod 容器的 securityContext 中设置 privileged: true,否则 pod 无法正常启动。而且 rootless 模式也有一定的限制,需要依赖一些内核的特性,目前也只是实验阶段,没有特殊的需求还是尽量不要使用 rootless 特性吧。

[root@localhost ~]# kubectl logs -f dinp-sidecarerror: a container name must be specified for pod dinp-sidecar, choose one of: [debug dind][root@localhost ~]# kubectl logs -f dinp-sidecar -c dindDevice "ip_tables" does not exist.ip_tables              27126  4 iptable_raw,iptable_mangle,iptable_filter,iptable_natmodprobe: can't change directory to '/lib/modules': No such file or directoryWARN[0000] failed to mount sysfs, falling back to read-only mount: operation not permittedWARN[0000] failed to mount sysfs: operation not permittedopen: No such file or directory[rootlesskit:child ] error: executing [[ip tuntap add name tap0 mode tap] [ip link set tap0 address 02:50:00:00:00:01]]: exit status 1

rootless user.max_user_namespaces

rootless 模式下需要依赖一些内核参数 Run the Docker daemon as a non-root user (Rootless mode)。在 CentOS 7.9 上会出现 dind-rootless: failed to start up dind rootless in k8s due to max_user_namespaces 问题。解决方案是在修改一下 user.max_user_namespaces=28633 内核参数。

Add user.max_user_namespaces=28633 to /etc/sysctl.conf (or /etc/sysctl.d) and run sudo sysctl -p

[root@localhost ~]# kubectl get pod -wNAME                              READY   STATUS   RESTARTS     AGEdinp-deployment-cf488bfd8-g8vxx   0/1     CrashLoopBackOff   1 (2s ago)   4s[root@localhost ~]# kubectl logs -f dinp-deployment-cf488bfd8-m5cmsDevice "ip_tables" does not exist.ip_tables              27126  5 iptable_raw,iptable_mangle,iptable_filter,iptable_natmodprobe: can't change directory to '/lib/modules': No such file or directoryerror: attempting to run rootless dockerd but need 'user.max_user_namespaces' (/proc/sys/user/max_user_namespaces) set to a sufficiently large value

非 rootless 模式下同一 node 节点只能运行一个 dinp

如果是使用 deployment 方式部署 dinp,一个 node 节点上只能有一个 dinp Pod,多余的 Pod 无法正常启动。因此如果想要运行多个 dinp Pod,建议使用 daemonset 方式运行它;

[root@localhost ~]# kubectl get deployNAME              READY   UP-TO-DATE   AVAILABLE   AGEdinp-deployment   1/3     3            1           4m16s[root@localhost ~]# kubectl get pod -wNAME                               READY   STATUS    RESTARTS      AGEdinp-deployment-547bd9bb6d-2mn6c   0/1     Running   3 (61s ago)   4m9sdinp-deployment-547bd9bb6d-8ht8l   1/1     Running   0             4m9sdinp-deployment-547bd9bb6d-x5vpv   0/1     Running   3 (61s ago)   4m9s[root@localhost ~]# kubectl logs -f dinp-deployment-547bd9bb6d-2mn6cINFO[2022-03-14T14:14:10.905652548Z] Starting upWARN[2022-03-14T14:14:10.906986721Z] could not change group /var/run/docker.sock to docker: group docker not foundWARN[2022-03-14T14:14:10.907249071Z] Binding to IP address without --tlsverify is insecure and gives root access on this machine to everyone who has access to your network.  host="tcp://0.0.0.0:2375"WARN[2022-03-14T14:14:10.907269951Z] Binding to an IP address, even on localhost, can also give access to scripts run in a browser. Be safe out there!  host="tcp://0.0.0.0:2375"WARN[2022-03-14T14:14:11.908057635Z] Binding to an IP address without --tlsverify is deprecated. Startup is intentionally being slowed down to show this message  host="tcp://0.0.0.0:2375"WARN[2022-03-14T14:14:11.908103696Z] Please consider generating tls certificates with client validation to prevent exposing unauthenticated root access to your network  host="tcp://0.0.0.0:2375"WARN[2022-03-14T14:14:11.908114541Z] You can override this by explicitly specifying '--tls=false' or '--tlsverify=false'  host="tcp://0.0.0.0:2375"WARN[2022-03-14T14:14:11.908125477Z] Support for listening on TCP without authentication or explicit intent to run without authentication will be removed in the next release  host="tcp://0.0.0.0:2375"INFO[2022-03-14T14:14:26.914587276Z] libcontainerd: started new containerd process  pid=41INFO[2022-03-14T14:14:26.914697125Z] parsed scheme: "unix"                         module=grpcINFO[2022-03-14T14:14:26.914710376Z] scheme "unix" not registered, fallback to default scheme  module=grpcINFO[2022-03-14T14:14:26.914785052Z] ccResolverWrapper: sending update to cc: {[{unix:///var/run/docker/containerd/containerd.sock  <nil> 0 <nil>}] <nil> <nil>}  module=grpcINFO[2022-03-14T14:14:26.914796039Z] ClientConn switching balancer to "pick_first"  module=grpcINFO[2022-03-14T14:14:26.930311832Z] starting containerd                           revision=7b11cfaabd73bb80907dd23182b9347b4245eb5d version=v1.4.12INFO[2022-03-14T14:14:26.953641900Z] loading plugin "io.containerd.content.v1.content"...  type=io.containerd.content.v1INFO[2022-03-14T14:14:26.953721059Z] loading plugin "io.containerd.snapshotter.v1.aufs"...  type=io.containerd.snapshotter.v1INFO[2022-03-14T14:14:26.960295816Z] skip loading plugin "io.containerd.snapshotter.v1.aufs"...  error="aufs is not supported (modprobe aufs failed: exit status 1 \"ip: can't find device 'aufs'\\nmodprobe: can't change directory to '/lib/modules': No such file or directory\\n\"): skip plugin" type=io.containerd.snapshotter.v1INFO[2022-03-14T14:14:26.960329840Z] loading plugin "io.containerd.snapshotter.v1.btrfs"...  type=io.containerd.snapshotter.v1INFO[2022-03-14T14:14:26.960524514Z] skip loading plugin "io.containerd.snapshotter.v1.btrfs"...  error="path /var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.btrfs (xfs) must be a btrfs filesystem to be used with the btrfs snapshotter: skip plugin" type=io.containerd.snapshotter.v1INFO[2022-03-14T14:14:26.960537441Z] loading plugin "io.containerd.snapshotter.v1.devmapper"...  type=io.containerd.snapshotter.v1WARN[2022-03-14T14:14:26.960558843Z] failed to load plugin io.containerd.snapshotter.v1.devmapper  error="devmapper not configured"INFO[2022-03-14T14:14:26.960569516Z] loading plugin "io.containerd.snapshotter.v1.native"...  type=io.containerd.snapshotter.v1INFO[2022-03-14T14:14:26.960593224Z] loading plugin "io.containerd.snapshotter.v1.overlayfs"...  type=io.containerd.snapshotter.v1INFO[2022-03-14T14:14:26.960678728Z] loading plugin "io.containerd.snapshotter.v1.zfs"...  type=io.containerd.snapshotter.v1INFO[2022-03-14T14:14:26.960814844Z] skip loading plugin "io.containerd.snapshotter.v1.zfs"...  error="path /var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.zfs must be a zfs filesystem to be used with the zfs snapshotter: skip plugin" type=io.containerd.snapshotter.v1INFO[2022-03-14T14:14:26.960827133Z] loading plugin "io.containerd.metadata.v1.bolt"...  type=io.containerd.metadata.v1WARN[2022-03-14T14:14:26.960839223Z] could not use snapshotter devmapper in metadata plugin  error="devmapper not configured"INFO[2022-03-14T14:14:26.960848698Z] metadata content store policy set             policy=sharedWARN[2022-03-14T14:14:27.915528371Z] grpc: addrConn.createTransport failed to connect to {unix:///var/run/docker/containerd/containerd.sock  <nil> 0 <nil>}. Err :connection error: desc = "transport: error while dialing: dial unix:///var/run/docker/containerd/containerd.sock: timeout". Reconnecting...  module=grpcWARN[2022-03-14T14:14:30.722257725Z] grpc: addrConn.createTransport failed to connect to {unix:///var/run/docker/containerd/containerd.sock  <nil> 0 <nil>}. Err :connection error: desc = "transport: error while dialing: dial unix:///var/run/docker/containerd/containerd.sock: timeout". Reconnecting...  module=grpcWARN[2022-03-14T14:14:35.549453706Z] grpc: addrConn.createTransport failed to connect to {unix:///var/run/docker/containerd/containerd.sock  <nil> 0 <nil>}. Err :connection error: desc = "transport: error while dialing: dial unix:///var/run/docker/containerd/containerd.sock: timeout". Reconnecting...  module=grpcWARN[2022-03-14T14:14:41.759010407Z] grpc: addrConn.createTransport failed to connect to {unix:///var/run/docker/containerd/containerd.sock  <nil> 0 <nil>}. Err :connection error: desc = "transport: error while dialing: dial unix:///var/run/docker/containerd/containerd.sock: timeout". Reconnecting...  module=grpcfailed to start containerd: timeout waiting for containerd to start

/var/lib/docker 不支持共享存储

陈少文博主曾在 《/var/lib/docker 能不能挂载远端存储》提到过 docker 目前并支持将 /var/lib/docker 挂载远程存储使用,因此建议使用 hostPath 的方式保存 docker 的持久化存储数据。

本次测试使用的 Docker 版本为 20.10.6,不能将 /var/lib/docker 挂载远程存储使用。主要原因是容器的实现依赖于内核的能力(xttrs),而类似 NFS Server 这种远程存储无法提供这些能力。如果采用 Device Mapper 进行映射,使用磁盘挂载存在可行性,但只能用于迁移而不能实现共享。

INFO[2022-03-13T13:43:08.750810130Z] ClientConn switching balancer to "pick_first"  module=grpcERRO[2022-03-13T13:43:08.781932359Z] failed to mount overlay: invalid argument     storage-driver=overlay2ERRO[2022-03-13T13:43:08.782078828Z] exec: "fuse-overlayfs": executable file not found in $PATH  storage-driver=fuse-overlayfsERRO[2022-03-13T13:43:08.793311119Z] AUFS was not found in /proc/filesystems       storage-driver=aufsERRO[2022-03-13T13:43:08.813505621Z] failed to mount overlay: invalid argument     storage-driver=overlayERRO[2022-03-13T13:43:08.813529990Z] Failed to built-in GetDriver graph devicemapper /var/lib/dockerINFO[2022-03-13T13:43:08.897769363Z] Loading containers: start.WARN[2022-03-13T13:43:08.919252078Z] Running modprobe bridge br_netfilter failed with message: ip: can't find device 'bridge'[root@localhost dinp]# kubectl exec -it dinp-sidecar -c debug sh/ # docker pull alpineUsing default tag: latestError response from daemon: error creating temporary lease: file resize error: truncate /var/lib/docker/containerd/daemon/io.containerd.metadata.v1.bolt/meta.db: bad file descriptor: unknown

参考

☑️ ☆

使用 kubectl 自动归档 argo workflow 日志

项目上使用到 argo-workflow 作为工作流引擎来编排运行一些 超融合 集群部署相关的任务,整套环境运行在一个单节点的 K3s 上。之所以选择 argo-workflow + K3s 的搭配主要是想尽可能少地占用系统资源,因为这套环境将来会运行在各种硬件配置不同的笔记本电脑上 😂。综合调研了一些常见的 K8s 部署工具最终就选择了系统资源占用较少的 K3s。

现在项目的一个需求就是在集群部署完成或失败之后需要将 workflow 的日志归档保存下来。虽然可以在 workflow 的 spec 字段中使用 archiveLogs: true 来让 argo 帮我们自动归档日志,但这个特性依赖于一个 S3 对象存储 Artifact Repository 。这就意味着还要再部署一个支持 S3 对象存储的组件比如 Minio ,直接把我给整不会了 🌚

其实嘛这个需求很简单的,我就想保存一个日志文件而已,你还再让我安装一个 Minio,实在是太过分了!本来系统的资源十分有限,需要尽可能减少安装一些不必要依赖,为的就是将资源利用率将到最低。但现在为了归档存储一个日志文件储而大动干戈装一个 minio 实在是不划算。这就好比你费了好大功夫部署一套 3 节点的 kubernetes 集群,然而就为了运行一个静态博客那样滑稽 😂

Deployed my blog on Kubernetes pic.twitter.com/XHXWLrmYO4

— For DevOps Eyes Only (@dexhorthy) April 24, 2017

对于咱这种 用不起 S3 对象存储的穷人家孩子,还是想一些其他办法吧,毕竟自己动手丰衣足食。

kubectl

实现起来也比较简单,对于咱这种 YAML 工程师来说,kubectl 自然再熟悉不过了。想要获取 workflow 的日志,只需要通过 kubectl logs 命令获取出 workflow 所创的 pod 日志就行了呀,要什么 S3 对象存储 😖

筛选 pod

对于同一个 workflow 来将,每个 stage 所创建出来的 pod name 有一定的规律。在定义 workflow 的时候,generateName 参数通常使用 ${name}- 格式。以 - 作为分隔符,最后一个字段是随机生成的一个数字 ID,倒数第二个字段则是 argo 随机生成的 workflow ID,剩余前面的字符则是我们定义的 generateName。

apiVersion: argoproj.io/v1alpha1kind: Workflowmetadata:  generateName: archive-log-test-
archive-log-test-jzt8n-3498199655                          0/2     Completed   0               4m18sarchive-log-test-jzt8n-3618624526                          0/2     Completed   0               4m8sarchive-log-test-jzt8n-2123203324                          0/2     Completed   0               3m58s

在 pod 的 labels 中同样也包含着该 workflow 所对应的 ID,因此我们可以根据此 labels 过滤出该 workflow 所创建出来的 pod。

apiVersion: v1kind: Podmetadata:  annotations:    workflows.argoproj.io/node-id: archive-log-test-jzt8n-3498199655    workflows.argoproj.io/node-name: archive-log-test-jzt8n[0].list-default-running-pods  creationTimestamp: "2022-02-28T12:53:32Z"  labels:    workflows.argoproj.io/completed: "true"    workflows.argoproj.io/workflow: archive-log-test-jzt8n  name: archive-log-test-jzt8n-3498199655  namespace: default  ownerReferences:  - apiVersion: argoproj.io/v1alpha1    blockOwnerDeletion: true    controller: true    kind: Workflow    name: archive-log-test-jzt8n    uid: e91df2cb-b567-4cf0-9be5-3dd6c72854cd  resourceVersion: "1251330"  uid: ce37a709-8236-445b-8d00-a7926fa18ed0

通过 -l lables 过滤出一个 workflow 所创建的 pod;通过 --sort-by 以创建时间进行排序;通过 -o name 只输出 pod 的 name:

$ kubectl get pods -l workflows.argoproj.io/workflow=archive-log-test-jzt8n --sort-by='.metadata.creationTimestamp' -o namepod/archive-log-test-jzt8n-3498199655pod/archive-log-test-jzt8n-3618624526pod/archive-log-test-jzt8n-2123203324

获取日志

通过上面的步骤我们就可以获取到一个 workflow 所创建的 pod 列表。然后再通过 kubectl logs 命令获取 pod 中 main 容器的日志,为方便区分日志的所对应的 workflow ,我们就以 workflow 的 ID 为前缀名。

$ kubectl logs archive-log-test-jzt8n-3618624526 -c main
LOG_PATH=/var/logNAME=archive-log-test-jzt8nkubectl get pods -l workflows.argoproj.io/workflow=${NAME} \--sort-by='.metadata.creationTimestamp' -o name \| xargs -I {} -t kubectl logs {} -c main >> ${LOG_PATH}/${NAME}.log

workflow

根据 argo-workflow 官方提供的 exit-handlers.yaml example,我们就照葫芦画瓢搓一个 workflow 退出后自动调用使用 kubectl 获取 workflow 日志的一个 step,定义的 exit-handler 内容如下:

  - name: exit-handler    container:      name: "kubectl"      image: lachlanevenson/k8s-kubectl:v1.23.2      command:        - sh        - -c        - |          kubectl get pods -l workflows.argoproj.io/workflow=${POD_NAME%-*} \          --sort-by=".metadata.creationTimestamp" -o name | grep -v ${POD_NAME} \          | xargs -I {} -t kubectl logs {} -c main >> ${LOG_PATH}/${POD_NAME%-*}.log      env:        - name: POD_NAME          valueFrom:            fieldRef:              fieldPath: metadata.name        - name: LOG_PATH          value: /var/log/workflow      resources: {}      volumeMounts:        - name: nfs-datastore          mountPath: /var/log/workflow    retryStrategy:      limit: "5"      retryPolicy: OnFailureentrypoint: archive-log-testserviceAccountName: defaultvolumes:  - name: nfs-datastore    nfs:      server: NFS_SERVER      path: /data/workflow/logonExit: exit-handler

将上述定义的 exit-handler 内容复制粘贴到你的 workflow spec 配置中就可以。由于日志需要持久化存储,我这里使用的是 NFS 存储,也可以根据自己的需要换成其他存储,只需要修改一下 volumes 配置即可。

完整的 workflow example 如下:

apiVersion: argoproj.io/v1alpha1kind: Workflowmetadata:  generateName: archive-log-test-  namespace: defaultspec:  templates:    - name: archive-log-test      steps:        - - name: list-default-running-pods            template: kubectl            arguments:              parameters:                - name: namespace                  value: default        - - name: list-kube-system-running-pods            template: kubectl            arguments:              parameters:                - name: namespace                  value: kube-system    - name: kubectl      inputs:        parameters:          - name: namespace      container:        name: "kubectl"        image: lachlanevenson/k8s-kubectl:v1.23.2        command:          - sh          - -c          - |            kubectl get pods --field-selector=status.phase=Running -n {{inputs.parameters.namespace}}    - name: exit-handler      container:        name: "kubectl"        image: lachlanevenson/k8s-kubectl:v1.23.2        command:          - sh          - -c          - |            kubectl get pods -l workflows.argoproj.io/workflow=${POD_NAME%-*} \            --sort-by=".metadata.creationTimestamp" -o name | grep -v ${POD_NAME} \            | xargs -I {} -t kubectl logs {} -c main >> ${LOG_PATH}/${POD_NAME%-*}.log        env:          - name: POD_NAME            valueFrom:              fieldRef:                fieldPath: metadata.name          - name: LOG_PATH            value: /var/log/workflow        resources: {}        volumeMounts:          - name: nfs-datastore            mountPath: /var/log/workflow      retryStrategy:        limit: "5"        retryPolicy: OnFailure  entrypoint: archive-log-test  serviceAccountName: default  volumes:    - name: nfs-datastore      nfs:        server: NFS_SERVER        path: /data/workflow/log  onExit: exit-handler
☑️ ☆

VMware Tanzu kubernetes 发行版部署尝鲜

之前接触的 Kubernetes 集群部署工具大多数都是依赖于 ssh 连接到待部署的节点上进行部署操作,这样就要求部署前需要提前准备好集群节点,且要保证这些节点的网络互通以及时钟同步等问题。类似于 kubespray 或者 kubekey 这些部署工具是不会去管这些底层的 IaaS 资源的创建,是要自己提前准备好。但是在一些企业私有云环境中,使用了如 VMware vShpereOpenStack 这些虚拟化平台,是可以将 K8s 集群部署与 IaaS 资源创建这两步统一起来的,这样就可以避免手动创建和配置虚拟机这些繁琐的步骤。

目前将 IaaS 资源创建与 K8s 集群部署结合起来也有比较成熟的方案,比如基于 cluster-api 项目的 tanzu 。本文就以 VMware Tanzu 社区版 为例在一台物理服务器上,从安装 ESXi OS 到部署完成 Tanzu Workload 集群,来体验一下这种部署方案的与众不同之处。

部署流程

  • 下载依赖文件
  • 安装 govc 依赖
  • 安装 ESXi OS
  • 安装 vCenter
  • 配置 vCenter
  • 创建 bootstrap 虚拟机
  • 初始化 bootstrap 节点
  • 部署 Tanzu Manager 集群
  • 部署 Tanzu Workload 集群

劝退三连 😂

  • 需要有一个 VMware 的账户 用于下载一些 ISO 镜像和虚拟机模版;
  • 需要有一台物理服务器,推荐最低配置 8C 32G,至少 256GB 存储;
  • 需要一台 DHCP 服务器,由于默认是使用 DHCP 获取 IP 来分配给虚拟机的,因此 ESXi 所在的 VM Network 网络中必须有一台 DHCP 服务器用于给虚拟机分配 IP;

下载依赖文件

整个部署流程所需要的依文件赖如下,可以先将这些依赖下载到本地的机器上,方便后续使用。

root@devbox:/root/tanzu # tree -sh.├── [  12M]  govc_Linux_x86_64.tar.gz├── [ 895M]  photon-3-kube-v1.21.2+vmware.1-tkg.2-12816990095845873721.ova├── [ 225M]  photon-ova-4.0-c001795b80.ova├── [ 170M]  tce-linux-amd64-v0.9.1.tar.gz├── [ 9.0G]  VMware-VCSA-all-7.0.3-18778458.iso└── [ 390M]  VMware-VMvisor-Installer-7.0U2a-17867351.x86_64.iso
文件用途下载方式
VMware-VMvisor-Installer-7.0U2a-17867351.x86_64.iso安装 ESXi OSVMware 需账户
VMware-VCSA-all-7.0.3-19234570.iso安装 vCenterVMware 需账户
photon-ova-4.0-c001795b80.ovabootstrap 节点VMware
photon-3-kube-v1.21.2+vmware.1-tkg.2-12816990095845873721.ovatanzu 集群节点VMware 需账户
tce-linux-amd64-v0.9.1.tar.gztanzu 社区版GitHub release
govc_Linux_x86_64.tar.gz安装/配置 vCenterGitHub release

注意 ESXi 和 vCenter 的版本最好是 7.0 及以上,我只在 ESXi 7.0.2 和 vCenter 7.0.3 上测试过,其他版本可能会有些差异;另外 ESXi 的版本不建议使用最新的 7.0.3,因为有比较严重的 bug,官方也建议用户生产环境不要使用该版本了 vSphere 7.0 Update 3 Critical Known Issues - Workarounds & Fix (86287)

安装 govc 及依赖

在本地机器上安装好 govc 和 jq,这两个工具后面在配置 vCenter 的时候会用到。

  • macOS
$ brew install govc jq
  • Debian/Ubuntu
$ tar -xf govc_Linux_x86_64.tar.gz -C /usr/local/bin$ apt install jq -y
  • 其他 Linux 可以在 govc 和 jq 的 GitHub 上下载对应的安装文件进行安装。

安装 ESXi OS

ESXi OS 的安装网上有很多教程,没有太多值得讲解的地方,因此就参照一下其他大佬写的博客或者官方的安装文档 VMware ESXi 安装和设置 来就行;需要注意一点,ESXi OS 安装时 VMFSL 分区将会占用大量的存储空间,这将会使得 ESXi OS 安装所在的磁盘最终创建出来的 datastore 比预期小很多,而且这个 VMFSL 分区在安装好之后就很难再做调整了。因此如果磁盘存储空间比较紧张,在安装 ESXi OS 之前可以考虑下如何去掉这个分区;或者和我一样将 ESXI OS 安装在了一个 16G 的 USB Dom 盘上,不过生产环境不建议采用这种方案 😂(其实个人觉着安装在 U 盘上问题不大,ESXi OS 启动之后是加载到内存中运行的,不会对 U 盘有大量的读写操作,只不过在机房中 U 盘被人不小心拔走就凉了。

  • 设置 govc 环境变量
# ESXi 节点的 IPexport ESXI_IP="192.168.18.47"# ESXi 登录的用户名,初次安装后默认为 rootexport GOVC_USERNAME="root"# 在 ESXi 安装时设置的 root 密码export GOVC_PASSWORD="admin@2022"# 允许不安全的 SSL 连接export GOVC_INSECURE=trueexport GOVC_URL="https://${ESXI_IP}"export GOVC_DATASTORE=datastore1
  • 测试 govc 是否能正常连接 ESXi 主机
Name:              localhost.local  Path:            /ha-datacenter/host/localhost/localhost  Manufacturer:    Dell  Logical CPUs:    20 CPUs @ 2394MHz  Processor type:  Intel(R) Xeon(R) Silver 4210R CPU @ 2.40GHz  CPU usage:       579 MHz (1.2%)  Memory:          261765MB  Memory usage:    16457 MB (6.3%)  Boot time:       2022-02-02 11:53:59.630124 +0000 UTC  State:           connected

安装 vCenter

按照 VMware 官方的 vCenter 安装文档 关于 vCenter Server 安装和设置 来安装实在是过于繁琐,其实官方的 ISO 安装方式无非是运行一个 installer web 服务,然后在浏览器上配置好 vCenter 虚拟机的参数,再将填写的配置信息在部署 vcsa 虚拟机的时候注入到 ova 的配置参数中。

知道这个安装过程的原理之后我们也可以自己配置 vCenter 的参数信息,然后通过 govc 来部署 ova;这比使用 UI 的方式简单方便很多,最终只需要填写一个配置文件,一条命令就可以部署完成啦。

  • 首先是挂载 vCenter 的 ISO,找到 vcsa ova 文件,它是 vCenter 虚拟机的模版
$ mount -o loop VMware-VCSA-all-7.0.3-18778458.iso /mnt$ ls /mnt/vcsa/VMware-vCenter-Server-Appliance-7.0.3.00100-18778458_OVF10.ova/mnt/vcsa/VMware-vCenter-Server-Appliance-7.0.3.00100-18778458_OVF10.ova
  • 根据自己的环境信息修改下面安装脚本中的相关配置:
#!/usr/bin/env bashVCSA_OVA_FILE=$1set -o errexitset -o nounsetset -o pipefail# ESXi 的 IP 地址export ESXI_IP="192.168.18.47"# ESXi 的用户名export GOVC_USERNAME="root"# ESXI 的密码export GOVC_PASSWORD="admin@2020"# 安装 vCenter 虚拟机使用的 datastore 名称export GOVC_DATASTORE=datastore1export GOVC_INSECURE=trueexport GOVC_URL="https://${ESXI_IP}"# vCenter 的登录密码VM_PASSWORD="admin@2020"# vCenter 的 IP 地址VM_IP=192.168.20.92# vCenter 虚拟机的名称VM_NAME=vCenter-Server-Appliance# vCenter 虚拟机使用的网络VM_NETWORK="VM Network"# DNS 服务器VM_DNS="223.6.6.6"# NTP 服务器VM_NTP="0.pool.ntp.org"deploy_vcsa_vm(){    config=$(govc host.info -k -json | jq -r '.HostSystems[].Config')    gateway=$(jq -r '.Network.IpRouteConfig.DefaultGateway' <<<"$config")    route=$(jq -r '.Network.RouteTableInfo.IpRoute[] | select(.DeviceName == "vmk0") | select(.Gateway == "0.0.0.0")' <<<"$config")    prefix=$(jq -r '.PrefixLength' <<<"$route")    opts=(        cis.vmdir.password=${VM_PASSWORD}        cis.appliance.root.passwd=${VM_PASSWORD}        cis.appliance.root.shell=/bin/bash        cis.deployment.node.type=embedded        cis.vmdir.domain-name=vsphere.local        cis.vmdir.site-name=VCSA        cis.appliance.net.addr.family=ipv4        cis.appliance.ssh.enabled=True        cis.ceip_enabled=False        cis.deployment.autoconfig=True        cis.appliance.net.addr=${VM_IP}        cis.appliance.net.prefix=${prefix}        cis.appliance.net.dns.servers=${VM_DNS}        cis.appliance.net.gateway=$gateway        cis.appliance.ntp.servers="${VM_NTP}"        cis.appliance.net.mode=static    )    props=$(printf -- "guestinfo.%s\n" "${opts[@]}" | jq --slurp -R 'split("\n") | map(select(. != "")) | map(split("=")) | map({"Key": .[0], "Value": .[1]})')    cat <<EOF | govc import.${VCSA_OVA_FILE##*.} -options - "${VCSA_OVA_FILE}"    {    "Name": "${VM_NAME}",    "Deployment": "tiny",    "DiskProvisioning": "thin",    "IPProtocol": "IPv4",    "Annotation": "VMware vCenter Server Appliance",    "PowerOn": false,    "WaitForIP": false,    "InjectOvfEnv": true,    "NetworkMapping": [        {        "Name": "Network 1",        "Network": "${VM_NETWORK}"        }    ],    "PropertyMapping": ${props}    }EOF}deploy_vcsa_vmgovc vm.change -vm "${VM_NAME}" -g vmwarePhoton64Guestgovc vm.power -on "${VM_NAME}"govc vm.ip -a "${VM_NAME}"
  • 通过脚本安装 vCenter,指定第一参数为 OVA 的绝对路径。运行完后将会自动将 ova 导入到 vCenter,并启动虚拟机;
# 执行该脚本,第一个参数传入 vCenter ISO 中 vcsa ova 文件的绝对路径$ bash install-vcsa.sh /mnt/vcsa/VMware-vCenter-Server-Appliance-7.0.3.00100-18778458_OVF10.ova[03-02-22 18:40:19] Uploading VMware-vCenter-Server-Appliance-7.0.3.00100-18778458_OVF10-disk1.vmdk... OK[03-02-22 18:41:09] Uploading VMware-vCenter-Server-Appliance-7.0.3.00100-18778458_OVF10-disk2.vmdk... (29%, 52.5MiB/s)[03-02-22 18:43:08] Uploading VMware-vCenter-Server-Appliance-7.0.3.00100-18778458_OVF10-disk2.vmdk... OK[03-02-22 18:43:08] Injecting OVF environment...Powering on VirtualMachine:3... OKfe80::20c:29ff:fe03:2f80
  • 设置 vCenter 登录的环境变量,我们使用 govc 来配置 vCenter,通过浏览器 Web UI 的方式配置起来效率有点低,不如 govc 命令一把梭方便 😂
export GOVC_URL="https://192.168.20.92"export GOVC_USERNAME="administrator@vsphere.local"export GOVC_PASSWORD="admin@2022"export GOVC_INSECURE=trueexport GOVC_DATASTORE=datastore1
  • 虚拟机启动后将自动进行 vCenter 的安装配置,等待一段时间 vCenter 安装好之后,使用 govc about 查看 vCenter 的信息,如果能正确或渠道说明 vCenter 就安装好了;
$ govc aboutFullName:     VMware vCenter Server 7.0.3 build-18778458Name:         VMware vCenter ServerVendor:       VMware, Inc.Version:      7.0.3Build:        18778458OS type:      linux-x64API type:     VirtualCenterAPI version:  7.0.3.0Product ID:   vpxUUID:         0b49e119-e38f-4fbc-84a8-d7a0e548027d

配置 vCenter

这一步骤主要是配置 vCenter:创建 Datacenter、cluster、folder 等资源,并将 ESXi 主机添加到 cluster 中;

  • 配置 vCenter
# 创建 Datacenter 数据中心$ govc datacenter.create SH-IDC# 创建 Cluster 集群$ govc cluster.create -dc=SH-IDC Tanzu-Cluster# 将 ESXi 主机添加到 Cluster 当中$ govc cluster.add -dc=SH-IDC -cluster=Tanzu-Cluster -hostname=192.168.18.47 --username=root -password='admin@2020' -noverify# 创建 folder,用于将 Tanzu 的节点虚拟机存放到该文件夹下$ govc folder.create /SH-IDC/vm/Tanzu-node# 导入 tanzu 汲取节点的虚拟机 ova 模版$ govc import.ova -dc='SH-IDC' -ds='datastore1' photon-3-kube-v1.21.2+vmware.1-tkg.2-12816990095845873721.ova# 将虚拟机转换为模版,后续 tanzu 集群将以该模版创建虚拟机$ govc vm.markastemplate photon-3-kube-v1.21.2

初始化 bootstrap 节点

bootstrap 节点节点是用于运行 tanzu 部署工具的节点,官方是支持 Linux/macOS/Windows 三种操作系统的,但有一些比较严格的要求:

Arch: x86; ARM is currently unsupported
RAM: 6 GB
CPU: 2
Docker Add your non-root user account to the docker user group. Create the group if it does not already exist. This lets the Tanzu CLI access the Docker socket, which is owned by the root user. For more information, see steps 1 to 4 in the Manage Docker as a non-root user procedure in the Docker documentation.
Kubectl
Latest version of Chrome, Firefox, Safari, Internet Explorer, or Edge
System time is synchronized with a Network Time Protocol (NTP) server.
Ensure your bootstrap machine is using cgroup v1. For more information, see Check and set the cgroup.

在这里为了避免这些麻烦的配置,我就直接使用的 VMware 官方的 Photon OS 4.0 Rev2 ,下载 OVA 格式的镜像直接导入到 ESXi 主机启动一台虚拟机即可,能节省不少麻烦的配置;还有一个好处就是在一台单独的虚拟机上运行 tanzu 部署工具不会污染本地的开发环境。

$ wget https://packages.vmware.com/photon/4.0/Rev2/ova/photon-ova-4.0-c001795b80.ova# 导入 OVA 虚拟机模版$ govc import.ova -ds='datastore1' -name bootstrap-node photon-ova-4.0-c001795b80.ova# 修改一下虚拟机的配置,调整为 4C8G$ govc vm.change -c 4 -m 8192 -vm bootstrap-node# 开启虚拟机$ govc vm.power -on bootstrap-node# 查看虚拟机获取到的 IPv4 地址$ govc vm.ip -a -wait 1m bootstrap-node$ ssh root@192.168.74.10# 密码默认为 changeme,输入完密码之后提示在输入一遍 changeme,然后再修改新的密码root@photon-machine [ ~ ]# cat /etc/os-releaseNAME="VMware Photon OS"VERSION="4.0"ID=photonVERSION_ID=4.0PRETTY_NAME="VMware Photon OS/Linux"ANSI_COLOR="1;34"HOME_URL="https://vmware.github.io/photon/"BUG_REPORT_URL="https://github.com/vmware/photon/issues"
  • 安装部署时需要的一些工具(切,Photon OS 里竟然连个 tar 命令都没有 😠
root@photon-machine [ ~ ]# tdnf install sudo tar -yroot@photon-machine [ ~ ]# curl -LO https://dl.k8s.io/release/v1.21.2/bin/linux/amd64/kubectlroot@photon-machine [ ~ ]# sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
  • 启动 docker,bootstrap 节点会以 kind 的方式运行一个 K8s 集群,需要用到 docker。虽然可以使用外部的 k8s 集群,但不是很推荐,因为 cluster-api 依赖 k8s 的版本,不能太高也不能太低;
root@photon-machine [ ~ ]# systemctl enable docker --now
root@photon-machine [ ~ ]# curl -LO  https://github.com/vmware-tanzu/community-edition/releases/download/v0.9.1/tce-linux-amd64-v0.9.1.tar.gzroot@photon-machine [ ~ ]# tar -xf tce-linux-amd64-v0.9.1.tar.gzroot@photon-machine [ ~ ]# cd tce-linux-amd64-v0.9.1/root@photon-machine [ ~ ]# bash install.sh

然而不幸地翻车了, install.sh 脚本中禁止 root 用户运行

+ ALLOW_INSTALL_AS_ROOT=+ [[ 0 -eq 0 ]]+ [[ '' != \t\r\u\e ]]+ echo 'Do not run this script as root'Do not run this script as root+ exit 1

我就偏偏要以 root 用户来运行怎么惹 😡

# sed 去掉第一个 exit 1 就可以了root@photon-machine [ ~ ]# sed -i.bak "s/exit 1//" install.shroot@photon-machine [ ~ ]# bash install.sh

安装好之后会输出 Installation complete!(讲真官方的 install.sh 脚本输出很不友好,污染我的 terminal

+ tanzu init| initializing ✔  successfully initialized CLI++ tanzu plugin repo list++ grep tce+ TCE_REPO=+ [[ -z '' ]]+ tanzu plugin repo add --name tce --gcp-bucket-name tce-tanzu-cli-plugins --gcp-root-path artifacts++ tanzu plugin repo list++ grep core-admin+ TCE_REPO=+ [[ -z '' ]]+ tanzu plugin repo add --name core-admin --gcp-bucket-name tce-tanzu-cli-framework-admin --gcp-root-path artifacts-admin+ echo 'Installation complete!'Installation complete!

部署管理集群

先是部署一个 tanzu 的管理集群,有两种方式,一种是通过 官方文档 提到的通过 Web UI 的方式。目前这个 UI 界面比较拉垮,它主要是用来让用户填写一些配置参数,然后调用后台的 tanzu 命令来部署集群。并把集群部署的日志和进度展示出来;部署完成之后,这个 UI 又不能管理这些集群,又不支持部署 workload 集群(

另一种就是通过 tanzu 命令指定配置文件来部署,这种方式不需要通过浏览器在 web 页面上傻乎乎地点来点去填一些参数,只需要提前填写好一个 yaml 格式的配置文件即可。下面我们就采用 tanzu 命令来部署集群,管理集群的配置文件模版如下:

  • tanzu-mgt-cluster.yaml
# Cluster Pod IP 的 CIDRCLUSTER_CIDR: 100.96.0.0/11# Service 的 CIDRSERVICE_CIDR: 100.64.0.0/13# 集群的名称CLUSTER_NAME: tanzu-control-plan# 集群的类型CLUSTER_PLAN: dev# 集群节点的 archOS_ARCH: amd64# 集群节点的 OS 名称OS_NAME: photon# 集群节点 OS 版本OS_VERSION: "3"# 基础设施资源的提供方INFRASTRUCTURE_PROVIDER: vsphere# 集群的 VIPVSPHERE_CONTROL_PLANE_ENDPOINT: 192.168.75.194# control-plan 节点的磁盘大小VSPHERE_CONTROL_PLANE_DISK_GIB: "20"# control-plan 节点的内存大小VSPHERE_CONTROL_PLANE_MEM_MIB: "8192"# control-plan 节点的 CPU 核心数量VSPHERE_CONTROL_PLANE_NUM_CPUS: "4"# work 节点的磁盘大小VSPHERE_WORKER_DISK_GIB: "20"# work 节点的内存大小VSPHERE_WORKER_MEM_MIB: "4096"# work 节点的 CPU 核心数量VSPHERE_WORKER_NUM_CPUS: "2"# vCenter 的 Datacenter 路径VSPHERE_DATACENTER: /SH-IDC# 虚拟机创建的 Datastore 路径VSPHERE_DATASTORE: /SH-IDC/datastore/datastore1# 虚拟机创建的文件夹VSPHERE_FOLDER: /SH-IDC/vm/Tanzu-node# 虚拟机使用的网络VSPHERE_NETWORK: /SH-IDC/network/VM Network# 虚拟机关联的资源池VSPHERE_RESOURCE_POOL: /SH-IDC/host/Tanzu-Cluster/Resources# vCenter 的 IPVSPHERE_SERVER: 192.168.75.110# vCenter 的用户名VSPHERE_USERNAME: administrator@vsphere.local# vCenter 的密码,以 base64 编码VSPHERE_PASSWORD: <encoded:base64password># vCenter 的证书指纹,可以通过 govc about.cert -json | jq -r '.ThumbprintSHA1' 获取VSPHERE_TLS_THUMBPRINT: EB:F3:D8:7A:E8:3D:1A:59:B0:DE:73:96:DC:B9:5F:13:86:EF:B6:27# 虚拟机注入的 ssh 公钥,需要用它来 ssh 登录集群节点VSPHERE_SSH_AUTHORIZED_KEY: ssh-rsa# 一些默认参数AVI_ENABLE: "false"IDENTITY_MANAGEMENT_TYPE: noneENABLE_AUDIT_LOGGING: "false"ENABLE_CEIP_PARTICIPATION: "false"TKG_HTTP_PROXY_ENABLED: "false"DEPLOY_TKG_ON_VSPHERE7: "true"
  • 通过 tanzu CLI 部署管理集群
$ tanzu management-cluster create --file tanzu-mgt-cluster.yaml -v6# 如果没有配置 VSPHERE_TLS_THUMBPRINT 会有一个确认 vSphere thumbprint 的交互,输入 Y 就可以Validating the pre-requisites...Do you want to continue with the vSphere thumbprint EB:F3:D8:7A:E8:3D:1A:59:B0:DE:73:96:DC:B9:5F:13:86:EF:B6:27 [y/N]: y

部署日志

root@photon-machine [ ~ ]# tanzu management-cluster create --file tanzu-mgt-cluster.yaml -v 6compatibility file (/root/.config/tanzu/tkg/compatibility/tkg-compatibility.yaml) already exists, skipping downloadBOM files inside /root/.config/tanzu/tkg/bom already exists, skipping downloadCEIP Opt-in status: falseValidating the pre-requisites...vSphere 7.0 Environment Detected.You have connected to a vSphere 7.0 environment which does not have vSphere with Tanzu enabled. vSphere with Tanzu includesan integrated Tanzu Kubernetes Grid Service which turns a vSphere cluster into a platform for running Kubernetes workloads in dedicatedresource pools. Configuring Tanzu Kubernetes Grid Service is done through vSphere HTML5 client.Tanzu Kubernetes Grid Service is the preferred way to consume Tanzu Kubernetes Grid in vSphere 7.0 environments. Alternatively you maydeploy a non-integrated Tanzu Kubernetes Grid instance on vSphere 7.0.Deploying TKG management cluster on vSphere 7.0 ...Identity Provider not configured. Some authentication features won't work.Checking if VSPHERE_CONTROL_PLANE_ENDPOINT 192.168.20.94 is already in useSetting up management cluster...Validating configuration...Using infrastructure provider vsphere:v0.7.10Generating cluster configuration...Setting up bootstrapper...Fetching configuration for kind node image...kindConfig: &{{Cluster kind.x-k8s.io/v1alpha4}  [{  map[] [{/var/run/docker.sock /var/run/docker.sock false false }] [] [] []}] { 0  100.96.0.0/11 100.64.0.0/13 false } map[] map[] [apiVersion: kubeadm.k8s.io/v1beta2kind: ClusterConfigurationimageRepository: projects.registry.vmware.com/tkgetcd:  local:    imageRepository: projects.registry.vmware.com/tkg    imageTag: v3.4.13_vmware.15dns:  type: CoreDNS  imageRepository: projects.registry.vmware.com/tkg  imageTag: v1.8.0_vmware.5] [] [] []}Creating kind cluster: tkg-kind-c7vj6kds0a6sf43e6210Creating cluster "tkg-kind-c7vj6kds0a6sf43e6210" ...Ensuring node image (projects.registry.vmware.com/tkg/kind/node:v1.21.2_vmware.1) ...Pulling image: projects.registry.vmware.com/tkg/kind/node:v1.21.2_vmware.1 ...Preparing nodes ...Writing configuration ...Starting control-plane ...Installing CNI ...Installing StorageClass ...Waiting 2m0s for control-plane = Ready ...Ready after 19sBootstrapper created. Kubeconfig: /root/.kube-tkg/tmp/config_3fkzTCOLInstalling providers on bootstrapper...Fetching providersInstalling cert-manager Version="v1.1.0"Waiting for cert-manager to be available...Installing Provider="cluster-api" Version="v0.3.23" TargetNamespace="capi-system"Installing Provider="bootstrap-kubeadm" Version="v0.3.23" TargetNamespace="capi-kubeadm-bootstrap-system"Installing Provider="control-plane-kubeadm" Version="v0.3.23" TargetNamespace="capi-kubeadm-control-plane-system"Installing Provider="infrastructure-vsphere" Version="v0.7.10" TargetNamespace="capv-system"installed  Component=="cluster-api"  Type=="CoreProvider"  Version=="v0.3.23"installed  Component=="kubeadm"  Type=="BootstrapProvider"  Version=="v0.3.23"installed  Component=="kubeadm"  Type=="ControlPlaneProvider"  Version=="v0.3.23"installed  Component=="vsphere"  Type=="InfrastructureProvider"  Version=="v0.7.10"Waiting for provider infrastructure-vsphereWaiting for provider control-plane-kubeadmWaiting for provider cluster-apiWaiting for provider bootstrap-kubeadmWaiting for resource capi-kubeadm-control-plane-controller-manager of type *v1.Deployment to be up and runningpods are not yet running for deployment 'capi-kubeadm-control-plane-controller-manager' in namespace 'capi-kubeadm-control-plane-system', retryingPassed waiting on provider bootstrap-kubeadm after 25.205820854spods are not yet running for deployment 'capi-controller-manager' in namespace 'capi-webhook-system', retryingPassed waiting on provider infrastructure-vsphere after 30.185406332sPassed waiting on provider cluster-api after 30.213216243sSuccess waiting on all providers.Start creating management cluster...patch cluster object with operation status:{"metadata": {"annotations": {"TKGOperationInfo" : "{\"Operation\":\"Create\",\"OperationStartTimestamp\":\"2022-02-06 02:35:34.30219421 +0000 UTC\",\"OperationTimeout\":1800}","TKGOperationLastObservedTimestamp" : "2022-02-06 02:35:34.30219421 +0000 UTC"}}}cluster control plane is still being initialized, retryingGetting secret for clusterWaiting for resource tanzu-control-plan-kubeconfig of type *v1.Secret to be up and runningSaving management cluster kubeconfig into /root/.kube/configInstalling providers on management cluster...Fetching providersInstalling cert-manager Version="v1.1.0"Waiting for cert-manager to be available...Installing Provider="cluster-api" Version="v0.3.23" TargetNamespace="capi-system"Installing Provider="bootstrap-kubeadm" Version="v0.3.23" TargetNamespace="capi-kubeadm-bootstrap-system"Installing Provider="control-plane-kubeadm" Version="v0.3.23" TargetNamespace="capi-kubeadm-control-plane-system"Installing Provider="infrastructure-vsphere" Version="v0.7.10" TargetNamespace="capv-system"installed  Component=="cluster-api"  Type=="CoreProvider"  Version=="v0.3.23"installed  Component=="kubeadm"  Type=="BootstrapProvider"  Version=="v0.3.23"installed  Component=="kubeadm"  Type=="ControlPlaneProvider"  Version=="v0.3.23"installed  Component=="vsphere"  Type=="InfrastructureProvider"  Version=="v0.7.10"Waiting for provider control-plane-kubeadmWaiting for provider bootstrap-kubeadmWaiting for provider infrastructure-vsphereWaiting for provider cluster-apiWaiting for resource capi-kubeadm-control-plane-controller-manager of type *v1.Deployment to be up and runningPassed waiting on provider control-plane-kubeadm after 10.046865402sWaiting for resource antrea-controller of type *v1.Deployment to be up and runningMoving all Cluster API objects from bootstrap cluster to management cluster...Performing move...Discovering Cluster API objectsMoving Cluster API objects Clusters=1Creating objects in the target clusterDeleting objects from the source clusterWaiting for additional components to be up and running...Waiting for packages to be up and running...Waiting for package: antreaWaiting for package: metrics-serverWaiting for package: tanzu-addons-managerWaiting for package: vsphere-cpiWaiting for package: vsphere-csiWaiting for resource antrea of type *v1alpha1.PackageInstall to be up and runningWaiting for resource vsphere-cpi of type *v1alpha1.PackageInstall to be up and runningWaiting for resource vsphere-csi of type *v1alpha1.PackageInstall to be up and runningWaiting for resource metrics-server of type *v1alpha1.PackageInstall to be up and runningWaiting for resource tanzu-addons-manager of type *v1alpha1.PackageInstall to be up and runningSuccessfully reconciled package: antreaSuccessfully reconciled package: vsphere-csiSuccessfully reconciled package: metrics-serverContext set for management cluster tanzu-control-plan as 'tanzu-control-plan-admin@tanzu-control-plan'.Deleting kind cluster: tkg-kind-c7vj6kds0a6sf43e6210Management cluster created!You can now create your first workload cluster by running the following:  tanzu cluster create [name] -f [file]Some addons might be getting installed! Check their status by running the following:  kubectl get apps -A
  • 部署完成之后,将管理集群的 kubeconfig 文件复制到 kubectl 默认的目录下
root@photon-machine [ ~ ]# cp ${HOME}/.kube-tkg/config ${HOME}/.kube/config
  • 查看集群状态信息
# 管理集群的 cluster 资源信息,管理集群的 CR 默认保存在了 tkg-system namespace 下root@photon-machine [ ~ ]# kubectl get cluster -ANAMESPACE    NAME                 PHASEtkg-system   tanzu-control-plan   Provisioned# 管理集群的 machine 资源信息root@photon-machine [ ~ ]# kubectl get machine -ANAMESPACE    NAME                                       PROVIDERID                                       PHASE         VERSIONtkg-system   tanzu-control-plan-control-plane-gs4bl     vsphere://4239c450-f621-d78e-3c44-4ac8890c0cd3   Running       v1.21.2+vmware.1tkg-system   tanzu-control-plan-md-0-7cdc97c7c6-kxcnx   vsphere://4239d776-c04c-aacc-db12-3380542a6d03   Provisioned   v1.21.2+vmware.1# 运行的组件状态root@photon-machine [ ~ ]# kubectl get pod -ANAMESPACE                           NAME                                                             READY   STATUS    RESTARTS   AGEcapi-kubeadm-bootstrap-system       capi-kubeadm-bootstrap-controller-manager-6494884869-wlzhx       2/2     Running   0          8m37scapi-kubeadm-control-plane-system   capi-kubeadm-control-plane-controller-manager-857d687b9d-tpznv   2/2     Running   0          8m35scapi-system                         capi-controller-manager-778bd4dfb9-tkvwg                         2/2     Running   0          8m41scapi-webhook-system                 capi-controller-manager-9995bdc94-svjm2                          2/2     Running   0          8m41scapi-webhook-system                 capi-kubeadm-bootstrap-controller-manager-68845b65f8-sllgv       2/2     Running   0          8m38scapi-webhook-system                 capi-kubeadm-control-plane-controller-manager-9847c6747-vvz6g    2/2     Running   0          8m35scapi-webhook-system                 capv-controller-manager-55bf67fbd5-4t46v                         2/2     Running   0          8m31scapv-system                         capv-controller-manager-587fbf697f-bbzs9                         2/2     Running   0          8m31scert-manager                        cert-manager-77f6fb8fd5-8tq6n                                    1/1     Running   0          11mcert-manager                        cert-manager-cainjector-6bd4cff7bb-6vlzx                         1/1     Running   0          11mcert-manager                        cert-manager-webhook-fbfcb9d6c-qpkbc                             1/1     Running   0          11mkube-system                         antrea-agent-5m9d4                                               2/2     Running   0          6mkube-system                         antrea-agent-8mpr7                                               2/2     Running   0          5m40skube-system                         antrea-controller-5bbcb98667-hklss                               1/1     Running   0          5m50skube-system                         coredns-8dcb5c56b-ckvb7                                          1/1     Running   0          12mkube-system                         coredns-8dcb5c56b-d98hf                                          1/1     Running   0          12mkube-system                         etcd-tanzu-control-plan-control-plane-gs4bl                      1/1     Running   0          12mkube-system                         kube-apiserver-tanzu-control-plan-control-plane-gs4bl            1/1     Running   0          12mkube-system                         kube-controller-manager-tanzu-control-plan-control-plane-gs4bl   1/1     Running   0          12mkube-system                         kube-proxy-d4wq4                                                 1/1     Running   0          12mkube-system                         kube-proxy-nhkgg                                                 1/1     Running   0          11mkube-system                         kube-scheduler-tanzu-control-plan-control-plane-gs4bl            1/1     Running   0          12mkube-system                         kube-vip-tanzu-control-plan-control-plane-gs4bl                  1/1     Running   0          12mkube-system                         metrics-server-59fcb9fcf-xjznj                                   1/1     Running   0          6m29skube-system                         vsphere-cloud-controller-manager-kzffm                           1/1     Running   0          5m50skube-system                         vsphere-csi-controller-74675c9488-q9h5c                          6/6     Running   0          6m31skube-system                         vsphere-csi-node-dmvvr                                           3/3     Running   0          6m31skube-system                         vsphere-csi-node-k6x98                                           3/3     Running   0          6m31stkg-system                          kapp-controller-6499b8866-xnql7                                  1/1     Running   0          10mtkg-system                          tanzu-addons-controller-manager-657c587556-rpbjm                 1/1     Running   0          7m58stkg-system                          tanzu-capabilities-controller-manager-6ff97656b8-cq7m7           1/1     Running   0          11mtkr-system                          tkr-controller-manager-6bc455b5d4-wm98s                          1/1     Running   0          10m

部署流程

结合 tanzu 的源码 和部署输出的日志我们大体可以得知,tanzu 管理集群部署大致分为如下几步:

// https://github.com/vmware-tanzu/tanzu-framework/blob/main/pkg/v1/tkg/client/init.go// management cluster init step constantsconst (StepConfigPrerequisite                 = "Configure prerequisite"StepValidateConfiguration              = "Validate configuration"StepGenerateClusterConfiguration       = "Generate cluster configuration"StepSetupBootstrapCluster              = "Setup bootstrap cluster"StepInstallProvidersOnBootstrapCluster = "Install providers on bootstrap cluster"StepCreateManagementCluster            = "Create management cluster"StepInstallProvidersOnRegionalCluster  = "Install providers on management cluster"StepMoveClusterAPIObjects              = "Move cluster-api objects from bootstrap cluster to management cluster")// InitRegionSteps management cluster init step sequencevar InitRegionSteps = []string{StepConfigPrerequisite,StepValidateConfiguration,StepGenerateClusterConfiguration,StepSetupBootstrapCluster,StepInstallProvidersOnBootstrapCluster,StepCreateManagementCluster,StepInstallProvidersOnRegionalCluster,StepMoveClusterAPIObjects,}
  • ConfigPrerequisite 准备阶段,会下载 tkg-compatibilitytkg-bom 镜像,用于检查环境的兼容性;
Downloading TKG compatibility file from 'projects.registry.vmware.com/tkg/framework-zshippable/tkg-compatibility'Downloading the TKG Bill of Materials (BOM) file from 'projects.registry.vmware.com/tkg/tkg-bom:v1.4.0'Downloading the TKr Bill of Materials (BOM) file from 'projects.registry.vmware.com/tkg/tkr-bom:v1.21.2_vmware.1-tkg.1'ERROR 2022/02/06 02:24:46 svType != tvType; key=release, st=map[string]interface {}, tt=<nil>, sv=map[version:], tv=<nil>CEIP Opt-in status: false
  • ValidateConfiguration 配置文件校验,根据填写的参数校验配置是否正确,以及检查 vCenter 当中有无匹配的虚拟机模版;
Validating the pre-requisites...vSphere 7.0 Environment Detected.You have connected to a vSphere 7.0 environment which does not have vSphere with Tanzu enabled. vSphere with Tanzu includesan integrated Tanzu Kubernetes Grid Service which turns a vSphere cluster into a platform for running Kubernetes workloads in dedicatedresource pools. Configuring Tanzu Kubernetes Grid Service is done through vSphere HTML5 client.Tanzu Kubernetes Grid Service is the preferred way to consume Tanzu Kubernetes Grid in vSphere 7.0 environments. Alternatively you maydeploy a non-integrated Tanzu Kubernetes Grid instance on vSphere 7.0.Deploying TKG management cluster on vSphere 7.0 ...Identity Provider not configured. Some authentication features won't work.Checking if VSPHERE_CONTROL_PLANE_ENDPOINT 192.168.20.94 is already in useSetting up management cluster...Validating configuration...Using infrastructure provider vsphere:v0.7.10
  • GenerateClusterConfiguration 生成集群配置文件信息;
Generating cluster configuration...
  • SetupBootstrapCluster 设置 bootstrap 集群,目前默认为 kind。会运行一个 docker 容器,里面套娃运行着一个 k8s 集群;这个 bootstrap k8s 集群只是临时运行 cluster-api 来部署管理集群用的,部署完成之后 bootstrap 集群也就没用了,会自动删掉;
Setting up bootstrapper...Fetching configuration for kind node image...kindConfig: &{{Cluster kind.x-k8s.io/v1alpha4}  [{  map[] [{/var/run/docker.sock /var/run/docker.sock false false }] [] [] []}] { 0  100.96.0.0/11 100.64.0.0/13 false } map[] map[] [apiVersion: kubeadm.k8s.io/v1beta2kind: ClusterConfigurationimageRepository: projects.registry.vmware.com/tkgetcd:  local:    imageRepository: projects.registry.vmware.com/tkg    imageTag: v3.4.13_vmware.15dns:  type: CoreDNS  imageRepository: projects.registry.vmware.com/tkg  imageTag: v1.8.0_vmware.5] [] [] []}Creating kind cluster: tkg-kind-c7vj6kds0a6sf43e6210Creating cluster "tkg-kind-c7vj6kds0a6sf43e6210" ...Ensuring node image (projects.registry.vmware.com/tkg/kind/node:v1.21.2_vmware.1) ...Pulling image: projects.registry.vmware.com/tkg/kind/node:v1.21.2_vmware.1 ...Preparing nodes ...Writing configuration ...Starting control-plane ...Installing CNI ...Installing StorageClass ...Waiting 2m0s for control-plane = Ready ...Ready after 19sBootstrapper created. Kubeconfig: /root/.kube-tkg/tmp/config_3fkzTCOL
  • InstallProvidersOnBootstrapCluster 在 bootstrap 集群上安装 cluste-api 相关组件;
Installing providers on bootstrapper...Fetching providers# 安装 cert-manager 主要是为了生成 k8s 集群部署所依赖的那一堆证书Installing cert-manager Version="v1.1.0"Waiting for cert-manager to be available...Installing Provider="cluster-api" Version="v0.3.23" TargetNamespace="capi-system"Installing Provider="bootstrap-kubeadm" Version="v0.3.23" TargetNamespace="capi-kubeadm-bootstrap-system"Installing Provider="control-plane-kubeadm" Version="v0.3.23" TargetNamespace="capi-kubeadm-control-plane-system"Installing Provider="infrastructure-vsphere" Version="v0.7.10" TargetNamespace="capv-system"installed  Component=="cluster-api"  Type=="CoreProvider"  Version=="v0.3.23"installed  Component=="kubeadm"  Type=="BootstrapProvider"  Version=="v0.3.23"installed  Component=="kubeadm"  Type=="ControlPlaneProvider"  Version=="v0.3.23"installed  Component=="vsphere"  Type=="InfrastructureProvider"  Version=="v0.7.10"Waiting for provider infrastructure-vsphereWaiting for provider control-plane-kubeadmWaiting for provider cluster-apiWaiting for provider bootstrap-kubeadmPassed waiting on provider infrastructure-vsphere after 30.185406332sPassed waiting on provider cluster-api after 30.213216243sSuccess waiting on all providers.
  • CreateManagementCluster 创建管理集群,这一步主要是创建虚拟机、初始化节点、运行 kubeadm 部署 k8s 集群;
Start creating management cluster...patch cluster object with operation status:{"metadata": {"annotations": {"TKGOperationInfo" : "{\"Operation\":\"Create\",\"OperationStartTimestamp\":\"2022-02-06 02:35:34.30219421 +0000 UTC\",\"OperationTimeout\":1800}","TKGOperationLastObservedTimestamp" : "2022-02-06 02:35:34.30219421 +0000 UTC"}}}cluster control plane is still being initialized, retryingGetting secret for clusterWaiting for resource tanzu-control-plan-kubeconfig of type *v1.Secret to be up and runningSaving management cluster kubeconfig into /root/.kube/config
  • InstallProvidersOnRegionalCluster 在管理集群上安装 cluster-api 相关组件;
Installing providers on management cluster...Fetching providersInstalling cert-manager Version="v1.1.0"Waiting for cert-manager to be available...Installing Provider="cluster-api" Version="v0.3.23" TargetNamespace="capi-system"Installing Provider="bootstrap-kubeadm" Version="v0.3.23" TargetNamespace="capi-kubeadm-bootstrap-system"Installing Provider="control-plane-kubeadm" Version="v0.3.23" TargetNamespace="capi-kubeadm-control-plane-system"Installing Provider="infrastructure-vsphere" Version="v0.7.10" TargetNamespace="capv-system"installed  Component=="cluster-api"  Type=="CoreProvider"  Version=="v0.3.23"installed  Component=="kubeadm"  Type=="BootstrapProvider"  Version=="v0.3.23"installed  Component=="kubeadm"  Type=="ControlPlaneProvider"  Version=="v0.3.23"installed  Component=="vsphere"  Type=="InfrastructureProvider"  Version=="v0.7.10"Waiting for provider control-plane-kubeadmWaiting for provider bootstrap-kubeadmWaiting for provider infrastructure-vsphereWaiting for provider cluster-apiWaiting for resource capv-controller-manager of type *v1.Deployment to be up and runningPassed waiting on provider infrastructure-vsphere after 20.091935635sPassed waiting on provider cluster-api after 20.109419304sSuccess waiting on all providers.Waiting for the management cluster to get ready for move...Waiting for resource tanzu-control-plan of type *v1alpha3.Cluster to be up and runningWaiting for resources type *v1alpha3.MachineDeploymentList to be up and runningWaiting for resources type *v1alpha3.MachineList to be up and runningWaiting for addons installation...Waiting for resources type *v1alpha3.ClusterResourceSetList to be up and runningWaiting for resource antrea-controller of type *v1.Deployment to be up and running
  • MoveClusterAPIObjects 将 bootstrap 集群上 cluster-api 相关的资源转移到管理集群上。这一步的目的是为了达到 self-hosted 自托管的功能:即管理集群自身的扩缩容也是通过 cluster-api 来完成,这样就不用再依赖先前的那个 bootstrap 集群了;
Moving all Cluster API objects from bootstrap cluster to management cluster...Performing move...Discovering Cluster API objectsMoving Cluster API objects Clusters=1Creating objects in the target clusterDeleting objects from the source clusterContext set for management cluster tanzu-control-plan as 'tanzu-control-plan-admin@tanzu-control-plan'.Deleting kind cluster: tkg-kind-c7vj6kds0a6sf43e6210Management cluster created!You can now create your first workload cluster by running the following:  tanzu cluster create [name] -f [file]Some addons might be getting installed! Check their status by running the following:  kubectl get apps -A

部署完成后会删除 bootstrap 集群,因为 bootstrap 集群中的资源已经转移到了管理集群中,它继续存在的意义不大。

部署 workload 集群

上面我们只是部署好了一个 tanzu 管理集群,我们真正的工作负载并不适合运行在这个集群上,因此我们还需要再部署一个 workload 集群,类似于 k8s 集群中的 worker 节点。部署 workload 集群的时候不再依赖 bootstrap 集群,而是使用管理集群。

根据官方文档 vSphere Workload Cluster Template 中给出的模版创建一个配置文件,然后再通过 tanzu 命令来部署即可。配置文件内容如下:

# Cluster Pod IP 的 CIDRCLUSTER_CIDR: 100.96.0.0/11# Service 的 CIDRSERVICE_CIDR: 100.64.0.0/13# 集群的名称CLUSTER_NAME: tanzu-workload-cluster# 集群的类型CLUSTER_PLAN: dev# 集群节点的 archOS_ARCH: amd64# 集群节点的 OS 名称OS_NAME: photon# 集群节点 OS 版本OS_VERSION: "3"# 基础设施资源的提供方INFRASTRUCTURE_PROVIDER: vsphere# cluster, machine 等自定义资源创建的 namespaceNAMESPACE: default# CNI 选用类型,目前应该只支持 VMware 自家的 antreaCNI: antrea# 集群的 VIPVSPHERE_CONTROL_PLANE_ENDPOINT: 192.168.20.95# control-plan 节点的磁盘大小VSPHERE_CONTROL_PLANE_DISK_GIB: "20"# control-plan 节点的内存大小VSPHERE_CONTROL_PLANE_MEM_MIB: "8192"# control-plan 节点的 CPU 核心数量VSPHERE_CONTROL_PLANE_NUM_CPUS: "4"# work 节点的磁盘大小VSPHERE_WORKER_DISK_GIB: "20"# work 节点的内存大小VSPHERE_WORKER_MEM_MIB: "4096"# work 节点的 CPU 核心数量VSPHERE_WORKER_NUM_CPUS: "2"# vCenter 的 Datacenter 路径VSPHERE_DATACENTER: /SH-IDC# 虚拟机创建的 Datastore 路径VSPHERE_DATASTORE: /SH-IDC/datastore/datastore1# 虚拟机创建的文件夹VSPHERE_FOLDER: /SH-IDC/vm/Tanzu-node# 虚拟机使用的网络VSPHERE_NETWORK: /SH-IDC/network/VM Network# 虚拟机关联的资源池VSPHERE_RESOURCE_POOL: /SH-IDC/host/Tanzu-Cluster/Resources# vCenter 的 IPVSPHERE_SERVER: 192.168.20.92# vCenter 的用户名VSPHERE_USERNAME: administrator@vsphere.local# vCenter 的密码,以 base64 编码VSPHERE_PASSWORD: <encoded:YWRtaW5AMjAyMA==># vCenter 的证书指纹,可以通过 govc about.cert -json | jq -r '.ThumbprintSHA1' 获取VSPHERE_TLS_THUMBPRINT: CB:23:48:E8:93:34:AD:27:D8:FD:88:1C:D7:08:4B:47:9B:12:F4:E0# 虚拟机注入的 ssh 公钥,需要用它来 ssh 登录集群节点VSPHERE_SSH_AUTHORIZED_KEY: ssh-rsa# 一些默认参数AVI_ENABLE: "false"IDENTITY_MANAGEMENT_TYPE: noneENABLE_AUDIT_LOGGING: "false"ENABLE_CEIP_PARTICIPATION: "false"TKG_HTTP_PROXY_ENABLED: "false"DEPLOY_TKG_ON_VSPHERE7: "true"# 是否开启虚拟机健康检查ENABLE_MHC: trueMHC_UNKNOWN_STATUS_TIMEOUT: 5mMHC_FALSE_STATUS_TIMEOUT: 12m# 是否部署 vsphere cis 组件ENABLE_DEFAULT_STORAGE_CLASS: true# 是否开启集群自动扩缩容ENABLE_AUTOSCALER: false
  • 通过 tanzu 命令来部署 workload 集群
root@photon-machine [ ~ ]# tanzu cluster create tanzu-workload-cluster --file tanzu-workload-cluster.yamlValidating configuration...Warning: Pinniped configuration not found. Skipping pinniped configuration in workload cluster. Please refer to the documentation to check if you can configure pinniped on workload cluster manuallyCreating workload cluster 'tanzu-workload-cluster'...Waiting for cluster to be initialized...Waiting for cluster nodes to be available...Waiting for cluster autoscaler to be available...Unable to wait for autoscaler deployment to be ready. reason: deployments.apps "tanzu-workload-cluster-cluster-autoscaler" not foundWaiting for addons installation...Waiting for packages to be up and running...Workload cluster 'tanzu-workload-cluster' created
  • 部署完成之后查看一下集群的 CR 信息
root@photon-machine [ ~ ]# kubectl get clusterNAME                     PHASEtanzu-workload-cluster   Provisioned# machine 状态处于 Running 说明节点已经正常运行了root@photon-machine [ ~ ]# kubectl get machineNAME                                          PROVIDERID                                       PHASE     VERSIONtanzu-workload-cluster-control-plane-4tdwq    vsphere://423950ac-1c6d-e5ef-3132-77b6a53cf626   Running   v1.21.2+vmware.1tanzu-workload-cluster-md-0-8555bbbfc-74vdg   vsphere://4239b83b-6003-d990-4555-a72ac4dec484   Running   v1.21.2+vmware.1

扩容集群

集群部署好之后,如果想对集群节点进行扩缩容,我们可以像 deployment 的一样,只需要修改一些 CR 的信息即可。cluster-api 相关组件会 watch 到这些 CR 的变化,并根据它的 spec 信息进行一系列调谐操作。如果当前集群节点数量低于所定义的节点副本数量,则会自动调用对应的 Provider 创建虚拟机,并对虚拟机进行初始化操作,将它转换为 k8s 里的一个 node 资源;

扩容 control-plan 节点

即扩容 master 节点,通过修改 KubeadmControlPlane 这个 CR 中的 replicas 副本数即可:

root@photon-machine [ ~ ]# kubectl scale kcp tanzu-workload-cluster-control-plane --replicas=3# 可以看到 machine 已经处于 Provisioning 状态,说明集群节点对应的虚拟机正在创建中root@photon-machine [ ~ ]# kubectl get machineNAME                                          PROVIDERID                                       PHASE          VERSIONtanzu-workload-cluster-control-plane-4tdwq    vsphere://423950ac-1c6d-e5ef-3132-77b6a53cf626   Running        v1.21.2+vmware.1tanzu-workload-cluster-control-plane-mkmd2                                                     Provisioning   v1.21.2+vmware.1tanzu-workload-cluster-md-0-8555bbbfc-74vdg   vsphere://4239b83b-6003-d990-4555-a72ac4dec484   Running        v1.21.2+vmware.1

扩容 work 节点

扩容 worker 节点,通过修改 MachineDeployment 这个 CR 中的 replicas 副本数即可:

root@photon-machine [ ~ ]# kubectl scale md tanzu-workload-cluster-md-0 --replicas=3root@photon-machine [ ~ ]# kubectl get machineNAME                                          PROVIDERID                                       PHASE     VERSIONtanzu-workload-cluster-control-plane-4tdwq    vsphere://423950ac-1c6d-e5ef-3132-77b6a53cf626   Running   v1.21.2+vmware.1tanzu-workload-cluster-control-plane-mkmd2    vsphere://4239278c-0503-f03a-08b8-df92286bcdd7   Running   v1.21.2+vmware.1tanzu-workload-cluster-control-plane-rt5mb    vsphere://4239c882-2fe5-a394-60c0-616941a6363e   Running   v1.21.2+vmware.1tanzu-workload-cluster-md-0-8555bbbfc-4hlqk   vsphere://42395deb-e706-8b4b-a44f-c755c222575c   Running   v1.21.2+vmware.1tanzu-workload-cluster-md-0-8555bbbfc-74vdg   vsphere://4239b83b-6003-d990-4555-a72ac4dec484   Running   v1.21.2+vmware.1tanzu-workload-cluster-md-0-8555bbbfc-ftmlp   vsphere://42399640-8e94-85e5-c4bd-8436d84966e0   Running   v1.21.2+vmware.1

后续

本文只是介绍了 tanzu 集群部署的大体流程,里面包含了 cluster-api 相关的概念在本文并没有做深入的分析,因为实在是太复杂了 😂,到现在我还是没太理解其中的一些原理,因此后续我再单独写一篇博客来讲解一些 cluster-api 相关的内容,到那时候在结合本文来看就容易理解很多。

参考

🔲 ☆

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

笔者之前在字节跳动的时候是负责 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 那一层,不会真正影响到只读层的内容。

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

推荐阅读

☑️ ☆

Kubespray 2.18 版本特性预览

最近 kubernetes-sig 社区的 kubespray 项目正式 release 了 v2.18.0 版本,同时对应 k8s-conformancev1.21v1.22 版本 kubespray 也都已经得到 CNCF 的一致性认证。于是今天就借这个新版本 release 的机会整理一下 2.18 版本的 kubespray 有哪些有趣的变化。

组件版本

以下是 v2.18.0 版本中 kubespray 部署组件的一些版本信息:

K8s 核心组件

  • kubespray 支持的 Kubernetes 支持从 v1.22.0 到 v1.23.1 之间的所有正式版本,默认部署的版本为 v1.22.5,并且 v1.22 版本得到了 CNCF 官方的一致性认证;
  • etcd 从原来的 v3.4.13 升级到了 v3.5.0;
  • coredns 版本升级到了 v1.8.0,它的搭档 dnsautoscaler 则为 1.8.5;
  • pod_infra 即 pause 镜像的版本没有变化依旧为 3.3;
AddonVersion
kubev1.22.5
pod_infra3.3
etcdv3.5.0
corednsv1.8.0

容器运行时

目前市面上所有的 Kubernetes 集群部署工具中,对容器运行时的支持 kubespray 无疑是最丰富的。部署能支持 docker、containerd、crun、kata、cri-o。默认的容器运行时已经从之前的 docker 切换到了 containerd,containerd 的版本是 v1.5.8。

AddonVersion
containerd1.5.8
docker20.10
docker_containerd1.4.12
crun1.3
runcv1.0.3
crio1.22
kata_containers2.2.3
gvisor20210921

CNI

同样,目前市面上所有的 Kubernetes 集群部署工具中,对 CNI 的支持 kubespray 也无疑是最为丰富的,能支持 9 种 CNI 以及多种 CNI 组合部署的 multus

AddonVersion
calicov3.20.3
flannelv0.15.1
flannel_cniv1.0.0
cniv1.0.1
weave2.8.1
ciliumv1.9.11
kube_ovnv1.8.1
kube_routerv1.3.2
multus_cni0.4.0
multusv3.8

Kubernetes-app

同时,kubespray 还支持一些 CLI 工具以及第三方应用的部署。

  • CLI 工具

一些 CLI 工具,比如 helm、nerdctl、krew、crictl。其中 nerdctl 的部署支持是咱在 #7500 中加入支持的,目的是为 containerd 用户提供一个相对友好的命令行操作体验,以替代 docker CLI。

addonversion
helmv3.7.1
nerdctl0.15.0
krewv0.4.2
crictlv1.22.0
  • app

个人感觉部署一些像 dnsautoscalerargoCD 这样的应用,还是使用 helm 比较好。因为基于 ansible 的 kubespray 维护这么多第三方组件,以及它们的升级管理都远不如 helm 方便。因此考虑到这些组件的升级维护成本,个人还是不太建议使用 kubespray 来部署这些组件。

AddonVersion
dnsautoscaler1.8.5
netcheckv1.2.2
nodelocaldns1.21.1
metrics_serverv0.5.0
cert_managerv1.5.4
addon_resizer1.8.11
cinder_blockstoragev3
external_vsphere6.7u3
nvidia_driver390.87
oci_cloud_controller0.7.0
metallbv0.10.3
argocdv2.1.6

支持的 OS

distributionversion
Amazon Linux2
Fedora CoreOS34.x/35.x
Flatcar Container Linux by Kinvolk
Alma Linux8
Rocky Linux8
CentOS/RHEL7/8
Oracle Linux7/8
Debian8/9/10/11
Ubuntu16.04/18.04/20.04
Fedora34/35
openSUSELeap 15.x/Tumbleweed

主要变化

废除

  • #8086 中移除了对 Ambassador 的支持;
  • #8327 中移除了对 registry-proxy 的支持;
  • #8246 中移除了对 Fedora 33 的支持,因为 Fedora 33 在 2021-11-30 就已经 EOL 了,所以被废弃支持也理所当然;
  • #8265 中移除了对 Mitogen 的支持,Mitogen 的作用就是用来优化 Ansible 的性能,但 Mitogen 对于一些新的 Linux 发行版支持的额并不是很友好,在 Kubespray 中维护的成本也比较大,因此社区就废弃它了;

新特性

  • #7895 中新增了 ArgoCD 的部署支持,通过设置 argocd_enabled 即可在部署集群的时候安装 ArgoCD。不过个人认为,ArgoCD 这玩意儿不太适合放在 K8s 部署当中来,看看 ks-installer 的代码你就能明白了 😂。
  • #8175 中默认使用 containerd 作为默认的容器运行时,替代掉了 docker。不过需要注意的是,当前版本的 kubespray 是使用 containerd 官方 repo release 的二进制安装包,但二进制安装包并没有 arm64 版本的。所以如果要部署的集群节点包含 arm64 的机器,最好还是使用 docker 作为容器运行时。
  • #8291 新增了 registry 部署支持多种 ServiceTypes 的支持;
  • #8229 中新增了支持 registry 认证的方式,私有化部署的时候使用带有认证的 registry 会用到;

已知问题

  • #8239cristicalin 大佬引入了一个修改,如果是 containerd 运行时,则使用 nerdctl 下载镜像。这将会导致配置了 containerd registry mirrors 的参数将会失效。
❌