本文說明如何結合 Vagrant 與 Docker 建立微服務開發環境,藉此提升開發效率與環境一致性。首先,我們會利用預先建置的 Docker 映像檔,其中已包含所有必要的工具和函式庫,例如 MongoDB、NodeJS、Java、Scala 等,大幅簡化環境設定流程。接著,示範如何使用 Docker Compose 更有效率地管理 Docker 容器,透過 YAML 檔案定義容器引數,簡化複雜的 Docker 命令。最後,本文逐步闡述佈署Pipeline的初始階段,涵蓋從程式碼簽出、執行佈署前測試、編譯封裝到建構 Docker 映像檔的完整流程,並深入解析 Dockerfile 的撰寫技巧,例如基礎映像檔選擇、指令合併以及環境變數設定等,以利讀者建構高效且易於維護的 Docker 映像檔。
使用 Vagrant 和 Docker 建立開發環境
Docker 的優勢
Docker 是一種根據容器技術的虛擬化解決方案,它與傳統的虛擬機器(VM)有著本質的不同。傳統的 VM hypervisors 是透過模擬虛擬硬體來實作虛擬化,這導致了大量的資源浪費在硬體虛擬化上,通常佔用了 50% 或更多的硬體資源。
相較之下,Docker 使用分享作業系統(OS)的架構,這使得它更加高效。在定義良好的容器中,我們可以輕鬆地執行比傳統虛擬機器多 5 倍的應用程式。透過使用主機核心,容器能夠在不進行硬體虛擬化的情況下保持程式間的隔離。即使 Docker 沒有其他優勢,這一點也足以讓許多人開始使用它。
許多人認為容器是 Docker 發明的新技術,但事實上,容器至少從 2000 年就已經存在了。例如,Oracle Solaris Zones、LXC 和 OpenVZ 等都是早期的容器技術實作。Google 是最早使用容器的公司之一,早在 Docker 出現之前就已經開始使用了。那麼,Docker 為什麼會如此特別?Docker 使容器的使用變得簡單,並建立在 LXC 的基礎上。它使原本複雜的技術變得簡單易用,並圍繞它建立了一個強大的生態系統。
Docker 公司很快就與軟體產業的領袖們(Canonical、RedHat、Google、Microsoft 等)合作,推動了容器的標準化。這種合作也使得容器技術能夠在幾乎所有作業系統上執行。在撰寫本文時,Windows Server 2016 技術預覽版已經發布,並且原生支援 Docker 引擎。
開發者和 DevOps 都喜歡 Docker,因為它提供了一個簡單可靠的方式來封裝、運輸和執行自足的應用程式,這些應用程式可以佈署在幾乎任何地方。另一個重要的 Docker 工具是 Docker Hub,它包含了官方、非官方和私有的容器。不論你需要什麼,無論是應用程式、伺服器、資料函式庫還是其他東西,你都可以在 Docker Hub 中找到它,並透過單一命令在幾分鐘內啟動和執行。
開發環境的使用
目前,我們不會探討如何編寫 Dockerfile、構建容器以及將它們推播到公共或私有倉函式庫。這些將是後續章節的主題。目前,我們將重點放在執行預先構建的容器上,特別是 vfarcic/books-ms-tests 容器。該容器包含了開發 books-ms 服務所需的一切,包括 MongoDB、NodeJS、NPM、Git、Java、Scala、SBT、FireFox、Chrome 和 Gulp。它具有專案所需的所有 Java 和 JavaScript 函式庫,組態也已正確設定。
如果您恰好同時使用這些語言和框架進行開發,您可能已經在電腦上安裝了它們。然而,您很可能只使用其中一部分,而缺乏其他部分。即使您已經安裝了所有必要的工具,您仍然需要下載 Scala 和 JavaScript 的依賴項,調整一些組態,執行自己的 MongoDB 例項等等。對於單一的微服務來說,這些指示可能已經很繁瑣了。如果您的企業需要數十、數百甚至數千個微服務,情況將會更加複雜。
有多種型別的開發任務需要在 books-ms 服務上執行。請記住,它同時包含了後端(使用 Scala 和 Spray)和前端(使用 JavaScript/HTML/CSS 和 PolymerJS)。
例如,我們可以執行 Gulp watcher,每當客戶端的原始碼發生變化時,它就會執行所有前端測試。如果您正在實踐測試驅動開發,那麼持續獲得程式碼正確性的反饋是非常有用的。有關前端開發的更多資訊,請參閱「使用 Polymer Web Components 和測試驅動開發開發前端微服務」系列文章。
以下命令執行了 watcher:
sudo docker run -it --rm \
-v $PWD/client/components:/source/client/components \
-v $PWD/client/test:/source/client/test \
-v $PWD/src:/source/src \
-v $PWD/target:/source/target \
-p 8080:8080 \
--env TEST_TYPE=watch-front \
vfarcic/books-ms-tests
內容解密:
sudo docker run -it --rm:使用 Docker 執行一個容器,-it表示互動式終端,--rm表示容器離開後自動刪除。-v $PWD/client/components:/source/client/components:將主機上的client/components目錄掛載到容器中的/source/client/components目錄。-v $PWD/client/test:/source/client/test:將主機上的client/test目錄掛載到容器中的/source/client/test目錄。-v $PWD/src:/source/src:將主機上的src目錄掛載到容器中的/source/src目錄。-v $PWD/target:/source/target:將主機上的target目錄掛載到容器中的/source/target目錄。-p 8080:8080:將主機的 8080 連線埠對映到容器的 8080 連線埠。--env TEST_TYPE=watch-front:設定環境變數TEST_TYPE為watch-front,表示執行前端測試的 watcher。vfarcic/books-ms-tests:指定要執行的 Docker 映像檔名稱。
這個命令的作用是啟動一個包含所有必要工具和組態的容器,並且執行前端測試的 watcher。每當客戶端的原始碼發生變化時,它就會自動執行測試並提供反饋。
使用Vagrant和Docker設定開發環境
許多層需要被下載後才可以執行這個容器。這個容器大約佔用2.5GB的虛擬空間(實際物理大小要小得多)。與應該盡可能小的生產容器不同,用於開發的容器往往更大。例如,只有NodeJS模組就佔用了將近500MB,而這些只是前端開發依賴項。再加上Scala函式庫、執行檔、瀏覽器等,東西很快就會累積起來。 希望您有快速的網路連線,這樣就不會花太長時間下載所有層。您可以繼續閱讀,直到下載完成,或者直到看到執行另一個命令的指示。
輸出的部分內容應該如下所示(為了簡潔起見,省略了時間戳)。
1 … 2 MongoDB啟動:pid=6 port=27017 dbpath=/data/db/ 64位主機=072ec2400bf0 3 … 4 分配新資料檔案/data/db/local.ns,填滿零… 5 建立目錄/data/db/_tmp 6 完成分配資料檔案/data/db/local.ns,大小:16MB,花費0秒 7 分配新資料檔案/data/db/local.0,填滿零… 8 完成分配資料檔案/data/db/local.0,大小:64MB,花費0秒 9 等待連線埠27017上的連線 10 … 11 Firefox 43 測試透過 12 測試執行以極大的成功結束 13 14 Firefox 43 (93/0/0) 15 … 16 從127.0.0.1:46599接受連線 #1(現在有1個連線) 17 [akka://routingSystem/user/IO-HTTP/listener-0] 繫結到/0.0.0.0:8080 18 … 我們剛剛使用Firefox執行了93個測試,啟動了MongoDB,並使用Scala和Spray啟動了Web伺服器。所有Java和JavaScript依賴項、執行檔、瀏覽器、MongoDB、JDK、Scala、sbt、npm、bower、gulp等我們可能需要的東西都在這個容器裡。所有這一切都透過一條命令完成。您可以更改位於client/components目錄中的客戶端原始碼或client/test中的測試。您將看到,只要您儲存更改,測試就會再次執行。個人而言,我傾向於將螢幕分成兩半,一半用於程式碼,另一半用於執行測試的終端機。我們透過一條命令獲得了持續的反饋,並且沒有任何安裝或設定。
正如上面提到的,我們不僅使用這個命令執行前端測試,還執行了Web伺服器和MongoDB。透過這兩個,我們可以透過在您喜歡的瀏覽器中開啟http://10.100.199.200:8080/components/tc-books/demo/index.html⁴⁰來檢視我們的工作結果。您看到的是我們稍後將要使用的Web元件的演示。
簡化Docker命令
我們不會詳細介紹我們剛剛執行的命令中的每個引數的含義。這將在後面的章節中探討Docker CLI時進行詳細介紹。需要注意的重要事情是,我們執行了從Docker Hub下載的容器。稍後,我們將安裝自己的登入檔,以便在其中儲存我們的容器。另一個重要的事情是,一些本地目錄被掛載為容器卷,這使我們能夠在本地更改原始碼檔案並在容器內使用它們。
上述命令的主要問題是其長度。我個人無法記住這麼長的命令,我們也不能期望所有開發人員都知道它。雖然到目前為止我們所做的事情比設定開發環境的替代方法要容易得多,但這個命令本身與我們試圖實作的簡單性相衝突。執行Docker命令更好的方法是透過Docker Compose⁴¹。同樣,我們將在後面的章節中保留更深入的解釋。目前,讓我們先嚐試一下。請按CTRL+c停止當前正在執行的容器,然後執行以下命令。
sudo docker-compose -f docker-compose-dev.yml run feTestsLocal
如您所見,結果是相同的,但這次的命令要短得多。執行這個容器所需的所有引數都儲存在docker-compose-dev.yml⁴²檔案中的目標feTestsLocal下。該組態檔案使用YAML(Yet Another Markup Language)格式,對熟悉Docker的人來說非常容易編寫和閱讀。
這只是這個容器的許多用途之一。另一個用途是執行所有測試(後端和前端)、編譯Scala程式碼以及壓縮和準備JavaScript和HTML檔案以進行分發。
在繼續之前,請按CTRL+c停止當前正在執行的容器,然後執行以下命令。
sudo docker-compose -f docker-compose-dev.yml run testsLocal
這次,我們做了更多的事情。我們啟動了MongoDB,執行了後端功能測試和單元測試,停止了資料函式庫,執行了所有前端測試,最後建立了稍後將用於建立分發版本的JAR檔案,該檔案最終將被佈署到生產(或在我們的情況下,模擬生產)節點。稍後,當我們開始開發我們的持續佈署Pipeline時,將使用相同的容器。
停止虛擬機器
我們不再需要開發環境,因此讓我們停止虛擬機器。
exit
vagrant halt dev
這是Vagrant的另一個優勢。虛擬機器可以透過一條命令啟動、停止或銷毀。然而,即使您選擇後者,也可以輕鬆地從頭開始重新建立一個新的虛擬機器。目前,虛擬機器已經停止。我們可能稍後會需要它,下次啟動它時不會花太長時間。使用vagrant up dev,它將在幾秒鐘內啟動並執行。
實施佈署Pipeline:初始階段
讓我們從持續佈署Pipeline的一些基本(和最小)步驟開始。我們將簽出程式碼,執行佈署前測試,如果它們成功,則構建一個容器並將其推播到Docker登入檔。當容器安全地儲存在登入檔中時,我們將切換到不同的虛擬機器,該虛擬機器將作為生產伺服器的模擬,執行容器並執行佈署後測試,以確保一切正常運作。
這些步驟將涵蓋被視為持續佈署過程的最基本流程。稍後,在接下來的章節中,一旦我們對目前為止的流程感到滿意,我們將進一步探索。我們將研究我們的微服務安全可靠地到達生產伺服器所需的所有步驟,以實作零停機時間,易於擴充套件,能夠回復等。
啟動持續佈署虛擬機器
首先,我們將建立持續交付伺服器。我們將透過使用Vagrant建立一個虛擬機器來實作這一點。在「現實世界」場景中,您應該完全跳過虛擬機器,直接在伺服器上安裝所有內容。請記住,在許多情況下,容器是我們習慣使用虛擬機器做的某些事情的更好替代品,而像我們在本文中那樣同時使用兩者,在大多數情況下只是浪費資源。
話雖如此,讓我們建立cd和prod虛擬機器。我們將使用第一個作為持續佈署伺服器,第二個作為生產環境的模擬。
cd ..
git clone https://github.com/vfarcic/ms-lifecycle.git
cd ms-lifecycle
vagrant up cd
vagrant ssh cd
內容解密:
cd ..:切換到上層目錄。git clone https://github.com/vfarcic/ms-lifecycle.git:從GitHub克隆ms-lifecycle儲存函式庫。cd ms-lifecycle:切換到ms-lifecycle目錄。vagrant up cd:啟動名為cd的虛擬機器。vagrant ssh cd:透過SSH連線到名為cd的虛擬機器。
我們克隆了GitHub儲存函式庫,啟動了cd虛擬機器並進入其中。
佈署流程的實施:初始階段
在進一步探討佈署流程之前,我們需要了解一些基本的Vagrant操作,尤其是在需要停止和重新執行虛擬機器(VM)的情況下。瞭解這些基本操作可以幫助我們避免在跟隨書籍內容時遇到不必要的麻煩。
基本的Vagrant操作
若要停止目前的虛擬機器,可以執行以下命令:
vagrant halt
執行此命令後,虛擬機器將被停止,釋放系統資源。當需要重新啟動虛擬機器時,可以使用以下命令:
vagrant up --provision
vagrant ssh cd
其中,--provision 引數確保所有需要的容器都已啟動並執行。對於 prod 虛擬機器,因為沒有使用任何 provisioning,所以不需要新增 --provision 引數。
佈署流程的步驟
在虛擬機器啟動並執行的情況下,讓我們快速瀏覽一下佈署流程。我們需要執行以下步驟:
- 簽出程式碼
- 執行佈署前的測試
- 編譯及/或封裝程式碼
- 建立容器
- 將容器推播到註冊中心
- 將容器佈署到生產伺服器
- 整合容器
- 執行整合後的測試
- 將測試容器推播到註冊中心
圖示:Docker佈署流程
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Docker Vagrant 開發環境建置與佈署
package "Docker 架構" {
actor "開發者" as dev
package "Docker Engine" {
component [Docker Daemon] as daemon
component [Docker CLI] as cli
component [REST API] as api
}
package "容器運行時" {
component [containerd] as containerd
component [runc] as runc
}
package "儲存" {
database [Images] as images
database [Volumes] as volumes
database [Networks] as networks
}
cloud "Registry" as registry
}
dev --> cli : 命令操作
cli --> api : API 呼叫
api --> daemon : 處理請求
daemon --> containerd : 容器管理
containerd --> runc : 執行容器
daemon --> images : 映像檔管理
daemon --> registry : 拉取/推送
daemon --> volumes : 資料持久化
daemon --> networks : 網路配置
@enduml
此圖示說明瞭 Docker 佈署流程的基本步驟。
簽出程式碼
簽出程式碼非常簡單,我們已經執行過幾次了。
git clone https://github.com/vfarcic/books-ms.git
cd books-ms
執行佈署前的測試、編譯和封裝程式碼
有了程式碼之後,我們應該執行所有不需要佈署服務的測試。相關命令如下:
docker build \
-f Dockerfile.test \
-t 10.100.198.200:5000/books-ms-tests \
.
docker-compose \
-f docker-compose-dev.yml \
run --rm tests
ll target/scala-2.10/
內容解密:
- 第一個命令使用
Dockerfile.test建立測試容器,並將其標記為10.100.198.200:5000/books-ms-tests。 - 第二個命令執行所有佈署前的測試,並將 Scala 程式碼編譯成 JAR 檔案。
- 第三個命令用於驗證 JAR 檔案是否已建立並位於
scala-2.10目錄中。
建立Docker容器
在所有測試透過並建立 JAR 檔案後,我們可以建立將要佈署到生產環境的容器。首先,讓我們檢查定義容器的 Dockerfile。
FROM debian:jessie
MAINTAINER Viktor Farcic "[email protected]"
RUN apt-get update && \
apt-get install -y --force-yes --no-install-recommends openjdk-7-jdk && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
ENV DB_DBNAME books
ENV DB_COLLECTION books
COPY run.sh /run.sh
RUN chmod +x /run.sh
COPY target/scala-2.10/books-ms-assembly-1.0.jar /bs.jar
COPY client/components /client/components
CMD ["/run.sh"]
EXPOSE 8080
內容解密:
FROM debian:jessie指定了基礎映像為 Debian Jessie。MAINTAINER指令指定了維護者的資訊。RUN指令用於安裝 OpenJDK 7。ENV指令設定了環境變數。COPY指令將檔案從主機複製到容器中。CMD指令指定了容器啟動時要執行的命令。EXPOSE指令聲明瞭容器監聽的埠。
選擇 Debian 作為基礎映像的原因包括其相對較小的體積(約125 MB)和良好的維護性。Docker官方映像通常是最佳選擇,因為它們由 Docker 維護,品質更有保證。
容器基礎映像檔的選擇與 Dockerfile 指令解析
在建置 Docker 容器時,選擇適當的基礎映像檔(base image)是至關重要的第一步。不同的基礎映像檔具有不同的大小和功能特性,選擇合適的基礎映像檔可以最佳化容器的大小和效能。
基礎映像檔的選擇
Debian 是一個常見的基礎映像檔,提供了一個完整的 Linux 發行版,大小約為 80 MB。相較之下,CentOS 則提供了 RPM 套件管理系統,大小約為 175 MB。對於需要極小映像檔的情況,Alpine Linux 是一個很好的選擇,其大小僅約 5 MB。然而,由於 Alpine 的極簡設計,當執行較複雜的命令時可能會遇到困難。
在多容器系統中,基礎映像檔的大小並不如使用的基礎映像檔數量來得重要。因為 Docker 會快取每個映像檔,並在所有使用該映像檔的容器中重複使用。這意味著,如果多個容器都根據同一個基礎映像檔(如 Debian),則該映像檔只會被下載一次。
Dockerfile 指令詳解
Dockerfile 是用於建置 Docker 映像檔的指令檔。下面將解析幾個重要的指令:
MAINTAINER 指令
MAINTAINER Viktor Farcic "[email protected]"
此指令提供了關於維護該容器的作者資訊,純粹是資訊性質。
RUN 指令
RUN apt-get update && \
apt-get install -y --force-yes --no-install-recommends openjdk-7-jdk && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN 指令用於執行任何可以在命令列中執行的命令。在這個例子中,多個命令被連線在一起執行,以減少產生的映像檔層數。每個 Dockerfile 指令都會產生一個新的映像檔層,將多個命令合併成一個 RUN 指令可以減少最終映像檔的大小。
為何要合併 RUN 指令?
將多個命令合併成一個 RUN 指令可以減少最終映像檔的大小。如果每個命令都單獨作為一個 RUN 指令,最終產生的映像檔層數會增加,即使後續層刪除了前面的檔案,這些檔案仍存在於之前的層中。
ENV 指令
ENV DB_DBNAME books
ENV DB_COLLECTION books
ENV 指令用於設定環境變數,這些變數可以在容器執行時被修改或使用。這些環境變數對於在不同環境中執行容器時傳遞特定資訊非常有用。
COPY 指令
COPY 指令用於將檔案或目錄從主機複製到容器中。這裡沒有展示具體的 COPY 指令範例,但它是 Dockerfile 中用於複製檔案的重要指令。