Docker在英雄联盟游戏中的实践探索(五)

jopen 9年前

【编者的话】 这篇博客是Riot的Docker实践系列博客的第五篇,主要讨论了如何从头创建你的Docker镜像。


在以前的 帖子中, 我们讲解了如何创建一系列Docker镜像来部署Jenkins容器。虽然我们原本打算在生产环境中部署Jenkins服务器容器,但是目前尚未这么做。 我们的团队从这项工作中获得的真正价值是如何快速创建Jenkins测试环境。这些环境可以用来测试插件,或者重现我们遇到的问题。例如,使用容器可以更 容易地测试Jenkins升级对于特定配置的影响。我们可以使用数据卷来持久化数据,然后大胆地升级Jenkins主镜像。

这些例子也教会了我自己如何配置更多复杂的Docker选项。比如,真实世界的场景中,Docker是如何工作的?如何解决持久化问题?一年前, 我很难找到好的文档。我们以这一系列博客为基础,来探索其他的可能性,如使用Docker容器创建从节点。这种灵活性使得我们团队可以自行开发工具,来构 建流水线和构建环境。

一年前我们开始尝试时,还没有今天这么完备的Cloudbees Jenkins镜像。根据我们是如何在生产服务器上部署Jenkins,我们手动创建了jenkins-master镜像。这也就是这篇文章的主要内容: 拆解Cloudbees镜像,真正了解它是如何工作的。当我们在做这个的时候,我们学到了很多关于Jenkins的知识。通过完全控制自己的 Dockerfile,可以修改和消除我们不想要的依赖。

某种程度上,管理依赖是很主观的。对于我们来说,我们希望尽量减少公共依赖。在Docker术语中,这就是关于如何使用“FROM”语句,从哪里 获取基础镜像。如果你想知道你的镜像是从哪里获取基础镜像,镜像里有哪些内容,那么这篇文章正是你想要的。同样的,如果你想更改默认的操作系统、Java 版本,或者删除一些Cloudbees容器的特性,也可以参考这篇文章。如果你只是满足于“It just works”,那么这篇文章可能并不适合你。

编写自己的镜像有一下这些好处:

  • 控制镜像的默认操作系统。如果Dockerfile依赖于一连串的FROM语句,哪一个最先指定了操作系统?因此,要想改变镜像,就要先了解镜像是如何形成的。
  • 在继承链中的每一个镜像都可能来自于一个公共源,并有可能在未经警告的情况下改变,或者包含不想用的内容。这是一个安全隐患。

发现依赖

第一步是弄清楚dockerfile中的依赖项列表是什么。到目前为止,在所有课程中,我们使用的都是公共的Jenkins CI Dockerfile。让我们从这里开始。

首先,先找到定义了我们正在使用的镜像的Dockerfile。Dockerhub可以方便地帮助我们找到想要的Dockerfiles。为了找出我们所使用的镜像,我们需要看看我们先前创建的Jenkins主节点的Dockerfile。
FROM jenkins:1.609.1  MAINTAINER Maxfield Stewart    # Prep Jenkins Directories  USER root  RUN mkdir /var/log/jenkins  RUN mkdir /var/cache/jenkins  RUN chown -R jenkins:jenkins /var/log/jenkins  RUN chown -R jenkins:jenkins /var/cache/jenkins  USER jenkins    # Set list of plugins to download / update in plugins.txt like this  # pluginID:version  # credentials:1.18  # maven-plugin:2.7.1  # ...  # NOTE : Just set pluginID to download latest version of plugin.  # NOTE : All plugins need to be listed as there is no transitive dependency resolution.  COPY plugins.txt /usr/share/jenkins/plugins.txt  RUN /usr/local/bin/plugins.sh /usr/share/jenkins/plugins.txt    # Set Defaults  ENV JAVA_OPTS="-Xmx8192m"  ENV JENKINS_OPTS="--handlerCountStartup=100 --handlerCountMax=300 --logfile=/var/log/jenkins/jenkins.log  --webroot=/var/cache/jenkins/war"


