docker|Docker镜像层级设计指南-构建完美镜像

一个镜像的层级
一个Docker镜像也可以继承另外一个镜像。通常的使用场景是Docker镜像去继承
一个基础的操作系统镜像。你可以将它理解为面向对象中的像类层级一样。一个Docker镜像从另外一个镜像进行继承或者”扩展”,它就可以拥有这个镜像的所有功能。同时,它也可以替换或者覆盖这个基础镜像的功能。
利用Docker镜像继承的好处可以参考面向对象继承的概念

  • 复用性 – 给基础镜像添加功能对所有继承的镜像都可用
  • 扩展性 – 可以在维护继承功能的同时向镜像添加附加功能
  • 叠加性 – 基础镜像功能可以被替换
  • 结构一致性 – 基础镜像的文件系统布局跟所有继承它的的镜像布局一致
  • 成熟性 – 随着基础映像的发展,继承的映像也会随着发展。解决安全漏洞就是一个很好的例子
从操作系统开始
如上所述,有很多官方并且被认证的Linux发行版。最常见的方法是从Docker Hub里查看有哪些可用镜像。特别需要关注的是那些官方并且被认证的Linux发行版。可以点击这里。
在一个镜像的底层,操作系统的层级往往从标准的Alpine Linux发行版扩展而来。在Docker社区已经赢得很好的口碑,他是非盈利,高效资源以及安全的。
扩展一个Linux发行版可以让你自定义一些功能以至于满足你们组织的需求。下面例子展示了基于标准的操作系统镜像做的一些修改。这个Dockerfile可以在GitHub找到。
FROM alpine:3.10.2 LABEL relenteny.repository.url=https://github.com/relenteny/alpine LABEL relenteny.repository.tag=3.10.2 LABEL relenteny.alpine.version=3.10.2 RUN set -x && \ addgroup -g 1000 -S alpine && \ adduser -u 1000 -G alpine -h /home/alpine -D alpine && \ apk add –no-cache curl bind-tools USER alpine WORKDIR /home/alpine ENTRYPOINT [“/bin/sh”, “-l”] CMD []

【docker|Docker镜像层级设计指南-构建完美镜像】这个例子简单明了的说明了:
它创建了一个用户alpine,并将其设置为实例化容器的运行用户。按照配置,用户alpine不能作为特权用户运行。这是保护Docker镜像的一个重要方面。实例化容器将启动的目录被设置为alpine用户的主目录。
作为一个例子, curl以及bind-tools都会被添加到基础镜像中。当需要其他额外的包时, 这是一个构建基础镜像的博弈,关于这个镜像是否可以被后续镜像广泛使用与镜像大小,容器将简单地启动一个登录shell。这里不需要CMD[]指令,但是在Dockerfile中定义’ ENTRYPOINT ‘和’ CMD ‘通过指定完整的调用定义提供了一定程度的文档
实例化后, 容器直接执行启动命令. 我们在Dockerfile中并不需要定义CMD []指令, 但是如果定义了ENTRYPOINT与CMD,它通过指定完整的调用定义提供了一定程度的文档说明(译者补充: 可以认为CMD作为ENTRYPOINT的参数)。
LABEL可以有多种用途。它们纯粹是提供信息,但是在通过开发和部署过程跟踪镜像非常有用。
例子: 支持Python版本化
不管运行时平台是什么,都会出现平台版本的问题。随着时间的推移,预计不同的应用程序和服务将使用不同(或特定的)运行时平台版本。Python应用程序也不例外。
操作系统发行版将支持运行时平台版本的一部分。然而依赖于操作系统发行版提供的特定版本的运行时平台,会产生对操作系统版本的依赖,随着时间的推移,这种依赖会变得难以管理。为了与公共Docker镜像保持一致,运行时平台应该使用相同的父镜像进行构建。
幸运的是,大多数Linux版本都广泛支持多版本的应用程序运行时平台。需要设计一个模式来解决一些细节,通过这个模式在维护镜像层次结构的同时,多版本的运行时平台可以轻松并且快速的被组装。
运行时平台通常有多种安装和配置方式。对于Python运行时镜像,我的选择是使用pyenv。pyenv通常用于管理安装在单个系统上的多个Python版本,并且pyenv在安装和配置特定版本一般用途的Python很出色。
扩展刚才讨论的操作系统镜像,在接下来的Docker镜像层级中安装跟配置pyenv。Dockerfile同样可以在GitHub上找到。
FROM relenteny/alpine:3.10.2 LABEL relenteny.repository.url=https://github.com/relenteny/python-pyenv LABEL relenteny.repository.tag=1.2.14 LABEL relenteny.pyenv.version=1.2.14 LABEL relenteny.pyenv.virtualenv.version=1.1.5 COPY build /opt/build USER root RUN set -x && \ apk add –no-cache git bash build-base libffi-dev openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev && \ cp -r /opt/build/home/alpine/* /home/alpine && \ chmod +x /home/alpine/bin/*.sh && \ chown -R alpine.alpine /home/alpine && \ rm -rf /opt/build USER alpine RUN set -x && \ cd /home/alpine && \ git clone https://github.com/pyenv/pyenv.git /home/alpine/.pyenv && \ cd /home/alpine/.pyenv && \ git branch pyenv-1.2.14 v1.2.14 && \ git checkout pyenv-1.2.14 && \ cd /home/alpine && \ git clone https://github.com/pyenv/pyenv-virtualenv.git /home/alpine/.pyenv/plugins/pyenv-virtualenv && \ cd /home/alpine/.pyenv/plugins/pyenv-virtualenv && \ git branch virtualenv-1.1.5 v1.1.5 && \ git checkout virtualenv-1.1.5 && \ cd /home/alpine && \ echo ‘export PYENV_ROOT=”$HOME/.pyenv”‘ >> /home/alpine/.profile && \ echo ‘export PATH=”$PYENV_ROOT/bin:$PATH”‘ >> /home/alpine/.profile && \ echo ‘eval “$(pyenv init -)”‘ >> /home/alpine/.profile && \ echo ‘eval “$(pyenv virtualenv-init -)”‘ >> /home/alpine/.profile

这个Dockerfile参考pyenv的安装说明。还预装了一组将接下来要讨论的脚本。以下是一些关于这个Dockerfile的注意事项:
pyenv的安装指南可以从这里获取。针对在Alpine Linux上安装所需的包可以从pyenv的Wiki文档找到.
安装pyenv-virtualenv插件的细节可以从这里获取.
为了兼容以前的image版本,pyenv跟pyenv-virtualenv都指定了特定版本。更简洁的方法是通过版本标记创建本地分支。
用户alpine的环境变量文件.profile被修改,是为了支持pyenv环境。
  • COPY指令从源目录build复制文件到镜像目录/opt/build。在过去的几年中,我一直使用这种方式将文件复制到镜像中。在构建过程中,源目录包含一个或多个目标的子目录。对于这个镜像,源有一个home/alpine/bin的子目录结构,这个目录中的文件将被放入镜像目录/home/alpine/bin。
    添加易用脚本是为了后续的镜像:
  • install-python.sh用于配置特定版本的Python。其目的是通过执行该脚本去创建一个新镜像。例如,下面的Dockerfile代码片段将构建一个预装Python 3.7.4并为内置用户alpine提供python环境的镜像。
FROM relenteny/pyenv:1.2.14 RUN /home/alpine/bin/install-python.sh 3.7.4

通常,虽然不是必需的,但生成的镜像将会被存储在registry中,作为一个通用的python运行时镜像。使用上面的示例,镜像名称和标记是myregistry/python:3.7.4。 install-requirements.sh – 是一个工具脚本,它通过requirements.txt来安装额外的python模块。这个脚本只能在安装python的环境下执行。调用时,必须将包含要安装的模块的requirements.txt当成参数传递给这个脚本。如下所示/home/alpine/bin/install-requirments.sh /home/alpine/requirements.txt 在这里,requirements.txt被放在内置用户alpine的home目录上。当然,这取决于实际的Dockerfile是如何编写的。下面是它的用法示例。

