前言
在上一篇文章“Image镜像与Container容器基础篇”作者有提到可通过source-to-image(s2i)简化镜像构建过程,从此工具的名称我们可知其用途:将源码构建为镜像。
通过本文,读者将了解到如何制作s2i
自定义构建器1的细节,我们将对源码打包成镜像的构建细节隐藏到builder构建器中,于是,当我们选择了合适的构建器后,s2i
会将源码注入到构建器内,而后续对源码的处理全交由构建器实施,也就是说,s2i
构建器实现了自动将源代码制作为镜像的能力。
如何工作
首先,我们执行如下命令于主机上安装s2i
工具:
wget -O s2i.tgz https://github.com/openshift/source-to-image/releases/download/v1.3.0/source-to-image-v1.3.0-eed2850f-linux-amd64.tar.gz
tar -xf s2i.tgz -C /usr/local/bin/
接着执行s2i build
命令制作镜像,待其运行成功后,我们则可基于hello-python镜像启动一个容器,其可通过http://localhost:8080访问此应用。
s2i build https://github.com/sclorg/django-ex centos/python-35-centos7 hello-python
docker run -p 8080:8080 hello-python
在上述s2i build
命令中,我们选定一个builder构建器,其基于镜像centos/python-35-centos7,指定了存放在github仓库中(地址为https://github.com/sclorg/dja...)的源码路径,命令最终为我们生成了可运行的镜像,其为我们隐藏了构建镜像的细节,这些细节与操作交由构建器来实施。
注意:使用s2i build
构建镜像时依赖于docker容器引擎,倘若读者环境为其他容器引擎,如本人环境选用podman、crio容器运行时引擎,则执行s2i build
会报如下错误:
$ s2i build https://github.com/sclorg/django-ex centos/python-35-centos7 hello-python
FATAL: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
我们传递--as-dockerfile
参数告知s2i build
生成Dockerfile文件,而后选择合适的镜像构建工具通过此Dockerfile文件构建镜像,如作者将选用buildah镜像构建工具,此镜像构建工具不依赖于任何容器运行时。
$ s2i build https://github.com/sclorg/django-ex \
centos/python-35-centos7 \
--as-dockerfile /tmp/Dockerfile.gen
Application dockerfile generated in /tmp/Dockerfile.gen
通过研究Dockerfile我们可知通过s2i build
构建镜像时,若我们提供github仓库,其会将源码克隆到本地目录,而后将源码拷贝到构建镜像内,接着执行镜像内的s2i/assemble
命令对源码进行处理,如对于示例来说,其会执行pip install -r requirements.txt
安装python模块,最后通过CMD s2i/run
指定容器默认运行的命令。
$ cat /tmp/Dockerfile.gen
# 1. 选择构建器镜像
FROM centos/python-35-centos7
# 2. 添加一些标签
LABEL "io.k8s.display-name"="hello-python" \
...
# 2. 指定以root用户执行命令
USER root
# 3. 将源码拷贝到/tmp/src目录并赋权
COPY upload/src /tmp/src
RUN chown -R 1001:0 /tmp/src
# 4. 指定以此用户执行下述命令
USER 1001
# 5. 执行assemble命令
RUN /usr/libexec/s2i/assemble
# 6. 设置启动容器的默认命令
CMD /usr/libexec/s2i/run
buildah构建镜像工具不依赖于容器运行时,故我们可在任何容器运行时环境通过Dockerfile构建镜像,虽然相对于通过s2i build
直接构建镜像相比多了几个步骤,但其更通用,如对于使用cicd流水线来构建镜像,其多出的几个步骤完全不是问题。如若操作系统版本为centos 7.6以上,则可执行如下命令安装buildah工具:
yum -y install buidah
我们执行buildah bud
命令构建镜像,其构建后的镜像可被podman/crio
容器运行时识别,而若我们需推送到镜像仓库,则可使用buildah push
命令。
buildah bud --layers -f /tmp/Dockerfile.gen -t hello-python /tmp
创建构建器
通过上节的分析,我们知道构建器中的脚本s2i/assemble
负责处理源码编译等工作,而s2i/run
脚本负责运行程序,那么本节,我们将为下面的python程序编写一个自定义构建器。
$ mkdir hello-s2i-py-src && cd hello-s2i-py-src
$ cat > app.py <<'EOF'
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello, World!"
if __name__ == "__main__":
app.run(host='::', port=9080, threaded=True)
EOF
$ echo 'flask' > requirements.txt
执行如下命令使用向导创建一个s2i构建器工程,这里我们将于目录hello-s2i-py-builder下创建一个名为python-builder的构建器镜像。
s2i create python-builder hello-s2i-py-builder
工程目录结构如下所示:
$ tree hello-s2i-py-builder/
hello-s2i-py-builder/
├── Dockerfile
├── Makefile
├── README.md
├── s2i
│ └── bin
│ ├── assemble
│ ├── run
│ ├── save-artifacts
│ └── usage
└── test
├── run
└── test-app
└── index.html
我们调整Dockerfile内容,首先,因基础镜像openshift/base-centos7当前已不维护,故将其调整为centos/s2i-core-centos7或centos/s2i-base-centos7,前者含centos7 base与s2i,而后者在前者的基础上包含了一些开发工具,两基础镜像项目地址在这里;而后,我们安装python与pip。
$ cd hello-s2i-py-builder
$ cat > Dockerfile <<'EOF'
# 1. 选择s2i基础镜像
FROM centos/s2i-core-centos7
# 2. 可选。配置容器默认端口
EXPOSE 9080
# 3. 可选。此环境变量用于简要描述构建器用途
ENV SUMMARY="Platform for building and running Python 3 applications" \
DESCRIPTION="You python main code must named app.py"
# 4. 可选。这些标签用于描述构建器用途等
LABEL summary="$SUMMARY" \
description="$DESCRIPTION" \
io.k8s.description="$DESCRIPTION" \
io.openshift.expose-services="9080:http" \
io.k8s.display-name="python builder 3" \
io.openshift.tags="builder,python,python3" \
maintainer="Yanlin Zhou "
# 5. 主要步骤,使用yum安装python与pip
RUN INSTALL_PKGS="python3 \
python3-pip" && \
yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \
rpm -V $INSTALL_PKGS && \
yum -y clean all --enablerepo='*'
# 6. 此处将s2i脚本拷贝到基础镜像的s2i安装目录/usr/libexec/s2i下
COPY ./s2i/bin/ /usr/libexec/s2i
# 7. 可选。基础镜像默认用户为1001,此目录基础镜像默认权限为1001:0
RUN chown -R 1001:1001 /opt/app-root
# 8. 指定后续命令以此用户运行
USER 1001
# 9. 此处指定构建器默认命令为usage帮助命令,这也是基础构建器的默认命令
CMD ["/usr/libexec/s2i/usage"]
EOF
Makefile文件中使用docker命令构建镜像,我们按照环境实际拥有的镜像构建器调整此文件,如作者使用buildah
则将文件中的docker build
调整为buildah bud --layers
。
s2i/bin目录含如下4个文件:usage
为构建器默认执行的命令,其打印帮助信息,对于本示例我们保持默认不修改;save-artifacts
被用于增量构建,如对于示例python程序需执行pip install
安装flask模块,若利用增量构建,则下次构建时可重用之前已构建成功镜像中安装好的模块;assemble
用于对源码进行编译等操作;run
则用于启动应用进程。
$ cd s2i/bin
$ ls -l
-rwxr-xr-x 1 root root 876 Jun 14 18:16 assemble
-rwxr-xr-x 1 root root 281 Jun 14 18:16 run
-rwxr-xr-x 1 root root 398 Jun 14 18:16 save-artifacts
-rwxr-xr-x 1 root root 299 Jun 14 18:16 usage
本节我们重点关注assemble
与run
脚本,而增量构建脚本save-artifacts
所涉及的操作有点复杂,此处先我们不予考虑。
我们先配置assemble
脚本,首先将源代码从临时目录/tmp/src拷贝到当前工作目录,也就是/opt/app-root目录,而后使用pip install --user
安装python模块。注意:因为s2i
脚本将使用普通用户运行,故这里必须使用--user
使pip
将模块安装到用户的$HOME/.local目录而非系统路径下,否则将因为权限问题而报错。
$ cat > assemble <<'EOF'
#!/bin/bash -e
# 1. 打印帮助信息
if [[ "$1" == "-h" ]]; then
exec /usr/libexec/s2i/usage
fi
# 2. 从上一次构建的镜像中恢复工件,用于增量构建
# shopt -s dotglob使得*匹配隐藏文件与目录,故下面的mv可所有文件
if [ -d /tmp/artifacts ]; then
echo "---> Restoring build artifacts..."
shopt -s dotglob
mv /tmp/artifacts/* ./
shopt -u dotglob
fi
# 3. 将源码拷贝到当前工作目录
echo "---> Installing application source..."
cp -Rf /tmp/src/. ./
# 4. 编译应用,此处检查是否存在requirements.txt文件,若存在则调用pip安装模块
echo "---> Building application from source..."
if [[ -f requirements.txt ]]; then
echo "---> Installing python modules..."
export pypi_index_url=${pypi_index_url:-"https://mirrors.aliyun.com/pypi/simple"}
pip3 install -i $pypi_index_url --no-cache-dir -r requirements.txt --user
fi
EOF
而后我们配置run
脚本于其中添加启动应用的命令,如下所示:
$ cat > run <
接着,我们返回hello-s2i-py-builder目录执行make build
开始为构建器builder创建镜像。
$ make build
builder镜像构建完成后,我们使用此构建器将示例python源码打包成镜像,下面先执行s2i build --as-dockerfile
生成Dockerfile文件,此文件我们将其生成到临时目录/tmp/hello-py下面,而后执行buildah bud
命令生成最终应用镜像。
$ mkdir /tmp/hello-py
$ s2i build /root/hello-s2i-py-src python-builder \
--as-dockerfile /tmp/hello-py/Dockerfile
$ buildah bud --layers -f /tmp/hello-py/Dockerfile -t hello-python /tmp/hello-py
最后,我们以生成的应用镜像创建一个容器,而后可通过http://localhost:9080访问此容器。
$ podman run -p 9080:9080 --rm hello-python
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://[::]:9080/ (Press CTRL+C to quit)
$ curl http://localhost:9080
Hello, World!
传递环境变量
assemble
脚本中的有如下代码,此处到考虑国内访问官方pypi源速度问题,在安装模块时通过-i
指定pypi源,而源地址通过变量pypi_index_url赋予,若此环境变量没有显示提供的话,则默认赋值为阿里云pypi镜像站地址,这样的好处是:当未显示提供pypi镜像站时,其使用默认镜像站获取python模块,而同时赋予我们重置镜像站地址的能力,如我们使用nexus
搭建本地pypi镜像站,此时将使用提供的镜像站地址获取模块。
if [[ -f requirements.txt ]]; then
export pypi_index_url=${pypi_index_url:-"https://mirrors.aliyun.com/pypi/simple"}
pip3 install -i $pypi_index_url --no-cache-dir -r requirements.txt --user
fi
我们在执行s2i build
时可通过--env
或--environment-file
传递环境变量,如下所示:
# 设置两个环境变量hello与k
$ s2i build -e hello=word -e k=z ...
$ cat >/tmp/env.txt <
使用增量构建
镜像由只读层(layers)堆叠而成,而上层是对下层的引用,而在构建镜像时利用层可被缓存的特性提升构建效率,但若下层发生变动则会造成上层缓存失效,可参考“Image镜像与Container容器基础篇”这篇文章。
检查s2i build
生成的Dockerfile文件可知其构建顺序是:首先将源码拷贝到镜像后,而后执行s2i/assemble
脚本。也就是说,倘若我们源码不做任何改动,则再次执行构建将非常迅速,因s2i/assemble
不会实际执行,而是利用之前的层缓存,如下所示:
$ cat /tmp/hello-py/Dockerfile
...
COPY upload/src /tmp/src
...
RUN /usr/libexec/s2i/assemble
...
$ buildah bud --layers -f /tmp/hello-py/Dockerfile -t hello-python /tmp/hello-py
...
--> Using cache b0387eb662ad40f31be07958616d395e1678c05cba7c5730f904e24630bb50ba
STEP 7: RUN /usr/libexec/s2i/assemble
...
若是我们修改了源码,则将导致RUN s2i/assemble
无法利用缓存层,而对于类似pip
、maven
安装的模块,我们希望利用上次构建镜像内安装的产物,而不依赖于构建时的缓存层特性,这就是本节将介绍的s2i
增量构建。
为了使用s2i
的增量,我们需传递--incremental=true
参数,并提供一个已构建好的镜像作为缓存,此镜像告之增量构建从此处获取中间产物,如下所示:
$ s2i build /root/hello-s2i-py-src python-builder hello-python \
--as-dockerfile /tmp/hello-py/Dockerfile --incremental=true
观察生成的Dockerfile文件,可发现s2i
增量构建其实际上是利用了多节段构建特性,在第一阶段构建中,其利用已构建好的镜像hello-python作为缓存,执行镜像内的s2i/save-artifacts
将工件保存到一个tar包中,此工件是后续构建所需的产物;而第二阶段构建中,其从缓存镜像中获取tar包并解压到/tmp/artifacts目录下,而后期待我们的构建脚本s2i/assemble
去处理解压后的文件,如上节所示,在此文件中已经包含了对此目录的处理:将此目录内容拷贝到当前工作目录下。
$ cat /tmp/hello-py/Dockerfile
# 1. 使用提供的已构建好的镜像作为缓存
FROM hello-python as cached
USER 1001
# 2. 执行镜像内的s2i/save-artifacts脚本将所需的产物打包
RUN if [ -s /usr/libexec/s2i/save-artifacts ]; then \
/usr/libexec/s2i/save-artifacts > /tmp/artifacts.tar; \
else \
touch /tmp/artifacts.tar; \
fi
# 3. 使用builder构建器镜像
FROM python-builder
...
# 4. 从缓存镜像中拷贝工件压缩包
COPY --from=cached /tmp/artifacts.tar /tmp/artifacts.tar
...
# 5. 将工件解压到临时目录/tmp/artifacts下
RUN if [ -s /tmp/artifacts.tar ]; then \
mkdir -p /tmp/artifacts; \
tar -xf /tmp/artifacts.tar -C /tmp/artifacts; \
fi && \
rm /tmp/artifacts.tar
# 6. 若需利用缓存中的工件,我们在此文件中必须予以处理
RUN /usr/libexec/s2i/assemble
...
上节我们已在s2i/assemble
文件中包含了对临时目录/tmp/artifacts的处理逻辑,那么,为利用增量构建,我们当前需完善s2i/save-artifacts
脚本,于其中添加后续构建需要利用的中间产物,对于本例来说,我们需要利用pip install
安装的模块,故下面我们将压缩$HOME/.local目录,需注意的是,标准输出只允许存在tar流。
$ cd hello-s2i-py-builder/s2i/bin
$ cat > save-artifacts <<'EOF'
#!/bin/sh -e
pushd ${HOME} >/dev/null
if [ -d .local ]; then
tar cf - .local
fi
popd >/dev/null
EOF
我们重新执行make build
对构建器进行镜像打包,而后对s2i build --as-dockerfile
命令生成的Dockerfile运行下述命令以执行增量构建,可发现此时运行pip install
安装模块时被告之一些模块已经安装。
$ buildah bud --layers -f /tmp/hello-py/Dockerfile -t hello-python /tmp/hello-py
...
STEP 15: RUN /usr/libexec/s2i/assemble
---> Restoring build artifacts...
---> Installing application source...
---> Building application from source...
---> Installing python modules...
Requirement already satisfied
...
结束语
通过本文示例,我们了解到最终生成的应用镜像依赖于构建器镜像,这适用于如python、ruby这样的解析型语言编写的程序,但对于像C、Golang等编译型语言来说,如果最终生成的镜像包含编译环境,则应用镜像可能会很臃肿,对于此问题,我们通过传递--runtime-image
、--runtime-artifact
参数调用s2i
程序可将编译后的程序注入到一个运行时镜像中,但本文不再予以讲解。
除了使用s2i
工具来将源码自动化编译成镜像,我们还可使用cnb,后续文章作者将予以讲解cnb
构建技术,作者通过对比发现cnb
要优于s2i
,特别是在增量构建与配置运行时镜像方面有明显的优势,s2i
在配置增量构建时必须存在一个已创建好的镜像,这样对在cicd环境下配置pipeline流水线不够友好。
- builder: 官方项目https://github.com/sclorg上有... ↩