持續集成案例學習:Docker、Java與Maven
對于使用Java技術棧的企業,Maven往往是其持續集成的核心工具,在當前的Docker化的運動中,要如何把Docker鏡像的構建也加入到傳統的 Maven構建為基礎的持續集成流程中呢?Alooma公司在本文中分享了他們使用Maven對Docker鏡像構建進行持續集成的經驗。
在Alooma,我們非常非常非常喜愛Docker.
這是真的。 我們試著盡可能的在Docker容器內部運行我們的應用。 雖然在容器中打包模塊有大量的好處,我們在這里并不是要說服你用Docker。相反的是,我們將僅僅假定你像我們一樣熱愛Docker。
接下來,讓我們談談Alooma是如何在生產環境使用Docker精簡開發流程和快速push代碼的。
概述
Docker允許你把你的基礎架構當作代碼一樣來對待。這個代碼就是你的Dockerfile。像其它代碼一樣,我們想要使用一個緊密的改變->提交->構建->測試的周期(一個完整的持續集成解決方案)。為了實現這個目標,我們需要構建一個流暢的DevOps流水線。
讓我們把目標分解為更加詳細的需求:
- 在版本控制系統中管理Dockerfile
- 在CI服務器上為每個commit構建Docker鏡像
- 上傳構件并打標簽(這個構件要能夠簡單的部署) </ul>
- GitHub將repo的每一個push通知給Jenkins
- Jenkins觸發一個Maven build
- Maven 構建所有的東西,包括Docker鏡像
- 最后,Maven會把鏡像推送到私有的Docker Registry。 </ol>
- 讓你的基礎的機器鏡像(在EC2的例子下是AMI)包含一些你的Docker鏡像的基礎版本。這樣會使得docker pull只去pull那些改變了的層,即增量(相對于整個鏡像來說要小得多)。
- 在Docker Registry的前端放一個Redis緩存。這可以緩存標簽和元數據,減少和真實存儲(在我們的例子下是S3)的回環。 </ul>
我們的工作流
我們的DevOps流水線圍繞GitHub、Jenkins和Maven構建。下面是它的工作流程:這個工作流的好處是它允許我們能夠很容易的為每個發布版本打標簽(所有的commit都被構建并且在我們的Docker Registry中準備好了)。然后我們可以非常容易地通過pull和run這些Docker鏡像進行部署。
事實上這個部署過程是非常簡單的,我們通過發送一個命令給我們信任的Slack機器人:"Aloominion"(關于我們的機器人朋友的更多情況將在未來的文章中發表)開始這個過程。
你可能對這個工作流中的其他元素非常熟悉,因為它們都很常見。所以,讓我們來深入了解如何使用Maven構建Docker鏡像。
深入Docker 構建
Alooma是一個Java公司。我們已經使用Maven作為我們構建流水線的中心工具,所以很自然的想到把構建Docker的過程也加入到我們的Maven構建過程中去。當搜索和Docker交互的Maven插件時,出現了3個選項。我們選擇使用Spotify的maven-docker-plugin —— 雖然rhus的和alexec的同名插件看起來也是一個不錯的選擇。
另一個我們的構建計劃依賴的Maven插件是maven-git-commit-id-plugin。我們使用這個插件,所以我們的Docker鏡像能使用git的commit ID來打標簽 —— 這在部署過程中非常有幫助,我們可以了解運行的是哪個版本。
給我看代碼!
每一個docker鏡像有它自己的Maven模塊(所有上面提到的docker-maven 插件在一個模塊一個Dockerfile時都能順利地工作)讓我們從Spotify插件的一個簡單配置開始:
<plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.basedir}</dockerDirectory> <imageName>alooma/${project.artifactId}</imageName> </configuration> </plugin>
我們看到這里我們把插件的build目標和Maven的package階段綁定,我們也指導它去在我們模塊的根目錄下來尋找Dockerfile(使用dockerDirectory 元素來指定),我們還把鏡像名稱用它的構件Id來命名(用"alloma/"做前綴)。
我們注意到的第一件事情是這個鏡像沒有被push到任何地方,我們可以通過加入<pushImage>true</pushImage>到配置中來解決這個問題。
但是現在這個鏡像會被push到默認的Docker Hub Registry上。糟糕。
為了解決這個問題,我們定義了一個新的Maven屬性<docker.registry>docker-registry.alooma.io:5000/</docker.registry>并且把鏡像名稱imageName改為${docker.registry}alooma/${project.artifactId}。 你可能會想,“為什么需要為Docker Registry設置一個屬性?”, 你是對的!但是有這個屬性可以使我們在Regsitry URL改變的時候能夠更方便的修改。
有一個更重要的事情我們還沒有處理——我們想讓每一個鏡像用它的git commit ID來打標簽。這可以通過改變imageName為${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev}來實現。
${git.commit.id.abbrev}屬性是通過我上面提到的maven-git-commit-id-plugin插件來實現的。
所以,現在我們的插件配置看起來像下面這樣:
<plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.basedir}</dockerDirectory> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> <pushImage>true</pushImage> </configuration> </plugin>
我們的下一個挑戰是在我們的pom.xml中表達我們的Dockerfile的依賴。一些我們的Docker鏡像在構建時使用了FROM其它的Docker 鏡像作為基礎鏡像(也在同一個構建周期中構建)。例如,我們的webgate鏡像(是我們的機遇Tomcat的WebApp)基于我們的base鏡像(包含Java 8、更新到最新的 apt-get、等等)。
這些鏡像在同一個構建過程中構建意味著我們不能簡單的使用FROM docker-registry.alooma.io/alooma/base:some-tag因為我們需要這個標簽編程當前構建的標簽(即 git commit ID)。
為了在Dockerfile中獲得這些屬性,我們使用了Maven的resource filtering功能。這在一個資源文件中替換Maven 的屬性。
<resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource>
在Dockerfile的內部我們有一個這樣的FROM:
FROM ${docker.registry}alooma/base:${git.commit.id.abbrevs}
一些更多的事情.......我們需要的是我們的配置來找到正確的Dockerfile(過濾過之后的),這可以在target/classes文件夾內找到,所以我們把dockerDirectory改為${project.build.directory}/classes。
這意味著現在我們的配置文件長這樣:
<resources> <resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.build.directory}/classes</dockerDirectory> <pushImage>true</pushImage> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> </configuration> </plugin> </plugins> </pluginManagement>
此外,我們還要添加base構件作為webgate模塊的一個Maven依賴來保證正確的Maven構建順序。
但是我們還有另一個挑戰:我們如何把我們編譯和打包了的源文件添加到我們的Docker鏡像中呢?我們的Dockerfile依賴于很多其它文件,它們通過ADD或COPY命令插入。(你可以在這里讀到更多的關于Dockerfile的指導。)
為了讓這些文件可以被獲取,我們需要使用插件配置的resources標簽。
<resources> <resource> <targetPath>/</targetPath> <directory>${project.basedir}</directory> <excludes> <exclude>target/**/*</exclude> <exclude>pom.xml</exclude> <exclude>*.iml</exclude> </excludes> </resource> </resources>
注意到我們排除了一些文件。
記住這個resources標簽不應該和通常的Mavenresources標簽弄混,看看下面的例子,它來自于我們的pom.xml的一部分:
<resources> <!-- general Maven resources --> <resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.build.directory}/classes</dockerDirectory> <pushImage>true</pushImage> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> <resources> <!-- Dockerfile building resources --> <resource> <targetPath>/</targetPath> <directory>${project.basedir}</directory> <excludes> <exclude>target/**/*</exclude> <exclude>pom.xml</exclude> <exclude>*.iml</exclude> </excludes> </resource> </resources> </configuration> </plugin> </plugins> </pluginManagement>
前一個添加在我們想添加一些靜態資源到鏡像時工作,但是如果我們想要添加一個在同一個構建中構建的構件時需要更多的調整。
例如,我們的webgateDocker鏡像包含了我們的webgate.war,這是由另一個模塊構建的。
為了添加這個war作為資源,我們首先必須把它作為我們的Maven依賴加進來,然后使用maven-dependency-plugin插件的copy目標來把它加到我們當前的構建目錄中。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <goals> <goal>copy</goal> </goals> <configuration> <artifactItems> <artifactItem> <groupId>com.alooma</groupId> <artifactId>webgate</artifactId> <version>${project.parent.version}</version> <type>war</type> <outputDirectory>${project.build.directory}</outputDirectory> <destFileName>webgate.war</destFileName> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin>
現在這允許我們簡單的把這個文件加到Docker插件的resources中去。
<resources> <resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.build.directory}/classes</dockerDirectory> <pushImage>true</pushImage> <imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> <resources> <resource> <targetPath>/</targetPath> <directory>${project.basedir}</directory> <excludes> <exclude>target/**/*</exclude> <exclude>pom.xml</exclude> <exclude>*.iml</exclude> </excludes> </resource> <rescource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>webgate.war</include> </rescource> </resources> </configuration> </plugin> </plugins> </pluginManagement>
我們需要做的最后一件事情是讓我們的CI服務器(Jenkins)真的將鏡像push到Docker Registry上。請記住本地構件默認是不會push鏡像的。
為了push這些鏡像,我們改變我們的<pushImage>標簽的值從true變為${push.image}屬性,這默認是被設置為false,并且只會在CI服務器上設置為true。(譯注:這里的意思是,由于開發人員也要在本地構建然后測試之后才會提交,而測試的鏡像不應該被提交到Registry,所以<pushImage>應該使用一個屬性,默認為false,在CI服務器上覆蓋為true在構建后去push鏡像。)
這就完成了!讓我們看一下最終的代碼:
<resources> <resource> <directory>${project.basedir}</directory> <filtering>true</filtering> <includes> <include>**/Dockerfile</include> </includes> </resource> </resources> <pluginManagement> <plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <dockerDirectory>${project.build.directory}/classes</dockerDirectory><pushImage>${push.image}</pushImage> <!-- true when Jenkins builds, false otherwise -->
<imageName> ${docker.registry}alooma/${project.artifactId}:${git.commit.id.abbrev} </imageName> <resources> <resource> <targetPath>/</targetPath> <directory>${project.basedir}</directory> <excludes> <exclude>target/*/</exclude> <exclude>pom.xml</exclude> <exclude>*.iml</exclude> </excludes> </resource> <rescource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>webgate.war</include> </rescource> </resources> </configuration> </plugin> </plugins> </pluginManagement></pre>
性能
這個過程有兩個能夠提高你的構建和部署的性能的改進地方:
我們現在已經使用這個構建過程一段時間了,并且對它非常滿意。然而仍然有提高的空間,如果你有任何關于讓這個過程更加流暢的建議,我很樂意在評論中聽到你的想法。
原文鏈接:Continuous Integration for Dockers: Case study (翻譯:陳光) 來自: