持续集成的重要性与技术挑战

[编者的话] 当文字偶遇代码,当程序插上了翅膀,让分享成为我们彼此沟通的语言。我们期待可以构建这样一个平台让开发者们看到你们的智慧,挖掘你们的才华,让彼此在开源的路上不再孤独。“DaoCloud分享写作计划”已全面启动,欢迎投稿。


作者:章培昊


我从事了十多年IT项目开发工作,在摸爬滚打中,经历了项目管理的各种弊病,也一直在学习和探索新的项目管理模式。

曾经有一位产品经理对我抱怨说,他希望每天上班都希望能拿到一个昨天晚上完成的最新版本,不需要每个功能都完成,但是完成的每个功能他都希望尽快看到,以确认开发人员正在做他想要做的东西。而那时,我们只能在两周一次的迭代结束时,提交给他一个完整的版本,而此时如果发现问题,往往修改成本会很高,更重要的是这会严重影响团队的士气。

正是由于这种需求的存在,近年敏捷开发思想大行其道。每个团队都在尽力实现项目快速迭代。而实际执行过程中,往往会因为各个开发人员任务进度不一致,导致同步和整合代码困难。而且由于编译和发布的繁琐流程、测试与Bug确认和提交的耗时,给项目增加了巨大的工作量。

为此,我们曾经花费巨大人力来构建自动编译系统,希望能将SVN中的最新代码自动整合、编译和测试。为产品设计、测试和管理人员提交出最新的项目成果。但是,项目中有C/C++编写的服务器代码,需要在Linux上运行测试,有Java/PHP编写的Web应用代码,需要Maven和Apache等编译工具与运行环境,还有安卓和iOS客户端,编译环境和测试环境更加难以搭建。

Docker容器、Drone.io与DaoCloud.io

Docker是近两年最炙手可热的虚拟化技术。她与传统的虚拟化技术在操作系统层面构建系统不同,Docker使用Linux容器技术以进程方式运行隔离的操作系统,这不但大大提高了硬件使用效率也提供了更加灵活的资源调度。

在近两年各大平台都在通过Docker来提高处理能力和开发效率,而且出现了一批基于Docker的SaaS和PaaS的服务提供商。Drone.io与DaoCloud.io就是其中的两个。

Drone.io是基于github开源项目drone构建的一个平台,他整合了测试、打包和发布,为用户提供了持续集成的统一平台。很可惜,在中国这个平台无法正常使用。但是,DaoCloud.io为中国用户提供了类似的服务,某些地方已经做得更好。

实战与案例分析

好了,上面的东西其实都是瞎聊,:)。接下来的东西是实实在在的干货了,小伙伴们调整好坐姿,Let's go!

我们的Demo应用是一个基于Spring的Web项目,项目使用Maven编译,使用Nginx反向代理到Tomcat。项目代码可以从github上得到:springdemo

如果你对Spring、Maven和Tomcat不熟悉,也不用担心,你只需要知道,Maven编译器通过一个pom.xml文件编译Spring项目,并生产一个war项目包,这个项目包复制到Tomcat的发布目录下,启动Tomcat,应用就可以访问了。

此外,持续集成中一个至关重要的部分是测试用例。作为一个有节操的程序猿,除了写一手漂亮的代码,还需要能写挑战这些代码的测试用例。我们的Demo项目包含完整的测试用例,在代码提交之后,DaoCloud.io会自动运行测试用例,如果测试失败,那么你会得到错误提示,如果所有测试都通过,那么你的本次提交可以被发布出去。

首先,我们在DaoCloud.io上创建一个新的项目,设置代码源(之前你需要在github上对DaoCloud.io进行授权),如图

项目创建好以后,DaoCloud.io平台会自动从github上下载代码,并运行持续集成脚本(daocloud.yml)进行测试。

daocloud.yml文件的配置说明可以参考文档。yml文件格式可以参考协议

下面简单看看我们的daocloud.yml文件。

image: centos:centos7

services:

env:

install:  
    - yum install -y wget git tar
    - wget --no-cookies --no-check-certificate --header "Cookie:oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u20-b26/jdk-8u20-linux-x64.rpm -O /tmp/jdk-8u20-linux-x64.rpm
    - rpm -i /tmp/jdk-8u20-linux-x64.rpm
    - rm /tmp/jdk-8u20-linux-x64.rpm
    - wget -O /tmp/apache-maven-3.1.1-bin.tar.gz http://ftp.jaist.ac.jp/pub/apache/maven/maven-3/3.1.1/binaries/apache-maven-3.1.1-bin.tar.gz
    - cd /usr/local && tar xzf /tmp/apache-maven-3.1.1-bin.tar.gz
    - ln -s /usr/local/apache-maven-3.1.1 /usr/local/maven
    - rm /tmp/apache-maven-3.1.1-bin.tar.gz

before_script:  
    - echo "Setup"

script:  
    - cd webapp
    - /usr/local/maven/bin/mvn install -q -DskipTests=true
    - /usr/local/maven/bin/mvn test
  • 首先,通过image配置指定项目运行的Docker镜像,通常这里会设置成与Dockerfile中指定的基础镜像一致。

  • 后面是services配置。DaoCloud.io为测试环境提供了MySQL、Redis和MongoDB等服务。当然,我们也可以通过后面的install脚本来按照这些服务,不过这需要消耗不少时间。如果你要使用DaoCloud.io提供的服务,你的测试代码需要使用指定的服务器参数。

  • 后面是环境变量配置。需要注意,这里设置的环境变量通常是测试程序运行时会读取的环境变量。并且这些环境变量的设置在后面的install脚本执行之前,如果设置不当,可能会影响后面的install脚本的执行。

install脚本用于安装测试需要的工具与环境。在我使用过程中,发现以下一些地方要特别注意

  • daocloud.yml文件中不能使用tab,要用4个空格代替。

  • yml格式规定了冒号(:)的解析方式,所以,如果遇到安装脚本中使用了冒号(:)要特别注意,很可能会被错误解析成map项,而导致daocloud.yml文件解析失败。

  • 在执行install脚本时,我们的代码还没有下载到当前环境,你还不能执行你要测试代码中的脚本,或者读取测试代码中的配置。

  • 如果在测试前,你有需要特别运行的脚本,可以放在before_script中。这里和install脚本不同的是,这里你的测试代码已经复制到当前目录下。

  • script中是实际运行测试的脚本,CI通过测试程序的返回值判断测试是否通过。而我们可以通过Log来具体确认哪些测试用例失败了。

DaoCloud.io的“持续集成”运行界面如下图:

如果测试全部通过,你会得到如下图的结果提示:

现在,我们可以开始发布我们的项目了。在项目管理的“镜像构建”页,点击“手动构建”按钮,启动构建过程。DaoCloud.io执行docker build命令按照你项目的Dockerfile来构建项目镜像,页面上会显示整个构建过程的Log。如下图:

这里我们简单看一下我们的Dockerfile:

# Dockerfile for a Java webapp build by Maven and running on Tomcat + Apache

FROM centos:centos7

MAINTAINER Zhang Peihao (zhangpeihao@gmail.com)

# Other stuff can be installed with yum
# (Note that git is quite old. If you want 1.8.x, install from source.)
ADD ./docker/etc/nginx.repo /etc/yum/repos.d/nginx.repo  
RUN yum -y --noplugins --verbose update  
RUN yum -y --noplugins --verbose install nginx git wget tar

# Java installation.
#
RUN wget --no-cookies --no-check-certificate --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u20-b26/jdk-8u20-linux-x64.rpm -O /tmp/jdk-8u20-linux-x64.rpm  
RUN rpm -i /tmp/jdk-8u20-linux-x64.rpm  
RUN rm /tmp/jdk-8u20-linux-x64.rpm

# Tomcat 7
#
# Not available on yum, so install manually
RUN wget -O /tmp/apache-tomcat-7.0.62.tar.gz http://apache.mirror.rafal.ca/tomcat/tomcat-7/v7.0.62/bin/apache-tomcat-7.0.62.tar.gz  
RUN cd /usr/local && tar xzf /tmp/apache-tomcat-7.0.62.tar.gz  
RUN ln -s /usr/local/apache-tomcat-7.0.62 /usr/local/tomcat  
RUN rm /tmp/apache-tomcat-7.0.62.tar.gz