我们可以看到,FROM语句指向了Jenkins :1.609.1。在Dockerfile术语中,这里表示名为Jenkins、标记为1.609.1的镜像,这个标记也是Jenkins的版本号。让我们来看一下Dockerhub。
  1. 访问http://hub.docker.com
  2. 如果你想在Dockerhub中分享公开镜像,你可以注册一个帐户。不过,这篇教程并不需要它。
  3. 在搜索窗口中输入镜像名称:Jenkins。
    1.PNG
  4. 返回了一系列的镜像仓库,点击最上面的Jenkins。
    2.PNG
  5. 你现在可以看到这个镜像的详细描述。
  6. Jenkins镜像提供了指向1.625.2版本的链接。这意味着自从我开始这些教程到现在,版本已经更新了。点击这个链接
    3.PNG
  7. 这个链接可以把我们链接到GitHub上,也就是我们使用的Dockerfile。

我们的目标是复制这个Dockerfile,但自己管理依赖,因此保存下来这个Dockerfile。在本教程的结尾,我们会有一个完整的依赖项列表,并形成一个新的Dockerfile。目前的Jenkins Dockerfile是:
FROM java:8-jdk  RUN apt-get update && apt-get install -y wget git curl zip && rm -rf /var/lib/apt/lists/*    ENV JENKINS_HOME /var/jenkins_home  ENV JENKINS_SLAVE_AGENT_PORT 50000    # Jenkins is run with user `jenkins`, uid = 1000  # If you bind mount a volume from the host or a data container,  # ensure you use the same uid  RUN useradd -d "$JENKINS_HOME" -u 1000 -m -s /bin/bash jenkins    # Jenkins home directory is a volume, so configuration and build history  # can be persisted and survive image upgrades  VOLUME /var/jenkins_home    # `/usr/share/jenkins/ref/` contains all reference configuration we want  # to set on a fresh new installation. Use it to bundle additional plugins  # or config file with your custom jenkins Docker image.  RUN mkdir -p /usr/share/jenkins/ref/init.groovy.d    ENV TINI_SHA 066ad710107dc7ee05d3aa6e4974f01dc98f3888    # Use tini as subreaper in Docker container to adopt zombie processes  RUN curl -fL https://github.com/krallin/tini/releases/download/v0.5.0/tini-static -o /bin/tini && chmod +x /bin/tini \           && echo "$TINI_SHA /bin/tini" | sha1sum -c -    COPY init.groovy /usr/share/jenkins/ref/init.groovy.d/tcp-slave-agent-port.groovy    ENV JENKINS_VERSION 1.625.2  ENV JENKINS_SHA 395fe6975cf75d93d9fafdafe96d9aab1996233b    # could use ADD but this one does not check Last-Modified header  # see https://github.com/docker/docker/issues/8331  RUN curl -fL http://mirrors.jenkins-ci.org/war-stable/$JENKINS_VERSION/jenkins.war -o /usr/share/jenkins/jenkins.war \           && echo "$JENKINS_SHA /usr/share/jenkins/jenkins.war" | sha1sum -c -    ENV JENKINS_UC https://updates.jenkins-ci.org    RUN chown -R jenkins "$JENKINS_HOME" /usr/share/jenkins/ref    # for main web interface:  EXPOSE 8080    # will be used by attached slave agents:  EXPOSE 50000    ENV COPY_REFERENCE_FILE_LOG $JENKINS_HOME/copy_reference_file.log    USER jenkins    COPY jenkins.sh /usr/local/bin/jenkins.sh    ENTRYPOINT ["/bin/tini", "--", "/usr/local/bin/jenkins.sh"]    # from a derived Dockerfile, can use `RUN plugins.sh active.txt` to setup /usr/share/jenkins/ref/plugins from a support bundle  COPY plugins.sh /usr/local/bin/plugins.sh


最重要的是,Jenkins使用了FROM Java:8-jdk,接下来我们需要追踪这个Dockerfile。

在我们开始这么做之前,我们需要了解这个文件中的一切。Cloudbees已经投入了大量工作来完成一个可靠的Docker镜像,因此我们可以尽量利用这一点。要注意的要点如下:
  1. 环境变量有JENKINS_HOME, JENKINS_SLAVE_PORT, TINI_SHA, JENKINS_UC, JENKINS_VERSION和COPY_REFERNCE_FILE_LOG。
  2. 镜像使用Tini来管理僵尸进程,这很有趣。因为Cloudbees觉得这是有必要的,因此,我们也会保留这一点。
  3. Jenkins的war文件是通过curl下载到镜像中的。
  4. 使用apt-get安装了wget、curl和git。
  5. 有三个文件是被复制到容器中的: jenkins.sh, plugins.sh和init.groovy。
  6. 暴露了一些端口: 8080和50000,前者是Jenkins的侦听端口,后者是从节点与Jenkins的通信端口。

这需要花大量时间和精力来维护。

我们需要追踪每一个FROM语句,直到找到基础镜像中的操作系统。再次搜索Dockerhub中的下一个镜像:Java:8-jdk。
  1. 在Dockerhub搜索窗口中,搜索“java”。(确保你在Dockerhub主页,而不只是在搜索Jenkins的页面)。
  2. 就像搜索Jenkins一样,我们点击返回结果中的第一个。
    4.PNG
  3. 在“Supported tags”下面,我们可以看到Java有很多不同的标签和镜像。找到“8-jdk”这个标签,点击它的Dockerfile
    5.PNG

    这是一个有趣的镜像。Java 8-jdk镜像又引用另一个公共镜像buildpack-deps:jessie。因此,我们将要探索下一个镜像,但是我们还没有搞清楚这个镜像做了什么。

这一镜像做了以下事情:
  1. 安装Unzip。
  2. 使用apt-get安装opendjdk-8。
  3. 安装ca_certificates。
  4. 创建了debian:jessie backports,可以确认该镜像使用Debian:jessie。

以下是完整的Dockerfile:
FROM buildpack-deps:jessie-scm    # A few problems with compiling Java from source:  #  1. Oracle.  Licensing prevents us from redistributing the official JDK.  #  2. Compiling OpenJDK also requires the JDK to be installed, and it gets  #       really hairy.    RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*  RUN echo 'deb http://httpredir.debian.org/debian jessie-backports main' > /etc/apt/sources.list.d/jessie-backports.list    # Default to UTF-8 file.encoding  ENV LANG C.UTF-8    ENV JAVA_VERSION 8u66  ENV JAVA_DEBIAN_VERSION 8u66-b17-1~bpo8+1    # see https://bugs.debian.org/775775  # and https://github.com/docker-library/java/issues/19#issuecomment-70546872  ENV CA_CERTIFICATES_JAVA_VERSION 20140324    RUN set -x \      && apt-get update \      && apt-get install -y \          openjdk-8-jdk="$JAVA_DEBIAN_VERSION" \          ca-certificates-java="$CA_CERTIFICATES_JAVA_VERSION" \      && rm -rf /var/lib/apt/lists/*    # see CA_CERTIFICATES_JAVA_VERSION notes above  RUN /var/lib/dpkg/info/ca-certificates-java.postinst configure    # If you're reading this and have any feedback on how this image could be  #   improved, please open an issue or a pull request so we can discuss it!

我们需要复制这一切。现在,让我们去寻找下一个Dockerfile,也就是buildpack-deps:jessie-scm。我们重复之前的过程:
  1. 在Dockerhub主页面上搜索“buildpack-deps”,选择第一个结果。
    6.PNG
  2. jessie-scm是"Supported Tags "的第二项。点击Dockerfile
    7.PNG

这个Dockerfile很短小。 我们可以看到它依赖于buildpack-deps:jessie-curl。但除此之外,Dockerfile安装了五个东西。
  1. BZR
  2. Git
  3. mercurial
  4. openssh-client
  5. subversion

因为这是一个SCM镜像,所以这是合理的。你需要衡量是否需要复制这些特定行为。首先,Cloudbees Jenkins镜像已经安装了Git。如果你不需要使用bazaar、mercurial或subversion,那么你也许不需要安装它们,从而节省一 部分空间。为了完整起见,这里是完整的Dockerfile:
FROM buildpack-deps:jessie-curl    RUN apt-get update && apt-get install -y --no-install-recommends \          bzr \          git \          mercurial \          openssh-client \          subversion \      && rm -rf /var/lib/apt/lists/*

让我们继续看下一个依赖。回到Dockerhub搜索页面。
  1. 搜索"buildpack-deps",选择第一个结果。
  2. 点击jessie-curl

看到这里,我们终于找到了最后的依赖。这个镜像有一个FROM语句:debian:jessie,这是一个操作系统。我们可以看到,这个镜像安装了一些应用程序:
  1. wget
  2. curl
  3. ca-certificates

这很有趣,因为其他的镜像已经安装了这些项目。我们不需要这一镜像,因为它没有增加新的价值。

现在,我们已经完成了对Jenkins基础镜像的探索。我们找到了需要注意和复制的内容,也发现一些东西是不需要的。这是完整的依赖链:
 Docker在英雄联盟游戏中的实践探索(五)

不要忘记:在之前的教程中,我们是基于Jenkins镜像来创建Dockerfile,下一步是如何创建自己的Dockerfile。

自己做DOCKERFILE

通过以上对于依赖的研究,我们现在可以从头创建自己的Dockerfile。最简单的方法就是剪切和粘贴所有内容,然后去掉FROM语句。这是可行的,但也会产生一些冗余指令和缺陷。我们可以通过删除一些不需要的东西,来减少镜像大小。

完整的镜像链是构建在Debian:Jessie的基础上,在这个教程中,我会讲解如何创建这一切。最后,我会提供另一个链接,是以CentOS7为基础来构建的。哪一个OS都是可以的,Docker的好处之一就是允许你选择希望使用的操作系统。

下面,让我们开始制作一个完全更新的jenkins-master镜像。这是目前jenkins-mater的Dockerfile(如果你已经学习了所有的教程):
FROM jenkins:1.609.1  MAINTAINER Maxfield Stewart    # Prep Jenkins Directories  USER root  RUN mkdir /var/log/jenkins  RUN mkdir /var/cache/jenkins  RUN chown -R jenkins:jenkins /var/log/jenkins  RUN chown -R jenkins:jenkins /var/cache/jenkins  USER jenkins    # Set Defaults  ENV JAVA_OPTS="-Xmx8192m"  ENV JENKINS_OPTS="--handlerCountStartup=100 --handlerCountMax=300 --logfile=/var/log/jenkins/jenkins.log  --webroot=/var/cache/jenkins/war"

这里值得注意的是,我们从Jenkins 1.609.1迁移到了1.625.1。基于我们在过去几周里学到的内容,我们更新了一些JAVA_OPTS设置。

第一步:让我们修改FROM语句,使用Debian。
  1. 使用你最喜欢的编辑器,打开jenkins-master/Dockerfile。
  2. 替换FROM语句:FROM debian:jessie。

下一步,我们使用apt-get安装了所有的应用程序。添加以下内容:
RUN echo 'deb http://httpredir.debian.org/debian jessie-backports main' > /etc/apt/sources.list.d/jessie-backports.list    ENV LANG C.UTF-8  ENV JAVA_VERSION 8u66  ENV JAVA_DEBIAN_VERSION 8u66-b17-1~bpo8+1    # see https://bugs.debian.org/775775  # and https://github.com/docker-library/java/issues/19#issuecomment-70546872  ENV CA_CERTIFICATES_JAVA_VERSION 20140324    RUN apt-get update \      && apt-get install -y --no-install-recommends \      wget \      curl \      ca-certificates \      zip \      openssh-client \      unzip \      openjdk-8-jdk="$JAVA_DEBIAN_VERSION" \      ca-certificates-java="$CA_CERTIFICATES_JAVA_VERSION" \      && rm -rf /var/lib/apt/lists/*    RUN /var/lib/dpkg/info/ca-certificates-java.postinst configure

这里有很多内容。你会注意到我将所有Dockerfile中apt-get安装的应用集中到了一起。为了这么做,我需要设置所有必要的关于Java版本和证书的环境变量。我建议在继续添加内容之前,先充分地测试它。
docker build jenkins-master/

 Docker在英雄联盟游戏中的实践探索(五)

我们只是测试了安装程序是正常的,但是这个镜像还暂时不能用。你可能会遇到一个错误:a missing Jenkins user,这是OK的。因为我们修改基础镜像为Debian操作系统,我们暂时删除了创建用户的Jenkins镜像。

通过这样的安装方式,我们基本上吸收了Buildpack镜像和Java镜像。那么,剩下的就是将Jenkins镜像吸收进我们的主镜像。

首先,安装Tini(你可以从它的 GitHub了解Tini的更多信息)。Cloudbees推荐使用Tini来管理Jenkins容器中的子进程,因此我们将会在Dockerfile中保留它:
# Install Tini  ENV TINI_SHA 066ad710107dc7ee05d3aa6e4974f01dc98f3888    # Use tini as subreaper in Docker container to adopt zombie processes  RUN curl -fL https://github.com/krallin/tini/releases/download/v0.5.0/tini-static -o /bin/tini && chmod +x /bin/tini \    && echo "$TINI_SHA /bin/tini" | sha1sum -c -

安装Tini之后,我们将所有额外的环境变量集中在一起:
# SET Jenkins Environment Variables  ENV JENKINS_HOME /var/jenkins_home  ENV JENKINS_SLAVE_AGENT_PORT 50000  ENV JENKINS_VERSION 1.625.2  ENV JENKINS_SHA 395fe6975cf75d93d9fafdafe96d9aab1996233b  ENV JENKINS_UC https://updates.jenkins-ci.org  ENV COPY_REFERENCE_FILE_LOG $JENKINS_HOME/copy_reference_file.log  ENV JAVA_OPTS="-Xmx8192m"  ENV JENKINS_OPTS="--handlerCountStartup=100 --handlerCountMax=300 --logfile=/var/log/jenkins/jenkins.log  --webroot=/var/cache/jenkins/war"

在这里,我们添加了两个新的环境变量,JAVA_OPTS和JENKINS_OPTS。同时,我也设置了Cloudbees镜像中用来安装Jenkins的所有环境变量。

接下来,为了安装Jenkins,我会做三件事:创建Jenkins用户、创建一个卷挂载点和设置初始化目录。
# Jenkins is run with user `jenkins`, uid = 1000  # If you bind mount a volume from the host or a data container,  # ensure you use the same uid  RUN useradd -d "$JENKINS_HOME" -u 1000 -m -s /bin/bash jenkins    # Jenkins home directory is a volume, so configuration and build history  # can be persisted and survive image upgrades  VOLUME /var/jenkins_home    # `/usr/share/jenkins/ref/` contains all reference configuration we want  # to set on a fresh new installation. Use it to bundle additional plugins  # or config file with your custom jenkins Docker image.  RUN mkdir -p /usr/share/jenkins/ref/init.groovy.d

在这里,我们可以运行CURL命令来下载正确的jenkins.war文件。注意,这里使用了环境变量JENKINS_VERSION,所以如果你以后想修改版本,只要修改环境变量就好了。
# Install Jenkins  RUN curl -fL http://mirrors.jenkins-ci.org/war-stable/$JENKINS_VERSION/jenkins.war -o /usr/share/jenkins/jenkins.war \    && echo "$JENKINS_SHA /usr/share/jenkins/jenkins.war" | sha1sum -c -

接下来,我会处理相关的目录和用户权限。这些和之前的教程中的jenkins-master镜像是相同的,我们希望能够更好地隔离我们的Jenkins安装,并与我们的数据卷容器兼容。我们把从Cloudbees镜像中引入了jenkins/ref目录。
# Prep Jenkins Directories  RUN chown -R jenkins "$JENKINS_HOME" /usr/share/jenkins/ref  RUN mkdir /var/log/jenkins  RUN mkdir /var/cache/jenkins  RUN chown -R jenkins:jenkins /var/log/jenkins  RUN chown -R jenkins:jenkins /var/cache/jenkins

接下来,我会暴露我们需要的端口:
# Expose Ports for web and slave agents  EXPOSE 8080  EXPOSE 50000

剩下的,就是复制Cloudbees镜像中的utility文件,设置Jenkins用户,并运行startup命令。根据Dockerfile最佳实践,我们把COPY命令放在一起。这些是很可能改变的。这样一来,一旦他们改变了,也不会使文件缓存失效。
# Copy in local config files  COPY init.groovy /usr/share/jenkins/ref/init.groovy.d/tcp-slave-agent-port.groovy  COPY jenkins.sh /usr/local/bin/jenkins.sh  COPY plugins.sh /usr/local/bin/plugins.sh  RUN chmod +x /usr/local/bin/plugins.sh  RUN chmod +x /usr/local/bin/jenkins.sh

注意:直到我们把这些文件拷贝到我们的仓库为止,我们都不会构建Dockerfile。在下一部分,我们会测试说有内容。特别注意,我添加了chmod +x命令,这保证了添加的文件是可执行的。最后,设置Jenkins用户和入口点。
# Switch to the jenkins user  USER jenkins    # Tini as the entry point to manage zombie processes  ENTRYPOINT ["/bin/tini", "--", "/usr/local/bin/jenkins.sh"]

你可以在托管在Github上的 教程上找到完整的Dockerfile。现在,让我们来测试一下所有的修改。注意,当运行到COPY命令的时候,出错是正常的。
docker build jenkins-master/

 Docker在英雄联盟游戏中的实践探索(五)

一切都如预期一样,包括缺少shell脚本的错误。这是我们需要处理的最后一件事。

打开 Cloudbees Jenkins Dockerfile GitHub仓库。我们需要拷贝这三个文件:
  1. init.groovy
  2. plugins.sh
  3. jenkins.sh

下载或复制这些文件,并放在jenkins-master目录中。
  1. init.groovy Jenkins启动时会运行这个groovy文件,它的上下文环境就是Jenkins WAR。通过设置groovy文件,可以保证Jenkins每次启动都使用相同的配置,即使是第一次安装。
  2. plugins.sh 这个脚本会自动下载一个插件列表文件中的所有插件。你可以自己包含这个文件。在以后的博客中,我会用这个脚本来安装像Docker-plugin这样的插件。
  3. jenkins.sh 这是启动Jenkins的shell脚本,使用了JAVA_OPTS和JENKINS_OPTS两个环境变量。

Cloudbees提供一系列很有用的脚本,所以我建议你保留它们。我们创建自己的Dockerfile的缺点是,如果Cloudbees决定更新他们的镜像的话,我们的镜像不会自动更新。如果你想使用这些更新的话,你需要时刻注意Cloudbees所做的更新。

现在,jenkins-master镜像的Dockerfile准备好了。如果你学习了这个系列教程的话,你已经有一个makefile,并安装了docker-compose。下一步就是构建最终的镜像,并启动Jenkins应用。
docker-compose build  docker-compose up -d


从现在开始,你已经完全控制了Docker镜像,当然,使用的OS还是公开的OS镜像。从头构建操作系统镜像,已经超出了本文的范围。

如果你感兴趣的话,基于CentOS7的镜像也可以在我们的 Github仓库中找到。使用CentOS或者Debian都是可以的,它们最大的不同就是CentOS使用yum来安装程序,而不是用apt-get。

结论

完全控制你的Docker镜像并没有那么难。首先,需要注意你的依赖,以及它们是从哪里引入的,是如何构成你的容器的。其次,你可以删除一些不需要的东西,从而节省一些磁盘空间,使你的镜像更轻一些。你也可以降低其他依赖失效的风险。

另一方面,你的责任也更大了——你再也不会获得自动更新,你必须自己追踪Cloudbees Jenkins镜像的更新。这是否有利取决于你个人的开发策略。

无论你是否选择控制自己的镜像,我都建议你遵循同样的流程。在Dockerhub上,找到你继承的Docker镜像,来帮助你理解继承关系。理解 这条继承链上的所有Dockerfile做了些什么。你应该了解你的镜像中究竟包含了什么,毕竟它是跑在你的服务器上。至少,你可以从中了解使用的基础操 作系统、Docker镜像的生态以及一些有趣的实践,比如Cloudbees如何使用Tini来管理子进程。

像往常一样,你可以在GitHub上找到本文的所有 资源。在这上面,有很多不错的讨论。欢迎你们进行评论或者提问。

现在,你已经有了一个完整功能的Jenkins主服务器和环境。下一篇教程会讲解如何将从节点连接至主服务器。特别的是,我们会基于容器来构建从服务器。有很多种不同的方式来做这件事,我们会讨论其中的几种。

原文链接:TAKING CONTROL OF YOUR DOCKER IMAGE(翻译:夏彬 校对:李颖杰)

来自:http://dockone.io/article/913