Gitlab Webhooks自动化部署实战

废话在前

长期以来,我司都使用SVN + WinSCP的方式来管理代码库以及上传代码到正式环境,这种无异于刀耕火种的操作仅比直接在FTP里编缉代码先进了那么一点儿。在这个连前端都充斥着各种自动化工具的今天,简直无颜以对江东父老乡亲。
前阵子刚刚在团队中强推了Git,然后折腾起了一个Gitlab,代码管理稍稍能看了些。于是想着使用Gitlab做自动化部署,查阅了一些资料,得知Gitlab有两种自动化布署方式,一曰“持续集成”(Continuous Integration),一曰“歪脖钩子”(Webhooks)。持续集成功能更强大,囊括了测试、编译、部署等一系列动作,但相应地也更复杂一些。本文介绍的是另一种相对轻量,易操作的自动化部署方式:Webhooks。

原理

自动化部署听起来高大上,其实原理简单粗暴:

  • 每当执行Git的push、merge_request(或自定义的其它操作)时,Gitlab会向你指定的url发起一个POST请求。
  • 服务器端接收请求,检查请求是否合法(如来源,附带的Secret Token,允许的Git操作等),执行自定义脚本,在本地将代码pull下来。
  • 最好做些善后工作,如记录日志等。

准备工作

配置Gitlab Webhooks

我使用的Gitlab是最新的社区版 9.1.4(截止到2017年5月),配置Webhooks的位置较早先版本有所变化。我们选择要部署的项目,进入 Settings => Intergrations(集成)。

1.png

点击下方的绿色按钮Add webhook,即可在按扭下方看到添加成功的歪脖钩子。

image.png

是的,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按钮了!

image.png

补充

你可能要问,一定要另起一个web服务吗?不能在原项目里写个路由,去处理请求 / 执行脚本吗?哦我亲爱的康斯坦丁彼得洛维奇同志,只要你想,当然可以。但我还是会建议你另写一个Web Server,这样可以降低项目的“耦合”(大佬们都喜欢用这个词)度,而且更易于维护。

另外需要注意的是,我在登入服务器、启动Web Server,以及执行脚本时,使用的都是root帐户(这是一个很坏的做法,但我就是控寄不巨我记己啊!),甚至由于历史原因,连nginx帐户都被分配到了root用户组,所以似乎没有遇到Permission denied的问题,如果你使用的是普通帐户,就要注意了:nginx帐户对项目目录必须有读写权限。

结束语

我想我就在这里结束。

—— Andrew Wiles. 1994.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容