Docker在英雄联盟游戏中的实践探索(五)
【编者的话】 这篇博客是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。
- 访问http://hub.docker.com。
- 如果你想在Dockerhub中分享公开镜像,你可以注册一个帐户。不过,这篇教程并不需要它。
- 在搜索窗口中输入镜像名称:Jenkins。
- 返回了一系列的镜像仓库,点击最上面的Jenkins。
- 你现在可以看到这个镜像的详细描述。
- Jenkins镜像提供了指向1.625.2版本的链接。这意味着自从我开始这些教程到现在,版本已经更新了。点击这个链接。
- 这个链接可以把我们链接到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镜像,因此我们可以尽量利用这一点。要注意的要点如下:
- 环境变量有JENKINS_HOME, JENKINS_SLAVE_PORT, TINI_SHA, JENKINS_UC, JENKINS_VERSION和COPY_REFERNCE_FILE_LOG。
- 镜像使用Tini来管理僵尸进程,这很有趣。因为Cloudbees觉得这是有必要的,因此,我们也会保留这一点。
- Jenkins的war文件是通过curl下载到镜像中的。
- 使用apt-get安装了wget、curl和git。
- 有三个文件是被复制到容器中的: jenkins.sh, plugins.sh和init.groovy。
- 暴露了一些端口: 8080和50000,前者是Jenkins的侦听端口,后者是从节点与Jenkins的通信端口。
这需要花大量时间和精力来维护。
我们需要追踪每一个FROM语句,直到找到基础镜像中的操作系统。再次搜索Dockerhub中的下一个镜像:Java:8-jdk。
- 在Dockerhub搜索窗口中,搜索“java”。(确保你在Dockerhub主页,而不只是在搜索Jenkins的页面)。
- 就像搜索Jenkins一样,我们点击返回结果中的第一个。
- 在“Supported tags”下面,我们可以看到Java有很多不同的标签和镜像。找到“8-jdk”这个标签,点击它的Dockerfile。
这是一个有趣的镜像。Java 8-jdk镜像又引用另一个公共镜像buildpack-deps:jessie。因此,我们将要探索下一个镜像,但是我们还没有搞清楚这个镜像做了什么。
这一镜像做了以下事情:
- 安装Unzip。
- 使用apt-get安装opendjdk-8。
- 安装ca_certificates。
- 创建了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。我们重复之前的过程:
- 在Dockerhub主页面上搜索“buildpack-deps”,选择第一个结果。
- jessie-scm是"Supported Tags "的第二项。点击Dockerfile。
这个Dockerfile很短小。 我们可以看到它依赖于buildpack-deps:jessie-curl。但除此之外,Dockerfile安装了五个东西。
- BZR
- Git
- mercurial
- openssh-client
- 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搜索页面。
- 搜索"buildpack-deps",选择第一个结果。
- 点击jessie-curl。
看到这里,我们终于找到了最后的依赖。这个镜像有一个FROM语句:debian:jessie,这是一个操作系统。我们可以看到,这个镜像安装了一些应用程序:
- wget
- curl
- ca-certificates
这很有趣,因为其他的镜像已经安装了这些项目。我们不需要这一镜像,因为它没有增加新的价值。
现在,我们已经完成了对Jenkins基础镜像的探索。我们找到了需要注意和复制的内容,也发现一些东西是不需要的。这是完整的依赖链:
不要忘记:在之前的教程中,我们是基于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。
- 使用你最喜欢的编辑器,打开jenkins-master/Dockerfile。
- 替换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/
我们只是测试了安装程序是正常的,但是这个镜像还暂时不能用。你可能会遇到一个错误: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/
一切都如预期一样,包括缺少shell脚本的错误。这是我们需要处理的最后一件事。
打开 Cloudbees Jenkins Dockerfile GitHub仓库。我们需要拷贝这三个文件:
- init.groovy
- plugins.sh
- jenkins.sh
下载或复制这些文件,并放在jenkins-master目录中。
- init.groovy Jenkins启动时会运行这个groovy文件,它的上下文环境就是Jenkins WAR。通过设置groovy文件,可以保证Jenkins每次启动都使用相同的配置,即使是第一次安装。
- plugins.sh 这个脚本会自动下载一个插件列表文件中的所有插件。你可以自己包含这个文件。在以后的博客中,我会用这个脚本来安装像Docker-plugin这样的插件。
- 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
- 打开http://yourdockermachineip
- Jenkins应该已经启动了
(http://engineering.riotgames.c ... se.gif)
从现在开始,你已经完全控制了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