【nodejs原理&源碼賞析(9)】用node-ssh實現輕量級自動化部署

  • 2019 年 10 月 3 日
  • 筆記

示例程式碼託管在:http://www.github.com/dashnowords/blogs

部落格園地址:《大史住在大前端》原創博文目錄

華為雲社區地址:【你要的前端打怪升級指南】

一. 需求描述

前端工程出包後實現簡易的自動化部署。

二. 預備知識

網站的建設可以使用任何自己熟悉的框架,三大框架都有自己的官方Cli工具,從程式碼編寫到生成可用於生產環境部署的包基本都有自動化命令,各個打包工具也在零配置的追求上做了很多工作。本篇中從得到一個生產環境的包以後開始,對站點部署的相關知識進行一些介紹。

首先你需要一個Web伺服器,常見的有:

  • Nginx
  • Tomcat
  • Apache或相關集成環境
    • XAMMPApache+MySQL+PHP+PERL
    • LAMPLinux+Apache+MySQL+PHP
  • nodejs或相關框架+守護進程
    • Express
    • Koa2

以上任何一種在伺服器上運行起來後都可以擔任Web伺服器的角色,只是具備的擴展功能和應用場景有區別,Nginx基本上是正式環境部署的首選方案。常見的基本部署方案如下:

IP+埠訪問

使用訪問,可直接訪問對應埠的服務,部署方式相對簡單:

域名訪問

使用域名訪問時,通常會使用A記錄進行解析,它只能映射到80埠(https時映射到443),這時就需要使用反向代理將80埠的請求分發到本地不同的內部埠來訪問對應服務:

本例中使用域名+IP的方式進行部署。

三. Nodejs應用的手動部署

Express為例,步驟如下:

  1. 首先通過yarn global add express-generatornpm install express-generator -g全局安裝腳手架
  2. 完成後在工作目錄通過命令行express mydemo --ejs生成一個使用ejs作為模板渲染引擎的express工程
  3. 命令行輸入cd mydemo && yarncd mydemo && npm install安裝依賴
  4. /bin/www文件中修改埠號為期望的埠號(自動生成的是80埠),例如3001
  5. 將前端工程build出的包整體複製粘貼到/public目錄中
  6. 此時在本地工程根目錄下輸入npm start後,在瀏覽器中http://localhost:3001就可以訪問到網站了
  7. 使用FTP工具(如FlashFxpFileZilla Client等)連接到部署機器,將mydemo目錄壓縮為zip包後上傳到伺服器指定目錄。
  8. 使用SSH工具(如XshellMobaXter)登錄遠程機器,假設為linux系統,輸入unzip mydemo.zip解壓壓縮包,然後cd mydemo進入服務端工程,輸入npm start即可在伺服器上開啟Web服務,通過ip地址:3001就可以訪問到網站。
  9. 但是如果此時SSH工具斷開連接,就會發現express應用無法繼續訪問了,所以還需要一個守護進程來維持應用的啟動狀態,在服務端通過npm install pm2 -g來安裝nodejs應用的部署管理模組,它可以實現多應用管理、Hook更新、自動重啟等等許多常用功能,詳細資訊可以訪問 【PM2官方網站】
  10. 最後,在工程根目錄輸入pm2 start ./bin/www即可以後台模式運行應用。

四. 基於nodejs的自動部署

4.1 package.json中的scripts

了解了手動部署的過程後,就可以通過自動化腳本來實現後續的更新和部署。nodejs工程的自動化是依賴於package.json文件中的scripts配置項來實現的,例如使用vue-cli搭建的工程中就會帶有:

{      ...      "scripts": {          "serve": "vue-cli-service serve",          "build": "vue-cli-service build",          "lint": "vue-cli-service lint"        },      ...  }

在項目根目錄下打開命令行,輸入npm run [script-key]或者yarn [script-key][script-key]指上面示例中的serve,build,lint這些鍵名),就會執行對應的scripts[key]對應的命令。我們先添加一條用於自動部署的腳本指令:

{      ...      "scripts": {          "build": "vue-cli-service build",          "deploy" "node ./scripts/deploy/deploy.js"        },      ...  }

當輸入npm run deployyarn deploy時,實際上就相當於用node去執行./scripts/deploy/deploy.js這個腳本,其中就編寫了自動化發布的指令。scripts還提供了生命周期鉤子,比如你對接的是一個測試環境,希望每次build後自動發布,就可以使用post鉤子來實現:

{      ...      "scripts": {          "build": "vue-cli-service build",          "postbuild":"npm run deploy",          "deploy" "node ./scripts/deploy/deploy.js"        },      ...  }

這樣每次build執行完畢後,就會自動執行npm run deploy,也就是運行發布的腳本。

