學習使用Docker、Docker-Compose和Rancher搭建部署Pipeline(一)

這篇文章是一系列文章的第一篇,在這一系列文章中,咱們想要分享咱們如何使用Docker、Docker-Compose和Rancher完成容器部署工做流的故事。咱們想帶你從頭開始走過pipeline的革命歷程,重點指出咱們這一路上遇到的痛點和作出的決定,而不僅是單純的回顧。幸虧有不少優秀的資源能夠幫助你使用Docker設置持續集成和部署工做流。這篇文章並不屬於這些資源之一。一個簡單的部署工做流相對比較容易設置。可是咱們的經驗代表,構建一個部署系統的複雜性主要在於本來容易的部分須要在擁有不少依賴的遺留環境中完成,以及當你的開發團隊和運營組織發生變化以支持新的過程的時候。但願咱們在解決構建咱們的pipeline的困難時積累下的經驗會幫助你解決你在構建你的pipeline時遇到的困難。java

在這第一篇文章裏,咱們將從頭開始,看一看只用Docker時咱們開發的初步的工做流。在接下來的文章中,咱們將進一步介紹Docker-compose,最後介紹如何將Rancher應用到咱們的工做流中。git

爲了爲以後的工做鋪平道路,假設接下來的事件都發生在一家SaaS提供商那裏,咱們曾經在SaaS提供商那裏提供過長時間服務。僅爲了這篇文章的撰寫,咱們姑且稱這家SaaS提供商爲Acme Business Company, Inc,即ABC。這項工程開始時,ABC正處在將大部分基於Java的微服務棧從裸機服務器上的本地部署遷移到運行在AWS上的Docker部署的最初階段。這項工程的目標很常見:發佈新功能時更少的前置時間(lead time)以及更可靠的部署服務。docker

爲了達到該目標,軟件的部署計劃大體是這樣的:shell

圖片描述

這個過程從代碼的變動、提交、推送到git倉庫開始。當代碼推送到git倉庫後,咱們的CI系統會被告知運行單元測試。若是測試經過,就會編譯代碼並將結果做爲產出物(artifact)存儲起來。若是上一步成功了,就會觸發下一步的工做,利用咱們的代碼產出物建立一個Docker鏡像並將鏡像推送到一個Docker私有註冊表(private Docker registry)中。最後,咱們將咱們的新鏡像部署到一個環境中。後端

要完成這個過程,以下幾點是必需要有的:api

  • 一個源代碼倉庫。ABC已經將他們的代碼存放在GitHub私有倉庫上了。安全

  • 一個持續集成和部署的工具。ABC已經在本地安裝了Jenkins。bash

  • 一個私有registry。咱們部署了一個Docker registry容器,由Amazon S3支持。服務器

  • 一個主機運行Docker的環境。ABC擁有幾個目標環境,每一個目標環境都包含過渡性(staging)部署和生產部署。app

這樣去看的話,這個過程表面上簡單,然而實際過程當中會複雜一些。像許多其它公司同樣,ABC曾經(如今仍然是)將開發團隊和運營團隊劃分爲不一樣的組織。當代碼準備好部署時,會建立一個包含應用程序和目標環境詳細信息的任務單(ticket)。這個任務單會被分配到運營團隊,並將會在幾周的部署窗口內執行。如今,咱們已經不能清晰地看到一個持續部署和分發的方法了。

最開始,部署任務單可能看起來是這樣的:

DEPLOY-111:
App: JavaService1, branch "release/1.0.1"
Environment: Production

部署過程是:

  • 部署工程師用了一週時間在Jenkins上工做,對相關的工程執行」Build Now「,將分支名做爲參數傳遞。以後彈出了一個被標記的Docker鏡像。這個鏡像被自動的推送到了註冊表中。工程師選擇了環境中的一臺當前沒有在負載均衡器中被激活的Docker主機。工程師登錄到這臺主機並從註冊表中獲取新的版本。

docker pull registry.abc.net/javaservice1:release-1.0.1
  • 找到現存的容器。

docker ps
  • 終止現存容器運行。

