基於 Travis CI + PM2 實現 NodeJS 應用的持續集成和部署

前言

我發現一旦手頭的項目變多,且隨着項目複雜度的提高,原本編碼就已是個夠頭痛的問題,再加上部署到生產環境就更心累了 😵。javascript

以前在公司實習時,有一個依據用戶輸入網址進行截屏的項目,同時包含了 React 應用和 Node 應用。css

部署 React 應用比較方便,只要經過 scp 將 build 後的 dist 目錄放置在服務器上。前端

而 Node 應用則較爲複雜:java

  • 因爲它使用 TS 編寫,一樣須要將 build 後 dist 目錄放置在服務器上
  • 在根目錄下新建目錄並使用 chmod 修改權限,用於暫時放置截屏快照
  • 更新 npm 包
  • 重啓 PM2(Node 進程管理工具)

在項目初期,版本迭代很是快,我天天都要反覆執行以上步驟數次,waste time!node

況且,在標準的開發流程中,咱們還需引入 單元測試覆蓋率報告代碼風格檢測 ……,並將應用部署到 不一樣環境的服務器(開發、測試、生產)中,這無疑是一項繁瑣的工做,本着 不想當運維的前端不是一個好全棧 的核心思想,我迫切須要解放個人雙手。git

TIP:結尾有源碼連接github

CI & CD

所謂前人栽樹,後人乘涼,個人訴求早就在開發領域中被定義爲兩個專有名詞:npm

  • 持續集成(Continuous Integration),簡稱 CI
  • 持續部署(Continuous Deployment),簡稱 CD

聽起來很高大上,我嘗試經過一張圖來解釋:json

一個完整項目的迭代須要經歷:編碼 ➡️ 打包構建 ➡️ 測試 ➡️ 新代碼和原有代碼正確地集成在一塊兒。安全

這一過程稱爲集成,而 持續集成強調了開發人員提交了新代碼(git push)以後,馬上進行以上步驟,無需人爲干預

同理,持續部署在持續集成的基礎上,加了一個步驟: 將應用自動部署到指定環境(服務器)

試想,當你提交代碼後,CI/CD 服務會按照你的預設命令自動化以上步驟,那是多美妙的一件事!

爲了提升軟件開發的效率,咱們有必要使用 CI/CD,而市面上熟知的 CI/CD 服務有:Jenkins、gitlab,不過它們的使用成本很高。

我要推薦的是 Travis CI,它綁定 Github 上面的項目,只要有新的代碼,就會自動抓取。而後,提供一個運行環境,執行測試,完成構建,還能部署到服務器。

準備工做

爲了確保你能順利進行實踐部分,請作好如下準備工做:

  • 一臺遠程 Linux 服務器,用做部署
  • 使用 Github 帳戶登入 Travis CI,並讓 Travis 監聽 GitHub 倉庫的更新

我會從零開始搭建一個用於 API 服務的 NodeJS 應用,並引入單元測試和 ESLint,最終實現 CI/CD。

整個思路以下:

  • 新建 Github 倉庫,用於存放 NodeJS 應用代碼
  • 被監聽的 Github 倉庫收到 git push 命令,觸發 Travis CI 進行構建(yarn run test、yarn run eslint)
  • Travis CI 構建無誤後,調用 PM2 的 deploy 命令,使遠程服務器拉取 Github 倉庫最新代碼,安裝依賴包,而後自動重啓服務

實現本機、GitHub、遠程(部署)服務器三者互通

爲了簡便,我將本機稱爲 local,遠程服務器稱爲 remote

萬事開頭難,首先意識到如下兩點:

  • PM2 Deployment 要求 remote 能拉取 Github 倉庫的代碼
  • 若是 Travis CI 有訪問 remote 的需求,則確保 local 能訪問 remote

因爲 Travis CI 和 PM2 Deployment 在運行時不提供交互式界面,它只會按照預設的腳本命令去依次執行,當須要你輸入密碼時就會卡住,因此咱們須要 SSH 無密登陸,達到如下所示關係:

local => GitHub;
remote => GitHub;
local => remote;
複製代碼

local 鏈接 GitHub

首先生成 ssh key 私鑰公鑰對,一路回車,無需 passphrase.

$ ssh-keygen -t rsa -b 4096 -C "<your-github-email>"
複製代碼

在 local 的 ~/.ssh 目錄下,會生成如下文件:

