废话在前
长期以来,我司都使用SVN + WinSCP的方式来管理代码库以及上传代码到正式环境,这种无异于刀耕火种的操作仅比直接在FTP里编缉代码先进了那么一点儿。在这个连前端都充斥着各种自动化工具的今天,简直无颜以对江东父老乡亲。
前阵子刚刚在团队中强推了Git,然后折腾起了一个Gitlab,代码管理稍稍能看了些。于是想着使用Gitlab做自动化部署,查阅了一些资料,得知Gitlab有两种自动化布署方式,一曰“持续集成”(Continuous Integration
),一曰“歪脖钩子”(Webhooks
)。持续集成功能更强大,囊括了测试、编译、部署等一系列动作,但相应地也更复杂一些。本文介绍的是另一种相对轻量,易操作的自动化部署方式:Webhooks。
原理
自动化部署听起来高大上,其实原理简单粗暴:
- 每当执行Git的push、merge_request(或自定义的其它操作)时,Gitlab会向你指定的url发起一个POST请求。
- 服务器端接收请求,检查请求是否合法(如来源,附带的Secret Token,允许的Git操作等),执行自定义脚本,在本地将代码pull下来。
- 最好做些善后工作,如记录日志等。
准备工作
- Gitlab. 从哪里获取Gitlab,以及如何安装
- 服务器一台(我司是CentOS6.5)
- 咖啡一杯(用于提神醒目)
配置Gitlab Webhooks
我使用的Gitlab是最新的社区版 9.1.4(截止到2017年5月),配置Webhooks的位置较早先版本有所变化。我们选择要部署的项目,进入 Settings
=> Intergrations
(集成)。
点击下方的绿色按钮Add webhook
,即可在按扭下方看到添加成功的歪脖钩子。
是的,Gitlab要做的就是这么多。
服务器端
服务器端这边要做的工作就多了。
首先,我们写一个简单的shell脚本,功能简单,寥寥数行:
#! /bin/shell
WEB_PATH=/var/www/mySite #你的项目目录
cd $WEB_PATH
git reset --hard origin/master
git clean -f
git pull
git checkout master
#简单记录日志
echo $(date)" --- git pull success" >> ./deploy.log
大家可以根据自己的需要适当扩展。
当然,服务器要从Gitlab上拉取代码,前提是已经连接到Gitlab上,并且Gitlab上存有服务器的公钥。这涉及Git的知识在此略过不表。
接下来是根据webhooks定义的URL,去写相对应的服务器端脚本。我司主要使用的服务器语言是Node.js,所以以下以Node.js示例。至于使用PHP或Java的同学也不必灰心,因为我们要实现的逻辑非常简单。
对于Webhooks,伟大的NPM给我们提供了数量可观的第三方包,包括Github和Gitlab的,但这些第三方包的下载量堪忧(也许是使用Webhooks做自动化部署的Node.js团队非常之少?),质量也难以保证(诸君有兴趣可以自己下几个研究其源码),在这里,我们决定不使用第三包,自己做一个简单的实现。
好了,接下来是 Talk is cheap, show me the code 时间。
deploy.js
'use strict';
const http = require('http');
const url = require('url');
const webhook = require('./webhook');
const path = '/webhook'; //服务端允许的pathname
// 统一返回状态码及文本信息
function resText(res, args) {
res.writeHead(args.stateCode, {'Content-Type': 'text/plain; charset=utf-8'});
res.end(args.msg);
}
/**
* 创建HTTP Server
* 监听7777端口,土豪随意。
*/
http.createServer(function(req, res) {
//仅接受/webhook路径,其余返回404
if(path !== url.parse(req.url, true).pathname) {
resText(res, {
stateCode: 404,
msg: '404 Not found.'
});
return;
}
let post = '';
let headers = req.headers;
req.on('data', function(chunk) {
post += chunk;
});
req.on('end', function(){
try{
post = JSON.parse(post);
} catch(e) {
resText('400', {
stateCode: 400,
msg: 'Bad request.'
});
return;
}
//执行钩子
webhook({headers, post}, function(result){
if(!result) {
resText('400', {
stateCode: 400,
msg: 'Bad request.'
});
return;
}
});
res.end('done');
});
}).listen(7777);
接下来是webhook.js
'use strict';
const exec = require('child_process').exec;
const cmd = './deploy.sh'; //shell脚本路径
const token = 'JiuBuGaoSuNi'; //接头暗号
function webhook(args, callback) {
let header = args.headers;
let body = args.post;
//允许的事件
let allowEvent = {
push: true,
merge_request: true
}
//验证webhooks头信息
if(!header['x-gitlab-event'] || header['x-gitlab-token'] !== token) {
console.error('wrong x-gitlab-event OR x-gitlab-token');
callback(null);
return;
}
//检查允许的事件
if(!allowEvent[body['object_kind']]) {
callback(null);
return;
}
//push时仅master分支
if(
body['object_kind'] === 'push' &&
body.ref.split('/').pop() !== 'master'
) {
callback(null);
return;
}
//merge_request时仅merged状态
if(
body['object_kind'] === 'merge_request' &&
body['object_attributes']['state'] !== 'merged'
) {
console.error(
'merge_request state: ',
body['object_attributes']['state']
)
callback(null);
return;
}
//执行脚本
exec(cmd, function(err, stdout, stderr) {
if(err) {
console.error(err);
callback(null);
return;
}
console.log('stdout----->', stdout);
console.log('stderr----->', stderr);
});
}
module.exports = webhook;
到此服务端的代码就写完了,我们使用forever
将脚本启动起来,一个简单的web服务便这样成了:
forever start -l /var/www/mySite/deployment/log/forever.log \
-e /var/www/mySite/deployment/log/error.log \
/var/www/deployment/deploy.js
当然,我们需要配置Nginx或是Apache什么的,将它作为正常的域名让gitlab服务器来访问(假设你的Gitlab服务器和项目不在同一台机器上):
server {
listen 80;
server_name deployment.xxx.com;
root /var/www/mySite/deployment/;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:7777;
}
}
重启你的Nginx服务:
nginx -s reload
现在,你可以去点那个可爱的Test按钮了!
补充
你可能要问,一定要另起一个web服务吗?不能在原项目里写个路由,去处理请求 / 执行脚本吗?哦我亲爱的康斯坦丁彼得洛维奇同志,只要你想,当然可以。但我还是会建议你另写一个Web Server,这样可以降低项目的“耦合”(大佬们都喜欢用这个词)度,而且更易于维护。
另外需要注意的是,我在登入服务器、启动Web Server,以及执行脚本时,使用的都是root帐户(这是一个很坏的做法,但我就是控寄不巨我记己啊!),甚至由于历史原因,连nginx帐户都被分配到了root用户组,所以似乎没有遇到Permission denied的问题,如果你使用的是普通帐户,就要注意了:nginx帐户对项目目录必须有读写权限。
结束语
我想我就在这里结束。
—— Andrew Wiles. 1994.