docker stop [container_id]
  • 開啓一個新容器,這個容器必須擁有全部正確啓動容器所需的標誌。這些標誌能夠從以前運行的容器那裏,主機上的shell歷史,或者其它地方的文檔借鑑。

docker run -d -p 8080:8080 … registry.abc.net/javaservice1:release-1.0.1
  • 鏈接這個服務並作一些手工測試肯定服務正常工做。

curl localhost:8080/api/v1/version
  • 在生產維護窗口中,更新負載均衡器使其指向更新過的主機。

  • 一旦經過驗證,這個更新會被應用到環境中全部其它主機上,以防未來須要故障切換(failover)。

不能否認的是,這個部署過程並不怎麼讓人印象深入,但這是通往持續部署偉大的第一步。這裏有好多地方仍可改進,但咱們先考慮一下這麼作的優勢:

  • 運營工程師有一套部署的方案,而且每一個應用的部署都使用相同的步驟。在Docker運行那一步中須要爲每一個服務查找參數,可是大致步驟老是相同的:Docker pull、Docker stop、Docker run。這個過程很是簡單,並且很難忘掉其中一步。

  • 當環境中最少有兩臺主機時,咱們便擁有了一個可管理的藍綠部署(blue-green deployment)。一個生產窗口只是簡單地從負載均衡器配置轉換過來。這個生產窗口擁有明顯且快速的回滾方法。當部署變得更加動態時,升級、回滾以及發現後端服務器變得愈發困難,須要更多地協調工做。由於部署是手動的,藍綠部署代價是最小的,而且一樣能提供優於就地升級的主要優勢。

好吧,如今看一看痛點:

  • 重複輸入相同的命令。或者更準確地說,重複地在bash命令行裏敲擊輸入。解決這一點很簡單:使用自動化技術!有不少工具能夠幫助你啓動Docker容器。對於運營工程師,最明顯的解決方案是將重複的邏輯包裝成bash腳本,這樣只需一條命令就能夠執行相應邏輯。若是你將本身稱做一個開發-運營(devops)工程師,你可能會去使用Ansible、Puppet、Chef或者SaltStack。編寫腳本或劇本(playbooks)很簡單,可是這裏仍有幾個問題須要說明:部署邏輯到底放在那裏?你怎樣追蹤每一個服務的不一樣參數?這些問題將帶領咱們進入下一點。

  • 即使一個運營工程師擁有超能力,在辦公室工做一成天后的深夜裏仍能避免拼寫錯誤,而且清晰的思考,他也不會知道有一個服務正在監聽一個不一樣的端口而且須要改變Docker端口參數。問題的癥結在於開發者確實瞭解應用運行的詳細信息(希望如此),可是這些信息須要被傳遞給運營團隊。不少時候,運營邏輯放在另外的代碼倉庫中或這根本沒有代碼倉庫。這種狀況下保持應用相關部署邏輯的同步會變得困難。因爲這個緣由,一個很好的作法是將你的部署邏輯只提交到包含你的Dockerfile的代碼倉庫。若是在一些狀況下沒法作到這點,有一些方法可使這麼作可行(更多細節將在稍後談到)。把細節信息提交到某處是重要的。代碼要比部署任務單好,雖然在一些人的腦海中始終認爲部署任務單更好。

  • 可見性。對一個容器進行一個故障檢測需要登錄主機而且運行相應命令。在現實中,這就意味着登錄許多主機而後運行「docker ps」和「docker logs –tail=100」的命令組合。有不少解決方案能夠作到集中登錄。若是你有時間的話,仍是至關值得設置成集中登錄的。咱們發現,一般狀況下咱們缺乏的能力是查看哪些容器運行在那些主機上的。這對於開發者而言是個問題。開發者想要知道什麼版本被部署在怎樣的範圍內。對於運營人員來講,這也是個主要問題。他們需要捕獲到要進行升級或故障檢測的容器。

基於以上的狀況,咱們開始作出一些改變,解決這些痛點。

第一個改進是寫一個bash腳本將部署中相同的步驟包裝起來。一個簡單的包裝腳本能夠是這樣的:

!/bin/bash
APPLICATION=$1
VERSION=$2

docker pull "registry.abc.net/${APPLICATION}:${VERSION}"
docker rm -f $APPLICATION
docker run -d --name "${APPLICATION}" "registry.abc.net/${APPLICATION}:${VERSION}"

