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(集成)。

《Gitlab Webhooks自动化部署实战》 1.png

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

《Gitlab Webhooks自动化部署实战》 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按钮了!

《Gitlab Webhooks自动化部署实战》 image.png

补充

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

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

结束语

我想我就在这里结束。

—— Andrew Wiles. 1994.

    原文作者:五月二十四
    原文地址: https://www.jianshu.com/p/d0d601af3797
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