第6章 持续集成:加快开发流水线

    本章主要内容

    • 将Docker Hub工作流作为CI工具使用
    • 提升IO密集型构建的速度
    • 使用Selenium进行自动化测试
    • 在Docker里运行Jenkins
    • 把Docker作为Jenkins从节点使用
    • 在开发团队内扩展可用的运算能力

    本章中将说明几个使用Docker来启用并提升持续集成(continuous integration,CI)效率的技巧。

    到目前为止,读者应该清楚Docker是非常适合用于自动化的。它的轻量级特性以及它具有的在不同场所进行环境移植的能力,让它成为了持续集成的关键推动者。实践表明,本章中的技巧在实现业务持续集成流程中的价值不可估量。

    保证构建环境的稳定性和可重现性、使用测试工具要求大量设置、对构建容量进行扩展,这些都是读者可能遇到的问题,而Docker可以提供相应的支持。

    持续集成 持续集成是指用于加快开发流水线的一个软件生命周期策略。在每次代码库发生重大修改时,通过自动重新运行测试,可以获得更快且更稳定的交付,因为被交付的软件具有一个基础层次的稳定性。

    Docker Hub自动化构建功能已经在技巧9中提到过,只是未深入其细节。简而言之,如果(将项目)指向一个包含Dockerfile的Git仓库,Docker Hub将会负责处理镜像构建及提供下载的过程。Git仓库中发生任何变更都将触发一次镜像重新构建,这对于持续集成流程来说相当有用。

    本技巧将介绍Docker Hub工作流,通过它可触发镜像的重新构建。

    需要docker.com账号 在本节中,需要一个docker.com账号,并链接到GitHub或Bitbucket账号。如果读者还未设置及建立链接,可在github.com和bitbucket.org的首页找到说明。

    问题

    想要在代码发生变更时自动测试并将变更推送到镜像中。

    解决方案

    建立一个Docker Hub仓库并将其链接到代码上。

    讨论

    尽管Docker Hub构建并不复杂,还是有一些必要的步骤,因此将其分解成表6-1中的小块,以此作为该流程的概述。

    表6-1 建立一个链接的Docker Hub仓库

    序  号

    步  骤

    1

    在GitHub或Bitbucket上创建仓库

    2

    克隆新的Git仓库

    3

    将代码添加到Git仓库中

    4

    提交源文件

    5

    推送Git仓库

    6

    在Docker Hub上创建一个新仓库

    7

    将Docker Hub仓库链接到Git仓库上

    8

    等待Docker Hub构建完成

    9

    提交并推送一项变更到源文件中

    10

    等待第二次Docker Hub构建完成

    Git仓库与Docker仓库 Git和Docker都使用仓库这个术语来指向一个项目。这可能会对用户造成困扰。即便此处将Git仓库和Docker仓库链接在一起,这两个类型的仓库也并不是一回事。

    1.在GitHub或Bitbucket上创建仓库

    在GitHub或Bitbucket上创建一个新仓库。可以给它起任何一个想要的名字。

    2.克隆新的Git仓库

    将这个新的Git仓库克隆到宿主机上。可以在Git项目首页找到执行这一步的命令。

    将目录切换到这个仓库里。

    3.将代码添加到Git仓库中

    现在需要将代码添加到该项目中。

    此处可以添加任何所需的Dockerfile,不过代码清单6-1展示的是一个可以工作的示例。它包含两个文件,展示的是一个简单的开发工具环境。它会安装一些首选工具,并打印出当前的bash版本。

    代码清单6-1 Dockerfile——简单的开发工具容器

    1. FROM ubuntu:14.04
    2. ENV DEBIANFRONTEND noninteractive
    3. RUN apt-get update
    4. RUN apt-get install -y curl 1)           
    5. RUN apt-get install -y nmap
    6. RUN apt-get install -y socat
    7. RUN apt-get install -y openssh-client
    8. RUN apt-get install -y openssl
    9. RUN apt-get install -y iotop
    10. RUN apt-get install -y strace
    11. RUN apt-get install -y tcpdump
    12. RUN apt-get install -y lsof
    13. RUN apt-get install -y inotify-tools
    14. RUN apt-get install -y sysstat
    15. RUN apt-get install -y build-essential      
    16. RUN echo source /root/bash_extra >> /root/.bashrc 2
    17. ADD bash_extra /root/bash_extra 3) 
    18. CMD [“/bin/bash”]

    (1)安装有用的软件包

    (2)在root的bashrc中添加一行用以加载bash_extra

    (3)将源文件中的bash extra添加到容器中

    现在需要创建上面引用的bashextra文件,其内容如代码清单6-2所示。

    代码清单6-2 bash_extra——额外的bash命令

    1. bash version
    4.提交源文件

    要提交这些源文件,可使用以下命令:

    1. git commit -am Initial commit
    5.推送Git仓库

    现在可以使用以下命令将源文件推送到Git服务器上:

    1. git push origin master
    6.在Docker Hub上创建一个新仓库

    接下来需要在Docker Hub上为这个项目创建一个仓库。打开https://hub.docker.com并确保已经登录,然后点击“Add Repository”(添加仓库)并选择“Automated Build”(自动化构建)[1]

    7.将Docker Hub仓库链接到Git仓库上

    此时将看到一个选择Git服务的界面。选取所使用的源代码服务(GitHub或Bitbucket),然后从所提供的清单中选择新仓库。(如果这一步不成功,可能需要先建立Docker Hub账号与Git服务之间的链接。)

    接着将看到一个构建配置选项页面。可以保留默认值并点击下面的“Create Repository”(创建仓库)。

    8.等待Docker Hub构建完成

    这时将看到一个说明链接工作正常的页面。点击“Build Details”(构建详情)链接。

    接下来,将看到一个展示构建细节的页面。在“Builds History”(构建历史)下面会有第一次构建的条目。如果什么也没看到,可能需要点击按钮[2]来手工触发构建。构建ID后面的“Status”(状态)字段将显示“Pending”(挂起)[3]、“Finished”(完成)[4]、“Building”(正在构建)或“Error”(错误)。如果一切顺利,将看到前3个状态之一。如果看到了“Error”,就说明存在问题,需要点击构建ID查看其错误信息。

    构建可能很花时间 构建启动可能需要花费一段时间,因此有时在等待时看到“Pending”是非常正常的。

    可以时不时点击“Refresh”(刷新),直到看到构建完成。一旦构建完成,就可以通过页面顶部列出的docker pull命令拉取这个镜像。

    9.提交并推送一项变更到源文件中

    假设现在想要在登录时获取更多的环境信息,如输出正在运行的发行版详情。要实现这一点,可在bash_extra文件中添加这几行,而此时它看起来是这样的:

    1. bash version
    2. cat /etc/issue

    然后按第4步和第5步所示进行提交和推送。

    10.等待(第二次)DockerHub构建完成

    如果返回构建页面,新的一行将出现在“Builds History”(构建历史)一节的下面,可以按步骤8所述对此次构建进行跟踪。

    只在出错时收到电子邮件 如果构建出现错误将会收到相关电子邮件(如果一切正常则不会有电子邮件),因此一旦适应了这个工作流,只需要在收到电子邮件时进行检查。

    现在,可以使用Docker Hub工作流了!读者将很快适应这个框架,并发现它在保持构建更新和减少手工重新构建Dockerfile的认知负荷这两方面非常有价值。

    CI意味着软件和测试更频繁的重新构建。尽管Docker使交付CI更加容易,但接下来可能会遇到运算资源负载上升的问题。

    这里将介绍几种用于缓解磁盘IO、网络带宽及自动化测试方面的压力的方法。

    因为Docker非常适合用于自动化构建,随着时间推移,可能会用它来执行大量的I/O密集型构建。Jenkins作业、数据库重建脚本以及大量的代码签出都将对磁盘造成冲击。在这种情况下,任何能获得的速度提升对用户来说都大有裨益,这不仅能节省时间,还能极大减少资源竞争造成的大量额外开销。

    本技巧已经被证实可以提升高达1/3的速度,而实际经验也支持这一点。其作用不容小觑。

    问题

    想要加快I/O密集型构建的速度。

    解决方案

    在镜像上安装eatmydata。

    讨论

    eatmydata是一个使用系统调用来写入数据,并通过绕开持久化变更所需工作从而大大提升速度的程序。这会造成部分安全性的缺失,因此不建议作为常规使用,不过对于那些不需要持久化的环境,如测试环境,这就非常有用了。

    1.安装

    要安装eatmydata,有很多种选择。

    如果运行的是基于deb的发行版,可以运行apt-get install来安装。

    如果运行的是基于rpm的发行版,可以通过网站上搜索并下载它,然后运行rpm —install来安装。类似rpmfind.net这样的网站是一个不错的入口。

    在不得已的情况下,如果安装了一个编译器,可以按代码清单6-3所示直接下载并编译它。

    代码清单6-3 编译并安装eatmydata

    1. $ url=https://www.flamingspork.com/projects/libeatmydata/
    2. libeatmydata-105.tar.gz 1
    3. $ wget -qO- $url | tar -zxf - 2
    4. $ ./configure prefix=/usr 3)     
    5. $ make 4)      
    6. $ sudo make install 5

    (1)flamingspork.com是其维护人员的网站

    (2)如果无法下载这个版本,请到其网站上检查它是否已经更新到105之后的版本

    (3)如果想把eatmydata可执行文件安装在/usr/bin之外的其他地方,可修改此前缀目录

    (4)构建eatmydata可执行文件

    (5)安装该软件,这一步要求使用root权限

    2.使用eatmydata

    一旦libeatmydata被安装到镜像上(不论是使用软件包或是源文件),就可以在任何命令之前运行eatmydata包装脚本来使用它:

    1. docker run -d mybuildautomation eatmydata /run_tests.sh

    图6-1从高层次展示了eatmydata是如何节省处理时间的。

    6

    图6-1 应用程序写入磁盘时不使用和使用eatmydata的对比

    谨慎使用! eatmydata跳过了用于保证数据被安全地写入磁盘的步骤,因此存在一定的风险,即在程序认为数据已经写入磁盘时可能这一步还没完成。对于测试环节而言,这通常无关紧要,因为其数据可任意处理,但不要在任何数据很重要的环境中使用eatmydata来提升速度!

    由于Docker非常适合开发环境、测试环境和生产环境服务的频繁重新构建,读者很快会发现这会对网络反复造成冲击。其中的主要原因之一是从互联网下载软件包文件。即使是在一台机器上,这也可能是一个缓慢(且昂贵)的开销。本技巧展示了如何为软件包下载设置一个本地缓存,同时涵盖apt与yum。

    问题

    想要通过减少网络I/O来加快构建速度。

    解决方案

    为包管理器安装一个Squid代理。

    讨论

    图6-2演示了本技巧在理论上是如何工作的。

    6

    图6-2 使用一个Squid代理来缓存软件包

    因为软件包的调用会首先到达本地Squid代理,而只有第一次会通过互联网进行请求,对每一个软件包而言,只会有一次互联网请求。如果有数百个容器全部都在从互联网拉取相同的大型软件包,这将节省大量的时间和金钱。

    网络设置! 在宿主机上进行安装时可能会碰到网络配置问题。后续几节会给出用以判定这种情况的建议,不过如果读者不确定如何处理,可能需要寻求来自友好的网络管理员的帮助。

    1.Debian

    对于Debian(或通常所说的apt或.deb)软件包,安装要简单得多,因为存在一个打包好的版本。

    在基于Debian的宿主机上运行下面这条命令:

    1. sudo apt-get install squid-deb-proxy

    通过telnet连接到8000端口来确认该服务已经启动:

    1. $ telnet localhost 8000
    2. Trying ::1
    3. Connected to localhost.
    4. Escape character is ‘^]’.

    如果看到了上述输出,可按下Ctrl+]再按Ctrl+d退出。如果没看到这个输出,则要么Squid未被正确安装,要么它被安装在了一个非标准端口上。

    为了设置容器使用这个代理,这里提供了如下示例Dockerfile。需要注意的是,从容器的角度看,宿主机的IP地址每次运行都可能发生改变。在安装新软件前,可能需要将这个Dockerfile转换成一个在容器里运行的脚本。

    1. FROM debian
    2. RUN apt-get update -y && apt-get install net-tools 1
    3. RUN echo Acquire::http::Proxy \”http://$( \ (2)
    4. route -n | awk ‘/^0.0.0.0/ {print $2}’ \ 3
    5. ):8000\”;” \
    6. > /etc/apt/apt.conf.d/30proxy 4)           
    7. RUN echo Acquire::http::Proxy::ppa.launchpad.net DIRECT;” >> \
    8.   /etc/apt/apt.conf.d/30proxy
    9. CMD [“/bin/bash”]

    (1)确保route工具已安装

    (2)8000端口用于连接宿主机上的Squid代理

    (3)为了确定从容器角度看到的宿主机IP地址,运行route命令,并使用awk从输出中提取相关IP地址(见技巧59)

    (4)带有正确的 IP 地址和配置的输出行被添加到apt的代理配置文件中

    2.yum

    在宿主机上,使用软件包管理器安装“squid”软件包,确保Squid安装就位。

    然后需要修改Squid配置文件来创建一个更大的缓存空间。打开/etc/squid/squid.conf文件并用cache_dir ufs /var/spool/ squid 10000 16 256替换以#cache_dir ufs /var/spool/squid开头的那行注释。这将创建一个10 000 MB的空间,应该够用了。

    通过telnet连接到3128端口来确保服务已经启动:

    1. $ telnet localhost 3128
    2. Trying ::1
    3. Connected to localhost.
    4. Escape character is ‘^]’.

    如果看到了上述输出,可按下Ctrl+]再按Ctrl+d退出。如果没看到这个输出,则要么Squid未被正确安装,要么它被安装在了一个非标准端口上。

    为了设置容器使用这个代理,这里提供了如下示例Dockerfile。需要注意的是,从容器的角度看,宿主机的IP地址每次运行都可能发生改变。在安装新软件前,可能需要将这个Dockerfile转换成一个在容器里运行的脚本。

    1. FROM centos:centos7
    2. RUN yum update -y && yum install -y net-tools1
    3. RUN echo proxy=http://$(route -n | \ (2)      
    4. awk ‘/^0.0.0.0/ {print $2}’):3128 >> /etc/yum.conf 3)  
    5. RUN sed -i s/^mirrorlist/#mirrorlist/‘ \
    6. /etc/yum.repos.d/CentOS-Base.repo           
    7. RUN sed -i s/^#baseurl/baseurl/‘ \(4
    8. /etc/yum.repos.d/CentOS-Base.repo           
    9. RUN rm -f /etc/yum/pluginconf.d/fastestmirror.conf5
    10. RUN yum update y 6)              
    11. CMD [“/bin/bash”]

    (1)确保route工具已安装

    (2)为了确定从容器角度看到的宿主机IP地址,运行route命令,并使用awk从输出中提取相关IP地址(见技巧59)

    (3)3128端口用于连接宿主机上的Squid代理

    (4)为了避免可能出现的缓存未命中,移除镜像清单,只使用基础的URL。这将确保只会命中一组URL用于获取软件包,从而更有可能命中缓存的文件

    (5)移除fastest- mirror插件,因为已经不再需要它了

    (6)确保检查所有镜像。在运行yum update时,配置文件里列出的镜像可能包含了过期信息,因此第一次更新可能会很慢

    如果按这种方式设置两个容器,并依次在上面安装相同的大型软件包,就会发现第二次安装在下载它的必要软件时比第一次快得多。

    Docker化Squid代理 读者可能已经注意到可以在一个容器里而不在宿主机上运行Squid代理。为了保持说明简单,这里没有展示这一做法(在某些情况下,要让Squid工作在一个容器里需要更多步骤)。读者可以在https://github.com/jpetazzo/squid-in-a-can阅读更多这方面的内容,以及如何让容器自动使用这个代理。

    本书尚未深入讲解的Docker用例之一是运行图形化应用程序。在第3章中,在开发环境的“保存游戏”中VNC被用来连接容器(技巧18),但这可能过于笨重了——窗口被包含在VNC Viewer窗口里面,并且桌面互动性可能比较有限。这里介绍一种替代方法,将演示如何使用Selenium来编写图形化测试,同时展示如何作为CI工作流程的一部分使用这个镜像来运行测试。

    问题

    想要在CI过程中运行图形化程序,同时能将这些图形化程序显示到自己的屏幕上。

    解决方案

    共享X11服务器套接字以便在自己的屏幕上查看程序,同时在CI过程中使用xvfb。

    讨论

    不管启动容器需要做什么其他事情,都必须把X11用来显示窗口的Unix套接字作为一个卷挂载到容器里,同时需要指定窗口要显示到哪个显示器上。可以通过运行以下命令确认这两样东西是否被设置为其默认值:

    1. ~ $ ls /tmp/.X11-unix/
    2. X0
    3. ~ $ echo $DISPLAY
    4. :0

    第一个命令检查的是X11服务器Unix套接字正运行在本技巧后续内容所假定的位置上。第二个命令检查的是应用程序用于查找X11套接字的环境变量。如果运行这些命令的输出与这里的输出不一致,可能需要修改本技巧中的某些命令参数。

    检查好机器设置,现在要把运行在一个容器内的应用程序无缝地显示在容器外。需要解决的主要问题是:计算机为了防止其他人连接该机器、接管显示器以及悄悄记录按键动作所实施的安全性。在技巧26中我们已经大致看到如何完成这一步,不过当时并未说明它的工作原理以及其替代方案。

    X11具有多种对使用X套接字的容器进行认证的方式。先来看一下.Xauthority文件——它应该存在于用户的主目录中。它包含了主机名以及每台主机连接时必须使用的“私密cookie”。通过赋予Docker容器与机器相同的主机名(hostname),可以使用现有的X认证文件:

    1. $ ls $HOME/.Xauthority
    2. /home/myuser/.Xauthority
    3. $ docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \
    4.   —hostname=$HOSTNAME -v $HOME/.Xauthority:/root/.Xauthority \
    5.   -it ubuntu:14.04 bash

    第二种允许Docker访问该套接字的方法是一个较为低级的工具,但它具有安全问题,因为它会禁用 X 带来的所有防护措施。如果无人能访问该电脑,那么这是一个可以接受的解决方案,不过应该优先尝试使用X认证文件。在尝试以下步骤之后,可以通过运行xhost -来恢复安全性(不过这会把Docker容器阻挡在外):

    1. $ xhost +
    2. access control disabled, clients can connect from any host
    3. $ docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \
    4.   -it ubuntu:14.04 bash

    第一行禁用了对X的所有访问控制,而第二行将运行容器。值得注意的是无须设置主机名或挂载X套接字的任何部分。

    一旦容器启动,接下来就要检查一下它是否工作正常。可以通过运行如下命令来进行这一步:

    1. root@ef351febcee4:/# apt-get update && apt-get install -y x11-apps
    2. […]
    3. root@ef351febcee4:/# xeyes

    这将启动检测X是否正常工作的一个经典的应用程序——xeyes。我们可以看到跟随鼠标在屏幕上移动的一双眼睛。需要注意的是,(与VNC不同)该应用程序是整合到桌面里的——如果多次启动xeyes,将看到多个窗口。

    现在可以开始使用 Selenium 了。假如读者之前从未使用过它,它是一个能够实现浏览器动作自动化的工具,常常用于测试网站代码——它需要一个用于运行浏览器的图形显示器。尽管它最经常与Java一起使用,但为了获取更多互动性,这里将使用Python:

    1. root@ef351febcee4:/# apt-get install -y python2.7 python-pip firefox
    2. […]
    3. root@ef351febcee4:/# pip install selenium
    4. Downloading/unpacking selenium==2.47.3
    5. […]
    6. Successfully installed selenium==2.47.3
    7. Cleaning up
    8. root@ef351febcee4:/# python
    9. Python 2.7.6 (default, Mar 22 2014, 22:59:56)
    10. [GCC 4.8.2] on linux2
    11. Type help”, copyright”, credits or license for more information.
    12. >>> from selenium import webdriver
    13. >>> b = webdriver.Firefox()

    正如所看到的,Firefox浏览器已经启动并出现在屏幕上!上述代码所做的是安装Python、Firefox以及Python包管理器。然后它使用Python包管理器来安装Selenium的Python软件包。

    现在可以对Selenium进行尝试。下面是一个针对GitHub运行的示例会话——要理解这里的内容需要对CSS选择器有一个基本的了解。值得注意的是,网站经常会变,因此要让这段特定的代码正确地工作可能需要做一些修改:

    1. >>> b.get(‘http://github.com‘)
    2. >>> searchselector = ‘.js-site-search-form input[type=”text”]’
    3. >>> searchbox = b.find_element_by_css_selector(searchselector)
    4. >>> searchbox.send_keys(‘docker-in-practice\n’)
    5. >>> usersxpath = //nav//a[contains(text(), “Users”)]’
    6. >>> userslink = b.find_element_by_xpath(usersxpath)
    7. >>> userslink.click()
    8. >>> dlinkselector = ‘.user-list-info a
    9. >>> dlink = b.find_elements_by_css_selector(dlinkselector)[0]
    10. >>> dlink.click()
    11. >>> mlinkselector = ‘.org-header a.meta-link
    12. >>> mlink = b.find_element_by_css_selector(mlinkselector)
    13. >>> mlink.click()

    这里的细节并不重要。只需要注意,我们在容器里使用Python编写命令,并看到它们在运行于容器内部的Firefox窗口中生效,却显示在桌面上。

    这对于调试用户编写的测试非常有用,但要如何使用同一个Docker镜像将它们整合到一个CI流水线里呢?一个CI服务器通常不需要图形显示器,因此无须挂载自己的X服务器套接字即可工作,但Firefox仍然需要运行在一个X服务器里。有一个很有用的工具应运而生,它的名字叫xvfb,它会伪装运行一个可供应用程序使用的X服务器,但它不需要显示器。

    为了看一下这是如何工作的,现在来安装xvfb,提交这个容器,给它打上selenium标签,并创建一个测试脚本:

    1. >>> exit()
    2. root@ef351febcee4:/# apt-get install -y xvfb
    3. […]
    4. root@ef351febcee4:/# exit
    5. $ docker commit ef351febcee4 selenium
    6. d1cbfbc76790cae5f4ae95805a8ca4fc4cd1353c72d7a90b90ccfb79de4f2f9b
    7. $ cat > myscript.py << EOF
    8. from selenium import webdriver
    9. b = webdriver.Firefox()
    10. print Visiting github
    11. b.get(‘http://github.com‘)
    12. print Performing search
    13. searchselector = ‘.js-site-search-form input[type=”text”]’
    14. searchbox = b.find_element_by_css_selector(searchselector)
    15. searchbox.send_keys(‘docker-in-practice\n’)
    16. print Switching to user search
    17. usersxpath = //nav//a[contains(text(), “Users”)]’
    18. userslink = b.find_element_by_xpath(usersxpath)
    19. userslink.click()
    20. print Opening docker in practice user page
    21. dlinkselector = ‘.user-list-info a
    22. dlink = b.find_elements_by_css_selector(dlinkselector)[99]
    23. dlink.click()
    24. print Visiting docker in practice site
    25. mlinkselector = ‘.org-header a.meta-link
    26. mlink = b.find_element_by_css_selector(mlinkselector)
    27. mlink.click()
    28. print Done!’
    29. EOF

    注意dlink变量赋值语句的细微差异。通过尝试获取包含文本“Docker in Practice”的第100个结果,将触发一个错误,这将导致Docker容器以非零状态退出,然后在CI流水线中触发故障。

    马上来试试:

    1. $ docker run rm -v $(pwd):/mnt selenium sh -c \
    2. xvfb-run -s ‘-screen 0 1024x768x24 -extension RANDR\
    3. python /mnt/myscript.py
    4. Visiting github
    5. Performing search
    6. Switching to user search
    7. Opening docker in practice user page
    8. Traceback (most recent call last):
    9.  File myscript.py”, line 15, in <module>
    10.    dlink = b.find_elements_by_css_selector(dlinkselector)[99]
    11.    IndexError: list index out of range
    12. $ echo $?
    13. 1

    上面运行了一个自我删除的容器,它将执行这个运行在虚拟X服务器之下的Python测试脚本。和预期一样,它失败了并返回一个非零的退出码。

    CMD与入口点 sh -c “命令字符串”是Docker对CMD值的默认处理方式的一个不良后果。如果将它放到Dockerfile中,就可以去除sh -c而将xvfb-run作为入口点,这样就可以运行任何所需的测试脚本了。

    正如上面所演示的,Docker是一个灵活的工具,可以实现一些乍看起来很神奇的用途(如这里的图形化应用)。有人在Docker内部运行所有的图形化应用,包括游戏!我们不会这么疯狂,不过我们发现重新审视对于Docker的假设可能会带来令人意想不到的使用场景。

    一旦团队之间具有一个一致的开发过程,具有一个一致的构建过程也同样重要。随机失败的构建将破坏Docker的优势。

    因此,将整个CI过程容器化是合情合理的。这不仅能确保构建是可重复的,还可以将CI过程迁移到任何地方而不用担心遗落配置的某些关键部分(可能在经历种种挫折后才发现问题所在)。

    在本节的技巧中,我们将使用Jenkins(因为这是使用最广泛的CI工具),不过同样的技巧对其他CI工具应该也适用。这里不会假定读者对Jenkins非常熟悉,不过也不打算对标准的测试和构建进行说明。对这里的技巧而言这类信息不是重点。

    Docker的可移植性和轻量性,使其成为CI从节点(一台供CI主服务器连接以便执行构建的机器)的理想选择。与虚拟机从节点相比,Docker CI从节点向前迈了一大步(相对构建裸机更是一个飞跃)。它可以使用一台宿主机在多种环境上执行构建、快速销毁并创建整洁的环境来确保不受污染的构建,来使用所有熟悉的Docker工具来管理构建环境。

    能把CI从节点当作另一个Docker容器这一点特别有趣。在其中一台Docker CI从节点上出现了神秘的构建失败?把镜像拉取下来,并自己尝试这个构建。

    问题

    想要扩展并修改Jenkins从节点。

    解决方案

    将从节点配置封装在一个Docker镜像中,然后部署。

    讨论

    很多组织会建立一个重量级的Jenkins从节点(通常与主服务器在一台宿主机上),由一个集中的IT职能部门维护,这在一段时间内会起到很有益的作用。随着时间推移,团队在不断壮大他们的代码库和分支,为了保证作业运行,需要安装、更新或变更越来越多的软件。

    图6-3展示的是这种情景的一个简化版本。想象一下,数百个软件包及多重的新请求将让早已疲惫不堪的基础设施团队头痛不已。

    6

    图6-3 一台超负荷的Jenkins服务器

    说明性的、不可移植的示例 制定这个技巧是为了展示在一个容器内运行Jenkins从节点的基本要素。其结果的可移植性差一些,但是课程更易于掌握。一旦读者理解了本章的所有技巧,就能够创建一个更具可移植性的版本。

    僵持局面随之而来,因为系统管理员担心打乱其他人的构建,可能不愿意为一群人更新他们的配置管理脚本,而变更的迟缓将使各个团队变得越来越沮丧。

    Docker(天然地)提供了一个解决方案,多个团队可以使用一个基础镜像作为自己的个人Jenkins 从节点,与此同时使用与之前相同的硬件。可以创建一个具有必要的共享工具的镜像,并且允许团队对其进行变更以满足他们自己的需要。

    有些贡献人员已经在Docker Hub上传了他们自用的从节点,可以在Docker Hub上通过搜索“jenkins slave”查找。代码清单6-4展示了一个最小的Jenkins从节点Dockerfile。

    代码清单6-4 基础的Jenkins从节点Dockerfile[5]

    1. FROM ubuntu:14.04
    2. ENV DEBIAN_FRONTEND noninteractive
    3. RUN groupadd -g 1000 jenkins_slave 1)     
    4. RUN useradd -d /home/jenkins_slave -s /bin/bash \ 
    5. -m jenkins_slave -u 1000 -g jenkins_slave     
    6. RUN echo jenkins_slave:jpass | chpasswd 2
    7. RUN apt-get update && \
    8. apt-get install -y openssh-server openjdk-7-jre wget iproute3
    9. RUN mkdir -p /var/run/sshd 4)          
    10. CMD ip route | grep default via \
    11. | awk ‘{print $3}’ && /usr/sbin/sshd -D

    (1)创建Jenkins从节点用户与用户组

    (2)设置Jenkins用户密码为jpass。在更复杂的设置中,最好使用其他认证方式

    (3)安装Jenkins从节点工作所需的软件

    (4)在启动时,输出以容器的角度看到的宿主机的IP地址,并启动SSH服务器

    构建该从节点镜像,并给它打上jenkins_slave标签:

    1. $ docker build -t jenkins_slave .

    使用如下命令来运行它:

    1. $ docker run name jenkins_slave -ti -p 2222:22 jenkins_slave
    2. 172.17.42.1

    Jenkins服务器必须处于运行状态

    如果在宿主机上还未运行Jenkins服务器,请确保 Jenkins 服务器如前面技巧所述那样运行。如果读者比较心急,可运行下面这个命令:

    1. $ docker run name jenkins_server -p 8080:8080 -p 50000:50000 \
    2. dockerinpractice/jenkins:server

    如果是在本地机器上运行这个命令,可通过http://localhost:8080访问Jenkins服务器。

    如果浏览Jenkins服务器,将看到图6-4所示的欢迎页。

    要添加一个从节点,可以点击“Build Executor Status”(构建执行器状态)>“New Node”(新节点),并添加节点的名字作为“Dump Slave”(哑节点),如图6-5所示。将其命名为mydockerslave

    6

    图6-4 Jenkins的欢迎页

    6

    图6-5 命名一个新节点

    点击“OK”并使用如下设置来配置它,如图6-6所示:

    • 设置“Remote Root Directory”(远程根目录)为/home/jenkins_slave;
    • 点击“Advanced”(高级)以显示端口字段,并将其设置为2222;
    • 点击“Add”(添加)来添加凭证,并将用户名设置为jenkins_slave,密码设置为jpass
    • 确保选择的是“Launch Slave Agents on Unix Machines Via SSH”(通过SSH启动Unix机器上的从节点代理)选项;
    • 设置宿主机为容器内所看到的路由IP(此前docker run的输出);
    • 设置“Label”(标签)为dockerslave
    • 点击“Save”(保存)。

    6

    图6-6 配置新的节点

    现在,点击“Launch Slave Agent”(启动从节点代理)(假设其未自动启动),将看到该从节点代理现在被标记为在线状态。

    点击左上角的“Jenkins”返回到首页,并点击“New Item”(新项目)。创建一个名为test的“Freestyle Project”(自由式项目),点击“Build”(构建)区域下方的“Add Build Step”(添加构建步骤)>“Execute shell”(执行shell),并填入命令echo done。滚动到上方并选中“Restrict Where Project Can Be Run”(限制项目可运行的位置),并在“Label Expression”(标签表达式)中输入dockerslave。将看到“Slaves In Label”(标签中的从节点)被设置为1

    该作业现在被链接到Docker从节点上了。点击“Build Now”(马上构建),然后点击左侧下方出现的构建条目,然后点击“Console Output”(控制台输出),将在主窗口中看到类似这样的输出:

    1. Started by user anonymous
    2. Building remotely on testslave (dockerslave) in workspace
    3. /home/jenkins_slave/workspace/ls
    4. [ls] $ /bin/sh -xe /tmp/hudson4490746748063684780.sh
    5. + echo done
    6. done
    7. Finished: SUCCESS

    干得漂亮!你已经成功创建了自己的Jenkins从节点。

    现在如果读者想创建个人定制的从节点,只需要根据自己的需要修改从节点镜像的Dockerfile,并代替示例中的版本进行运行即可。

    已在 GitHub 上可用 本技巧及其相关内容的代码可从 GitHub 获取,地址是 https://github.com/ docker- in-practice/jenkins。

    将 Jenkins 主服务器放到一个容器里不像从节点那样有很多好处,不过确实可以带来Docker的不可变镜像的常规优势。

    我们发现,能对有效的主服务器配置和插件进行提交,可以极大地减轻实验负担。

    问题

    想要一个可移植的Jenkins服务器。

    解决方案

    使用一个Jenkins Docker镜像。

    讨论

    相比直接在宿主机上安装,在一个Docker容器里运行Jenkins具有一定的优势。办公室时常出现这样的叫喊:“不要动我的Jenkins服务器配置!”甚至是更糟的:“谁动了我的Jenkins服务器?”对正在运行的容器执行docker export可以克隆出Jenkins服务器的状态,以此进行升级和修改尝试将有助于消除这些抱怨。同样,备份和移植也变得更加容易。

    在本技巧中,将采用官方的Jenkins Docker镜像并做一些修改,以满足后续一些技巧对访问Docker套接字的需要,例如,在Jenkins里进行Docker构建。

    直奔源代码 本书中与Jenkins相关的示例都可以在GitHub中找到,地址是https://github.com/ docker-in- practice/jenkins.git。

    共同的基础 该Jenkins镜像及其run命令在本书与Jenkins相关的技巧中都将作为服务器来使用。

    1.构建服务器

    首先准备一个所需的服务器插件清单,并将其放置在一个名为jenkins_plugins.txt的文件中:

    1. swarm:1.22

    这个简短的清单包括了Jenkins Swarm插件(与Docker Swarm无关),这个插件在后续技巧中将会用到。

    代码清单6-5展示的是构建Jenkins服务器的Dockerfile。

    代码清单6-5 Jenkins服务器构建

    1. FROM Jenkins 1)     
    2. COPY jenkins_plugins.txt /tmp/jenkins_plugins.txt 2
    3. RUN /usr/local/bin/plugins.sh /tmp/jenkins_plugins.txt 3
    4. USER root 4)                       
    5. RUN rm /tmp/jenkins_plugins.txt         
    6. RUN groupadd -g 142 docker 5)         
    7. RUN addgroup -a jenkins docker       
    8. USER Jenkins 6

    (1)使用官方的 Jenkins镜像作为基础

    (2)复制要安装的插件清单

    (3)将插件安装到服务器中

    (4)切换到 root 用户并删除插件文件

    (5)使用与宿主机相同的用户组 ID 将Docker组添加到容器中(此数字可能与读者的有所不同)

    (6)切换回容器里的Jenkins用户

    这里没有CMDENTRYPOINT指令,因为要继承官方Jenkins镜像中定义的启动命令。

    读者的宿主机上的Docker的用户组ID可能会不一样。要想查看这个ID,可运行下面这条命令来查看本地用户组ID:

    1. $ grep -w ^docker /etc/group
    2. docker:x:142:imiell

    如果这个值不是142,则使用相应值来替换它。

    跨环境匹配用户组 ID 如果打算在 Jenkins Docker 容器中运行 Docker,Jenkins 服务器环境及Jenkins从节点环境中的用户组ID必须一致。在这种情况下,在迁移服务器时将可能出现移植问题(读者应该已经在原生服务器安装中遇到过同样的问题)。环境变量本身在这里无法起作用,因为用户组是在构建期设置的,无法进行动态配置。

    运行下面这条命令来构建这个场景中的镜像:

    1. docker build -t jenkins_server .
    2.运行服务器

    现在可以使用这个命令在Docker下运行服务器:

    1. docker run name jenkins_server -p 8080:8080 \ 1
    2. -p 50000:50000 \ 2
    3. -v /var/run/docker.sock:/var/run/docker.sock \ 3
    4. -v /tmp:/var/jenkins_home \ 4)       
    5. -d \ 5)           
    6. jenkins_server

    (1)将Jenkins服务器端口映射到宿主机的8080端口上

    (2)如果想附加构建从服务器,容器的50000端口需要打开

    (3)挂载Docker套接字,以便能在容器里与Docker守护进程互动

    (4)将 Jenkins 应用程序数据挂载到宿主机的/tmp上,这样就不会出现文件权限错误。如果要投入实际使用,可将其挂载到一个任何人都可写的目录上

    (5)以守护进程来运行该服务器

    如果访问http://localhost:8080,将看到Jenkins服务器已准备就绪,并安装了相应的插件。要确认这一点,可进入“Manage Jenkins”(管理Jenkins)>“Manage Plugins”(管理插件)>“Installed”(已安装)并查找Swarm来确认它已被安装。

    已在 GitHub 上可用 本技巧及其相关内容的代码可从GitHub获取,地址是 https://github.com/ docker-in-practice/jenkins。

    能再现环境是一个巨大的胜利,但构建能力还是受限于所拥有的专用于构建的机器的数量。如果想借用新发现的Docker从节点的灵活性在不同的环境上做实验,其结果可能会让人沮丧。构建能力可能也会因为更现实的原因——团队的成长——而变成一个问题!

    问题

    想要CI运算能力与开发工作效率一起提高。

    解决方案

    使用Jenkins的Swarm插件及一个Docker Swarm从节点来动态地配备Jenkins从节点。

    讨论

    很多中小型企业具有这样的CI模型:一台或多台Jenkins服务器致力于提供运行Jenkins作业所需的资源。图6-7展示了这一点。

    这在一段时间内可以运行良好,但随着CI过程变得越来越内嵌,经常会达到其容量限制。多数的Jenkins工作负载是受代码控制的签入动作触发的,因此当更多的开发人员进行签入时,其工作负载将上升。由于忙碌的开发人员对构建结果的等待忍耐有限,对运维团队的投诉数量将会激增。

    一个巧妙的解决方案是运行与签入代码人数相当的Jenkins从节点,如图6-8所示。

    6

    图6-7 之前:Jenkins服务器——只有一个开发人员时没问题,但无法扩展

    6

    图6-8 之后:运算能力随着团队提升

    代码清单6-6中所示的Dockerfile创建的是一个安装了Jenkins Swarm客户端插件的镜像,允许具有恰当的Jenkins Swarm服务器插件的Jenkins主服务器进行连接并运行作业。它与上一个技巧中正常的Jenkins从节点Dockerfiler的启动方式相同。

    代码清单6-6 Dockerfile[6]

    1. FROM ubuntu:14.04
    2. ENV DEBIAN_FRONTEND noninteractive
    3. RUN groupadd -g 1000 jenkins_slave
    4. RUN useradd -d /home/jenkins_slave -s /bin/bash \
    5. -m jenkins_slave -u 1000 -g jenkins_slave
    6. RUN echo jenkins_slave:jpass | chpasswd
    7. RUN apt-get update && apt-get install -y openjdk-7-jre wget unzip
    8. RUN wget -O /home/jenkins_slave/swarm-client-1.22-jar-with-dependencies.jar \
    9. http://maven.jenkins-ci.org/content/repositories/releases/org/jenkins-ci/
    10. plugins/swarm-client/1.22/swarm-client-1.22-jar-with-dependencies.jar1) 
    11. COPY startup.sh /usr/bin/startup.sh 2)     
    12. RUN chmod +x /usr/bin/startup.sh 3)    
    13. ENTRYPOINT [“/usr/bin/startup.sh”] 4

    (1)获取Jenkins Swarm插件

    (2)将启动脚本复制到容器中

    (3)将启动脚本标记为可执行

    (4)将启动脚本设置为默认的运行命令

    代码清单6-7给出的是复制到上述Dockerfile的启动脚本。

    代码清单6-7 startup.sh

    1. #!/bin/bash
    2. HOST_IP=$(ip route | grep ^default | awk ‘{print $3}’) 1) 
    3. DOCKER_IP=${DOCKER_IP:-$HOST_IP} 2
    4. JENKINS_PORT=${JENKINS_PORT:-8080} 3)     
    5. JENKINS_LABELS=${JENKINS_LABELS:-swarm}(4
    6. JENKINS_HOME=${JENKINS_HOME:-$HOME}(5
    7. echo Starting up swarm client with args:”
    8. echo $@
    9. echo and env:”
    10. echo $(env)”
    11. set x 6)     
    12. java -jar \ 7)                  
    13. /home/jenkins_slave/swarm-client-1.22-jar-with-dependencies.jar \
    14.  -fsroot $JENKINS_HOME \ 8
    15.  -labels $JENKINS_LABELS \ 9)           
    16.  -master http://$DOCKER_IP:$JENKINS_PORT $@ (10)  
    17. sleep infinity 11

    (1)确定宿主机的IP地址

    (2)除非DOCKER_IP在调用该脚本的环境变量中做了设置,否则使用宿主机ID作为Docker IP

    (3)设置Jenkins端口为默认的8080

    (4)设置该从节点的Jenkins标签为swarm

    (5)设置Jenkins主目录默认为jenkins slave用户的主目录

    (6)从此处开始将运行的命令作为脚本的输出部分进行记录

    (7)运行Jenkins Swarm客户端

    (8)将Jenkins的主目录设置为根目录

    (9)设置标签用于识别执行作业的客户端

    (10)设置从节点所要指向的Jenkins服务器

    (11)确保脚本(也就是容器)永远运行下去

    上述脚本的大部分是在为最后的Java调用设置和输出环境变量。此Java调用将运行Swarm客户端,把运行它的机器转换成一个动态Jenkins从节点,其根目录在-fsroot标志中指定,运行由-labels标志标记的作业并指向由-master标志指定的Jenkins服务器。具有echo的几行只是提供一些参数和环境设置的调试信息。

    构建和运行该容器很简单,按众所周知的样板运行即可:

    1. $ docker build -t jenkins_swarm_slave .
    2. $ docker run -d name \
    3. jenkins_swarm_slave jenkins_swarm_slave

    现在已经在这台机器上设置了一个从节点,可以在上面运行Jenkins作业了。可像平常那样设置一个Jenkins作业,只不过在Restrict Where This Pruject Can Be Run(限制项目可运行的位置)中添加swarm作为一个标签表达式(见技巧59)。

    建立一个系统服务来传播 可以在所有的PC上将其设置为supervised系统服务来实现这个过程的自动化(见技巧75)。

    对从节点机器的性能冲击 Jenkins作业可能是一些繁重的进程,其运行完全有可能对笔记本电脑造成负面影响。如果该作业很繁重,可以在作业和相应的Swarm客户端上设置标签。例如,可以设置一个作业的标签为4CPU8G,并将它匹配到运行在具有8 GB内存、4个CPU的机器的Swarm容器。

    本技巧对Docker概念做了一些展示。一个可预测、可移植的环境可以放置在多台宿主机上,从而降低昂贵的服务器的负载,并将所需配置降到最低。

    尽管本技巧实施时不能不考虑性能,我们还是认为这里面有很大的发挥空间,可以将开发人员的计算机资源转换成某种形式的游戏,从而提升开发组织的工作效率,而无须昂贵的新硬件。

    已在 GitHub 上可用 本技巧及其相关内容的代码可从GitHub获取,地址是https://github.com/ docker-in-practice/jenkins。

    本章中展示了如何在组织内部使用Docker来启用和推动CI。读者可以看到CI的很多障碍在Docker的协助下可以被扫清,如原始运算能力的可用性、与他人共享资源等。

    在本章中,读者了解到:

    • 通过使用eatmydata及软件包缓存,可以极大提升构建速度;
    • 可以在Docker内运行GUI测试(如Selenium);
    • 一个Docker CI 从节点可以保持对环境的完全控制;
    • 使用Docker及Jenkins的Swarm插件,可以将构建进程分包到整个团队中。

    在下一章中,我们将从CI转移到部署,并讨论与持续交付相关的技巧,这是DevOps图景的另一个关键组成部分。


    [1]  读者看到这本书的时候,DockerHub的界面可能已经更新,操作的按钮可能有所不同。翻译本书时,界面中创建仓库需要点击右上角的“Create”(创建)菜单并选择“CreateAutomatedBuild”(创建自动化构建)。——译者注

    [2]  “BuildSettings”(构建设置)页面中的“Trigger”(触发)按钮。——译者注

    [3]  翻译本书时,界面中“Pending”已经更换成“Queued”。——译者注

    [4]  翻译本书时,界面中“Finished”已经更换成“Success”。——译者注

    [5]  原文未指定版本,翻译本书时的最新版的镜像会出现jre无法安装以及ip命令不存在的问题。——译者注

    [6]  原文未指定版本,最新版的镜像可能会出现jre无法安装以及ip命令不存在的问题。——译者注