1. 程式人生 > >學習使用Docker、Docker-Compose和Rancher搭建部署Pipeline(一)

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

docker 部署 rancher 雲 微服務 說明

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

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

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

為了達到該目標,軟件的部署計劃大致是這樣的:


技術分享


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

要完成這個過程,如下幾點是必須要有的:


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

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

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

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


這樣去看的話,這個過程表面上簡單,然而實際過程中會復雜一些。像許多其它公司一樣,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過程。


原文來源:Rancher Labs

本文出自 “12452495” 博客,請務必保留此出處http://12462495.blog.51cto.com/12452495/1933287

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