這樣作行得通,但僅對於最簡單的容器而言,也就是那種用戶不須要鏈接到的容器。爲了可以實現主機端口映射和卷掛載(volume mounts),咱們需要增長應用程序特定的邏輯。這裏給出一個使用蠻力實現的方法:

APPLICATION=$1
VERSION=$2

case "$APPLICATION" in
java-service-1)
 EXTRA_ARGS="-p 8080:8080";;
java-service-2)
 EXTRA_ARGS="-p 8888:8888 --privileged";;
*)
 EXTRA_ARGS="";;
esac

docker pull "registry.abc.net/${APPLICATION}:${VERSION}"
docker stop $APPLICATION
docker run -d --name "${APPLICATION}" $EXTRA_ARGS "registry.abc.net/${APPLICATION}:${VERSION}"

如今這段腳本被安裝在了每一臺Docker主機上以幫助部署。運營工程師會登錄到主機並傳遞必要的參數,以後腳本會完成剩下的工做。部署時的工做被簡化了,工程師的須要作的事情變少了。然而將部署代碼化的問題仍然存在。咱們回到過去,把它變成一個關於向一個共同腳本提交改變而且將這些改變分發到主機上的問題。一般來講,這樣作很值得。將代碼提交到倉庫會給諸如代碼審查、測試、改變歷史以及可重複性帶來巨大的好處。在關鍵時刻,你要考慮的事情越少越好。

理想情況下,一個應用的相關部署細節和應用自己應當存在於同一個源代碼倉庫中。有不少緣由致使現實狀況不是這樣,最突出的緣由是開發人員可能會反對將運營相關的東西放入他們的代碼倉庫中。尤爲對於一個用於部署的bash腳本,這種狀況更可能發生,固然Dockerfile文件自己也常常如此。

這變成了一個文化問題而且只要有可能的話就值得被解決。儘管爲你的部署代碼維持兩個分開的倉庫確實是可行的,可是你將不得不耗費額外的精力保持兩個倉庫的同步。本篇文章固然會努力達到更好的效果,即使實現起來更困難。在ABC,Dockerfiles最開始在一個專門的倉庫中,每一個工程都對應一個文件夾,部署腳本存在於它本身的倉庫中。

圖片描述

Dockerfiles倉庫擁有一個工做副本,保存在Jenkins主機上一個熟知的地址中(就好比是‘/opt/abc/Dockerfiles’)。爲了爲一個應用建立Docker鏡像,Jenkins會搜索Dockerfile的路徑,在運行」docker build「前將Dockerfile和伴隨的腳本複製進來。因爲Dockerfile老是在掌控中,你即可能發現你是否處在Dockerfile超前(或落後)應用配置的狀態,雖然實際中大部分時候都會處在正常狀態。這是來自Jenkins構建邏輯的一段摘錄:

if [ -f docker/Dockerfile ]; then
 docker_dir=Docker
elif [ -f /opt/abc/dockerfiles/$APPLICATION/Dockerfile ]; then
 docker_dir=/opt/abc/dockerfiles/$APPLICATION
else
 echo "No docker files. Can’t continue!"
 exit 1
if
docker build -t $APPLICATION:$VERSION $docker_dir

隨着時間的推移,Dockerfiles以及支持腳本會被遷移到應用程序的源碼倉庫中。因爲Jenkins最開始已經查看了本地的倉庫,pipeline的構建再也不須要任何變化。在遷移了第一個服務後,倉庫的結構大體是這樣的:

圖片描述

咱們使用分離的倉庫時遇到的一個問題是,若是應用源碼或打包邏輯任意一個發生改變,Jenkins就會觸發應用的重建。因爲Dockerfiles倉庫包含了許多項目的代碼,當改變發生時咱們不想觸發全部的倉庫重建。解決方法是:使用在Jenkins Git插件中一個很隱蔽的選項,叫作Included Regions。當配置完成後,Jenkins將一個變化引發的重建隔離在倉庫的某個特定子集裏面。這容許咱們將全部的Dockerfiles放在一個倉庫裏,而且仍然能作到當一個改變發生時只會觸發特定的構建(與當改變發生在倉庫裏特定的目錄時構建全部的鏡像相比)。

