上一章节中我们完成了对Docker的使用和部署。
本文中我们将完成 GitLab → Jenkins → Docker 的环境,并完成基于GitLab、Jenkins、Docker的面向Web开发的快速部署方案。
我们最终得到的结果,将是部署人员只在部署环境下只敲一句命令,就完成整个部署工作。
最终的目的就是接近一键部署,烧锅炉的大叔都会给服务器部署服务了,开除工程师。
我们最终要得到的交付成果,应是一个 Docker Image。
在某种程度上,不少公司在开发测试阶段可能会考虑直接在开发服务器上生成本地镜像,或是使用 docker save
方式导出tar镜像包文件,在不同机器上进行部署。这种方式对于公司网络环境比较好的情况下确实可以实行,而且对于线上生产环境,某种意义 上人工验收后部署要比全自动部署更放心。
然而更多的企业,外部网络带宽有限,内部网络优化更是渣的可怜。一个镜像包一般都在百M左右,用网络复制可能需要几十分钟甚至半个小时,部署一次环境,等待网络复制的时间,要比 拿一个优盘抱着笔记本乘上几十层电梯钻进机房忍着低温和轰鸣 蛋疼的多。
(已见过不止 3 家公司内部网络是这种情况。而且一般公司的IT部也都是所谓的网管,出问题后 重启、重装、换机 三步走的风格与一般小农村网吧网管也没什么区别。这种职位多数都是公司内部解决职工亲属就业问题内推产生的。神州泰岳全公司都在用12.12.0.0/16这种公网地址当私网地址用,为服务器申请一个静态DHCP地址都会搞得吵架。神州数码这种大公司的网管也在给新员工配置新电脑的时候,装个盗版操作系统都要看手册,还总出错)
所以我们要在这里,充分利用Docker镜像的分层文件系统:没有更新的部分不会生成新的镜像,不会浪费硬盘空间,拉取新镜像时旧镜像也不会消耗流量和带宽。
你可以直接执行:
1 |
docker run -d -p 5000:5000 -v /var/docker_registry:/var/lib/registry registry |
来启动一个仓库容器,默认端口5000,数据将会保存于/var/docker_registry。
注意这是Version 2,对于其他版本请参考其它文档。
注意这是一个HTTP服务,没有加密传输,对于安全性需求,建议自行解决。
相关文档:https://github.com/docker-library/docs/tree/master/registry
1 |
docker tag eea53635d0c6 your_registry_domain/private-docker-registry/www-data:0.0.1 |
这不是一个移动操作,而是一个复制软链接诶操作,在docker images中会新增一个ID相同但tag不同的镜像,不占用硬盘空间。
1 |
docker push your_registry_domain/private-docker-registry/www-data:0.0.1 |
会将这个镜像推送到your_registry_domain/private-docker-registry/
pull 和 run,不解释
注意,因为GitLab是我们自建的仓库,拥有完全的使用权,所以对于仓库的可视权限将要比GitHub多出两种选项。
建议公司内部的代码仓库都设置为 Internal。
GitLab等代码仓库其实也支持CI/CD操作,然而我们将要执行的操作对于GitLab来讲过于复杂了,所以这些操作要交给Jenkins这种专门的持续集成工具。为了能在需要时自动触发Jenkins自动操作,我们要使用GitLab的Webhook进行操作。
Deploy Keys
你可以为Jenkins专门制作Deploy Keys,或者干脆在GitLab上增加一个Jenkins账户,用户Jenkins拉取代码。两种方法都可以。
配置Webhook
根据我们之前的安装部署,Jenkins的端口是8081。假设我们的主机域名(地址)为your-what-server,项目为catscarlet/test1.git,则url为
1 |
http://your-jenkins-server:8081/git/notifyCommit?url=git@your-gitlab-server:catscarlet/test1.git |
触发条件建议选择 Merge Request event,或干脆手动触发。
这里就有GitLab安装在宿主机的好处了,端口很标准,不然改个ssh端口还要使用git的话,会变得很麻烦。至于https,相信搞得定Let’s encrypt的人很多,但搞不到公司域名的人更多。
安装这些插件:
Jenkins默认根据用户系统来显示语言,然而其自身的中文翻译挺糟糕的,所以换成纯英文更好一些。
在安装 Locale plugin 插件之后,点击 Manage Jenkins – Configure System,在Default Language处填写en
,保存。
因为接下来我们会把项目代码复制到一个统一的临时目录下进行处理,如果这时候有其他项目也在处理,则这个临时文件夹内可能就会有文件混乱的问题。所以我们要把Jenkins的并发数改为1,参数为 # of executors 。对于需要并发处理的正式环境,您需要手动修改临时目录的位置。
登录到Jenkins主页,New Item 新建一个 Freestyle project ,命名为test1。
在 Source Code Management 这里,选择Git,并在Repositories填写你的Git项目地址catscarlet/test1.git,Credentials选择你的证书,Branch Specifier选择对应Branch。在 Additional Behaviours 中增加 Clean before checkout。
Build Triggers选择Poll SCM,激活Webhook功能。内容留空,不定时Build,完全由Webhook触发。
在 Build 这里,Add build stemp 增加两个项目:Execute shell 和 Execute shell script on remote host using ssh。
不能选择直接在本地 Execute shell ,因为Jenkins目前是运行在Docker容器内的,与宿主机完全隔离,除了基本的Linux Shell和JAVA的SDK环境之外,容器内没有任何其他东西了,不能使用Docker命令,没有node和npm,不能编译,几乎什么都做不了。
所以我们在这里要分开做两件事:
更改Jenkins的启动方式,额外绑定一个数据卷目录。
1 |
docker run -d -v /var/jenkins_home:/var/jenkins_home -v /tmp:/tmp -p 8081:8081 -p 50000:50000 jenkins_modified |
我们已经在Source Code Management选择了Git,所以在执行Execute shell之前,Jenkins会先用Git把代码拉取到本地,不需要自己手动执行git pull。
Execute shell:
1 2 3 4 5 6 7 8 9 10 11 |
#!/bin/bash echo '------------------------------' echo '--- Execute shell ---' touch BUILD_${BUILD_NUMBER} echo ${BUILD_NUMBER} > BUILD_VERSION tar zcvf tmp_archive.tgz ./* rm -rf /tmp/tmp/* mkdir -p /tmp/tmp/ cp tmp_archive.tgz /tmp/tmp/ echo '--- Execute shell finished ---' echo '------------------------------' |
Execute shell script on remote host using ssh:
需要先在 Jenkins – configuration – SSH remote hosts 中添加对应的服务器登录方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#!/bin/bash echo '-----------------------------------------------------' echo '--- Execute shell script on remote host using ssh ---' cd /tmp/tmp/ tar zxf tmp_archive.tgz echo '--- Finished tar zxf tmp_archive.tgz ---' if [ ! -f project-build.sh ]; then echo "--- Error! project-build.sh not avaliable! ---" exit 1 fi echo '--- Prepare to build ---' bash project-build.sh test1 your_registry_domain:5000 private-docker-registry echo '-----------------------------------------------------' |
这里建议调用从git仓库上拉下来的脚本,而不是把要执行的命令写在Jenkins这里。
project-build.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#!/bin/bash BUILD_NUMBER=$1 REGISTRY_URL=$2 REGISTRY_NAME=$3 LOGFILE=build.log touch $LOGFILE
echo "- BUILD_NUMBER:$1" >> $LOGFILE echo "- REGISTRY_URL:$2" >> $LOGFILE echo "- REGISTRY_NAME:$3" >> $LOGFILE
if !([ $1 -a $2 -a $3 ]); then echo '--- PRAMA ERROR ---' >> $LOGFILE exit 1 fi
if [ -f IMAGEIDS ]; then rm IMAGEIDS fi
touch IMAGEIDS
echo -n "$REGISTRY_URL/$REGISTRY_NAME/test1-nginx_modified:" >> IMAGEIDS docker build -q -t $REGISTRY_URL/$REGISTRY_NAME/test1-nginx_modified:$BUILD_NUMBER nginx_modified/ >> IMAGEIDS echo -n "$REGISTRY_URL/$REGISTRY_NAME/test1-php-fpm_modified:" >> IMAGEIDS docker build -q -t $REGISTRY_URL/$REGISTRY_NAME/test1-php-fpm_modified:$BUILD_NUMBER php-fpm_modified/ >> IMAGEIDS echo -n "$REGISTRY_URL/$REGISTRY_NAME/test1-www-data:" >> IMAGEIDS docker build -q -t $REGISTRY_URL/$REGISTRY_NAME/test1-www-data:$BUILD_NUMBER www-data/ >> IMAGEIDS
if [ -f tmp_shell.sh ]; then rm tmp_shell.sh fi
while read line do echo $line | sed "s/\(.*\):sha256:\(.*\)/docker tag \2 \1:latest/g" >> tmp_shell.sh echo $line | sed "s/\(.*\):sha256:\(.*\)/docker push \1/g" >> tmp_shell.sh done < IMAGEIDS
if [ -f PUSH.log ]; then rm PUSH.log fi
if [ ! -f tmp_shell.sh ]; then echo '--- ERROR! tmp_shell.sh is missing! ---' exit 1 fi bash tmp_shell.sh >> docker_push.log cat docker_push.log |
这里建议对每个镜像都手动打包,而不是使用compose打包。一方面compose无法针对镜像打tag(compose更关注于容器而非镜像,容器是NAME而镜像是TAG),另一方面最终部署所需的 docker-compose.yml 中需要编写的是 带仓库路径的 image 而非 build。
我们的理想情况是,部署人员从GitLab获取 docker-compose.yml 之后,执行 docker-compose up,便完成容器部署操作。(面向傻瓜的程序设计,一键部署吔)
GitLab端
在GitLab的Webhooks页面有一个Test按钮,点一下就会触发Webhooks,如果提示 Hook executed successfully: HTTP 200 那么GitLab这端就工作正常。
Jenkins端
Jenkins接到Webhooks后就会开始打包。
点击进入后可通过左侧的 Console Output 查看控制台的情况,比如 Shell 执行出错在哪里。
注意Jenkins会在Git检查代码变更情况,如果GitLab那段没有新提交的话,Jenkins会认为代码相同,没有必要,会忽略这次打包。所以如果要进行测试的话,可能需要删除上一次的打包结果。你可以在 Polling Log 中查看相关信息。
部署人员从GitLab获取 docker-compose.yml 并复制到目的机,执行 docker-compose up。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
test1-www-data: image: your_registry_domain:5000/private-docker-registry/test1-www-data
test1-php-fpm: image: your_registry_domain:5000/private-docker-registry/test1-php-fpm_modified expose: - 9000 volumes_from: - test1-www-data
test1-nginx_app: image: your_registry_domain:5000/private-docker-registry/test1-nginx_modified expose: - 80 links: - test1-php-fpm volumes_from: - test1-www-data ports: - "9080:80" |
这回烧锅炉的大叔都会给服务器部署服务了,终于可以把工程师都开除了。
本系列文章简单讲述了基于GitLab、Jenkins、Docker的面向Web开发的快速部署方案。文章中没有涉及Docker 1.12以及swarn等相关概念。
Docker 1.11中,基于compose的DAB方案,是以在同一服务器上部署所有服务镜像的方式实现的,这是一种容易简单理解的实现方式。在集群中,每个节点都运行相同的容器组合。多个节点运行相同的容器群,容器群之间隔离没有交互,而容器间紧密结合。
但这是一种可能浪费资源的模式。负责某些服务的容器可能负载非常大,但上下游容器则比较空闲。这种情况下,更希望多个节点都能分担压力大的服务,而对比较闲的服务则减少数量。
另外同一节点上可能会有多个项目,而项目间可能会有相同的无擦别服务,比如php-fpm。更希望复用这一个容器,而不是启用多个相同容器只占内存不干活。
Docker 1.12中实现了这种需求,将分布式的概念从项目分解到容器应用。但目前仍是试验中,所以没有仔细研究,也不建议使用。阿里云在Docker还在1.11版本期间,自己也开发了一个阿里版Docker,实现了1.12中的这个功能,但是限制太多,脚本不兼容,不建议使用。