1. 程式人生 > >Docker學習總結(8)——利用Docker開啟持續交付之路

Docker學習總結(8)——利用Docker開啟持續交付之路

持續交付即Continuous Delivery,簡稱CD,隨著DevOps的流行正越來越被傳統企業所重視。持續交付講求以短週期、小細粒度,自動化的方式頻繁的交付軟體,在這個過 程中要求開發、測試、使用者體驗等角色緊密合作,快速收集反饋,從而不斷改善軟體質量並減少浪費。然而,在我所接觸的傳統企業中,對於持續交付實踐的實施都 還非常初級,坦白說,大部分還停留的手工生成釋出包,手工替換檔案進行部署的階段,這樣做無疑缺乏管理且容易出錯。如果究其原因,我想主要是因為構建一個 可實際執行且適合企業自身環境的持續釋出流程並不簡單。然而,Docker作為輕量級的基於容器的解決方案,它對系統侵入性低,容易移植,天生就適合做自 動化部署,這些特性非常有助於降低構建持續交付流程的複雜度。本文將通過一個實際案例分享我們在一個真實專案中就如何使用Docker構建持續釋出流程的 經驗總結,這些實踐也許不是最先進的,但確是非常實際和符合當時環境的。

專案背景

我們的客戶來自物流行業,由於近幾年業務的飛速發展,其老的入口網站對於日常訪問和訂單查詢還勉強可以支撐,但每當遇到像雙十一這樣訪問量成倍增長的情況就很難招架了。因此,客戶希望我們幫助他們開發一個全新的入口網站。

新網站採用了動靜分離的策略,使用Java語言,基於REST架構,並結合CMS系統。簡單來說,可以把它看成是時下非常典型的一個基於Java的Web應用,它具體包含如下幾個部分:

  • 基於Jersey的動態服務(處理客戶端的動態請求)
  • 二次開發的OpenCMS系統,用於靜態匯出站點
  • 基於js的前端應用並可以打包成為一個OpenCMS支援的站點
  • 後臺任務處理服務(用於處理實時性要求不高的任務,如:郵件傳送等)

以下是系統的邏輯軟體架構圖:

利用Docker開啟持續交付之路

面臨的挑戰以及為什麼選擇Docker

在設計持續交付流程的過程中,客戶有一個非常合理的需求:是否可以在測試環境中儘量模擬真實軟體架構(例如:模擬靜態伺服器的水平擴充套件),以便儘早 發現潛在問題?基於這個需求,可以嘗試將多臺機器劃分不同的職責並將相應服務按照職責進行部署。然而,我們遇到的第一個挑戰是:硬體資源嚴重不足盡 管客戶非常積極的配合,但無奈於企業內部層層的審批制度。經過兩個星期的努力,我們很艱難的申請到了兩臺四核CPU加8G記憶體的物理機(如果申請虛擬機器可 能還要等一段時間),同時還獲得了一個Oracle資料庫例項。因此,最終我們的任務就變為把所有服務外加持續整合伺服器(Jenkins)全部部署在這 兩臺機器上,並且,還要模擬出這些服務真的像是分別執行在不同職責的機器上並進行互動。如果採用傳統的部署方式,要在兩臺機器上完成這麼多服務的部署是非 常困難的,需要小心的調整和修改各個服務以及中介軟體的配置,而且還面臨著一旦出錯就有可能耗費大量時間排錯甚至需要重灌系統的風險。第二個挑戰是:企業內 部對UAT(與產品環境配置一致,只是資料不同)和產品環境管控嚴格,我們無法訪問,也就無法自動化。這就意味著,整個持續釋出流程不僅要支援自動化部 署,同時也要允許下載獨立釋出包進行手工部署。

最終,我們選擇了Docker解決上述兩個挑戰,主要原因如下:

  • Docker是容器,容器和容器之間相互隔離互不影響,利用這個特性就可以非常容易在一臺機器上模擬出多臺機器的效果
  • Docker對作業系統的侵入性很低,因其使用LXC虛擬化技術(Linux核心從2.6.24開始支援),所以在大部分Linux發行版下不需要安裝額外的軟體就可執行。那麼,安裝一臺機器也就變為安裝Linux作業系統並安裝Docker,接著它就可以服役了
  • Docker容器可重複運,且Docker本身提供了多種途徑分享容器,例如:通過export/import或者save/load命令以檔案的形式分享,也可以通過將容器提交至私有Registry進行分享,另外,別忘了還有Docker Hub

