iOS App 程式開發

給 Swift 工程師的後端指南:用 Kitura 來架設自己的 API 後台

給 Swift 工程師的後端指南:用 Kitura 來架設自己的 API 後台
給 Swift 工程師的後端指南:用 Kitura 來架設自己的 API 後台
In: iOS App 程式開發, Swift 程式語言

身為一個 Swift 開發者,每天撰寫著 iOS/macOS/tvOS/watchOS 的程式,你的經驗讓你的效率高到破表,一天就可以輕易做出一個線上聊天室介面,兩天就可以做出塗鴉牆介面,你覺得自己的開發速度天下無敵;但不知道為甚麼,突然你覺得有點惆悵:啊,只有介面的程式,就像沒有加珍珠的奶茶,我怎麼樣都喝不下去啊!沒錯,再強的前端,都有可能需要一個後端來配合,才有辦法做出更多樣的應用,以往我們需要團隊作戰,但現在你可以全部都自己來,當個真正的一條龍工程師(不要哭,我知道領 22k 的我們都已經是了)!今天我們就要來介紹,如何用 Swift 來撰寫後端,並且讓後端程式上線服務。

雖然 Swift 是開發 iOS/macOS 的主要語言,但 Swift 能做的事情,遠遠超過開發 Mobile/Desktop UI。Swift 現在的發展,正朝向通用的程式語言前進,除了手機應用程式之外,Swift 也被用來撰寫系統管理工具、作科學分析、寫後端程式等等。而其中最熱門的應用,就是利用 Swift 來寫後端程式了!現在不少用 Swift 撰寫的後端都慢慢地上了 production 環境,也證實了利用 Swift 來撰寫後端程式是可行且可靠的。

這是一篇偏向實務的後端簡介,你可以從中了解實務上的開發與發佈流程。具體來說,你可以學習到:

  • 選用 Swift 寫後端程式的好處
  • Kitura 的基本架構與語法
  • Kitura 與資料庫的溝通(以 Postgree SQL 為例)
  • 如何系統化後端發佈:甚麼是 Container
  • 利用 Docker 打包程式,放到 Google Kubernetics 上對外提供服務

這裡沒有像是如何做一個購物車、或是一個 To Do List 這樣的手把手教學,但是我希望透過這篇文章,讓更多人了解如何建構 production-ready 的 Server-Side Swift,讓大家能夠更放心地開始用 Swift 撰寫自己的專案,或甚至公司的專案。另外,這篇文章也不會在基本 http request、 restful API 等概念著墨太多,就先假設大家都會了(很敷衍)。

作為後端語言,Swift 有甚麼優勢?

利用 Swift 寫後端有很多好處,最棒的當然就是終於能夠使用 Swift 寫後端了(這是甚麼解釋)。身為 Swift 工程師,能夠使用自己熟悉的語言開發用戶端、系統工具、和後端,而不用再花時間學習其它後端語言,這本身就已經是一件非常有效率的事情。Swift 本身就已經有很多語法特性與功能,當你已經在 Swift 開發方面很有經驗,很熟悉 protocol extension、conditional protocol conformance、enum associated value 等,現在突然要你用不了解的語言來撰寫後端,你一開始可能會想辦法找這些方法的替代來實作,但這樣一來,時間上絕對不如直接使用 Swift 來撰寫快速,更不用說你用熟悉的語言寫出來的 code 一定會更整潔乾淨(或者亂得有特色)。

另外,Swift 社群目前正努力朝向全應用的方向前進,不讓自己局限在只有 Mobile 或是 macOS 上,這帶來了非常多的社群資源。舉例來說,Kitura 或 Vipor 的相關問題,幾乎都可以在 stackoverflow 找到答案,而這兩個框架的文件也都非常齊全好懂,加上 Apple 在 Swift 成為後端語言投入的資源(可以參考 Swift Server Update – YouTube 以及 Swift.org – Swift Server Work Group (SSWG)),都讓投入 Swift 後端開發變得安全而可維護。

當然作為後端語言,不可免俗的就是要來個 benchmark 大比拼,這邊小弟稍微拾一下 Vapor 的牙慧,從 Server Side Swift vs. The Other Guys — 2: Speed – VAPOR – Medium 這邊可以看得出來,在 plain text request 的處理能力上, Vapor 只輸給 Go,但就贏過大多主流的架構像是 RoR 跟 express。而在 JSON parsing 方面,Vapor 也只輸給 Go 跟 express,還是比 RoR 跟 flask 快上不少。而語言本身的速度就更不用說,已經有不少 benchmark 證明,Swift 在執行速度上是一個非常有效率的語言。

