概述

这一篇是一个野路子CI/CD,并不是正规化的,因为实现的方式并不是想象中的使用jenkins/teamcity/gitlab/gocd/etc...,当然docker是必不可少的。方案也具备一定的针对性,但是思路才是最重要的不是?

为什么没有使用流行工具呢?

  • jenkins老是安装插件失败,我真的很郁闷,这是我前几年的傍身工具,居然现在用不上。
  • teamcity很好,我的新宠,但是构建的docker文件必须放到hub.docker.com中
  • gocd太难玩,还没入门
  • 自建SSL支撑的Docker registry这一路子还不熟
  • Git代码在云端(Github、Gitlab、Coding.net、Gitee、etc...)

但是我们要实现“每60秒检查一次云端的Git源码的对应分支是否有更新,如果有,则更新当前分支源码,并清理之前的构建,重新构建,对产出的Jar包构建docker镜像,并使用这个镜像启动新的docker container”的需求,其实简单来说就是我这更新了代码,本地测试服务器就自动打包部署了。

既然知道需求了,接下来就是思考方案了。

方案

CI/CD tools

使用CI/CD工具是我最初的方案,毕竟jenkins用得老6了,所以方案就是

Jenkins -> clean -> build -> build image -> upload image

这样一来,每次都会Push新的docker image到hub中,这个方案简直好得很

  • 由于JVM项目一般都有profile的说法,所以一个image,通过传递不同的profile就能在不同环境正确运行。
  • docker image在hub中,这样无论在什么地方,都可以去hub中下载到这个image
  • ci/cd tools还能根据版本来给image tag不同的版本,对于运维来说可以随时切换不同的版本进行运行。
  • 每一步错误都能通过邮件短信等发送到相关责任人,个别Tools还有贴心的钉钉插件通知你哦。

整个都是一条龙服务,完美无缺。但是,有一个问题就是,我们有好几个JVM项目,但是免费的hub只能存储一个image,老板不给钱,这是硬伤。所以有了下面的野路子解决方案。

Shell it!

回到原始时代,一切都是我们自己来吧。命令行也有春天。方案就是

crontab(per 60s) -> deploy.sh

而这个deploy.sh就包含了

git pull -> clean -> build -> docker build -> docker run

这个野路子方案就没有上一个方案好了,他只解决了基本需求

  • 能够定时更新代码,并打包部署

他没有解决的是

  • 每个步骤中的错误不能够通知到相关责任人
  • image不在公共hub中,只有本机docker能够使用
  • docker仅仅作为JVM容器,而没有 tag 版本的功能

当然,这些缺点都是可以技术实现的,但我这里只要做到基本需求就好。

实施

我们采用"Shell it"的方案进行实施。接下来我们创建如下的目录结构。

workdir
   |  --- Dockerfile
   |  --- app.jar
   |  --- deploy.sh
   |  --- logs<dir>
   |  --- source_root<dir>

上述目录结构中的app.jar文件会在处理完之后删除或者移动。

获取源码

第一步当然是要获取源码了,我们以源码在Coding.net中为例子,在获取源码时,既然是能够自动获取,我们当然不希望还需要我们手动去输入用户密码的情况,所以我们需要使用ssh-key的方式。

打开命令行终端输入 ssh-keygen -t rsa -C "your_email@example.com"( 你的邮箱),连续点击 Enter 键即可。

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
# Creates a new ssh key, using the provided email as a label
# Generating public/private rsa key pair.
Enter file in which to save the key (/Users/you/.ssh/id_rsa): [Press enter]  // 推荐使用默认地址
Enter passphrase (empty for no passphrase):   //此处点击 Enter 键即可

成功之后,你的key放置在"/Users/you/.ssh/id_rsa"下,将id_rsa.pub文件中的内容放到coding.net的部署公钥中即可。这样一来,使用git更新代码便畅通无阻。

比对更新

更新源码不是总是能够有新的代码出现,毕竟我们每60秒就更新一次,但是怎么判断是否有更新呢?这里的思路就是比对本地head的sha和远程最新head的sha值是否相同,如果不同,则肯定有更新。相关脚本如下:

l1=`git rev-parse HEAD`
l2=`git ls-remote git@e.coding.net:/you/app.git HEAD | awk '{ print $1 }'`

if [ $l1 = $l2 ];then
    echo "no change"
    exit 1
else
   # clean build package deploy
fi

定时检查

在/etc/crontab中加入如下内容:

*/1 *   * * *   root    /workdir/deploy.sh >> /workdir/logs/cron.log 2>&1

这里的意思是以root用户的角度来执行deploy.sh脚本,并将脚本的输出信息追加在cron.log文件中。
值得注意的是为什么要用root用户呢?原因是docker需要用到root用户,当然也可以不用,但是又给我们找事儿了不是?

编译源码

我们假定我们的项目使用的是Gradle,当然如果你的是Maven,可以类推。

    echo "buiding..."
    if ./gradlew clean build bootJar -x test;then
        echo "build done"
    else
        exit 1
    fi

注意命令中的 "-x test"表示我们skip了测试。

创建镜像

创建镜像就是标准的Docker命令了,

docker build -t group/app-name -f Dockerfile .

Dockerfile:

FROM openjdk:8-jre-alpine
  
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
VOLUME /tmp
COPY app-1.0.0.jar app.jar
ENTRYPOINT ["java", "-Dspring.profiles.active=test","-jar", "/app.jar"]

简单说一下Dockerfile,使用openjdk:8-jre-alpine的原因是它够小,够用,够快。同时ENV和RUN解决了时区问题。

启动镜像

docker run -d -p 8800:8800 -v /workdir/logs/app/:/logs --restart=always --name test-app group/app-name

好了基本上就结束了。野路子可以玩儿了。

附件

完整的deploy.sh

#!/bin/sh
cd /workdir/source_dir

l1=`git rev-parse HEAD`
l2=`git ls-remote git@e.coding.net:/you/app.git HEAD | awk '{ print $1 }'`

if [ $l1 = $l2 ];then
    echo "no change"
    exit 1
else
    echo "pulling source code"
    if git pull;then
        echo "source pulled."
    else
        exit 1
    fi

    echo "buiding..."
    if ./gradlew clean build bootJar -x test;then
        echo "build done"
    else
        exit 1
    fi

    echo "copy jar..."
    cp ./build/libs/*.jar /workdir
    echo "copy done"

    cd /workdir
    docker stop test-app
    docker rm test-app
    docker rmi group/app-name
    if docker build -t group/app-name -f Dockerfile .;then
        if docker run -d -p 8800:8800 -v /workdir/logs/:/logs --restart=always --name test-app group/app-name;then
            rm app-1.0.0.jar
            echo "It's done!"
        else
           echo "docker container run fail"
           exit 1
        fi
    else 
        echo "docker image build fail"
        exit 1
    fi
fi