# Download Maven
RUN wget -O /tmp/apache-maven-3.1.1-bin.tar.gz http://ftp.jaist.ac.jp/pub/apache/maven/maven-3/3.1.1/binaries/apache-maven-3.1.1-bin.tar.gz  
RUN cd /usr/local && tar xzf /tmp/apache-maven-3.1.1-bin.tar.gz  
RUN ln -s /usr/local/apache-maven-3.1.1 /usr/local/maven  
RUN rm /tmp/apache-maven-3.1.1-bin.tar.gz

# Copy tomcat config file
RUN rm -f /usr/local/tomcat/conf/tomcat-users.xml  
ADD ./docker/tomcat-conf /usr/local/tomcat/conf

# Copy nginx config file and delete conflicting conf
ADD ./docker/nginx-conf /etc/nginx/conf.d  
RUN rm -f /etc/nginx/conf.d/default.conf

# Copy start script
ADD ./docker/start-script /usr/local  
RUN chmod a+x /usr/local/start-everything.sh

# Add the application itself
RUN mkdir /webapp  
Add ./webapp /webapp

# Environment variables
ENV JAVA_HOME /usr/java/latest  
ENV CATALINA_HOME /usr/local/tomcat  
ENV MAVEN_HOME /usr/local/maven  
ENV APP_HOME /webapp

# Build the app once, so we can include all the dependencies in the image
RUN cd /webapp && /usr/local/maven/bin/mvn -Dmaven.test.skip=true package && \  
    rm -rf $CATALINA_HOME/webapps/* && \
    cp target/spring-mvc-showcase.war $CATALINA_HOME/webapps/ROOT.war

# Set the start script as the default command (this will be overriden if a command is passed to Docker on the commandline).
# Note that we tail Tomcat's log in order to keep the process running
# so that Docker will not shutdown the container. This is a bit of a hack.
CMD /usr/local/start-everything.sh && tail -F /usr/local/tomcat/logs/catalina.out

# Forward HTTP ports
EXPOSE 80 8080  

关于Dockerfile的编写规则,你可以参考手册这里我简单说一下我们在Dockerfile里做了些什么

首先,我们指定基础镜像是centos系统的7.0版本;接下来安装编译运行所需要的环境和工具;我们通过RUN命令在虚拟环境运行安装命令,用ADD命令将我们预先准备的配置文件复制到虚拟环境,用ENV命令来设置运行需要的环境变量;当所有工具和环境准备就绪,我们使用CMD命令来设置运行容器时执行哪些命令;最后,我们使用EXPOSE命令还指定容器会向外暴露哪些端口。

DaoCloud.io按照我们在Dockerfile中指定的过程构建镜像,构建成功以后,点击“查看镜像”,进入镜像管理页面。这里你可以选择部署最新版本,或者部署指定版本。部署时,填上容器名称,如果需要设置运行环境变量,点击“立即部署”。你的项目就成功发布到DaoCloud.io上了。

你可点击项目URL链接打开你的项目页面。

到此,我们成功的在DaoCloud.io上创建和发布了一个项目,接下来,我们来试试当我们提交新版本时会发生什么。

我们试试修改代码,有意插入一个Bug,提交github之后,刷新项目管理页面,大约几秒钟内,持续集成就会自动执行,我们看到,Log显示测试用例存在错误。

这里,我建议大家先在开发环境下完成单元测试,在本地单元测试没有错误的情况下再提交代码。而DaoCloud.io上的测试更多的是确认多个模块集成后是否存在冲突与不一致。以防止由于一个模块的测试不通过而改造成其他模块的测试失败,当出现测试失败时,我建议先回滚代码,本地调试成功以后再提交。


本文来自“DaoCloud分享写作计划”,这项计划旨在为开发者提供一个平台,分享使用Docker的心得体会和技术经验。DaoCloud将为文章作者提供一定的物质奖励,具体方式请访问:DaoCloud写作分享计划 ,欢迎Docker爱好者和DaoCloud用户踊跃投稿。