如果以上這些好處,還是沒辦法說服你加入 Swift 後端的行列的話,那我們還有一個很吸引人的特點:使用 Swift 這個使用強型別、設計嚴謹、大多數的錯誤都可以在 compile phrase 就被抓出來的語言,來撰寫需要大量邏輯的後端,就系統的維護層面上來說,絕對是一個大好處!你可以省去撰寫簡單資料測試 unit test、和來來回回測試參數的時間;也可以在設計函式介面時強迫自己思考可讀性和重用性;在面對複雜的模組時,也比較容易設計出簡單易理解的好模組。

百家爭鳴的 Server-side Swift Framework

自從 Swift compiler 也能在 Linux 上運作後,Server-side Swift 架構也一個接著一個如雨後春筍般地出現,目前主要比較熱門的 framework 有:

  • Kitura:IBM 主導的 Swift 後端 framework,是 SSWG 的主要成員之一。
  • Vapor:一個變動很快很活躍的 Server-side framework,也是 SSWG 的成員之一。
  • Perfect:應該是最早的 Server-side Swift 架構,有完整的社群資源。
  • Smoke:Amazon 主導的輕量化 framework。

在這篇文章,我們主要會選用 Kitura 作為範例。選 Kitura 的原因,主要是因為 Kitura 最開始就是朝向 production-ready、方便 deploy、能夠整合進原本的 operation workflow 為目標來開發的,版本的穩定度也高,是最有潛力成為正式專案的架構,加上 IBM 也已經提供了很好的 docker 整合,所以理所當然地成為我們的首選。

其它幾個專案當然也各有優缺,以下是一些簡單的比較文章:

Kitura 設定

Kitura 是 IBM 公司開發的 Server-side Swift framework,目前是以免費且 open source 的方式在進行,整個專案是基於 Swift Package Manager (SPM) 架構而成的,可以在 macOS 及 Linux 系統下執行。

稍微簡介一下這篇文章的環境:

  • Swift 5
  • Xcode 10.2.1
  • Kitura 2.7
  • Docker 18.9.2

雖然不同版本的設定可能會有些不同,但是這邊我們會試著解釋各個步驟的理由跟原因,了解之後就算語法或設定改變,理想狀態下你應該也可以依樣畫葫蘆(吧)。

很快地來寫一個 Kitura 的 endpoint

首看,我們要來設定 Kitura。安裝這個 framework 非常簡單,你可以透過 homebrew 來安裝:

# 加入 IBM 維護的 tap 
brew tap ibm-swift/kitura 

# 安裝 Kitura
brew install kitura

我們裝完之後,可以來測試一下是否有正確安裝(可能需要重開 terminal):

kitura --version

接著就來初始化專案,切到一個空白的資料夾,並輸入指令:

kitura init

這個指令會幫你建立一個屬性為 executable 的 SPM 專案,你會看到資料夾底下產生了一些像是 Source 資料夾跟 Dockerfile 等的東西,我們之後會說明這些東西的用途。現在讓我們打開 .xcodeproj 檔,來研究一下在 Kitura 的專案結構:

Kitura-structure

在這裡,範例專案的名稱是 Demo。

在 Source 底下,Application 就是我們主要放 code 的地方,在這個資料夾裡的 code 會被編譯成 Application.framework,而 main.swift 就是 executable SPM 主程式。接下來,讓我們來瞧瞧 Application.swift

public class App {
    let router = Router()
    let cloudEnv = CloudEnv()

    public init() throws {
        // Run the metrics initializer
        initializeMetrics(router: router)
    }

    func postInit() throws {
        // Endpoints
        initializeHealthRoutes(app: self)
    }

    public func run() throws {
        try postInit()
        Kitura.addHTTPServer(onPort: cloudEnv.port, with: router)
        Kitura.run()
    }
}

