使用 GitHub Actions 自动化构建 yum/apt 离线源
离线部署
对于 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。
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。
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 在不同的包管理器之前的差异,如下:
[root@centos:]# yum list docker-ce --showduplicates | grep 19.03.15
docker-ce.x86_64 3:19.03.15-3.el7 docker-ce-stable
root@debian:/# apt-cache policy docker-ce
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
---
# https://docs.docker.com/engine/installation/linux/centos/#install-from-a-package
# https://download.docker.com/linux/centos/<centos_version>>/x86_64/stable/Packages/
# or do 'yum --showduplicates list docker-engine'
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
# https://download.docker.com/linux/ubuntu/
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。
# 使用 centos 7.9 作为 base 构建镜像
FROM centos:7.9.2009 as builder
# 定义 centos 的版本和处理器体系架构
ARG OS_VERSION=7
ARG ARCH=x86_64
# 在这里定义一些构建时需要的软件包
ARG BUILD_TOOLS="yum-utils createrepo centos-release-gluster epel-release curl"
# 安装构建工具和配置一些软件源 repo
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
# 需要安装 yq 个工具来处理 packages.yaml 配置文件
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
# 解析 packages.yml 配置文件,生成所需要的 packages.list 文件
WORKDIR /centos/$OS_VERSION/os/$ARCH
COPY packages.yaml packages.yaml
# 使用 yq 先将 YAML 文件转换成 json 格式的内容,再使用 jq 过滤出所需要的包,输出为一个列表
RUN yq eval '.common[],.yum[],.centos[]' packages.yaml | sort -u > packages.list \
&& rpm -qa >> packages.list
# 下载 packages.list 中的软件包,并生成 repo 索引文件
RUN cat packages.list | xargs yumdownloader --resolve \
&& createrepo -d .
# 将构建产物复制到一层空的镜像中,方便导出为 tar 包或目录的格式
FROM scratch
COPY /centos /centos
在最后的一个 FROM 镜像中,这里指定的是 scratch
,这是一个特殊的镜像名,它代表的是一个空的镜像 layer。
# 将构建产物复制到一层空的镜像中,方便导出为 tar 包或目录的格式
FROM scratch
COPY /centos /centos
也可以直接将构建出来的产物放到 nginx 容器中,这样直接运行 nginx 容器就能提供 yum/apt 源的服务
FROM nginx:1.19
COPY --from=centos7 /centos /usr/share/nginx/html
- 如果要构建为 tar 包或者本地目录的方式,需要为 Docker 开启
DOCKER_BUILDKIT=1
这个特性
# 构建为本地目录
root@debian: ~ # DOCKER_BUILDKIT=1 docker build -o type=local,dest=$PWD -f Dockerfile.centos .
# 构建为 tar 包
root@debian: ~ # DOCKER_BUILDKIT=1 docker build -o type=tar,dest=$PWD/centos7.tar -f Dockerfile.centos .
- 构建日志如下
[+] 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
- 构建产物如下
root@debian:/build # tree centos
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 相差不多,只是包管理器的使用方式不太相同而已,这里就不再做详细的源码介绍。
- Dockerfile.debian
FROM debian:stretch-slim as stretch
ARG OS_VERSION=stretch
ARG ARCH=amd64
ARG 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 scratch
COPY /debian /debian
Ubuntu
Ubuntu 离线源的制作步骤和 Debian 差不太多,只需要简单修改一下 Debian 的 Dockerfile 应该就 OK ,比如 's/debian/ubuntu/g'
,毕竟 Debian 是 Ubuntu 的爸爸嘛 ~~,所以 apt 使用的方式和包名几乎一模一样,这里就不再赘述了。
All-in-Oone
将上述几个 Linux 发行版的 Dockerfile 整合成一个,这样只需要一个 docker build 命令就能构建出所需要的所有 OS 的离线源了。
- Dockerfile
# CentOS 7.9 2009
FROM centos:7.9.2009 as centos7
ARG OS_VERSION=7
ARG ARCH=x86_64
ARG 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 .
# Debian 9 stretch
FROM debian:stretch-slim as stretch
ARG OS_VERSION=stretch
ARG ARCH=amd64
ARG 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
# Debian 10 buster
FROM debian:buster-slim as buster
ARG OS_VERSION=buster
ARG ARCH=amd64
ARG 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
# Ubuntu 18.04 bionic
FROM ubuntu:bionic as bionic
ARG OS_VERSION=bionic
ARG ARCH=amd64
ARG 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
# Ubuntu 20.04 focal
FROM ubuntu:focal as focal
ARG OS_VERSION=focal
ARG ARCH=amd64
ARG 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 scratch
COPY /centos /centos
COPY /debian /debian
COPY /debian /debian
COPY /ubuntu /ubuntu
COPY /ubuntu /ubuntu
使用
构建好了离线源之后,在部署的机器上运行一个 Nginx 服务,用于提供 HTTP 方式下载这些软件包,同时需要配置一下机器的包管理器 repo 配置文件。
- CentOS 7
[Inra-Mirror]
name=Infra Mirror Repository
baseurl=http://172.20.0.10/centos/7/
enabled=1
gpgcheck=1
- Debian 9 stretch
deb [trusted=yes] http://172.20.0.10:8080/debian stretch/
- Debian 10 buster
deb [trusted=yes] http://172.20.0.10:8080/debian buster/
- Ubuntu 18.04 bionic
deb [trusted=yes] http://172.20.0.10:8080/ubuntu bionic/
- Ubuntu 20.04 focal
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 里构建。
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
- 触发方式
---
name: Build os-packages image
on:
push:
tag:
- 'v*'
branch: [main, release-*, master]
workflow_dispatch:
- 全局变量
env:
# 镜像仓库域名
IMAGE_REGISTRY: "ghcr.io"
# 镜像仓库用户名
REGISTRY_USER: "${{ github.repository_owner }}"
# 镜像仓库登录凭据
REGISTRY_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
# 镜像仓库推送 repo
IMAGE_REPO: "ghcr.io/${{ github.repository_owner }}"
- 构建矩阵,这些 job 会各自运行一个 runner 来进行并行构建
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 构建环境
steps:
- name: Checkout
uses: actions/checkout@v2
with:
# fetch all git repo tag for define image tag
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
- name: Prepare for build images
shell: bash
run: |
git describe --tags --always | sed 's/^/IMAGE_TAG=/' >> $GITHUB_ENV
- 构建镜像并 push 到镜像仓库,后面打包一个 All-in-one 的包时候会用到
- 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,导出镜像到本地目录
- 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
- 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
- All-in-one 合并所有构建的镜像
upload:
needs: [build]
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
# fetch all git repo tag for define image tag
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 合并成一个,如下:
# 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 把这些占位的变量给替换一下,如下:
apt:
- docker-ce=5:19.03.15~3-0~__ID__-__VERSION_CODENAME__
使用 sed 处理一下生成的 packages.list 中的这些占位符变量
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 构建流水线中。