前言

在上一篇文章“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容器引擎,倘若读者环境为其他容器引擎,如本人环境选用podmancrio容器运行时引擎,则执行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-centos7centos/s2i-base-centos7,前者含centos7 bases2i,而后者在前者的基础上包含了一些开发工具,两基础镜像项目地址在这里;而后,我们安装pythonpip

$ 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 <zylpsrs@sina.cn>"

# 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

本节我们重点关注assemblerun脚本,而增量构建脚本save-artifacts所涉及的操作有点复杂,此处先我们不予考虑。

我们先配置assemble脚本,首先将源代码从临时目录/tmp/src拷贝到当前工作目录,也就是/opt/app-root目录,而后使用pip install --user安装python模块。注意:因为s2i脚本将使用普通用户运行,故这里必须使用--user使pip将模块安装到用户的&dollar;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 <<EOF
#!/bin/bash -e

exec python3 app.py
EOF

接着,我们返回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 <<EOF
pypi_index_url=https://localhost/pypi/simple
hello=word
EOF
$ s2i build /root/hello-s2i-py-src python-builder \
      --as-dockerfile /tmp/hello-py/Dockerfile \
      --environment-file=/tmp/env.txt
$ cat /tmp/hello-py/Dockerfile
...
ENV pypi_index_url="https://localhost/pypi/simple" \
    hello="word"
...

使用增量构建

镜像由只读层(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无法利用缓存层,而对于类似pipmaven安装的模块,我们希望利用上次构建镜像内安装的产物,而不依赖于构建时的缓存层特性,这就是本节将介绍的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
...

结束语

通过本文示例,我们了解到最终生成的应用镜像依赖于构建器镜像,这适用于如pythonruby这样的解析型语言编写的程序,但对于像CGolang等编译型语言来说,如果最终生成的镜像包含编译环境,则应用镜像可能会很臃肿,对于此问题,我们通过传递--runtime-image--runtime-artifact参数调用s2i程序可将编译后的程序注入到一个运行时镜像中,但本文不再予以讲解。

除了使用s2i工具来将源码自动化编译成镜像,我们还可使用cnb,后续文章作者将予以讲解cnb构建技术,作者通过对比发现cnb要优于s2i,特别是在增量构建与配置运行时镜像方面有明显的优势,s2i在配置增量构建时必须存在一个已创建好的镜像,这样对在cicd环境下配置pipeline流水线不够友好。


  1. builder: 官方项目https://github.com/sclorg上有...

我是读书人
114 声望136 粉丝

随意记录,想着啥,写啥