這個 Application.swift 裡的 App Class,是 Kitura 的入口。App class 的上面有兩個 property,第一個是 router,負責接收連到 server 的 request,決定要怎樣分配處理 request;第二個是 router 底下的 cloudEnv,負責提供特定 server 的 environment parameter,像是 access token 等參數設定。而最底下的 run(),就是這個 framework 最一開始被執行的 function,通常我們會在這裡設定 router 跟 port,也就是它裡面的 Kitura.addHTTPServer(onPort:, with:)。Kitura 在這個範例裡也已經先建立了一個 postInit(),讓你初始化 API endpoints。

現在我們來試試看,加入第一個 API endpoint 吧!首先我們在 postInit() 裡面加入以下的 code :

func postInit() throws {
    // Endpoints
    initializeHealthRoutes(app: self)

    // Greeting generator
    router.get("/greeting") { (request, response, next) in
        response.send("hello 🌎!")
        next()
    }
}

上面這段 code 會在 Kitura 建立一個 endpoint "/greeting",未來可以透過呼叫 http://<BASE URL>/greeting 來觸發後面 handler 的執行。

要測試這段 code 也很容易,首先確定 scheme 有選到 target 是 executable 的這個:

kitura-1

然後按下 Command(⌘) + R 執行 server 。

讓我們來測試一下吧:

curl -X GET http://localhost:8080/greeting

hello 🌎!

是不是很簡單呢!

Request 與 Routing

在上文,我們看過了 Router 這個類別所能處理的一個簡單任務,但這個 Router 類別可以說是 Kitura 最強大的賣點之一,它讓 server-side 必要的 routing 變得很容易,設計上也跟 Swift 語法緊密結合。

所以甚麼是 Kitura 的 routing?從 server 的角度來看,routing 指的是從接收到 client 的 request、到決定要怎樣處理這個 request 的過程。舉例來說,client 端發了一個 http post 需求過來:

curl -X POST 'https://<server host>/handle_post' -d data=testdata

這個時候,我們 server 這邊應該要知道,如何處理 path 是 handle_post、而http method 是 POST 的 request。除此之外,還要能接收到 post body 裡的 data=testdata 資料,並丟給該負責的人處理,這樣整個過程就是一個 routing 的對應。

Kitura 提供了許多不同的 routing 方法,但基本概念都是類似的。一開始的範例是一個非常基本的 routing:

router.get("/greeting") { (request, response, next) in
    response.send("hello 🌎!")
    next()
}

這個 func 的介面長這樣:func get(_ path: , handler: @escaping RouterHandler) -> Router。透過呼叫這個 func,我們向 router 註冊了一個 routing 對應,http method 是 GET,並且指定 path 為 /greeting。後面的 handler 參數,就是當收到 client 端來的 request 時,就交給 handler 去執行對應的處理。

再往下看,handler 這個參數的型別是一個 closure,定義如下:typealias RouterHandler = (RouterRequest, RouterResponse, @escaping () -> Void) throws -> Void。從 RouterRequest 這一個參數,你可以收到所有 client 來的資訊,像是 query parameter、header、body 等等。而 RouterResponse 則負責處理回覆,你可以把要給 client 端回覆的資訊,丟給 RouterResponse 物件處理。最後面的 closure 是用來支援一個 path 多個 handler 的狀況,當這個 handler 處理完某個 request 之後,只要呼叫 next() ,被指派到同一個 path 的下一個 handler 就會開始工作。

實驗一下簡單的 Routing 設定

首先,我們來修改一下上面的程式:

router.get("/greeting") { (request, response, next) in
    let message = request.queryParameters["message"] ?? "nobody"
    response.send("hello \(message)!")
    next()
}

