Microservice 產品交付

Dockerize 與 Zeit JavaScript 跨平台解決方案

Michael Hsu
15 min readAug 11, 2017

繼上一篇,使用 CircleCI 2.0 Workflows 挑戰三倍速 中將 Docker 導入 CI 設定測試環境外,這次著重在 CD 與產品交付,也試著使用 Docker 來建置 Production 機器,並且提交到其他服務平台上。

選擇交付方式

首先,從使用者最終會拿到什麼討論。產品交付有很多手段,常見可以透過部署公有雲、上架 APP 或是發布套件等等,另外也有像 MediaTek IoT Cloud 離線版,整體架構採用 NW.js 來開發桌面程序,只要幾個點擊就讓前端網頁以及伺服器端運行起來,並且輸出跨平台的執行檔。

當不需要 GUI,或單純想把 HTTP Service 專案跑起來,常碰的問題在於執行時所造成不可預期的錯誤,因此可以考慮連同執行環境一同交付。採用 Docker 來封裝是一個不錯的選擇,只要提供 Image,就能夠透過統一的接口來執行 Container。另外一種方法是本篇將提到 Zeit 的 Pkg,透過 Node.js 輸出 Binary 執行檔的做法。

JavaScript 的解決方案

列出其中幾個有名氣的官方產品。https://zeit.co/oss

開始關注 Zeit 是因為其前端的 Next.js 框架,Zeit 專案幾乎都是 JavaScript 實作的,所以對於前端工程師要追 OSS 原始碼非常地方便。他們在這一兩年也持續不斷地推出其他非常實用的產品,每當發布 Twitter 就是一陣好評,是一個值得學習的組織。

Zeit 旗下產品有一個很大的特性,只專注於處理單一事情上,接口也設計跟他的 Logo 一樣的簡潔,這一點非常符合 Microservice 的特性。本篇文章也是基於其底下產品的使用心得分享。

另外還有很多由社群開發的延伸套件與工具,可以查看 Awesome-zeit 列表,而這次我也做了兩個 Side Project Microservice 來驗證這整個流程,可以參考並玩玩看,接下來段落會討論四種部署方法以及工具:

  1. Micro-medium-api:封裝 Medium API,撈取文章列表 [DEMO]。
  2. Micro-github-release:下載最新的 GitHub release asset [DEMO]。
Side Project 驗證四種交付流程

Micro

Asynchronous HTTP microservices.

透過 Micro 要建立一個 HTTP Service 再簡單不過了,只要揭露一個 Handler Function,就能快速地跑起來。可以參考上面 Micro 8 的文章,Zeit 是怎麼針對 HTTP Server 進行優化的,主要把 HTTP Server 分為 Production 使用的 micro 以及 Development 使用的 micro-dev。開發上如果是 Node 8.0 以上版本,就可以直接寫 async/await 語法:

// index.js
const handler = async (req, res) => {
const response = await fetch('http://...')
return response.json()
};
module.exports = handler;

單一職責的 Microservice 設定單一 Entrypoint 來處理 Request 應該就足夠,如果有 Routing 的需求,可以使用社群開發的 micro-router,至於 Middleware 部分其實就是層層的 Function Composition,可以使用 Ramda 來達成:

// index.js
const R = require('ramda');
const { router, get } = require('microrouter');
const enhance = R.compose(
cors({
allowMethods: ['GET'],
origin: '*',
}),
compress,
)
module.exports = router(
get('/:id', enhance(handler)),
)

執行上用 npm script 來區分,本機端使用 npm run dev,當你部署到正式環境,就可以直接執行 npm start

// package.json
"scripts": {
"start": "micro",
"dev": "micro-dev"
}

雖然 Micro 實做上很簡單,處理稍微複雜的系統,也可以有很不同的玩法,建議可以瀏覽 Awesome-micro 來看看其他人是怎麼設計 Microservices 的。

a. 𝚫 Now.sh Service

Realtime global deployments.

部署前端專案雖然我還是最推薦 Netlify 的服務,不過其只能 Serve Static Files,因此通常有 Server 需求的專案,就必須找一些 PaaS 來使用。因此推薦 Now.sh,他們的服務非常的精簡,包含部署上也不太需要什麼設定,比起以前用過類似的 Heroku 服務簡單很多。

簡易的 JSON 設定格式