构建一个标准规范的Python镜像
如前一节所讨论的,pyenv镜像的设计目的是构建后续的Python镜像。通过配置一个标准的操作系统镜像,以及一个安装和配置多版本Python的镜像,可以非常容易地构建可用于多种用途的Python镜像。
在我们层次结构中,接下来的镜像是安装Python 3.7.4。Dockerfile可以在GitHub上找到。
FROM relenteny/pyenv:1.2.14 LABEL relenteny.repository.url=https://github.com/relenteny/python LABEL relenteny.repository.tag=3.7.4 LABEL relenteny.python.version=3.7.4 RUN /home/alpine/bin/install-python.sh 3.7.4

就是这样。在Dockerfile中只需几个简单的指令,就可以通过之前的基础Python镜像创建出新版本。
添加一个应用程序框架
在任何语言平台中,仅仅使用平台提供的构造器从头搭建应用程序是很少见的。通常,在构建应用程序时会添加额外的框架、工具。
下面的案例示范是如何安装流行的web框架Flask。Dockerfile可以在GitHub上找到。
FROM relenteny/python:3.7.4 LABEL relenteny.repository.url=https://github.com/relenteny/flask LABEL relenteny.repository.tag=1.1.1 LABEL relenteny.flask.version=1.1.1 COPY build /opt/build USER root RUN set -x && \ cp -r /opt/build/home/alpine/* /home/alpine && \ chown -R alpine.alpine /home/alpine/* && \ rm -rf /opt/build USER alpine RUN set -x && \ cd /home/alpine && \ bin/install-requirements.sh /home/alpine/requirements.txt && \ rm /home/alpine/requirements.txt

除了LABEL指令外,在这个Dockerfile中没有任何东西可以说明Flask正在被安装。这是一个可重复的模式。无论您使用的是Flask、Django、TensorFlow、Ansible,还是众多Python框架中的任何一个,都需要添加一个requirements.txt文件,然后调用install-requirements.sh。执行最终的自定义脚本,您就拥有了具有可以共享的基础镜像,它可以跨组织共享。 这里使用的复制指令有一点需要注意。源目录build的内容被复制到镜像中的/opt/build。此外,COPY如果没有指定任何UID,用户就是root。这可以简单的通过使用--chown的选项的方式调用COPY将源构建目录的内容复制到/home/alpine。这就消除了用户在root和alpine之间进行切换。如之前所说,我发现这个模式很有用。它最适用于更复杂的镜像构建过程,其中文件可能需要安装在不同的文件系统位置(例如/等),在这些位置用户必须是root用户才能执行操作。所以,虽然这不是必须的,它仍然是构建镜像的一个既定模式。

最后,一个应用
现在我们已经做到了这里,需要在这个镜像层次结构中构建包含实际应用程序的镜像。至少是一个简单的例子。我们似乎走了很长的路才走到这一步。虽然我希望你能够理解为跨组织重用而设计镜像层次结构的价值,但是达到这一点还有一个额外的好处。构建Docker镜像需要一些时间。无论是在开发人员的工作站上或CI/CD pipeline, 镜像花几分钟去构建会阻碍生产力。或者说,作为团队成员在等待他们的测试镜像时候会感到焦虑。通过继承已经完成了大部分组装工作的镜像,最终镜像构建步骤落地和配置应用程序组件,理论上应该只需要几秒钟。
借用Miguel Grinberg写的Flask杰出教程 第一部分: Hello, World!,下面的镜像来自于本教程,并将其配置为一个Docker镜像来执行。Dockerfile可以在GitHub上找到。
FROM relenteny/flask:1.1.1 LABEL relenteny.repository.url=https://github.com/relenteny/flask-helloworld LABEL relenteny.repository.tag=1.0.0 RUN set -x && \ cd /home/alpine && \ git clone https://github.com/miguelgrinberg/microblog.git && \ cd microblog && \ git checkout v0.1 && \ echo “FLASK_APP=microblog.py” > .flaskenv WORKDIR /home/alpine/microblog ENTRYPOINT [ “/bin/sh”, “-lc”, “flask run –host 0.0.0.0”]

如果你构建了这个镜像并且给它打上”myrepository/flask-helloworld:1.0.0″的标记,用命令docker run -it -p 5000:5000 myrepository/flask-helloworld:1.0.0去实例化这个容器,然后使用curl命令或者一个浏览器,读取http://localhost:5000或者http://localhost:5000/index的URL,它会返回”Hello World”.

本文转载自:k8s中文社区

    推荐阅读