request.queryParameters["message”]這段 code 會從query parameter 裡面,取出 key 是 message 的值,再把值丟到 response.send("hello \(message)!") ,回覆給 client 端。按下 Command + R 重新編譯跟執行後,以下是執行結果:

curl -X GET 'http://localhost:8080/greeting?message=Jakku' 
hello Jakku! 

如果我們註冊兩個同樣的 path:

router.get("/greeting") { (request, response, next) in
    let message = request.queryParameters["message"] ?? "🌎"
    response.send("hello \(message)! \n")
    next()
}
router.get("/greeting") { (request, response, next) in
    let message = request.queryParameters["message"] ?? "🌎"
    response.send("hello \(message) twice! \n")
    next()
}

第一個 request 處理完後呼叫 next(),第二個 request 的 handler 就會被觸發,所以我們會得到:

curl -X GET 'http://localhost:8080/greeting?message=Jakku' 
hello Jakku! 
hello Jakku twice! 

最後來看一下 POST 的範例:

router.post(middleware: BodyParser())
router.post("/greeting") { (request, response, next) in
    let message = request.body?.asURLEncoded?["message"] ?? "🌎"
    response.send("hello \(message)!")
    next()
}

POST request body 通常有幾種常見的 ContentType,像是 application/x-www-form-urlencodedapplication/json、或是 multipart/form-data。在 Kitura 中,如果你想要取出正確格式的 body,你要先指定 body 的 parser,告訴它該怎樣處理這個 request body。最上面的 router.post(middleware: BodyParser()),就是針對所有的 path(沒有設定 path 參數就是全部都通用的意思),設定一個類型是 BodyParser 的中間人,負責在收到 body 資料之後,針對 ContentType 去找出對應的方法來處理。少了這段,在 handler closure 裡面的 request,就只能取到 Data,需要自己去轉換。

再來我們往下看到 post 的 handler closure,因為我們預期 ContentType 是 x-www-form-urlencoded,所以我們透過 request.body?.asURLEncoded?["message"] 來把資料取出來,再回傳給 client。執行結果如下:

curl -X POST \
  http://localhost:8080/greeting \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'message=Coruscant'
hello Coruscant!

以上就是各種簡單的 routing 小試驗,雖然目前為止,Routing 看起來很簡單,但 Swift 工程師一定很了解,http 上的資料是 serialize 過的;也就是說,雖然 Swift 的物件都是強型別的,但是要傳送或接收,都必需要先經過 serialize,轉成文字後才能進行傳輸。而對 server-side 來說,主要的任務當然就是處理這些資料的傳出傳入,如果能夠簡化 serialize/deserialize (encode/decode) 的過程,就會方便很多。所以接下來,我們就要來介紹 Kitura 的一大特色功能:Codable Routing!

跟 Swift 緊密整合的 Codable Rounting

當我們 server 端跟 client 決定好溝通的資料型態都是 JSON 後,我們就可以利用這個 Codable Routing 來大幅簡化 routing 的過程。

假設我們現在要設計一個送訊息的 API,傳入一個 JSON 指明要送的訊息內容,還有要送給誰, Server 在傳送完訊息後,會回傳訊息狀態和傳送的時間,這個 API 的輸入會長這樣:

{
    "sendTo": "Deckard",
    "message": "Are you Nexus?"
}

Server 在成功後應該要回傳:

{
    "status": "`Are you Nexus?` has been sent to Deckard",
    "sendTime": "2019-05-18 15:33:17"
}

這樣我們該要怎樣設計 server code 呢?首先,輸入跟輸出都是 JSON 格式,所以我們可以利用 Codable Routing 來簡化從 JSON 到 Swift 物件的過程。我們先宣告兩個 Swift struct,代表輸入跟輸出的物件:

struct SendMessageRequest: Codable {
    let sendTo: String
    let message: String
}

struct SendMessageResponse: Codable {
    let status: String
    let sendTime: String
}

根據需求,我們的 handler 會長這樣:

注意:這個範例程式不會直接送出訊息,這邊只會做一個假的回應。
    func sendMessageHandler(request: SendMessageRequest, responseClosure: (SendMessageResponse?, RequestError?) -> Void) {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd hh:mm:ss"

        let response = SendMessageResponse(status: #"`\#(request.message)` has been sent to \#(request.sendTo)"#, sendTime: formatter.string(from: Date()))
        responseClosure(response, nil)
    }

這個 handler 的第一個參數 request,是一個從 Kitura 傳來的、已經轉成物件的 SendMessageRequest,它的資料來源來自 request 的 body、或是 query parameter。另外一個參數 responseClosure,則是當我們處理完 request,要回傳給 client 端就會需要使用它。這個 closure 的第一個參數 SendMessageResponse,就是一個回傳給 client 的物件,最後會被轉成 JSON 送回 client。Closure 的第二個參數則是回傳錯誤用。

有了這個 handler 之後,我們就可以這樣新增一個 routing:

func postInit() {
    router.post("/send", handler: sendMessageHandler)
}

func func sendMessageHandler(request: SendMessageRequest, responseClosure: (SendMessageResponse?, RequestError?) -> Void) {
    ...
}

這裡呼叫的是這個 func:

func post<I: Codable, O: Codable>(_ route: String, handler: @escaping CodableClosure<I, O>)

而 CodableClosure 則是另外一個 typealias:

typealias CodableClosure<I: Codable, O: Codable> = (I, @escaping CodableResultClosure<O>) -> Void

所以,我們定義好參數為 Codable 的 handler closure(上面的sendMessageHandler)後,在 router.post(…) 這邊,complier 就會直接認定是要呼叫上面的 Codable Routing,然後直接轉換輸入跟輸出的參數格式。最後我們來測試一下:

curl -X POST \
  http://localhost:8080/send \
  -H 'Content-Type: application/json' \
  -d '{
    "sendTo": "Deckard",
    "message": "Are you Nexus?"
}'

{"status":"`Are you Nexus?` has been sent to Deckard","sendTime":"2019-05-18 15:36:29"}

有了這個 Codable Routing 之後,對於 JSON 的處理可以更加方便,將 JSON deserialize 成強型別的物件也能夠充份利用 Swift 的優勢,不但避免許多人為錯誤,也減輕寫 code 時的負擔。雖然它還有許多功能正在開發中,不過非常建議在你的專案中使用 Codable Routing 簡化你的介面設計!

Deploy 是 Server 的宿命

一旦完成了 Server-side Swift 專案,就要來面對這個無可避免的問題:如何把它丟到雲端讓它對外服務(Deploy)?

Kitura 是一個基於 Swift 的框架,在 deploy 前,必須要先 build 你的專案,而且 build 的環境需要跟 deploy 的環境一樣。也就是說,在電腦上開發完之後,你需要把專案丟到雲端主機上,用雲端的主機再 build 一次專案,得到的 executable 才是能夠在雲端主機上執行的程式。這點跟常見的 intepreter language 框架,如 Ruby 的 RoR、python 的 Django 很不一樣。

因為 Kitura 專案是一個 SPM 的專案,所以 build 非常簡單,把你的專案丟到目標 server,然後執行:

swift build -c release

Compile 完後,再執行:

sh .build/release/Demo

這樣就可以啟動你的 service 了。

不過在這邊,我們會遇到許多現實生活會遇到的問題。首先,最大的問題是,只要換不同的環境,我們都要重新設定環境並 compile 一次。原本對於只有一台主機的後端 server 來說,可以針對這台主機寫個 script 自動化就好。但是如果我同時要維護兩台主機,或者因為流量起伏,我需要動態增加或減少 server 數量,這時候原本的方法就很有可能會遇到問題。另外,雖然 Swift 的環境設定相對單純,但是對於從未經手過專案的人來說,要啟動這個專案仍然需要時間去了解環境設定。如果能夠把所有設定都統一,不管到哪一台電腦、哪一種系統,都能夠順利執行,而不需要安裝 dependency 、設定環境變數等等,那是不是就很方便呢!

而我們現在就要來介紹,解決這些問題的熱門方案之一:Docker!

站在鯨魚的肩膀上 – Docker

Docker

Docker 可以說是近幾年來最火紅的 server 端技術,主要目的就是要減少因為不同環境而造成 operation 上的困難、讓 load balncing 更有彈性、還有降低 operation 人為介入的比例。Docker 表面上看起來很複雜,因為它包含了許多不同的概念跟名詞,但圍繞著的中心概念都是一致的。現在,就讓我們簡單介紹一下 docker 的核心概念,還有一些簡單的基本名詞。

下面這張圖簡單地介紹了 docker 實務上的架構:

docker-structure

首先,我們要了解甚麼是 image。在 docker 裡,image 指的是一個只能讀取、不能寫入的樣板,它可以被視為在某個時間點主機的狀態的存檔。相對地,container 指的是一個正在執行中的 instance,一個 container 可以被視為是一台虛擬主機,而 container 的樣貌,就是來自於 image。用上面的圖來解釋,在開發完畢後,我們會先把 code 做出一個模板 (image),然後把這個模板丟到雲端上;接著在 server 上利用這個模板,產生出幾台長得一模一樣、跑著一樣的 code 的虛擬主機 (container)。而 docker daemon 則是一個管理 local 的 image 跟 container 的本機端程式。

這個流程讓 container 的產生跟關閉都變得很容易。Container 有點像是用過即丟的工具人(別哭)。在需要的時候,我們會利用 image 生出許多 container 來幫忙處理大量 request;相反地,在沒事的時候,就直接把 container 關掉。因為從 image 到產生 container 這個過程非常簡單,也都可以被寫成 script ,所以整個 server 的維護、還有 deploy 的流程都變得非常容易,這也是它這麼受歡迎的原因之一。

這篇文章不會帶入太多 docker 的說明,會盡量讓主題專注在 Kitura 的 deploy 上。更多關於 docker 的介紹,可以直接參考官網的簡介。

接下來,我們會介紹如何在自己的電腦上建構你的 Kitura image,並且跑一個 local 的 container。在進行下面章節之前,請先安裝 docker daemon。如果你是用 mac ,可以直接參考這裡來安裝 mac 版的 docker。現在就讓我們直接快轉到安裝完畢後!(是有多想跳過這段)

用 Docker 打包 Kitura 專案

安裝完畢之後,如果是 mac 使用者,請直接打開 docker 應用程式,你會看到右上角有一個小鯨魚 icon,表示 docker daemon 正常運作中。接著,我們就要開始來建構 Kitura 專案的 docker images了!

首先,先打開 terminal,切換到剛剛 Kitura 專案的目錄,你會發現目錄底下有一堆不認識的檔案,其中最重要的是下面這兩個檔案:

  • Dockerfile-tools
  • Dockerfile

Dockerfile 描述了建構 image 的方法,包括要放哪些檔案、要裝哪些程式到這個 image 等等。而 Kitura 專案,用了兩個 Dockerfile,也就是說,它將會產生出兩個 docker image。其中,Dockerfile-tools 會產生一個 image,專門拿來編譯專案用;另外一個 Dockerfile,則是拿來跑 server 執行檔用。下面,我們來了解一下這兩個檔案分別在做甚麼事情。

Dockerfile-tools

FROM ibmcom/swift-ubuntu:5.0.1
....
ADD https://raw.githubusercontent.com/IBM-Swift/swift-ubuntu-docker/master/utils/tools-utils.sh /swift-utils/tools-utils.sh
....
COPY . /swift-project

上面這個是摘錄的 Dockerfile-tools。第一個部分,FROM ibmcom/swift-ubuntu:5.0.1的意思是:這個 image 是基於 ibmcom/swift-ubuntu:5.0.1 image 建構而成的;而 FROM 引用的 image 預設可以在 Docker Hub 裡找到。在 docker 裡,image 是可以被繼承的,像這邊的 image 就是繼承自 IBM 已經可以執行 Swift 5 的 image,這樣環境的設定就單純很多。第二個部分使用了 ADD 這個關鍵字,意思是將某個網路上或本機上的資源,加到 image 裡。這邊我們加入的是已經寫好的 script:tools-utils.sh。最後面 COPY 則是把專案底下的所有檔案都 copy 到 image 裡面,以便之後的 build 流程。

Dockerfile

FROM ibmcom/swift-ubuntu-runtime:5.0.1
....
COPY . /swift-project
CMD [ "sh", "-c", "cd /swift-project && .build-ubuntu/release/Demo" ]

接著,我們來看一下 Dockerfile。跟剛剛 Dockerfile-tools 不一樣,Dockerfile-tools 主要會產生專門 compile 的 container,而 Dockerfile 主要會產生專門跑 server 程式的 container。所以第一個部分,我們會看到它繼承自不一樣的 image,這個 image 包含了能夠執行 Swift 5 的最小系統環境。它少了像是 Kitura framework 跟 SPM 等等的相依套件,所以大小也會比前面一個小,但拿來跑已經 build 好的專案綽綽有餘。再來你會看到,這個 image 一樣複製專案的內容到 image 裡。最後 CMD 則是執行一個 command 的意思,所以你會看到這邊我們切換到在 image 裡的 project 目錄,並且直接跑 compile 好的 .build-ubuntu/release/Demo executable。

了解了兩個 Dockerfile 的內容後,我們就要來親手將 docker images 給建出來!一開始我們要先用 docker build 來建一個 build 專案用的 image:

docker build -t demo-build -f Dockerfile-tools .

這邊,-t 的參數是要給 image 一個名稱,-f 的參數就是指定某個 docker file。不要漏掉最後的 .,它代表 build image 時的參考路徑。

下完這個指令後,docker 會先下載 ibmcom/swift-ubuntu:5.0.1,然後一步一步執行 Dockerfile-tools 裡面設定的指令。

等到這步驟結束後,我們可以用 docker images 來看看目前系統中的兩個 image:

docker images

REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
demo-build                                 latest              4a8b31f1d5c3        8 seconds ago       1.92GB
ibmcom/swift-ubuntu                        5.0.1               779a1166e275        2 weeks ago         1.68GB

接著,我們要用這個剛產生的 image 來 build 專案,好讓它可以跑在 linux 為主的 docker container 環境底下:

docker run -v $PWD:/root/project -w /root/project demo-build /swift-utils/tools-utils.sh build release

Docker run 這個指令會基於某個 image 啟動一個 container,並執行後面指定的命令。-v 這個參數指的是設定 Volume,可以把某個本機上的某個資料夾跟 docker container 裡的資料夾做連通,上面的設定就是把專案資料夾跟 container 裡的 /root/project 做連動,這樣 build 後的 executable 就可以再被存回本機,而不是只存在 container 裡面,關機後就消失了。-w 指的是後面的指令要在哪個 container 裡的資料夾被執行。而 demo-build 就是 image 的名稱,後面的所有內容都會在 container 被啟動後執行。

整個 docker run 的任務,就是用建立好的 demo-build image 產生一個 container,並且在 container 裡面執行 tools-utils.sh。這個步驟會產生一個 executable file 到專案的資料夾裡,同時這個 executable file 會是可以在 linux 環境底下被執行的。

接著我們就要來建立拿來跑 server 的 image 了,一樣我們用 docker build 來執行這個任務:

docker build -t demo-run .

這邊我們沒有利用 -f 指定Dockerfile,因為系統會預設直接抓檔名為 Dockerfile 的檔案做為設定檔。在名為 demo-run 的 image 被建立之後,我們就來啟動一個 container,跑我們辛苦建立的服務了💦。

docker run -p 8080:8080 demo-run

-p 是把 container 這個虛擬主機的 port 對應到真實主機的 port,好對外提供服務。這時候,你就可以看到我們的 Kitura 服務正式上線啦 🎉!

你可以透過 docker ps 指令,來看看有哪些 container 正在執行中:

docker ps 


CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
8e1877ed5176        demo-run            "sh -c 'cd /swift-pr…"   31 seconds ago      Up 30 seconds       0.0.0.0:8080->8080/tcp   upbeat_montalcini

一旦 container 所有任務都執行完畢,它就會直接被關掉。要看已經被關掉的 container,可以加上 -a 這個參數。在使用 container 時,要隨時注意 container 是 stateless 的,也就是說它只幫你執行工作,但不會保存任何資訊。所以,container 產出的內容都應該被視為隨時有可能會消失的,如果你有任何重要的內容,像是剛剛 build 後的 executable file,就應該要透過 Volume 或是外連的資料庫把資料存放在別的地方,container 只是工具人,通常不能認真對待它(寫著寫著眼框就濕了)。

Docker 的指令非常多而複雜,有興趣的可以直接上官網查詢,他們的文件一直都有在更新,並且寫的很清楚,你可以發現許多非常方便的工具跟應用。

在打包專案之後,我們就可以把手上建立好的 image,直接丟到雲端服務執行,像是 Google Cloud Platform 或是 IBM Cloud。這個部分礙於篇幅,就讓我們拖稿到下次再來解說吧!(很理所當然)

結論

在一開始,Kitura 就被設計為能夠在 production 環境中順利執行的框架,所以相對於 Vapor,在 API 介面設計上、還有原始碼上,或許不是那麼炫麗。但是同時它也提供了大量的工具還有資源,讓你能夠輕易地將專案 deploy 到常見的 container 系統中。Kitura 的 API 介面設計也相對地好理解且易讀,甚至對不熟 Swift 的人來說也非常友善。當然它也存在著一些問題,像是 http method 支援不足、Codable Routing 的客制化程度不高等等,但如果你手邊剛好有個 project 需要即時的後端,Kitura 絕對是一個非常好的選擇!

參考資料

作者
Huang ShihTing
I’m ShihTing Huang(黃士庭). I brew iOS app, front-end web app, and of course, coffee and beer!
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。