專案若是 Node.js Server,就直接透過預設的 npm 方式進行部署,特別要注意加上 NODE_ENV=production,才不會下載太多非必要的開發套件,也可以透過下方範例中的 env 來設定。當然,較複雜的環境或甚至是非 Node.js,Now.sh 也支援透過 Docker 來設定專案的環境,只要將 Dockerfile 推上 Now Service,就會建置 Docker Image。最後透過 now-cli 快速部署:

// package.json
"now": {
"type": "npm", // or 'docker'
"env": {
"NODE_ENV": "production"
}
},
# now-cli deploy
$ now

Subdomain Alias

因為 Now.sh 預設新增專案都會自動產生含一串 Hash 的網址,如果想要讓網址看起來簡單一點,可以註冊一個 Subdomain,只要透過下方的 alias 設定即可,然後同樣透過 CLI 執行:

// package.json
"now": {
"alias": "mcs-lite-app"
}
$ now alias

Instance automatically scaled

Now.sh 服務有一個 Sleep 機制,每當一段時間沒有流量或是超過你的 Instances 額度,就會暫時被 Frozen 直到下次啟動,如果你需要也可以將專案設定一個最大最小的範圍,讓 Now.sh 幫你依需求自動設定,又或是只設定最小 Instance 1 就不會暫停了:

$ now scale mcs-lite-app.now.sh 1

Continuous Delivery

官方提供完整的 Now CLI 套件,也可以拿來串接 CI,每當測試成功就執行部署到 Now.sh 上。下方流程中透過 Wait for ready 的指令,等待新的網站佈署成功,中間可以加上一點 End-to-End 測試,完成後將 Alias 指向新的 Instance,最後才把舊的 Instance 砍掉,來達到 Zero Downtime 的目的:

// .travis.yml
after_success:
# 1. Wair for deployment ready
- URL=$(now --public)
- await-url $URL
# 2. Alias and purge old services
- now alias set "$URL" "$ALIAS"
- now rm $PROJECT --safe --yes

Now.sh 對於 OSS 專案雖然是免費的,仍然有使用量限制,另外也沒支援 Docker Compose 或是多個 Port,不過還蠻適合拿來放一些 Side Project 或是 Demo page 的。特別要注意如果有用 DB 在每次重新部署都會洗掉,可能要搭配其他 Database 服務一併使用。

Deploy Button

Put a deploy button in the GitHub README file. (Link)

由社群開發的,類似 Heroku Deploy Button,可以放在 README 文件上讓使用者一鍵快速部署自己專屬的 Microservice,可以參考左圖 MCS Lite Demo 在文件上的部署流程。

b. Pkg

Package your Node.js project into an executable.

將 Node.js 執行環境也一並打包,只輸出單一執行檔,如此一來使用者就不需要額外下載 Node.js 環境。可以透過 package.json 來設定 Binary 的執行入口,接著透過 pkg 指令就能輸出預設的三種平台的檔案:

// package.json
"bin": "bin/server.js"
$ pkg . --out-path pkg> pkg@4.2.2
> Targets not specified. Assuming:
node8-linux-x64, node8-macos-x64, node8-win-x64

交付上,透過 TravisCI 設定自動部署,一旦有 Git Tag 推上 GitHub,就將 Pkg 輸出的執行檔上傳到 GitHub Release:

// .travis.yml
deploy:
provider: releases
api_key:
secure: ...
file_glob: true
skip_cleanup: true
file: "./pkg/*"
on:
repo: evenchange4/micro-medium-api
tags: true
GitHub Release via Travis

c. NPM Release

Node.js 使用者肯定偏好使用 NPM 來安裝,既然在上個段落 Pkg 已經設定好 package.jsonbin 檔,那不如就順便把它給 Release 到 NPM 上吧,同樣透過 Travis 來做交付:

// .travis.yml
provider: npm
email: evenchange4@gmail.com
api_key:
secure: ...
on:
repo: evenchange4/micro-medium-api
tags: true

CLI Argument

關於接口設計上,之前有用過其它套件做過,但都沒有 yargs 套件開發時來的簡潔,建議可以試試看。當然,最後也可以直接輸出當作文件用:

$ micro-medium-api --help
Usage: micro-medium-api <command> [options]

Options:
-p, --port HTTP server PORT [default: 3000]
-h, --help Show help [boolean]
-v, --version Show version number [boolean]

