EulerPublisher: 一键发布openEuler镜像背后的技术
作者:@wjunLu
1. EulerPublisher架构
eulerpublisher
作为一款openEuler“一键式”发布工具,提供openEuler容器镜像、云镜像、WSL等镜像定制、发布和测试的能力。本项目主要使用python语言实现,并且以CLI
的方式提供给用户使用。
eulerpublisher
按照使用场景,分为容器镜像(container)、云镜像(cloudimg)、WSL(wsl)这几个模块,而每个模块又按照子场景划分不同的子功能模块,每个子场景功能模块具备prepare、build、publish、check等能力,因而形成一种按照主场景、子场景、以及对应动作构成的分层软件架构,如图所示:
以下围绕eulerpublisher cli
的使用方式,来理解eulerpublisher
的软件架构,cli
通用形式如下:
1 | eulerpublisher <Command1> <Command2> <Command3> <Options> |
详细说明如下:
Command1
Command1
为主场景命令,使用时必填,取值有container
、cloudimg
、wsl
等Command2
Command2
确定子场景,使用时可选,具体情况如下:- Command1 ==
container
场景,使用Docker
完成镜像构建等一系列动作,暂无其他子场景,无 Command2; - Command1 ==
cloudimg
场景,会针对不同云厂商有着不同的镜像定制及发布要求,因此Command 2必选,取值有aws
、azure
、hwcloud
等。
- Command1 ==
Command3
Command3
是Command 1~2
组合场景下的“动作”,可选prepare
、build
、push
、publish
、check
等,具体Command 1~3
的可选组合请使用eulerpublisher --help
逐级查看Options
Options
是被执行命令的一组参数,每个不同的Command 1~3
组合会有不同的参数选择,使用eulerpublisher --help
查看。
示例如下:
container场景
1 | # 容器镜像发布 |
上述效果是向dockerhub(https://hub.docker.com)的openeuler/openeuler仓库发布由Dockerfile定制的tag为22.03-LTS-SP1的支持arm64、amd64多平台的openeuler容器镜像。
cloudimg场景
1 | # AMI镜像构建 |
此命令将在AWS的ap-southeast-2
区的AMI列表生成一个22.03-LTS-SP1
版本的aarch64
架构的openEuler镜像,其制作的原始镜像来源于openeuler
桶中。
2. EulerPublisher容器镜像构建原理
背景
eulerpublisher
基于Docker CLI和Dockerfile实现定制Docker容器镜像的功能。在介绍eulerpublisher
实现容器镜像构建的原理之前,有必要先梳理一下有关的背景知识:
1). 容器镜像
容器镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的配置参数。关于容器和容器镜像的关系,可以类比为程序设计中的类和实例,容器镜像是静态的类定义,容器是镜像运行时的实例。
2). Docker镜像分层
Docker镜像是由多个文件系统叠加而成。最底层是一个引导文件系统,即bootfs
,第二层是root文件系统 rootfs
,它位于bootfs
之上,其他的文件系统在rootfs
之上。
在Docker镜像中,所有的文件系统是只读的,这也体现了Docker镜像的静态属性。当Docker启动一个容器时,会加载这些只读层并在这些只读层的上面增加一个读写层,该读写层一般也被称为容器层。这时如果修改正在运行的容器中已有的文件,那么这个文件将会从只读层复制到读写层。该文件的只读版本还在,只是被上面读写层的该文件的副本隐藏。当删除docker,或者重新启动时,之前的更改将会消失。
3). Dockerfile
Dockerfile是构建Docker镜像的“生产手册”,决定着Docker镜像的内容,以下是Dockerfile指令
Dockerfile 指令 | 说明 |
---|---|
FROM | 指定基础镜像,用于后续的指令构建。 |
MAINTAINER | 指定Dockerfile的作者/维护者。(已弃用,推荐使用LABEL指令) |
LABEL | 添加镜像的元数据,使用键值对的形式。 |
RUN | 在构建过程中在镜像中执行命令。 |
CMD | 指定容器创建时的默认命令。(可以被覆盖) |
ENTRYPOINT | 设置容器创建时的主要命令。(不可被覆盖) |
EXPOSE | 声明容器运行时监听的特定网络端口。 |
ENV | 在容器内部设置环境变量。 |
ADD | 将文件、目录或远程URL复制到镜像中。 |
COPY | 将文件或目录复制到镜像中。 |
VOLUME | 为容器创建挂载点或声明卷。 |
WORKDIR | 设置后续指令的工作目录。 |
USER | 指定后续指令的用户上下文。 |
ARG | 定义在构建过程中传递给构建器的变量,可使用 “docker build” 命令设置。 |
ONBUILD | 当该镜像被用作另一个构建过程的基础时,添加触发器。 |
STOPSIGNAL | 设置发送给容器以退出的系统调用信号。 |
HEALTHCHECK | 定义周期性检查容器健康状态的命令。 |
SHELL | 覆盖Docker中默认的shell,用于RUN、CMD和ENTRYPOINT指令。 |
示例
1 | # eulerpublisher的默认Dockerfile |
解释
- FROM scratch
指定构建的Docker镜像的基础镜像是scratch,即一个空白镜像,没有预装任何软件包或依赖项。 - ARG TARGETARCH
声明一个构建变量TARGETARCH,在构建过程中会提供该变量的值。 - ADD openEuler-docker-rootfs.$TARGETARCH.tar.xz /
将根文件系统的压缩包(openEuler-docker-rootfs.$TARGETARCH.tar.xz)添加到Docker镜像的根目录(/)中。 - RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime && \ …
设置系统时区为UTC、设置TMOUT=0、更新yum并清理缓存。 - CMD [“bash”]
指定了在Docker镜像运行容器时要执行的默认命令为bash,即启动一个交互式的Bash shell会话。
需要注意的是,通过Dockerfile构建Docker镜像时,Dockerfile中的每一个写指令执行后都将会在最终镜像增加一层。因此,为了减少镜像的分层(又称为镜像裁剪),编写Dockerfile时尽可能一行命令执行多个写操作。如上述Dockerfile中RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime && \ sed -i "s/TMOUT=300/TMOUT=0/g" /etc/bashrc
使用一行命令执行2个操作也是出于此目的。
使用eulerpublisher默认Dockerfile构建的镜像分层(size不为0的)如下
1 | IMAGE CREATED CREATED BY SIZE COMMENT |
4). docker buildx build
eulerpublisher
使用docker buildx build
命令构建多架构(linux/arm64, linux/amd64)的openEuler镜像,核心命令示例如下:
1 | docker buildx build -t openeuler/openeuler:22.03-LTS-SP1 --platform linux/arm64,linux/amd64 --push . |
该命令使用默认Dockerfile(见上文)构建openEuler镜像,由于多平台镜像构建结果无法在本地缓存,只能使用--push
保存在openeuler/openeuler
仓库中。
Eulerpublisher容器镜像构建
有了上述基础以后,eulerpublisher
构建容器镜像的思路就很容易理解。
1)按照Dockerfile的内容,eulerpublisher
制作镜像前需要先获取openEuler-docker-rootfs.$TARGETARCH.tar.xz
。该步骤由prepare
功能实现,执行命令示例如下
1 | eulerpublisher container prepare --version 22.03-LTS-SP1 |
该命令从http://repo.openeuler.org/下载aarch64和x86_64的原始镜像,将主要文件重新压缩后得到openEuler-docker-rootfs.amd64.tar.xz
和openEuler-docker-rootfs.arm64.tar.xz
两个文件。
2)获取openEuler-docker-rootfs.$TARGETARCH.tar.xz
之后,使用push
功能构建并保存镜像,命令示例如下
1 | eulerpublisher container push --repo openeuler/openeuler --version 22.03-LTS-SP1 --registry registry-1.docker.io --dockerfile /Path/To/Dockerfile |
该命令的核心步骤就是执行docker buildx build
,构建的镜像将会保存在dockerhub的openeuler/openeuler
仓库,tag为22.03-LTS-SP1
。
3. EulerPublisher云镜像构建原理
这部分描述eulerpublisher构建符合云商marketplace发布要求的云镜像的基本原理,按厂商不同分开说明
AWS
镜像发布要求
AWS Marketplace对发布的镜像规格存在一定约束:
- Virtual size最小为8GB
- ssh禁用password登录
- ssh禁用root用户登录
- 禁止set_hostname
- 禁止生成用户默认密码
为了满足上述约束,eulerpublisher提供如下两个脚本
aws_resize.sh
用于改变镜像大小和格式,并配置arm版本的ena.ko使能aws_install.sh
用于修改镜像默认配置,并安装预置软件包
AMI构建原理
eulerpublisher构建AMI分为两个阶段:
1. prepare阶段
1 | # prepare命令 |
(a). 从repo.openeuler.org下载版本为VERSION
、架构为ARCH
的原始qcow2镜像
(b). (arm镜像)配置ena.ko使能
(c). 通过aws_resize.sh
脚本resize该qcow2镜像并转换格式为RAW
(d). 通过AWS CLI将处理后的RAW镜像upload到AWS S3存储桶BUCKET
中
以上操作均在本地完成,得到制作AMI的基础镜像。
2. build阶段
1 | # build命令 |
eulerpublisher通过packer在prepare的结果之上实现对AMI的定制,具体处理如下
(e). 使用packer启动REGION
中存储桶BUCKET
的 “基础云镜像” 虚拟机实例
(f). 在虚拟机中安装基础软件包,删除 root 密码、禁用password登录等。
(g). 最终制作的云镜像命名为openEuler-{VERSION}-{ARCH}-{DATETIME}-hvm
上述过程中需要准备好packer配置文件,packer配置文件的内容会由eulerpublisher进行填充。此外,若用户需要在最终镜像内预置软件包,需要提供rpmlist
1 | # x86镜像构建的packer配置文件 |
1 | # rpmlist示例 |