├── id_rsa
├── id_rsa.pub
複製代碼

id_rsa 是私鑰文件,表明 🔑;id_rsa.pub 是公鑰文件,表明 🔒.

只有私鑰才能打開公鑰

打開 github.com/settings/ke…,點擊 New SSH key,複製 id_rsa.pub 中的內容。

以後選擇你的任一倉庫,點擊 clone or download && Clone with SSH,若是能成功 clone,說明實現了 local 和 GitHub 的 SSH 鏈接。

local 鏈接 remote

ssh-copy-id 命令會默認將以前生成的公鑰:id_rsa.pub 複製到 remote 中。

⚠️ 換成你本身的 remote IP.

$ ssh-copy-id root@47.106.87.3
複製代碼

❗️ 若是 win 系統沒法識別該命令,請使用 git bash.

查看 remote 的 ~/.ssh 目錄,id_rsa.pub 中內容與 authorized_keys 一致。

├── authorized_keys
複製代碼

嘗試鏈接 remote.

$ ssh root@47.106.87.3
複製代碼

若是無需輸入密碼,則說明實現了 local 和 remote 的 SSH 鏈接。

remote 鏈接 GitHub

思路和 local 鏈接 GitHub 一致,因爲咱們已經在 GitHub 上存放了公鑰,咱們只需將私鑰:id_rsa 上傳到 remote 便可。

上傳完畢後,remote 的 ~/.ssh 目錄存在如下文件:

├── authorized_keys
├── id_rsa
複製代碼

同理,你能夠嘗試在 remote 上使用 Clone with SSH 下載 GitHub 倉庫來驗證是否鏈接成功。

至此,咱們實現了三者的 SSH 互通。

搭建 NodeJS 應用

先在 GitHub 上新建一個倉庫,隨後 Clone 到本地。

因爲該應用基於 koa 框架來實現 API 服務,因此進行一些初始化配置:

$ yarn init -y
$ yarn add koa
複製代碼

爲了後續編碼,你應該擁有如下目錄:

├── lib
│   ├── app.js
├── server.js
├── package.json
└── yarn.lock
複製代碼

開始編寫代碼:

// lib/app.js
const Koa = require("koa");

const app = new Koa();

app.use(ctx => {
  if (ctx.method == "GET" && ctx.path == "/user") {
    ctx.body = "hello, friend";
  }
});

module.exports = app;
複製代碼
// server.js
const app = require("./lib/app");

app.listen("8888", () => {
  console.log("server is running at http://localhost:8888");
});
複製代碼

啓動 Node 應用:

$ node server.js
複製代碼

我建立了一個最最簡單的 API 服務,當用戶訪問 http://localhost:8888/user 時,返回 "hello, friend".

使用 Travis CI

在這以前,你須要建立 Travis CI 的配置文件,在根目錄下新建 .travis.yml

# 構建環境
language: node_js
# node_js 版本
node_js:
 - 12
after_success:
 - echo 'I successfully done'
複製代碼

⚠️ Travis CI 默認會執行 install、script 這兩個生命週期,即便沒有顯式在配置文件中定義。

就當前的配置文件而言,啓動構建後,Travis CI 將執行 install ➡️ script ➡️ after_success.

而按照官方文檔 Building a JavaScript and Node.js project

  • install 會默認執行 npm install
  • script 會默認執行 npm test

而且,若是 Travis CI 檢測到 yarn.lock 的存在,則分別替換命令爲 yarnyarn test.

因此,咱們還需提供測試(test)腳本,在 package.json 中添加:

"scripts": {
  "test": "echo just test it",
},
複製代碼

最後,確保你在 /account/repositories 中,開啓了對該倉庫的監聽。

一切就緒,只需將修改後的代碼推送到遠程倉庫,來觸發 Travis CI。

$ git push
複製代碼

來到 travis-ci/dashboard,在 Active repositories 面板中選擇 travis-test,能夠看到如下信息:

查看下方日誌信息,關鍵的地方我用文字標註了:

持續集成已經跑通,但感受少了點什麼?對,訪問 remote 的命令還未添加。

因爲 Travis CI 至關於開啓了一個虛擬化容器來執行整個構建過程,因此有必要將私鑰:id_rsa 傳遞給它,來支持 remote 的 SSH 鏈接。那也總不能直接將 id_rsa 放到咱們的倉庫中吧,那豈不是泄露了私鑰,後果很是嚴重!