4.2 自動化發布腳本deploy.js

自動化發布腳本需要完成這樣幾個任務:

  • 將打包出的dist壓縮為zip
  • 使用SSH連接部署伺服器,將zip包發上去
  • 上傳完畢後,啟動事先寫好後續任務並放在伺服器上的shell腳本來完成剩餘的工作

涉及的幾個模組包括實現SSH連接的node-ssh模組(底層是ssh2模組,這個模組是一個Promise封裝),用於製作zip壓縮包的archiver模組。node-ssh提供了上傳本地目錄的方法,但實際使用過程中發現並不穩定,從告警資訊來看是node-stream模組在傳送時將不同格式的文件轉換為流時可能會出現異常,實測大約有一半概率觸發,嘗試修改了一些配置參數並未解決,所以採用archiver模組先壓縮為單個文件後再進行上傳。

參考程式碼如下:

const path = require('path');  const archiver =require('archiver');  const fs = require('fs');  const node_ssh = require('node-ssh');  const ssh = new node_ssh();  const srcPath = path.resolve(__dirname,'../../dist');  const configs = require('./config');    console.log('開始壓縮dist目錄...');  startZip();    //壓縮dist目錄為public.zip  function startZip() {      var archive = archiver('zip', {          zlib: { level: 5 } //遞歸掃描最多5層      }).on('error', function(err) {          throw err;//壓縮過程中如果有錯誤則拋出      });        var output = fs.createWriteStream(__dirname + '/public.zip')       .on('close', function(err) {           /*壓縮結束時會觸發close事件,然後才能開始上傳,             否則會上傳一個內容不全且無法使用的zip包*/           if (err) {              console.log('關閉archiver異常:',err);              return;           }           console.log('已生成zip包');           console.log('開始上傳public.zip至遠程機器...');           uploadFile();       });        archive.pipe(output);//典型的node流用法      archive.directory(srcPath,'/public');//將srcPach路徑對應的內容添加到zip包中/public路徑      archive.finalize();  }    //將dist目錄上傳至正式環境  function uploadFile() {      ssh.connect({ //configs存放的是連接遠程機器的資訊          host: configs.host,          username: configs.user,          password: configs.password,          port:22 //SSH連接默認在22埠      }).then(function () {          //上傳網站的發布包至configs中配置的遠程伺服器的指定地址          ssh.putFile(__dirname + '/public.zip', configs.path).then(function(status) {                  console.log('上傳文件成功');                  console.log('開始執行遠端腳本');                  startRemoteShell();//上傳成功後觸發遠端腳本            }).catch(err=>{               console.log('文件傳輸異常:',err);               process.exit(0);            });      }).catch(err=>{          console.log('ssh連接失敗:',err);          process.exit(0);      });  }    //執行遠端部署腳本  function startRemoteShell() {      //在伺服器上cwd配置的路徑下執行sh deploy.sh腳本來實現發布      ssh.execCommand('sh deploy.sh', { cwd:'/usr/bin/XXXXX' }).then(function(result) {          console.log('遠程STDOUT輸出: ' + result.stdout)          console.log('遠程STDERR輸出: ' + result.stderr)          if (!result.stderr){              console.log('發布成功!');              process.exit(0);          }      });  }

4.3 遠端腳本deploy.sh

當發布包上傳至遠程伺服器後,剩餘的工作在遠端來完成就可以了,你只需要將後續的工作寫進shell腳本並放在對應的目錄里就可以了,本例中deploy.sh放在了服務端項目目錄/mydemo中。示例如下(由於是自用系統,不考慮灰度發布等,直接暴力刪除靜態目錄public,然後替換為新的包):

#!/bin/bash  cd /usr1/AAA/mydemo  #刪除原靜態資源目錄  rm -rf public  cd /usr1/AAA  #解壓新的包  unzip public.zip  #將解壓出的public目錄移動到服務端程式目錄BBB中  mv public ./mydemo

提示:

如果腳本文件是在windows下編寫的,請注意將編輯器中的回車換行改為LF,windows下通常默認是CRLF,這可能會導致腳本在linux機器上無法正常執行。

至此,一個簡易的自動化部署就做完了。你只需要在本地輸入npm run deploy,後續的工作就會自動執行。

五. 小結

本篇只是一個簡易的自動化部署流程,由於部署環境沒有外網所以暫時無法藉助通用的自動化流水線實現全自動的DevOps流程。PM2實際上還有非常多實用的功能,可以管理多個不同的應用實例,以集群模式運行實例,或者預設發布流程,可以直接響應Web Hook並對接指定的程式碼倉,在根目錄下建立ecosystem.config.js配置文件就可以添加更多配置來指定pm2的表現,感興趣的讀者可以研究一下。