一旦有套件更新,使用者要怎麼知道呢?推薦使用 update-notifier 來幫你達成,其實最新的 npm 5 也是用這套來做的。在你的執行檔入口中,預先跑過版本檢查,當使用者下指令時就會跳出如下圖資訊:

// bin/cli.js
const updateNotifier = require('update-notifier')
const pkg = require('../package.json')
updateNotifier({ pkg }).notify()
update-notifier 更新通知

d. Dockerize

Dockerize 為跨平台提供了統一接口

Server 環境問題是專案初期設定上的盲點,要一勞永逸地解法就是封裝 Docker Image。目前許多平台都早已支援使用 Dockerfile 進行設定了,包含各大 CI/CD 服務工具等等。以下 MCS Lite 為例,其中封裝了運行環境、執行程序,最後只揭露 Port 即可。當使用者建置完 Docker Image 後,就可以簡單地使用 Docker Run 指令執行 Container:

# DockerfileFROM node:6.10.3
RUN ...
EXPOSE 3000
ENTRYPOINT ["npm", "start"]
$ docker run --rm -it -p 3000:3000 -p 8000:8000 mcslite/mcs-lite-app

DockerHub Automatic Build

DockerHub 連結 GitHub Automatic build (Link)

除了在本機端下載 Dockerfile 建置 Docker Image 外,也可以透過 DockerHub 自動地將 Image 預先建立好,其他人就能直接下載使用。左圖使用 DockerHub Automatic Build 功能,也就是能夠連動放在 GitHub 上的 Dockerfile,一旦 GitHub 有更新就會 Trigger DockerHub 建立出新的 Image,當然也可以設定 Git Tag 來做版本控制。

Multi-Stage Builds in Docker Cloud

Multi-stage Build 是 Docker 17.05 的新功能,可以設定建置、測試以及最後 Runtime Stage 來自不同的 Image,來達到最終揭露的 Image 最小化的目的。下方的 Dockerfile 可以看到有兩個 FROM ,輸出後從原本 266 MB 縮減為 16MB:

# Dockerfile# Build 266 MB
FROM node:8.2.1 AS builder
RUN npm install
# Runtime 16 MB
FROM mhart/alpine-node:base-8.2.1
COPY --from=builder /app .
CMD npm start

又舉例來說,你可能需要 Node.js 環境來建置前端靜態檔案,但是最終在 Server 上只需要 Python 跑 Web Server,就可以用這個方法讓 Production Image 有 Python 但不包含 Node.js 環境。可惜目前在 DockerHub 上仍然是 Docker 穩定版號 17.03,所以並不能使用。根據 Issues#1039 討論,看來要能等到 Multi-stage 支援遙遙無期,官方也沒辦法給個確切的日期,反倒是看到很多文章都在推 Docker Cloud:

Docker Cloud 是 Based on DockerHub 所開發,因此介面與功能上幾乎都有涵蓋到,並且多了許多設定可以做選擇,例如下圖可以選擇 Docker 版本。但缺點是需要登入才能看到 Dashboard,對於 OSS 專案資訊不夠透明:

Docker Cloud 選擇 Docker Version 17.05

另外在 Automatic Build 的設定上也多了一點彈性,例如想要將 npm 習慣上的 Git Tag v1.0.1 轉換成 1.0.1,也就是去掉 v 開頭。如此指定 Docker Image 版號時也比較符合一般大眾的格式 Image:1.0.1

後記

透過 Side Project 驗證的四種部署方式,才會真正的思考如何交付才能最順暢。將專案設定 Docker 也可幫助整合測試,例如以往要將前後端分離的數個專案跑起來才能進行 End-to-End 實測,封裝後就能簡單地跑起來。

下一步,也許會參考幾個 OSS 專案對於 Now.sh 與 GitHub 的整合,來模擬看看 Staging Continuous Deployment 的功能,如果你對後續嘗試感興趣,可以關注我的 Medium 喔。

最後,工商時間。快試試建置您專屬的物聯網雲平台吧!

Further Readings

  1. 使用 CircleCI 2.0 Workflows 挑戰三倍速
  2. React Stack 開發體驗與優化策略
  3. Build A Web App in MediaTek

References

  1. Deployment lifecycle and Scalability in now
  2. sholladay/await-url

*完整的專案放在 MCS-Lite/mcs-lite-app-demo 以及 evenchange4/Micro-github-release,如果你喜歡這系列文章,關於 Michael 在 OSS 專案開發心得,別忘了可以點個 ❤️ 讓我知道喔!

--

--