离线部署 对于 PaaS toB 产品来讲,客户往往会要求产品的部署方案必须做到离线安装,即在部署时不能依赖任何在线的资源,比如安装一些 OS 软件包时依赖的 yum/apt 源;docker.io、k8s.gcr.io 、quay.io 上面的容器镜像;GitHub 上开源软件的二进制下载文件等。
作为平台部署工具的开发者,始终被离线部署这个难题困扰着。在线的容器镜像和二进制文件比较好解决,因为这些资源是与 OS 无关的,只要下载下来放到安装包里,部署的时候启动一个 HTTP 服务器和镜像仓库服务提供这些资源的下载即可。
但是对于 yum/apt 之类的软件来讲并不那么简单:
首先由于各个包之间的依赖关系比较复杂,并不能将它们直接下载下来;
其次即便下载下来之后也无法直接通过 yum/apt 的方式安装指定的软件包,虽然也可以使用 scp 的方式将这些包复制到部署节点,通过 rpm 或 dpkg 的方式来安装上,但这样并不是很优雅,而且通用性能也不是很好;
最后需要适配的 Linux 发行版和包管理器种类也有多种,而且有些包的包名或者版本号在不同的包管理之间也相差甚大,无法做到统一管理。
要同时适配 arm64 和 amd64 的源及其困难
综上,将平台部署依赖的在线 yum/apt 之类的软件包资源制作成离线安装包是一件很棘手的事情。个人就这个问题折腾了一段时间,终于找到了一个比较合适的解决方案:即通过一个 YAML 配置文件来管理包,然后使用 Dockerfile 来构建成离线的 tar 包或者容器镜像。如果有类似需求的小伙伴,可以参考一下本方案。
Docker build 传统制作离线源的方式是找一台相应的 Linux 机器,在上面通过包管理器下载这些软件包,然后再创建这些软件包的 repo 索引文件。
可以看出这种方式十分不灵活,假如我想要制作 Debian 9 的 apt 离线源,我就需要一台 Debian 9 的机器。如果要适配多个 Linux 发行版就需要多个相应的 OS 机器。要管理和使用这么多种类的 OS 不是一件容易的事儿,而如今已经十分普遍使用的容器技术恰恰能帮助我们解决这类问题。比如我想运行一个 Debian9 的操作系统,我只需要运行一个 Debian 9 镜像的容器即可,而且不需要额外的管理成本,使用起来也十分地轻量。
日常工作中我们常使用容器来构建一些 Golang 写的后端组件,那么构建离线源是不是也可以这样做?实践证明确实可以,我们只需要为不同的 OS 和包管理器写一个相应的 Dockerfile 即可。使用 docker build 多阶段构建的特性,可以将多个 Dockerfile 合并成一个,然后最后使用 COPY –from 的方式将这个构建的产物复制到同一个镜像中,比如提供 HTTP 的 nginx 容器,或者使用 BuildKit 的特性将这些构建产物导出为 tar包 或者为本地目录。
适配 OS 根据自己的 PaaS toB 从业经验可知,目前国内的私有云客户生产环境中使用的 OS 中, CentOS 应该是最多的,其次是 Ubuntu 和 Debian。至于 RedHat 则需要付费订阅才能使用,DockerHub 上更是没有免费可使用的镜像,因此本方案无法确保适用于 RedHat。产品方面 CentOS 需要的版本只有 7.9;Ubuntu 需要支持 18.04 和 20.04;Debian 需要支持 9 和 10。因为时间和精力有限,本方案支持的 Linux 发行版和相应的版本只有 CentOS 7, Debian 9/10, Ubuntu 18.04/20.04 这五个。如果要支持其他 OS 的离线源比如 OpenSUSE,也可以参考本方案编写一个 Dockerfile 文件来实现适配。
构建 构建的过程十分简单,使用一个 YAML 格式的配置文件来管理不同的包管理器或 Linux 发行版安装不同的包,并在一个 Dockerfile 里完成所有的构建操作。实现源码在 github.com/muzi502/scripts/build-packages-repo 。
1 2 3 4 5 6 build ├── Dockerfile ├── Dockerfile.centos ├── Dockerfile.debian ├── Dockerfile.ubuntu └── packages.yaml
构建过程 使用 docker build 的方式构建离线源大致可以分为如下几个步骤:
在构建容器内配置 yum/apt 源,安装构建时需要工具;
生成系统内的 rpm/deb 包的列表和需要下载的包列表,解决一些软件包依赖的问题;
根据生成的包列表使用相应的包管理器工具下载需要的软件包;
生用相应的包管理器生成这些包的 index 文件,如 repodata 或 Packages.gz 文件;
将上述的构建产物 COPY 到同一个容器镜像里,比如 nginx ;也可以导出为 tar 包或目录;
packages.yaml 这个文件用来管理不同的包管理器或者 Linux 发行版需要安装的软件包。根据不同的包管理器和发行版我们可以将这些包大致划分为 4 类。
common:适用于一些所有包管理器中包名相同或者对版本无要求的包,比如 vim 、curl、wget 这类工具。一般情况下使用这些工具我们并不关心它的版本,并且这类包的包名在所有的包管理器中都是相同的,所以这类可以划分为公共包。
yum/apt/dnf:适用于不同的发行版使用相同的包管理器。比如 nfs 的包,在 yum 中包名为 nfs-utils 但在 apt 中为 nfs-common,这类软件包可以划分为一类。
OS:适用于一些该 OS 独有的包,比如安装一个 Ubuntu 中有但 Debian 中没有的包(比如 debian-builder 或 ubuntu-dev-tools)。
OS-发行版代号:这类包的版本和发行版代号绑定在一起,比如 docker-ce=5:19.03.15~3-0~debian-stretch。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 common: - vim - curl - wget - tree - lvm2 yum: - nfs-utils - yum-utils - createrepo - centos-release-gluster - epel-release apt: - nfs-common - apt-transport-https - ca-certificates - lsb-release - software-properties-common - aptitude - dpkg-dev centos: - centos-release debian: - debian-builder debian-buster: - docker-ce=5:19.03.15~3-0~debian-buster ubuntu: - ubuntu-dev-tools
在这里需要额外注意一下,在不同的包管理器之间指定包版本的方式也各不相同,比如在 yum 中如果要安装 19.03.15 版本的 docker-ce 包名为 docker-ce-19.03.15
,而在 debian 中包名则为 docker-ce=5:19.03.15~3-0~debian-stretch
。可以使用包管理器查看相同的一个包如 docker-ce 在不同的包管理器之前的差异,如下:
1 2 3 4 5 6 7 8 9 10 [root@centos:] docker-ce.x86_64 3:19.03.15-3.el7 docker-ce-stable root@debian:/ docker-ce: Installed: (none) Candidate: 5:19.03.15~3-0~debian-stretch Version table: 5:19.03.15~3-0~debian-stretch 500 500 https://download.docker.com/linux/debian stretch/stable amd64 Packages
这个版本号的问题在 kubespray 的源码中也是同样做了特殊处理,目前确实没有太好的方案来解决,只能手动维护这个版本号。
roles/container-engine/docker/vars/redhat.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 --- docker_versioned_pkg: 'latest': docker-ce '18.09': docker-ce-18.09.9-3.el7 '19.03': docker-ce-19.03.15-3.el{{ ansible_distribution_major_version }} '20.10': docker-ce-20.10.5-3.el{{ ansible_distribution_major_version }} 'stable': docker-ce-19.03.15-3.el{{ ansible_distribution_major_version }} 'edge': docker-ce-19.03.15-3.el{{ ansible_distribution_major_version }} docker_cli_versioned_pkg: 'latest': docker-ce-cli '18.09': docker-ce-cli-18.09.9-3.el7 '19.03': docker-ce-cli-19.03.15-3.el{{ ansible_distribution_major_version }} '20.10': docker-ce-cli-20.10.5-3.el{{ ansible_distribution_major_version }} docker_package_info: enablerepo: "docker-ce" pkgs: - "{{ containerd_versioned_pkg[containerd_version | string] }} " - "{{ docker_cli_versioned_pkg[docker_cli_version | string] }} " - "{{ docker_versioned_pkg[docker_version | string] }} "
roles/container-engine/docker/vars/ubuntu.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 docker_versioned_pkg: 'latest': docker-ce '18.09': docker-ce=5:18.09.9~3-0~ubuntu-{{ ansible_distribution_release|lower }} '19.03': docker-ce=5:19.03.15~3-0~ubuntu-{{ ansible_distribution_release|lower }} '20.10': docker-ce=5:20.10.5~3-0~ubuntu-{{ ansible_distribution_release|lower }} 'stable': docker-ce=5:19.03.15~3-0~ubuntu-{{ ansible_distribution_release|lower }} 'edge': docker-ce=5:19.03.15~3-0~ubuntu-{{ ansible_distribution_release|lower }} docker_cli_versioned_pkg: 'latest': docker-ce-cli '18.09': docker-ce-cli=5:18.09.9~3-0~ubuntu-{{ ansible_distribution_release|lower }} '19.03': docker-ce-cli=5:19.03.15~3-0~ubuntu-{{ ansible_distribution_release|lower }} '20.10': docker-ce-cli=5:20.10.5~3-0~ubuntu-{{ ansible_distribution_release|lower }} docker_package_info: pkgs: - "{{ containerd_versioned_pkg[containerd_version | string] }} " - "{{ docker_cli_versioned_pkg[docker_cli_version | string] }} " - "{{ docker_versioned_pkg[docker_version | string] }} "
CentOS7 介绍完上述的包配置文件之后,接下来我们就根据这个 packages.yml 配置文件使用 Dockerfile 构建这些包的离线源。以下是构建 CentOS 7 离线源的 Dockerfile。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 FROM centos:7.9 .2009 as builderARG OS_VERSION=7 ARG ARCH=x86_64ARG BUILD_TOOLS="yum-utils createrepo centos-release-gluster epel-release curl" RUN yum install -q -y $BUILD_TOOLS \ && yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo \ && yum makecache && yum update -y -q RUN curl -sL -o /usr/local /bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \ && chmod a+x /usr/local /bin/yq WORKDIR /centos/$OS_VERSION /os/$ARCH COPY packages.yaml packages.yaml RUN yq eval '.common[],.yum[],.centos[]' packages.yaml | sort -u > packages.list \ && rpm -qa >> packages.list RUN cat packages.list | xargs yumdownloader --resolve \ && createrepo -d . FROM scratchCOPY --from=centos7 /centos /centos
在最后的一个 FROM 镜像中,这里指定的是 scratch
,这是一个特殊的镜像名,它代表的是一个空的镜像 layer。
1 2 3 FROM scratchCOPY --from=centos7 /centos /centos
也可以直接将构建出来的产物放到 nginx 容器中,这样直接运行 nginx 容器就能提供 yum/apt 源的服务
1 2 FROM nginx:1.19 COPY --from=centos7 /centos /usr/share/nginx/html
如果要构建为 tar 包或者本地目录的方式,需要为 Docker 开启 DOCKER_BUILDKIT=1
这个特性
1 2 3 4 root@debian: ~ root@debian: ~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [+] Building 30.9s (13/13) FINISHED => [internal] load .dockerignore 0.0s => => transferring context: 109B 0.0s => [internal] load build definition from Dockerfile.centos 0.0s => => transferring dockerfile: 979B 0.0s => [internal] load metadata for docker.io/library/centos:7.9.2009 2.6s => [centos7 1/7] FROM docker.io/library/centos:7.9.2009@sha256:0f4ec88e21daf75124b8a9e5ca03c37a5e937e0e108a255d890492430789b60e 0.0s => [internal] load build context 0.0s => => transferring context: 818B 0.0s => CACHED [centos7 2/7] RUN yum install -q -y yum-utils createrepo centos-release-gluster epel-release curl && yum-config-manager --add-repo https://download.docker.c 0.0s => [centos7 3/7] WORKDIR /centos/7/os/x86_64 0.0s => [centos7 4/7] RUN curl -sL -o /usr/local /bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 && chmod a+x /usr/local /bin/yq && curl 3.2s => [centos7 5/7] COPY packages.yaml packages.yaml 0.1s => [centos7 6/7] RUN yq eval packages.yaml -j | jq -r '.common[],.yum[],.centos[]' | sort -u > packages.list && rpm -qa >> packages.list 1.0s => [centos7 7/7] RUN cat packages.list | xargs yumdownloader --resolve && createrepo -d . 21.6s => [stage-1 1/1] COPY --from=centos7 /centos /centos 0.5s => exporting to client 0.7s => => copying files 301.37MB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 root@debian:/build centos └── 7 └── os └── x86_64 ├── acl-2.2.51-15.el7.x86_64.rpm ├── ansible-2.9.21-1.el7.noarch.rpm ├── at-3.1.13-24.el7.x86_64.rpm ├── attr-2.4.46-13.el7.x86_64.rpm ├── audit-libs-2.8.5-4.el7.x86_64.rpm ├── audit-libs-python-2.8.5-4.el7.x86_64.rpm ├── avahi-libs-0.6.31-20.el7.x86_64.rpm ├── basesystem-10.0-7.el7.centos.noarch.rpm ├── bash-4.2.46-34.el7.x86_64.rpm …………………………………… ├── redhat-lsb-submod-security-4.1-27.el7.centos.1.x86_64.rpm ├── repodata │ ├── 28d2fe2d1dbd9b76d3e5385d42cf628ac9fc34d69e151edfe8d134fe6ac6a6d9-primary.xml.gz │ ├── 5264ca1af13ec7c870f25b2a28edb3c2843556ca201d07ac681eb4af7a28b47c-primary.sqlite.bz2 │ ├── 591d9c2d5be714356e8db39f006d07073f0e1e024a4a811d5960d8e200a874fb-other.xml.gz │ ├── c035d2112d55d23a72b6d006b9e86a2f67db78c0de45345e415884aa0782f40c-other.sqlite.bz2 │ ├── cd756169c3718d77201d08590c0613ebed80053f84a2db7acc719b5b9bca866f-filelists.xml.gz │ ├── ed0c5a36b12cf1d4100f90b4825b93dac832e6e21f83b23ae9d9753842801cee-filelists.sqlite.bz2 │ └── repomd.xml ├── yum-utils-1.1.31-54.el7_8.noarch.rpm └── zlib-1.2.7-19.el7_9.x86_64.rpm 4 directories, 368 files
Debian9 下面是 Debian9 构建 Dockerfile,流程上和 CentOS 相差不多,只是包管理器的使用方式不太相同而已,这里就不再做详细的源码介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 FROM debian:stretch-slim as stretchARG OS_VERSION=stretchARG ARCH=amd64ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev" RUN apt update -y -q \ && apt install -y --no-install-recommends $DEP_PACKAGES \ && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian ${OS_VERSION} stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null \ && apt update -y -q WORKDIR /debian/${OS_VERSION} RUN curl -sL -o /usr/local /bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \ && chmod a+x /usr/local /bin/yq COPY packages.yaml packages.yaml RUN yq eval '.common[],.apt[],.debian[]' packages.yaml | sort -u > packages.list \ && dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list RUN chown -R _apt /debian/$OS_VERSION \ && cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION /Packages.gz FROM scratchCOPY --from=builder /debian /debian
Ubuntu Ubuntu 离线源的制作步骤和 Debian 差不太多,只需要简单修改一下 Debian 的 Dockerfile 应该就 OK ,比如 's/debian/ubuntu/g'
,毕竟 Debian 是 Ubuntu 的爸爸嘛~~,所以 apt 使用的方式和包名几乎一模一样,这里就不再赘述了。
All-in-Oone 将上述几个 Linux 发行版的 Dockerfile 整合成一个,这样只需要一个 docker build 命令就能构建出所需要的所有 OS 的离线源了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 FROM centos:7.9 .2009 as centos7ARG OS_VERSION=7 ARG ARCH=x86_64ARG BUILD_TOOLS="yum-utils createrepo centos-release-gluster epel-release curl" RUN yum install -q -y $BUILD_TOOLS \ && yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo \ && yum makecache && yum update -y -q RUN curl -sL -o /usr/local /bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \ && chmod a+x /usr/local /bin/yq \ && curl -sL -o /usr/local /bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \ && chmod a+x /usr/local /bin/jq WORKDIR /centos/$OS_VERSION /os/$ARCH COPY packages.yaml packages.yaml RUN yq eval packages.yaml -j | jq -r '.common[],.yum[],.centos[]' | sort -u > packages.list \ && rpm -qa >> packages.list RUN cat packages.list | xargs yumdownloader --resolve \ && createrepo -d . FROM debian:stretch-slim as stretchARG OS_VERSION=stretchARG ARCH=amd64ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev" RUN apt update -y -q \ && apt install -y --no-install-recommends $DEP_PACKAGES \ && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian ${OS_VERSION} stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null \ && apt update -y -q RUN curl -sL -o /usr/local /bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \ && chmod a+x /usr/local /bin/yq \ && curl -sL -o /usr/local /bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \ && chmod a+x /usr/local /bin/jq WORKDIR /debian/${OS_VERSION} COPY packages.yaml packages.yaml RUN yq eval packages.yaml -j | jq -r '.common[],.apt[],.debian[]' | sort -u > packages.list \ && dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list RUN chown -R _apt /debian/$OS_VERSION \ && cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION /Packages.gz FROM debian:buster-slim as busterARG OS_VERSION=busterARG ARCH=amd64ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev" RUN apt update -y -q \ && apt install -y --no-install-recommends $DEP_PACKAGES \ && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian ${OS_VERSION} stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null \ && apt update -y -q RUN curl -sL -o /usr/local /bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \ && chmod a+x /usr/local /bin/yq \ && curl -sL -o /usr/local /bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \ && chmod a+x /usr/local /bin/jq WORKDIR /debian/${OS_VERSION} COPY packages.yaml packages.yaml RUN yq eval packages.yaml -j | jq -r '.common[],.apt[],.debian[]' | sort -u > packages.list \ && dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list RUN chown -R _apt /debian/$OS_VERSION \ && cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION /Packages.gz FROM ubuntu:bionic as bionicARG OS_VERSION=bionicARG ARCH=amd64ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev" RUN apt update -y -q \ && apt install -y --no-install-recommends $DEP_PACKAGES \ && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu ${OS_VERSION} stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null \ && apt update -y -q RUN curl -sL -o /usr/local /bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \ && chmod a+x /usr/local /bin/yq \ && curl -sL -o /usr/local /bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \ && chmod a+x /usr/local /bin/jq WORKDIR /ubuntu/${OS_VERSION} COPY packages.yaml packages.yaml RUN yq eval packages.yaml -j | jq -r '.common[],.apt[],.ubuntu[]' | sort -u > packages.list \ && dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list RUN chown -R _apt /ubuntu/$OS_VERSION \ && cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION /Packages.gz FROM ubuntu:focal as focalARG OS_VERSION=focalARG ARCH=amd64ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev" RUN apt update -y -q \ && apt install -y --no-install-recommends $DEP_PACKAGES \ && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu ${OS_VERSION} stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null \ && apt update -y -q RUN curl -sL -o /usr/local /bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \ && chmod a+x /usr/local /bin/yq \ && curl -sL -o /usr/local /bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \ && chmod a+x /usr/local /bin/jq WORKDIR /ubuntu/${OS_VERSION} COPY packages.yaml packages.yaml RUN yq eval packages.yaml -j | jq -r '.common[],.apt[],.ubuntu[]' | sort -u > packages.list \ && dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list RUN chown -R _apt /ubuntu/$OS_VERSION \ && cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \ --no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION /Packages.gz FROM scratchCOPY --from=centos7 /centos /centos COPY --from=stretch /debian /debian COPY --from=buster /debian /debian COPY --from=bionic /ubuntu /ubuntu COPY --from=focal /ubuntu /ubuntu
使用 构建好了离线源之后,在部署的机器上运行一个 Nginx 服务,用于提供 HTTP 方式下载这些软件包,同时需要配置一下机器的包管理器 repo 配置文件。
1 2 3 4 5 [Inra-Mirror] name=Infra Mirror Repository baseurl=http://172.20.0.10/centos/7/ enabled=1 gpgcheck=1
1 deb [trusted=yes] http://172.20.0.10:8080/debian stretch/
1 deb [trusted=yes] http://172.20.0.10:8080/debian buster/
1 deb [trusted=yes] http://172.20.0.10:8080/ubuntu bionic/
1 deb [trusted=yes] http://172.20.0.10:8080/debian focal/
GitHub Action 自动构建 准备好上面这些 Dockerfile 之后,接下来就要考虑构建的问题了。对于一个 PaaS 或者 IaaS 产品需要适配主流的 Linux 发行版,有时还需要适配 arm64 架构的机器。如果本地手动 docker build 来构建的话,效率很低。因此我们需要使用 GitHub actions 自动构建这些 rpm/deb 包的离线源,具体实现代码可参考 k8sli/os-packages
代码结构 在 build 目录里存放各种发行版的 Dockerfile。由于不同的发行版以及每个发行版的版本构建方法千差万别,因此每个发行版 OS 在一个单独的 Dockerfile 里构建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 os-packages/ ├── LICENSE ├── Makefile ├── README.md ├── build │ ├── Dockerfile.os.centos7 │ ├── Dockerfile.os.centos8 │ ├── Dockerfile.os.debian10 │ ├── Dockerfile.os.debian9 │ ├── Dockerfile.os.fedora33 │ ├── Dockerfile.os.fedora34 │ ├── Dockerfile.os.ubuntu1804 │ └── Dockerfile.os.ubuntu2004 ├── packages.yaml └── repos ├── CentOS-All-in-One.repo ├── Debian-buster-All-in-One.list ├── Fedora-All-in-One.repo └── Ubuntu-focal-All-in-One.list
Workflow
1 2 3 4 5 6 7 8 --- name: Build os-packages image on: push: tag: - 'v*' branch: [main, release-*, master] workflow_dispatch:
1 2 3 4 5 6 7 8 9 env: IMAGE_REGISTRY: "ghcr.io" REGISTRY_USER: "${{ github.repository_owner }} " REGISTRY_TOKEN: "${{ secrets.GITHUB_TOKEN }} " IMAGE_REPO: "ghcr.io/${{ github.repository_owner }} "
构建矩阵,这些 job 会各自运行一个 runner 来进行并行构建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 jobs: build: runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: include: - name: ubuntu-bionic image_name: os-packages-ubuntu1804 dockerfile: build/Dockerfile.os.ubuntu1804 - name: ubuntu-focal image_name: os-packages-ubuntu2004 dockerfile: build/Dockerfile.os.ubuntu2004 - name: centos-7 image_name: os-packages-centos7 dockerfile: build/Dockerfile.os.centos7 - name: centos-8 image_name: os-packages-centos8 dockerfile: build/Dockerfile.os.centos8 - name: debian-buster image_name: os-packages-debian10 dockerfile: build/Dockerfile.os.debian10 - name: debian-stretch image_name: os-packages-debian9 dockerfile: build/Dockerfile.os.debian9
checkout 代码,配置 buildx 构建环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Log in to GitHub Docker Registry uses: docker/login-action@v1 with: registry: ${{ env.IMAGE_REGISTRY }} username: ${{ env.REGISTRY_USER }} password: ${{ env.REGISTRY_TOKEN }}
通过 git describe --tags
方式生成一个唯一的镜像 tag
1 2 3 4 - name: Prepare for build images shell: bash run: | git describe --tags --always | sed 's/^/IMAGE_TAG=/' >> $GITHUB_ENV
构建镜像并 push 到镜像仓库,后面打包一个 All-in-one 的包时候会用到
1 2 3 4 5 6 7 8 9 - name: Build and push os-package images uses: docker/build-push-action@v2 with: context: . push: ${{ github.event_name != 'pull_request' }} file: ${{ matrix.dockerfile }} platforms: linux/amd64,linux/arm64 tags: | ${{ env.IMAGE_REPO }}/${{ matrix.image_name }}:${{ env.IMAGE_TAG }}
生成新的 Dockerfile,导出镜像到本地目录
1 2 3 4 5 6 7 8 9 10 11 12 - name: Gen new Dockerfile shell: bash run: | echo -e "FROM scratch\nCOPY --from=${{ env.IMAGE_REPO }} /${{ matrix.image_name }} :${{ env.IMAGE_TAG }} / /" > Dockerfile - name: Build kubeplay image to local uses: docker/build-push-action@v2 with: context: . file: Dockerfile platforms: linux/amd64,linux/arm64 outputs: type=local,dest=./
将最终构建产物打包上传到 GitHub release
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - name: Prepare for upload package shell: bash run: | mv linux_amd64/resources resources tar -I pigz -cf resources-${{ matrix.image_name }}-${IMAGE_TAG}-amd64.tar.gz resources --remove-files mv linux_arm64/resources resources tar -I pigz -cf resources-${{ matrix.image_name }}-${IMAGE_TAG}-arm64.tar.gz resources --remove-files sha256sum resources-${{ matrix.image_name }}-${IMAGE_TAG}-{amd64,arm64}.tar.gz > resources-${{ matrix.image_name }}-${IMAGE_TAG}.sha256sum.txt - name: Release and upload packages if: startsWith(github.ref, 'refs/tags/' ) uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: files: | resources-${{ matrix.image_name }}-${{ env.IMAGE_TAG }}.sha256sum.txt resources-${{ matrix.image_name }}-${{ env.IMAGE_TAG }}-amd64.tar.gz resources-${{ matrix.image_name }}-${{ env.IMAGE_TAG }}-arm64.tar.gz
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 upload: needs: [build] runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Log in to GitHub Docker Registry uses: docker/login-action@v1 with: registry: ${{ env.IMAGE_REGISTRY }} username: ${{ env.REGISTRY_USER }} password: ${{ env.REGISTRY_TOKEN }} - name: Prepare for build images shell: bash run: | git describe --tags --always | sed 's/^/IMAGE_TAG=/' >> $GITHUB_ENV source $GITHUB_ENV echo "FROM scratch" > Dockerfile echo "COPY --from=${{ env.IMAGE_REPO }} /os-packages-ubuntu1804:${IMAGE_TAG} / /" >> Dockerfile echo "COPY --from=${{ env.IMAGE_REPO }} /os-packages-ubuntu2004:${IMAGE_TAG} / /" >> Dockerfile echo "COPY --from=${{ env.IMAGE_REPO }} /os-packages-centos7:${IMAGE_TAG} / /" >> Dockerfile echo "COPY --from=${{ env.IMAGE_REPO }} /os-packages-centos8:${IMAGE_TAG} / /" >> Dockerfile echo "COPY --from=${{ env.IMAGE_REPO }} /os-packages-debian9:${IMAGE_TAG} / /" >> Dockerfile echo "COPY --from=${{ env.IMAGE_REPO }} /os-packages-debian10:${IMAGE_TAG} / /" >> Dockerfile - name: Build os-packages images to local uses: docker/build-push-action@v2 with: context: . file: Dockerfile platforms: linux/amd64,linux/arm64 outputs: type=local,dest=./ - name: Prepare for upload package shell: bash run: | mv linux_amd64/resources resources tar -I pigz -cf resources-os-packages-all-${IMAGE_TAG}-amd64.tar.gz resources --remove-files mv linux_arm64/resources resources tar -I pigz -cf resources-os-packages-all-${IMAGE_TAG}-arm64.tar.gz resources --remove-files sha256sum resources-os-packages-all-${IMAGE_TAG}-{amd64,arm64}.tar.gz > resources-os-packages-all-${IMAGE_TAG}.sha256sum.txt - name: Release and upload packages if: startsWith(github.ref, 'refs/tags/' ) uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: files: | resources-os-packages-all-${{ env.IMAGE_TAG }}.sha256sum.txt resources-os-packages-all-${{ env.IMAGE_TAG }}-amd64.tar.gz resources-os-packages-all-${{ env.IMAGE_TAG }}-arm64.tar.gz
优化 Dockerfile 可以考虑将 Dockerfile 中的构建过程合并成一个 shell 脚本,然后在 Dockerfile 中调用这个脚本即可,这样可优化 Dockerfile 代码的可维护性,同时后续适配多种 OS 的时候也可以复用部分相同的代码,但这样可能会导致 docker build 缓存的失效问题。
当然也可以使用脚本将多个 Dockerfile 合并成一个,如下:
1 2 3 4 # Merge all Dockerfile.xx to an all-in-one file ls Dockerfile.* | xargs -L1 grep -Ev 'FROM scratch|COPY --from=' > Dockerfile echo "FROM scratch" >> Dockerfile ls Dockerfile.* | xargs -L1 grep 'COPY --from=' >> Dockerfile
其实如果使用 GitHub actions 来构建的话,就不需要进行合并了,使用 actions 矩阵构建的特性可并行构建。
Package version 对于一些版本中包含 Linux 发行版本代号的包来讲,手动维护这个代号不太方便,可以考虑将它魔改成占位变量的方式,在构建容器内生成 package.list 文件后统一使用 sed 把这些占位的变量给替换一下,如下:
1 2 apt: - docker-ce=5:19.03.15~3-0~__ID__-__VERSION_CODENAME__
使用 sed 处理一下生成的 packages.list 中的这些占位符变量
1 sed -i "s|__ID__|$(sed -n 's|^ID=||p' /etc/os-release) |;s|__VERSION_CODENAME__|$(sed -n 's|^VERSION_CODENAME=||p' /etc/os-release) |" packages.list
虽然这样做很不美观,但这种方式确实可行 😂,最终能够的到正确的版本号。总之我们尽量地少维护一些包的版本,比如使用这种方式就可以将某个版本的 docker-ce 包放在配置文件的 apt 中,而不是 debian/ubuntu 中,通过一些环境变量或者 shell 脚本自动添加上这些特殊项,这样能减少一些维护成本。
踩坑
Fedora 指定包的版本时,也需要加上 Fedora 的版本
CentOS 7 和 CentOS 8 有些包的包名不同,需要单独处理一下
CentOS 7 和 CentOS 8 构建方式不同,最后生成 repodata 的时候 CentOS 8 需要单独处理一下
Fedora 33 和 Fedora34 使用 GitHub action 构建的时候 arm64 架构的会一直卡住,是由于 buildx 的 bug 所致,因此只给出了 Dockerfile,并未放在 GitHub actions 构建流水线中。
参考