Offline Deployment

For PaaS toB products, customers often require that the deployment solution must be installed offline, i.e., it cannot rely on any online resources during deployment, such as yum/apt sources for installing some OS packages; container images on docker.io, k8s.gcr.io, quay.io; binary downloads of open source software on GitHub, etc. download files, etc.

As a developer of platform deployment tools, I am always plagued by the challenge of offline deployment. Online container images and binaries are better to solve, because these resources are OS-independent, just download them into the installation package, and start an HTTP server and mirror repository service to provide the download of these resources when deploying.

But for yum/apt and the like, it’s not so simple.

  • Firstly, it is not possible to download the packages directly because of their complex dependencies.
  • Secondly, even after downloading, it is not possible to install the specified packages directly via yum/apt, although it is possible to copy the packages to the deployment node using scp and install them via rpm or dpkg, but this is not very elegant and the general performance is not very good.
  • Finally, there are various Linux distributions and package managers that need to be adapted, and some package names or version numbers vary greatly from one package manager to another, making it impossible to manage them uniformly.
  • It is difficult to adapt both arm64 and amd64 sources.

In summary, making offline installers from online yum/apt-type package resources that the platform deployment depends on is a tricky task. I’ve been tossing around this problem for a while and finally found a suitable solution: manage the packages with a YAML configuration file and then use Dockerfile to build them as offline tarballs or container images. If you have similar needs, you can take a look at this solution.

Docker build

The traditional way to make offline sources is to find an appropriate Linux machine, download the packages via the package manager on it, and then create repo index files for those packages.

As you can see this is a very inflexible approach, if I want to create apt offline sources for Debian 9, I need a Debian 9 machine. If I want to adapt multiple Linux distributions, I need multiple OS machines. It’s not easy to manage and use so many different OSes, and container technology, which is now very common, can help us solve this problem. For example, if I want to run a Debian 9 OS, I can just run a container of the Debian 9 image without any additional management costs and with a very light weight.

We often use containers to build backend components written in Golang in our daily work, so can we do the same for building offline sources? We just need to write a Dockerfile for different OS and package managers. Using the docker build multi-stage build feature, you can merge multiple Dockerfiles into one, and then finally copy that build to the same image using COPY -from, such as the nginx container that provides HTTP, or use the BuildKit feature to export as a tarball or as a local directory.

OS Adaptation

Based on my experience in PaaS toB, CentOS is the most popular OS used in the production environment of domestic private cloud customers, followed by Ubuntu and Debian, while RedHat requires a paid subscription, and there are no free images available on DockerHub, so this solution is not guaranteed to work with RedHat. For CentOS, only version 7.9 is required; for Ubuntu, 18.04 and 20.04 are required; for Debian, 9 and 10 are required. 20.04. If you want to support other OS offline sources such as OpenSUSE, you can also refer to this solution to write a Dockerfile file to achieve the adaptation.

Build

The build process is very simple, using a YAML-formatted configuration file to manage the installation of different packages by different package managers or Linux distributions, and doing all the build operations in a single Dockerfile. The source implementation is available at github.com/muzi502/scripts/build-packages-repo.

1
2
3
4
5
6
build
├── Dockerfile
├── Dockerfile.centos
├── Dockerfile.debian
├── Dockerfile.ubuntu
└── packages.yaml

Build process

Building an offline source using docker build can be roughly divided into the following steps.

  • Configuring the yum/apt source inside the build container and installing the tools needed for the build.
  • Generate a list of rpm/deb packages on the system and a list of packages that need to be downloaded to solve some package dependency problems.
  • downloading the required packages based on the generated package list using the appropriate package manager tool.
  • Generate index files for these packages using the appropriate package manager, such as repodata or Packages.gz files.
  • COPY the above build products into the same container image, such as nginx; you can also export them as tarballs or directories.

packages.yaml

