$cover

基于 Node.js 的服务器自动化部署搭建实录

摘要:本文主要记录我在服务器上使用 GitHub 的 Webhooks 进行网站自动化部署的过程。最终效果:开发终端向 Github 仓库推送代码后,服务器端自动拉取仓库并重启服务器。搭建过程主要参考了 lovelucy.info 的博客。

2019-03-19更新:给毕业设计搭建代码自动部署,遇到了脚本调用守护进程时,守护进程不能正常执行的问题。在此非常感谢node中创建服务进程这篇文章给予我的帮助,完美地解决了我的问题。

搭建环境:我正在使用的是 Vultr 的服务器,系统版本是 CentOS 7 x64。

在服务器上安装 Node.js

经过踩坑,通过更新EPEL库使用yum下载nodejs的方法,下载的版本比较老旧。我个人搭建过程中使用了 Node.js 官网 的 Linux 二进制文件来安装 Node.js,我个人也更推荐这种做法。下载安装包时要留意 Linux 的版本。(X86/X64)

// Node.js 安装目录
$ cd /usr/local/bin 

// Node.js 官网提供的 Linux 二进制文件
$ wget https://nodejs.org/dist/v8.12.0/node-v8.12.0-linux-x64.tar.xz

// 解压缩
$ tar xvJf node-v8.12.0-linux-x64.tar.xz

// 配置环境变量
$ vi /etc/profile

// 添加以下内容到文件末尾 ====================
export NODE_HOME=/usr/local/bin/nodejs

export PATH=$PATH:$NODE_HOME/bin

export NODE_PATH=$NODE_HOME/lib/node_modules

// 添加以上内容到文件末尾 ====================

// 保存退出后source一下,使其立即生效
$ source /etc/profile 

// 检查是否安装成功
$ node -v
$ npm -v

至此,成功在服务器上安装最新版本的 Node.js。

如果不是安装在/usr/local/bin目录,需要通过ln -s node安装目录/bin/node /usr/local/bin以及ln -s node安装目录/bin/npm /usr/local/bin 的方法来给 node 和 npm 创建链接。

编写拉取仓库、重启服务器脚本

一个可供参考的例子:deploy.sh

#!/bin/bash

WEB_PATH='/home/nodejs-be-demo'

echo "Start deployment"
cd $WEB_PATH
echo "pulling source code..."
git reset --hard origin/master
git clean -f
git pull
git checkout master
npm install
npm run start
echo "Finished."

可以按照自己的需要编写脚本。

配置 Github 仓库的 Webhook 设置

  1. 在要配置的 Github 的设置页面找到 Webhooks 选项,点击「Add webhook」。
  2. 配置 Payload URL(接受 POST 请求的服务器 URL)。
  3. Secret(可以理解为配对暗号)。
  4. Content-type 选择 application/json
  5. 其余默认设置即可。

配置 Node.js 脚本

在配置 Node.js 脚本之前,需要先安装依赖,这里用到了一个中间件 github-webhook-handler ,以及进程管理服务 forever

$ npm install -g github-webhook-handler
$ npm install -g forever

脚本内容如下:deploy.js

var http = require('http')
var createHandler = require('github-webhook-handler')
var handler = createHandler({ path: '/autodeploy', secret: 'mySecret' }) 
// 上面的 secret 保持和 GitHub 后台设置的一致

function run_cmd(cmd, args, callback) {
var spawn = require('child_process').spawn;
var child = spawn(cmd, args, {
    detached: true,
    stdio: [process.stdin,process.stdout,process.stderr]
});
var resp = "";

child.stdout.on('data', function(buffer) { resp += buffer.toString(); });
child.stdout.on('end', function() { callback (resp) });
}

http.createServer(function (req, res) {
handler(req, res, function (err) {
    res.statusCode = 404
    res.end('no such location')
})
}).listen(7777)
// 这里是监听的端口号

handler.on('error', function (err) {
console.error('Error:', err.message)
})

handler.on('push', function (event) {
console.log('Received a push event for %s to %s',
    event.payload.repository.name,
    event.payload.ref);
run_cmd('sh', ['./deploy.sh'], function(text){ console.log(text) });
})

/*
handler.on('issues', function (event) {
console.log('Received an issue event for % action=%s: #%d %s',
    event.payload.repository.name,
    event.payload.action,
    event.payload.issue.number,
    event.payload.issue.title)
})
*/

2019-03-19更新:
脚本在spawn的参数列表中加入了第三个参数:

{
  detached: true,
  stdio: [process.stdin,process.stdout,process.stderr]
}

在 node 环境下,如果不针对子进程的 stdio 做一些特殊处理父进程其实不会真正退出,而是直到子进程执行完毕后再退出。之所以出现这种情况是由于 node 创建子进程时默认会通过 pipe 方式将子进程的输出导流到父进程的 stream 中(childProcess.stdout、childProcess.stderr),提供在父进程中输出子进程消息的能力。
detached选项可以让node原生帮我们创建一个daemon进程,设置datached为true可以创建一个新的session和进程组,子进程的pid为新创建进程组的组pid,这与setsid起到相同的作用。此时的子进程已经和其父进程属于两个session,因此父进程的退出和中断信号不会传递给子进程,子进程不会接受到父进程的中断信号自然也不会退出。当父进程结束之后,子进程变为孤儿进程从而被init进程接收,ppid设置为1。