Travis CI 早就想到了這一點,它提供了針對私鑰的加密方案。

加密私鑰文件須要使用 travis 這個命令行工具,它是一個 ruby 包,使用 gem 安裝:

$ gem install travis
$ travis login
複製代碼

若是你安裝 travis 失敗,能夠查閱 github.com/travis-ci/t….

輸入帳號密碼登陸成功後,使用 travis encrypt-file 加密:

$ travis encrypt-file ~/.ssh/id_rsa --add
# Detected repository as B2D1/travis-test, is this correct? |yes| yes
# Overwrite the config file /root/travis-test/.travis.yml with the content below? (y/N) y

# Make sure to add id_rsa.enc to the git repository.
# Make sure not to add /root/.ssh/id_rsa to the git repository.
# Commit all changes to your .travis.yml.
複製代碼

上面命令執行完後,會生成一段解密命令並添加到 .travis.yml 中:

before_install:
- openssl aes-256-cbc -K $encrypted_9b2d7e19d83c_key -iv $encrypted_9b2d7e19d83c_iv
  -in id_rsa.enc -out ~/.ssh/id_rsa -d
複製代碼

而且提示 ❗️,必定要把加密後的 id_rsa.enc 複製到倉庫中,必定不要把未加密的 id_rsa 複製到倉庫中。

有可能你生成的是 -out ~\/.ssh/id_rsa -d,切記改爲 -out ~/.ssh/id_rsa -d

before_install 階段發生在 install 階段以前,這段代碼的意思是:用 encrypted_9b2d7e19d83c_ivencrypted_9b2d7e19d83c_key 這兩個環境變量,對倉庫中的 id_rsa.enc 進行解密,並在虛擬容器中的 ~/.ssh 目錄下生成私鑰:id_rsa

你能夠在 travis-ci.org ➡️ 你的倉庫 ➡️ More options ➡️ settings 中找到這對環境變量:

基本完成對 remote 的鏈接工做,但還有一些坑要填:

  • 下降 id_rsa 文件的權限,不然 ssh 處於安全方面的緣由會拒絕讀取祕鑰
  • 將 remote IP 加入到 Travis CI 虛擬容器的信任列表中,不然鏈接 remote 時會詢問是否信任 remote

更改後的 .travis.yml 配置以下:

# 構建環境
language: node_js
# node_js 版本
node_js:
 - 12
# 將遠程服務器加入信任列表
addons:
 ssh_known_hosts: 47.106.87.3
# 解密 id_rsa.enc,並修改 id_rsa 權限
before_install:
 - openssl aes-256-cbc -K $encrypted_9b2d7e19d83c_key -iv $encrypted_9b2d7e19d83c_iv
 -in id_rsa.enc -out ~/.ssh/id_rsa -d
 - chmod 600 ~/.ssh/id_rsa
# 鏈接遠程服務器,並打印系統版本
after_success:
 - ssh root@47.106.87.3 'cat /etc/issue'
複製代碼

提交代碼(git push),查看構建結果:

成功打印了我遠程服務器的版本信息:

添加單元測試

在上一節,爲了快速經過測試命令(yarn test),只是簡單使用了 echo 命令。

如今要正式爲 NodeJS 應用添加單元測試,建議選擇 Jest + SuperTest 來實現。

Jest 是 Facebook 的一套開源的 JavaScript 測試框架,它自動集成了斷言、JSDom、覆蓋率報告等開發者所須要的全部測試工具,是一款幾乎零配置的測試框架。

SuperTest: HTTP assertions made easy via superagent.

安裝 npm 包:

$ yarn add jest supertest --dev
複製代碼

更改 package.json:

"scripts": {
  "test": "jest"
},
複製代碼

在根目錄下新建 __test__/app.test.js,並編寫測試代碼:

const app = require("../lib/app");
const supertest = require("supertest");
const server = app.listen();
const request = supertest(server);

test("GET /user", async done => {
  const res = await request.get("/user");
  expect(res.status).toBe(200);
  expect(res.text).toBe("hello, friend");
  done();
});

afterAll(done => {
  server.close();
  done();
});
複製代碼

執行測試腳本:

$ yarn test
複製代碼

測試經過:

還能夠經過 --coverage 參數來提供覆蓋率報告:

添加 ESlint

這一節,繼續完善 NodeJS 應用,爲它添加 ESlint.