圖片描述

關於這個初步的工做流的另外一個方面是部署工程師必須在部署前強制構建一個應用鏡像。這將致使額外的延遲,尤爲是構建存在問題而且開發人員須要參與其中的時候。爲了減小這種延遲,併爲更加持續的部署鋪平道路,咱們開始爲熟知分支中的每個提交構建Docker鏡像。這要求每個鏡像有一個獨一無二的版本標識符,而若是咱們僅僅依賴官方的應用版本字符串每每不能知足這一點。最終,咱們使用官方版本字符串、提交次數和提交sha碼的組合做爲版本標識符。

commit_count=$(git rev-list --count HEAD)
commit_short=$(git rev-parse --short HEAD)
version_string="${version}-${commit_count}-${commit_short}"

這樣獲得的版本字符串看起來是這樣的:1.0.1-22-7e56158

在結束pipeline的Docker file部分的討論以前,還有一些參數值得說起。若是咱們不會在生產中操做大量的容器,咱們不多用到這些參數。可是,它們被證實有助於咱們維護Docker集羣的線上運行。

  • 重啓策略(Restart Policy)-一個重啓策略容許你指定當一個容器退出時,每一個容器採起什麼動做。儘管這個能夠被用做應用錯誤(application panic)時的恢復或當依賴上線時保持容器再次嘗試鏈接,但對運營人員來講真正的好處是在Docker守護進程(daemon)或者主機重啓後的自動恢復。從長遠來看,你將但願實現一個適當的調度程序(scheduler),它可以在新主機上重啓失敗的容器。在那天到來以前,節省一些工做,設置一個重啓策略吧。在現階段的ABC中,咱們將這項參數默認爲「–restart always」,這將會使容器始終重啓。簡單地擁有一個重啓策略就會使計劃的(和非計劃的)主機重啓變得輕鬆得多。

  • 資源約束(Resource Constraints)-使用運行時的資源約束,你能夠設置容器容許消耗的最大內存和CPU。它不會把你從通常的主機過載(over-subscription)中拯救出來,可是它能夠抑制住內存泄漏和失控的容器。咱們先對容器應用一個充足的內存限制(例如:–memory=」8g」) 。咱們知道當內存增加時這樣會產生問題。儘管擁有一個硬性限制意味着應用最終會達到內存不足(Out-of-Memory)的狀態併產生錯誤(panic),可是主機和其它容器會保持正確運行。

結合重啓策略和資源約束會給你的集羣帶來更好的穩定性,與此同時最小化失敗的影響,縮短恢復的時間。這種類型的安全防禦可讓你和開發人員一塊兒專一於「起火」的根本緣由,而不是忙於應付不斷擴大的火勢。

簡而言之,咱們從一個基礎的構建pipeline,即從咱們的源碼倉庫中建立被標記的Docker鏡像開始。從使用Docker CLI部署容器一路到使用腳本和代碼中定義的參數部署容器。咱們也涉及瞭如何管理咱們的部署代碼,而且強調了幾個幫助運營人員保持服務上線和運行的Docker參數。

此時此刻,在咱們的構建pipeline和部署步驟之間仍然存在空缺。部署工程師會經過登入一個服務器並運行部署腳本的方法填補這個空缺。

儘管較咱們剛開始時有所改進,但仍然有進一步提升自動化水平的空間。全部的部署邏輯都集中在單一的腳本內,當開發者須要安裝腳本以及應付它的複雜性時,會使本地測試會變得困可貴多。此時此刻,咱們的部署腳本也包含了經過環境變量處理任何環境特定信息的方法。追蹤一個服務設置的環境變量以及增長新的環境變量是乏味且容易出錯的。

在下一篇文章中,咱們將看一看怎樣經過解構(deconstructing)共同的包裝腳本解決這些痛點,並使部署邏輯向使用Docker Compose的應用更近一步。

您也能夠下載免費的電子書《Continuous Integration and Deployment with Docker and Rancher》,這本書講解了如何利用容器幫助你完成整個CI/CD過程。

相關文章
相關標籤/搜索