加入这两个参数的原因是因为,在使用原来的js脚本执行bash脚本时,如果bash脚本中使用pm2这样的守护程序,在bash脚本执行结束后,执行过程中创建的pm2进程也会中断掉。

上面引用段落引用自node中创建服务进程,我的个人理解是:

设置detachedtrue之后,spawn在运行时创建了新的session和进程组,父进程的的退出和中断信号不会传递给子进程。

设置stdio: [process.stdin,process.stdout,process.stderr]之后,子进程的stdio被设置为当前终端,这样一来bash脚本的环境就相当于提升了一级,直接和终端进行io交互。


这里 Node.js 监听的是 7777 端口,你也可以使用 Nginx 反向代理到 80 端口。

用下面的命令测试一下,接收到 push 之后控制台会有输出,你可以在配置 webhook 的页面下重复触发同一个hook,无需多次 commit & push。

$ node deploy.js

如果没什么问题,forever 就可以开起来了。

$ forever start deploy.js

2019-03-19更新:
进程守护工具更新:pm2
比起forever,pm2提供了更全面的监控功能,可用于产品级服务。
pm2 使用介绍

用法:

$ npm install pm2 -g     # 命令行安装 pm2 
$ pm2 start app.js -i 4  # 后台运行pm2,启动4个app.js 
                         # 也可以把'max' 参数传递给 start
                         # 正确的进程数目依赖于Cpu的核心数目
$ pm2 start app.js --name my-api # 命名进程
$ pm2 list               # 显示所有进程状态
$ pm2 monit              # 监视所有进程
$ pm2 logs               # 显示所有进程日志
$ pm2 stop all           # 停止所有进程
$ pm2 restart all        # 重启所有进程
$ pm2 reload all         # 0 秒停机重载进程 (用于 NETWORKED 进程)
$ pm2 stop 0             # 停止指定的进程
$ pm2 restart 0          # 重启指定的进程
$ pm2 startup            # 产生 init 脚本 保持进程活着
$ pm2 web                # 运行健壮的 computer API endpoint (http://localhost:9615)
$ pm2 delete 0           # 杀死指定的进程
$ pm2 delete all         # 杀死全部进程

运行进程的不同方式:

$ pm2 start app.js -i max    # 根据有效CPU数目启动最大进程数目
$ pm2 start app.js -i 3      # 启动3个进程
$ pm2 start app.js -x        #用fork模式启动 app.js 而不是使用 cluster
$ pm2 start app.js -x -- -a 23   # 用fork模式启动 app.js 并且传递参数 (-a 23)
$ pm2 start app.js --name serverone  # 启动一个进程并把它命名为 serverone
$ pm2 stop serverone       # 停止 serverone 进程
$ pm2 start app.json        # 启动进程, 在 app.json里设置选项
$ pm2 start app.js -i max -- -a 23                   #在--之后给 app.js 传递参数
$ pm2 start app.js -i max -e err.log -o out.log  # 启动 并 生成一个配置文件
你也可以执行用其他语言编写的app  ( fork 模式):
$ pm2 start my-bash-script.sh    -x --interpreter bash
$ pm2 start my-python-script.py -x --interpreter python

其他问题

部署过程中可能遇到无法访问对应端口的问题,需要检查一下服务器的防火墙设置。大部分服务器都是白名单机制,只开放特定的端口。

CentOS 7 下默认使用的防火墙是 FirewallD,之前版本请搜索 iptables
另:FirewallD 和 iptables 的区别?

FirewallD 相关命令:

  • 启动服务,并在系统引导式启动该服务

     sudo systemctl start firewalld
     sudo systemctl enable firewalld
    
  • 停止并禁用

     sudo systemctl stop firewalld
     sudo systemctl disable firewalld
    
  • 检查防火墙状态。输出应该是 running 或者 not running。

     sudo firewall-cmd --state
    
  • 允许或拒绝任意端口/协议(如:12345端口,该规则在 public 区域)

     sudo firewall-cmd --zone=public --add-port=12345/tcp --permanent
     sudo firewall-cmd --zone=public --remove-port=12345/tcp --permanent
    
  • 重新加载 FirewallD 使规则立即生效

     sudo firewall-cmd --reload
    
  • 查看特定区域的所有配置

     sudo firewall-cmd --zone=public --list-all
    

    示例输出:

     public (default, active)
         interfaces: ens160
         sources:
         services: dhcpv6-client http ssh
         ports: 12345/tcp
         masquerade: no
         forward-ports:
         icmp-blocks:
         rich rules:
    

更多详细的 FirewallD 配置,请看 CentOS 上的 FirewallD 简明指南