利用Docker開啟持續交付之路

jopen 9年前發布 | 26K 次閱讀 Docker

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

項目背景

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

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

  • 基于Jersey的動態服務(處理客戶端的動態請求)
  • 二次開發的OpenCMS系統,用于靜態導出站點
  • 基于js的前端應用并可以打包成為一個OpenCMS支持的站點
  • 后臺任務處理服務(用于處理實時性要求不高的任務,如:郵件發送等)
  • </ul>

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

    利用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
    • </ul>

      下圖是我們利用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就立刻擁有了構建該應用的能力。
      • </ul>

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

        Docker與自動化部署

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

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

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

          1. 標準化Docker鏡像
          2. </ol>

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

            利用Docker開啟持續交付之路

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

            基礎鏡像層

            • 負責配置最基本的、所有鏡像都需要的軟件及服務,例如上文提到的openssh-server
            • </ul>

              服務鏡像層

              • 負責構建符合企業標準化規范的鏡像,這一層很像SaaS
              • </ul>

                應用鏡像層

                • 和應用程序直接相關,CI的產出物
                • </ul>

                  分層后, 由于上層鏡像已經提供了應用所需要的全部軟件和服務,因此可以顯著加快應用層鏡像構建的速度。曾經有人擔心如果在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"]</pre>

                  接著,構建服務層基礎鏡像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. 更好的組織自動化發布腳本
                  2. </ol>

                    為了更好的組織自動化發布腳本,版本化控制是必須的。我們在項目中單獨創建了一個目錄: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. 構建本地虛擬化環境
                    2. </ol>

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

                      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一些實踐,它給我們的印象就是非常靈活,幾乎是一個多面手,給整個流程帶來了極大的靈活性和擴展性,并且也展現了極好的性能,符合它天生就為部署而生的特質。

                      來自:http://insights.thoughtworkers.org/start-continuous-delivery-with-docker/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!