ESLint 是一個插件化而且可配置的 JavaScript 語法規則和代碼風格的檢查工具。ESLint 可以幫你輕鬆寫出高質量的 JavaScript 代碼

安裝 npm 包:

$ yarn add eslint eslint-config-google --dev
複製代碼

更改 package.json:

"scripts": {
  "lint": "eslint .",
  "test": "jest",
  "pretest": "yarn run lint"
},
複製代碼

❗️pretest 腳本會在 yarn test 以前自動執行。

在根目錄下建立配置文件 .eslintrc.json

{
  "extends": ["eslint:recommended", "google"],
  "env": {
    "node": true
  },
  "parserOptions": {
    "ecmaVersion": 6
  },
  "rules": {
    "eqeqeq": 2
  },
  "ignorePatterns": ["ecosystem.config.js", "__tests__"]
}
複製代碼

這裏採用了預設的 lint 規則:recommended & google.

並新增一條規則:非嚴格相等符(==)的存在,會致使程序退出(0 表明關閉,1 表明警告,2 表明錯誤)。

其餘的配置項爲:設置代碼環境、ECMA 版本、指定哪些文件不參與檢查。

執行 lint 命令:

$ yarn run lint
複製代碼

發生瞭如下錯誤:

能夠嘗試運行 yarn run lint --fix 命令, ESlint 會自動修復錯誤。對於不能自動修復的,需手動修改。

使用 PM2 Deployment

通過上述步驟,已經基於 Travis CI 實現了 CI(持續集成)。

只差最後一步:將 NodeJS 應用部署到遠程服務器上。

參照官方文檔 PM2 Deployment,咱們只需建立配置文件便可,剩下的交給 PM2 來作。

在根目錄下建立 ecosystem.config.js

module.exports = {
  apps: [
    {
      // PM2 應用名稱
      name: "travis-test-deploy",
      // node 啓動文件
      script: "server.js",
    },
  ],
  deploy: {
    // "prod" 是環境名稱
    prod: {
      // 私鑰目錄
      key: "~/.ssh/id_rsa",
      // 登陸用戶
      user: "root",
      // 遠程服務器
      host: ["47.106.87.3"],
      // 自動將 github 加入遠程服務器的信任列表
      ssh_options: "StrictHostKeyChecking=no",
      // git 分支
      ref: "origin/master",
      // git 倉庫地址(ssh)
      repo: "git@github.com:B2D1/travis-test.git",
      // 項目在遠程服務器的存放路徑
      path: "/root/travis-test-deploy",
      // PM2拉取最新分支後,安裝 npm 包,並啓動(重啓)NodeJS 應用
      "post-deploy":
        "source ~/.nvm/nvm.sh && yarn install && pm2 startOrRestart ecosystem.config.js",
    },
  },
};
複製代碼

同時修改 .travis.yml

# 構建環境
language: node_js
# node_js 版本
node_js:
 - 12
# 將遠程服務器加入信任列表
addons:
 ssh_known_hosts: 47.106.87.3
# 解密 id_rsa.enc,並修改 id_rsa 權限
before_install:
 - openssl aes-256-cbc -K $encrypted_9b2d7e19d83c_key -iv $encrypted_9b2d7e19d83c_iv
 -in id_rsa.enc -out ~/.ssh/id_rsa -d
 - chmod 600 ~/.ssh/id_rsa
# PM2 deploy
after_success:
 - npm i -g pm2 && pm2 deploy ecosystem.config.js prod update
複製代碼

⚠️ 在首次部署時,咱們須要先在遠程服務器初始化項目

$ pm2 deploy ecosystem.config.js prod setup
複製代碼

❗️ 若是 win 系統出錯,請使用 git bash.

隨後提交代碼(git push),等待 Travis CI 構建 和 PM2 部署完畢。

訪問 Travis CI 顯示構建成功,登陸遠程服務器,輸入 pm2 list,如圖所示:

訪問 http://<your remote ip>:8888/user,顯示 "hello,friend".

總結

這個 NodeJS 應用雖然簡單,但涉及的知識點很是之多:建立 API 服務、單元測試、ESLint、CI/CD、SSH、Linux 運維,須要掌握必定的實踐能力。

因爲篇幅有限,還有不少坑、細節來不及去講,若有錯誤請聯繫我 📧.

項目源碼地址

相關文章
相關標籤/搜索