This file is used to manage the packages that need to be installed by different package managers or Linux distributions. We can divide these packages into 4 categories according to different package managers and distributions.

  • common: for packages that have the same name in all package managers or do not require a version, such as vim, curl, wget and other tools. In general, we don’t care about the version of these tools, and the package names of these packages are the same in all package managers, so they can be classified as common packages.
  • yum/apt/dnf: This applies to different distributions using the same package manager. For example, if the package for nfs is named nfs-utils in yum but nfs-common in apt, this type of package can be classified as a class.
  • OS: for some packages that are unique to that OS, such as installing a package that is available in Ubuntu but not in Debian (e.g. debian-builder or ubuntu-dev-tools).
  • OS-distribution codename: The version of such packages is tied to the distribution codename, e.g. `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

For example, if you want to install the 19.03.15 version of docker-ce in yum, the package name is docker-ce-19.03.15, while in debian the package name is docker-ce=5:19.03.15~3-0 ~debian-stretch . You can use a package manager to see the differences between the same one package such as docker-ce before different package managers, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[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

This version number issue is also specially handled in kubespray’s source code, and there is really no good solution to solve it, so we have to maintain this version number manually.

  • 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
    
    ---
    # 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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    # 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

After introducing the package configuration file above, we will then use Dockerfile to build the offline source of these packages based on this packages.yml configuration file. Here is the Dockerfile for building CentOS 7 offline sources.

 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
# 使用 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 --from=centos7 /centos /centos

In the last FROM image, scratch is specified here, which is a special image name that represents an empty image layer.

1
2
3
# 将构建产物复制到一层空的镜像中,方便导出为 tar 包或目录的格式
FROM scratch
COPY --from=centos7 /centos /centos

You can also put the build directly into the nginx container, so that running the nginx container directly will serve the yum/apt sources

1
2
FROM nginx:1.19
COPY --from=centos7 /centos /usr/share/nginx/html
  • To build as a tarball or local directory, you need to enable the DOCKER_BUILDKIT=1 feature for Docker

    1
    2
    3
    4
    
    # 构建为本地目录
    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 .
    
  • The build log is as follows

     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
    
  • The construction products are as follows

     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 # 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

The following is a Debian9 build Dockerfile, the process is similar to CentOS, only the package manager is used in a different way, so I won’t do a detailed source code introduction here.

  • Dockerfile.debian

     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 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 --from=builder /debian /debian
    

Ubuntu

The steps to create an Ubuntu offline source are not too different from Debian, just a simple modification of Debian’s Dockerfile should be OK, like 's/debian/ubuntu/g', after all Debian is Ubuntu’s father ~~, so apt uses almost the same way and package names, so I won’t go into it here.

All-in-Oone

By combining the Dockerfile of the above Linux distributions into one, you can build the offline sources for all the OS you need with just one docker build command.

  • 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
 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
# 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 --from=centos7 /centos /centos
COPY --from=stretch /debian /debian
COPY --from=buster /debian /debian
COPY --from=bionic /ubuntu /ubuntu
COPY --from=focal /ubuntu /ubuntu

Use

After building the offline sources, run an Nginx service on the deployed machine to provide HTTP downloads of these packages, and configure the machine’s package manager repo configuration file.

  • CentOS 7

    1
    2
    3
    4
    5
    
    [Inra-Mirror]
    name=Infra Mirror Repository
    baseurl=http://172.20.0.10/centos/7/
    enabled=1
    gpgcheck=1
    
  • Debian 9 stretch

    1
    
    deb [trusted=yes] http://172.20.0.10:8080/debian stretch/
    
  • Debian 10 buster

    1
    
    deb [trusted=yes] http://172.20.0.10:8080/debian buster/
    
  • Ubuntu 18.04 bionic

    1
    
    deb [trusted=yes] http://172.20.0.10:8080/ubuntu bionic/
    
  • Ubuntu 20.04 focal

    1
    
    deb [trusted=yes] http://172.20.0.10:8080/debian focal/
    

GitHub Action Automated Builds

Once you have the above Dockerfile ready, it’s time to think about the build. For a PaaS or IaaS product, you need to adapt it to mainstream Linux distributions, and sometimes to machines with arm64 architecture. If you build it locally with a manual docker build, it’s not very efficient. So we need to use GitHub actions to automatically build the offline source of these rpm/deb packages, as described in k8sli/os-packages

Code Structure

The build directory holds Dockerfiles for various distributions, and since different distributions and version building methods vary widely for each distribution, each distribution OS is built in a separate 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

  • Trigger method

    1
    2
    3
    4
    5
    6
    7
    8
    
    ---
    name: Build os-packages image
    on:
    push:
        tag:
        - 'v*'
        branch: [main, release-*, master]
    workflow_dispatch:
    
  • Global Variables

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    env:
    # 镜像仓库域名
    IMAGE_REGISTRY: "ghcr.io"
    # 镜像仓库用户名
    REGISTRY_USER: "${{ github.repository_owner }}"
    # 镜像仓库登录凭据
    REGISTRY_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
    # 镜像仓库推送 repo
    IMAGE_REPO: "ghcr.io/${{ github.repository_owner }}"
    
  • To build the matrix, each of these jobs will run a runner to do a parallel build

     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 the code and configure the buildx build environment

     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 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 }}
    
  • Generate a unique mirror tag by using git describe --tags

    1
    2
    3
    4
    
    - name: Prepare for build images
    shell: bash
    run: |
        git describe --tags --always | sed 's/^/IMAGE_TAG=/' >> $GITHUB_ENV
    
  • Build the image and push it to the mirror repository, which will be used later when packaging an all-in-one package

    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 }}
    
  • Generate a new Dockerfile and export the image to the local directory

     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=./
    
  • Package and upload the final build to 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    
    
  • All-in-one merges all built images

     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 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        
    

Optimization

Dockerfile

You can consider merging the build process in Dockerfile into a shell script, and then call this script in Dockerfile, which can optimize the maintainability of Dockerfile code and reuse some of the same code when adapting to multiple OSes, but this may lead to invalidation of the docker build cache.

Of course, you can also use a script to merge multiple Dockerfiles into one, as follows.

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

In fact, if you use GitHub actions to build, you don’t need to merge, you can build in parallel using the actions matrix build feature.

Package version

For some packages that contain Linux distribution designators, it is not convenient to maintain the designators manually, so you can consider changing them to placeholder variables by using sed to replace them after the package.list file is generated in the build container, as follows.

1
2
apt:
  - docker-ce=5:19.03.15~3-0~__ID__-__VERSION_CODENAME__

Use sed to process these placeholder variables in the resulting 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

Although this is unsightly, it does work 😂 and eventually we get the correct version number. Anyway, we try to maintain as few package versions as possible, for example, by putting a certain version of the docker-ce package in apt in the configuration file instead of debian/ubuntu, and adding these special items automatically through some environment variables or shell scripts, which can reduce some maintenance costs.

Tips

  • When Fedora specifies the package version, you also need to add the version of Fedora
  • Some packages in CentOS 7 and CentOS 8 have different package names, so you need to deal with them separately.
  • CentOS 7 and CentOS 8 build method is different, the final generation of repodata when CentOS 8 need to deal with a separate
  • Fedora 33 and Fedora 34 using GitHub actions build when the arm64 architecture will always be stuck, is due to the buildx bug caused, so only gave the Dockerfile, not put in the GitHub actions build pipeline.