下圖是我們利用Docker設計的持續釋出流程:

利用Docker開啟持續交付之路

圖中,我們專門設計了一個環節用於生成唯一發布包,它打包所有War/Jar、資料庫遷移指令碼以及配置資訊。因此,無論是手工部署還是利用 Docker容器自動化部署,我們都使用相同的釋出包,這樣做也滿足持續交付的單一製品原則(Single Source Of Truth,Single Artifact)。

Docker與持續整合

持續整合(以下簡稱CI)可以說是當前軟體開發的標準配置,重複使用率極高。而將CI與Docker結合後,會為CI的靈活性帶來顯著的提升。由於我們專案中使用Jenkins,下面會以Jenkins與Dcoker結合為例進行說明。

1.建立Jenkins容器

相比於直接把Jenkins安裝到主機上,我們選擇把它做為Docker容器單獨使用,這樣就省去了每次安裝Jenkins本身及其依賴的過程,真正做到了拿來就可以使用。

Jenkins容器使建立一個全新的CI變的非常簡單,只需一行命令就可完成:

docker run -d -p 9090:8080 ——name jenkins jenkins:1.576

該命令啟動Jenkins容器並將容器內部8080埠重定向到主機9090埠,此時訪問:主機IP:9090,就可以得到一個正在執行的Jenkins服務了。

為了降低升級和維護的成本,可將構建Jenkins容器的所有操作寫入Dockerfile並用版本工具管理,如若需要升級Jenkins,只要重新build一次Dockerfile:

FROM ubuntu
ADD sources.list /etc/apt/sources.list
RUN apt-get update && apt-get install -y -q wget
RUN wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | apt-key add -
ADD jenkins.list /etc/apt/sources.list.d/
RUN apt-get update
RUN apt-get install -y -q jenkins
ENV JENKINS_HOME /var/lib/jenkins/
EXPOSE 8080
CMD ["java", "-jar", "/usr/share/jenkins/jenkins.war"]

每次build時標註一個新的tag:

docker build -t jenkins:1.578 —rm .

另外,建議使用Docker volume功能將外部目錄掛載到JENKINS_HOME目錄(Jenkins會將安裝的外掛等檔案存放在這個目錄),這樣保證了升級Jenkins容 器後已安裝的外掛都還存在。例如:將主機/usr/local/jenkins/home目錄掛載到容器內部/var/lib/jenkins:

docker run -d -p 9090:8080 -v /usr/local/jenkins/home:/var/lib/jenkins ——name jenkins jenkins:1.578

2. 使用Docker容器作為Jenkins容器的Slave

在使用Jenkins容器時,我們有一個原則:不要在容器內部存放任何和專案相關的資料。因為執行中的容器不一定是穩定的,而Docker本身也可能有Bug,如果把專案資料存放在容器中,一旦出了問題,就有丟掉所有資料的風險。因此,我們建議Jenkins容器僅負責提供Jenkins服務而不負責構建,而是把構建工作代理給其他Docker容器做。

例如,為了構建Java專案,需要建立一個包含JDK及其構建工具的容器。依然使用Dockerfile構建該容器,以下是示例程式碼(可根據專案實際需要安裝其他工具,比如:Gradle等):

FROM ubuntu
RUN apt-get update && apt-get install -y -q openssh-server openjdk-7-jdk
RUN mkdir -p /var/run/sshd
RUN echo 'root:change' |chpasswd
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

在這裡安裝openssh-server的原因是Jenkins需要使用ssh的方式訪問和操作Slave,因此,ssh應作為每一個Slave必須安裝的服務。執行該容器:

docker run -d -P —name java java:1.7

其中,-P是讓Docker為容器內部的22埠自動分配重定向到主機的埠,這時如果執行命令:

docker ps
804b1d9e4202       java:1.7           /usr/sbin/sshd -D     6 minutes ago       Up 6 minutes       0.0.0.0:49153->22/tcp   java

埠22被重定向到了49153埠。這樣,Jenkins就可以通過ssh直接操作該容器了(在Jenkins的Manage Nodes中配置該Slave)。

有了包含構建Java專案的Slave容器後,我們依然要遵循容器中不能存放專案相關資料的原則。此時,又需要藉助volume:

docker run -d -v /usr/local/jenkins/workspace:/usr/local/jenkins -P —name java java:1.7

這樣,我們在Jenkins Slave中配置的Job、Workspace以及下載的原始碼都會被放置到主機目錄/usr/local/jenkins/workspace下,最終達成了不在容器中放置任何專案資料的目標。

通過上面的實踐,我們成功的將一個Docker容器配置成了Jenkins的Slave。相比直接將Jenkins安裝到主機上的方式,Jenkins容器的解決方案帶來了明顯的好處:

  • 重用更加簡單,只需一行命令就可獲得CI的服務;
  • 升級和維護也變的容易,只需要重新構建Jenkins容器即可;
  • 靈活配置Slave的能力,並可根據企業內部需要預先定製具有不同能力的Slave,比如:可以創建出具有構建Ruby On Rails能力的Slave,可以創建出具有構建NodeJS能力的Slave。當Jenkisn需要具備某種能力的Slave時,只需要docker run將該容器啟動,並配置為Slave,Jenkins就立刻擁有了構建該應用的能力。

如果一個組織內部專案繁多且技術棧複雜,那麼採用Jenkins結合Docker的方案會簡化很多配置工作,同時也帶來了相率的提升。

Docker與自動化部署

說到自動化部署,通常不僅僅代表以自動化的方式把某個應用放置在它應該在的位置,這只是基本功能,除此之外它還有更為重要的意義:

  • 以快速且低成本的部署方式驗證應用是否在目標環境中可執行(通常有TEST/UAT/PROD等環境);
  • 以不同的自動化部署策略滿足業務需求(例如:藍綠部署);
  • 降低了運維的成本並促使開發和運維人員以端到端的方式思考軟體開發(DevOps)。

在我們的案例中,由於上述挑戰二的存在,導致無法將UAT乃至產品環境的部署全部自動化。回想客戶希望驗證軟體架構的需求,我們的策略是:儘量使測試環境靠近產品環境。

  1. 標準化Docker映象

很多企業內部都存在一套叫做標準化的規範,在這套規範中定義了開發中所使用的語言、工具的版本資訊等等,這樣做可以統一開發環境並降低運維團隊負擔。在我們的專案上,依據客戶提供的標準化規範,我們建立了一系列容器並把它們按照不同的職能進行了分組,如下圖:

利用Docker開啟持續交付之路

圖中,我們把Docker映象分為三層:基礎映象層、服務映象層以及應用映象層,下層映象的構建依賴上層映象,越靠上層的映象越穩定越不容易變。

基礎映象層

  • 負責配置最基本的、所有映象都需要的軟體及服務,例如上文提到的openssh-server

服務映象層

  • 負責構建符合企業標準化規範的映象,這一層很像SaaS

應用映象層

  • 和應用程式直接相關,CI的產出物

分層後, 由於上層映象已經提供了應用所需要的全部軟體和服務,因此可以顯著加快應用層映象構建的速度。曾經有人擔心如果在CI中構建映象會不會太慢?經過這樣的分層就可以解決這個問題。

在Dockerfile中使用FROM命令可以幫助構建分層映象。例如:依據標準化規範,客戶的產品環境執行RHEL6.3,因此在測試環境中,我 們選擇了centos6.3來作為所有映象的基礎作業系統。這裡給出從構建base映象到Java映象的方法。首先是定義base映象的 Dockerfile:

FROM centos
# 可以在這裡定義使用企業內部自己的源
RUN yum install -y -q unzip openssh-server
RUN ssh-keygen -q -N "" -t dsa -f /etc/ssh/ssh_host_dsa_key && ssh-keygen -q -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key
RUN echo 'root:changeme' | chpasswd
RUN sed -i "s/#UsePrivilegeSeparation.*/UsePrivilegeSeparation no/g" /etc/ssh/sshd_config \
&& sed -i "s/UsePAM.*/UsePAM no/g" /etc/ssh/sshd_config
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

接著,構建服務層基礎映象Java,依據客戶的標準化規範,Java的版本為:jdk-6u38-linux-x64:

FROM base
ADD jdk-6u38-linux-x64-rpm.bin /var/local/
RUN chmod +x /var/local/jdk-6u38-linux-x64-rpm.bin
RUN yes | /var/local/jdk-6u38-linux-x64-rpm.bin &>/dev/null
ENV JAVA_HOME /usr/java/jdk1.6.0_38
RUN rm -rf var/local/*.bin
CMD ["/usr/sbin/sshd", "-D"]

如果再需要構建JBoss映象,就只需要將JBoss安裝到Java映象即可:

FROM java
ADD jboss-4.3-201307.zip /app/
RUN unzip /app/jboss-4.3-201307.zip -d /app/ &>/dev/null && rm -rf /app/jboss-4.3-201307.zip
ENV JBOSS_HOME /app/jboss/jboss-as
EXPOSE 8080
CMD ["/app/jboss/jboss-as/bin/run.sh", "-b", "0.0.0.0"]

這樣,所有使用JBoss的應用程式都保證了使用與標準化規範定義一致的Java版本以及JBoss版本,從而使測試環境靠近了產品環境。

  1. 更好的組織自動化釋出指令碼

為了更好的組織自動化釋出指令碼,版本化控制是必須的。我們在專案中單獨建立了一個目錄:deploy,在這個目錄下存放所有與釋出相關的檔案,包括:用於自動化釋出的指令碼(shell),用於構建映象的Dockerfile,與環境相關的配置檔案等等,其目錄結構是:

├── README.md
├── artifacts   # war/jar,資料庫遷移指令碼等
├── bin         # shell指令碼,用於自動化構建映象和部署
├── images       # 所有映象的Dockerfile
├── regions     # 環境相關的配置資訊,我們只包含本地環境及測試環境
└── roles       # 角色化部署指令碼,會本bin中指令碼呼叫

這樣,當需要向某一臺機器上安裝java和jboss映象時,只需要這樣一條命令:

bin/install.sh images -p 10.1.2.15 java jboss

而在部署的過程中,我們採用了角色化部署的方式,在roles目錄下,它是這樣的:

├── nginx
│   └── deploy.sh
├── opencms
│   └── deploy.sh
├── service-backend
│   └── deploy.sh
├── service-web
│   └── deploy.sh
└── utils.sh

這裡我們定義了四種角色:nginx,opencms,service-backend以及service-web。每個角色下都有自己的釋出指令碼。例如:當需要釋出service-web時,可以執行命令:

bin/deploy.sh -e test -p 10.1.2.15 service-web

該指令碼會載入由-e指定的test環境的配置資訊,並將service-web部署至IP地址為10.1.2.15的機器上,而最終,bin/deploy.sh會呼叫每個角色下的deploy.sh指令碼。

角色化後,使部署變的更為清晰明瞭,而每個角色單獨的deploy指令碼更有利於劃分責任避免和其他角色的干擾。

  1. 構建本地虛擬化環境

通常在聊到自動化部署指令碼時,大家都樂於說這些指令碼如何簡化工作增加效率,但是,其編寫過程通常都是痛苦和耗時,需要把指令碼放在相應的環境中反覆執 行來驗證是否工作正常。這就是我為什麼建議最好首先構建一個本地虛擬化環境,有了它,就可以在自己的機器上反覆測試而不受網路和環境的影響。

Vagrant(http://www.vagrantup.com/)是很好的本地虛擬化工具,和Docker結合可以很容易的在本地搭建起與測試環境幾乎相同的環境。以我們的專案為例,可以使用Vagrant模擬兩臺機器,以下是Vagrantfile示例:

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.define "server1", primary: true do |server1|
server1.vm.box = "raring-docker"
server1.vm.network :private_network, ip: "10.1.2.15"
end
config.vm.define "server2" do |server2|
server2.vm.box = "raring-docker"
server2.vm.network :private_network, ip: "10.1.2.16"
end
end

由於部署指令碼通常採用SSH當方式連線,所以,完全可以把這兩臺虛擬機器看做是網路中兩臺機器,呼叫部署指令碼驗證是否正確。限於篇幅,這裡就不多說了。

4 構建企業內部的Docker Registry

上文提到了諸多分層映象,如何管理這些映象?如何更好的分享?答案就是使用Docker Registry。Docker Registry是一個映象倉庫,它允許你向Registry中提交(push)映象同時又可以從中下載(pull)。

構建本地的Registry非常簡單,執行下面的命令:

docker run -p 5000:5000 registry

更多關於如何使用Registry可以參考地址:https://github.com/docker/docker-registry

當搭建好Registry後,就可以向它push你的映象了,例如:需要將base映象提交至Registry:

docker push your_registry_ip:5000/base:centos

而提交Java和JBoss也相似:

docker push your_registry_ip:5000/java:1.6
docker push your_registry_ip:5000/jboss:4.3

使用下面的方式下載映象:

docker pull your_registry_ip:5000/jboss:4.3

總結

本文總結我們在實際案例中使用Docker一些實踐,它給我們的印象就是非常靈活,幾乎是一個多面手,給整個流程帶來了極大的靈活性和擴充套件性,並且也展現了極好的效能,符合它天生就為部署而生的特質。