容器已經遍地開花🐒。即使你還沒有認定 Kubernetes 纔是將來之選,單爲 Docker 自身添枝加葉也很是容易。容器如今能夠同時簡化部署和 CI/CD 管道 (thenewstack.io/docker-base…)。java
官方的 Docker 最佳實踐 (docs.docker.com/develop/dev…) 頁面高度技術化而且更多地聚焦於 Dockerfile 的結構而非一般如何使用容器的基本信息。每一個 Docker 新手都早晚會理解 Docker 層的使用、它們如何被緩存,以及如何建立更小的 Docker 鏡像, 多階段構建 也算不上造火箭,Dockerfiles 的語法也至關易於理解。node
可是,使用容器的主要問題是企業沒法在更大的圖景上審視它,特別是容器/鏡像不可改變的角色問題。尤爲是不少企業試圖將其既有的基於虛擬機的生產過程轉化爲容器,從而形成了有問題的結果。有太多關於容器的低層級細節(如何建立並運行它們),高層級的最佳實踐卻太少。mysql
爲了縮小文檔的缺失,我爲你呈上一份高層級 Docker 最佳實踐的清單。鑑於沒法覆蓋全部企業的內部流程,我會轉而說明壞的實踐(也就是你不該該作的),希望這會給你一些應該如何使用容器的啓示。linux
這裏就是咱們將要考察的不良實踐的完整清單:git
在見識更多實際例子以前,先明確一個基本原則:容器不是虛擬機。乍一看,它們行爲相似,但實際上徹底不一樣。spring
網上有不少諸如「如何升級容器內的應用?」、「如何 ssh 到一個 Docker 容器中?」、「如何從容器中取得日誌?」、「如何在一個容器中運行多個程序?」之類的問題,從技術上講這些問題及相關的解答是行得通的,但全部這些問題都是典型的「XY 問題」(自覺得是的、非根本的問題)。這些提問背後的真正問題實際上是:sql
如何將可變、長運行、有狀態的 VM 實踐,改變爲 不可變、短週期、無狀態 的容器工做流呢?docker
許多企業試圖在容器世界中重用源自虛擬機的相同的實踐/工具/知識。一些企業甚至對他們在容器出現後都還還沒有完成從裸金屬到虛擬機的遷移渾然不知。數據庫
改變積習很是困難。大多數開始使用容器的人起初將之視爲他們既有實踐的一個額外的新抽象層:
實際上,容器須要一種徹底不一樣的視角,並改變現有的流程。你須要從新思考 全部 CI/CD 過程以適應容器。
相比於讀懂容器的本質、弄懂其構建模塊以及其歷史(了不得的 chroot 命令),對於這種反模式沒有更容易的解決之道。
若是你老是發現本身想要打開 ssh 會話運行容器以「更新」它們或是從外部手動取得日誌/文件的話,那你確定就是在使用 Docker 上走了歪路,須要格外地閱讀一些容器如何工做的內容了。
一個 Dockerfile 應該是透明且自包含的。它應該顯而易見地描述應用的全部組件。任何人都應該可以取得相同的 Dockerfile 並從新建立出相同的鏡像。從外部庫中下載(以版本化且控制良好的方式) Dockerfile 是 ok 的,但建立那種能執行「神奇」步驟的 Dockerfile 應被避免。
這就是個倍兒壞的例子:
FROM alpine:3.4
RUN apk add --no-cache \
ca-certificates \
pciutils \
ruby \
ruby-irb \
ruby-rdoc \
&& \
echo http://dl-4.alpinelinux.org/alpine/edge/community/ >> /etc/apk/repositories && \
apk add --no-cache shadow && \
gem install puppet:"5.5.1" facter:"2.5.1" && \
/usr/bin/puppet module install puppetlabs-apk
# Install Java application
RUN /usr/bin/puppet agent --onetime --no-daemonize
ENTRYPOINT ["java","-jar","/app/spring-boot-application.jar"]
複製代碼
先別誤會,我喜好 puppet 這個棒棒的工具(或是 Ansible、Chef 等相似的)。在虛擬機中濫用它部署應用可能還湊合,但對於容器就是災難性的了。
首先,這使得該 Dockerfile 依賴於所處的位置。你不得不將其構建在一臺能訪問到生產環境 puppet 服務器的的機器上。你的工做站知足條件嗎?若是是的話,那麼你的工做站真的應該能訪問到生產環境的 puppet 服務器嗎?
但最大的問題是這個 Docker 鏡像不能被輕易地從新建立。其內容依賴於當初始化構建之時 puppet 服務器上有什麼。若是在一天以內再次構建相同的 Dockerfile 則有可能獲得全然不一樣的鏡像。還有若是你沒法訪問 puppet 服務器或 puppet 服務器宕機了,你甚至根本都無法構建出鏡像。若是沒法訪問到 puppet 腳本,甚至也不知道應用的版本。
寫出這樣 Dockerfile 的團隊真是太懶了。已經有這麼個在虛擬機中安裝應用的 puppet 腳本,在編寫 Dockerfile 時翻新一下拿過來就要用。
這個問題的解決辦法是最小化 Dockerfile,讓其明確地描述所作之事。這裏是同一個應用的 「更合適的」 Dockerfile:
FROM openjdk:8-jdk-alpine
ENV MY_APP_VERSION="3.2"
RUN apk add --no-cache \
ca-certificates
WORKDIR /app
ADD http://artifactory.mycompany.com/releases/${MY_APP_VERSION}/spring-boot-application.jar .
ENTRYPOINT ["java","-jar","/app/spring-boot-application.jar"]
複製代碼
能夠注意到:
這只是個很是簡單(也是編造出來的)例子。現實中我見過不少依賴於「神奇」方法的 Dockerfile,對其可被構建的時機和位置都有特殊要求。請不要以這種給開發者(以及其它沒法訪問整個系統的人)在本地建立 Docker 鏡像製造巨大困難的方式編寫你的 Dockerfile。
一個更好的替代方式多是讓 Dockerfile 本身來(使用多階段構建)編譯 Java 代碼。這讓你對 Docker 鏡像中將要發生什麼盡收眼底。
想象一下,若是你是一名工做在使用來多種編程語言的大企業中的 運維/SRE 工程師的話,是很難成爲每種編程語言領域的專家併爲之構建系統的。
這是優先採用容器的主要優點之一。你應該能從任何開發團隊下載任何的 Dockerfile 並在不考慮反作用(由於就不該該有)的狀況下構建它。
構建一個 Docker 鏡像應該是個冪等的操做。對同一個 Dockerfile 構建一次仍是一千次,或是先在 CI 服務器上後在你的工做站上構建都不該該有問題。
可是,有些構建階段的 Dockerfile 則是這樣的:
容器提供了與宿主文件系統有關的隔離性,但沒有什麼能保護你從一個 Dockerfile 中包含的 RUN 指令中調用 curl 向你的內聯網 POST 一個 HTTP 負載。
這個簡單的例子演示了一個在同一次運行中既安裝依賴(安全操做)又發佈(不安全的操做)npm 應用的 Dockerfile:
FROM node:9
WORKDIR /app
COPY package.json ./package.json
COPY package-lock.json ./package-lock.json
RUN npm install
COPY . .
RUN npm test
ARG npm_token
RUN echo "//registry.npmjs.org/:_authToken=${npm_token}" > .npmrc
RUN npm publish --access public
EXPOSE 8080
CMD [ "npm", "start" ]
複製代碼
這個 Dockerfile 混淆了兩個不相干的關注點,即發佈某個版本的應用和爲之建立一個 Docker 鏡像。或許有時這兩個動做確實一塊兒發生,但這不是污染 Dockerfile 的藉口。
Docker 不是也永遠不該該是 一種通用的 CI 系統。不要把 Dockerfiles 濫用爲擁有無限威力的增強版 bash 腳本。容器運行時有反作用是 ok 的,但構建時不行。
解決之道是簡化 Dockerfile 並確保其只包含冪等操做:
同時,謹記 Docker 緩存文件系統層的方式。Docker 假設若是一個層及早於其的若干層沒有「被改變過」的話就能夠從緩存中重用它們。若是你的 Dockerfile 指令有反作用,你就從本質上破壞了 Docker 緩存機制。
FROM node:10.15-jessie
RUN apt-get update && apt-get install -y mysql-client && rm -rf /var/lib/apt
RUN mysql -u root --password="" < test/prepare-db-for-tests.sql
WORKDIR /app
COPY package.json ./package.json
COPY package-lock.json ./package-lock.json
RUN npm install
COPY . .
RUN npm integration-test
EXPOSE 8080
CMD [ "npm", "start" ]
複製代碼
假設當你嘗試構建該 Dockerfile 時你的測試失敗的話,你會對改變源碼並再試着從新構建一次。Docker 將假設清理數據庫的那層已經 RUN 過了而且能夠從緩存中重用它。因此你的新一次的測試將在數據庫未被清理且包含了以前那次運行的數據的狀況下被執行。
在本例中,Dockerfile 很小,有反作用的語句也容易定位 (mysql 命令) 並移動到合適的位置以修正層緩存。但在真實的 Dockerfile 中包含許多命令,若是你不知道 RUN 語句中哪條有反作用,要肯定它們的的正確順序很是困難。
若是要執行的全部動做都是隻讀且有本地做用域的,這樣的 Dockerfile 會簡化許多。
在任何採用了容器的企業,一般會有兩個分別的 Docker 鏡像目錄。
第一個目錄包含用做要發送到生產服務器的真實部署產物的鏡像;而部署鏡像中應該包含:
第二個目錄中是用於 CI/CD 系統或開發者的鏡像;鏡像中可能包含:
顯然因爲這兩個容器鏡像目錄各有不一樣的用途和目標,應該被分別處理。要部署到服務器的鏡像應該是最小化、安全的和通過檢驗的。用於 CI/CD 過程的鏡像不須要真正部署,因此它們不須要多少嚴格的限制(對於尺寸和安全性)。
但出於一些緣由,人們並不老是能理解這種差異。我見過好多嘗試去使用一樣的鏡像用於開發和部署的企業,幾乎老是會發生的是其生產環境 Docker 鏡像中都包含了一堆絕不相干的工具和框架。
生產環境的 Docker 鏡像絕無理由包含 git、測試框架或是編譯器/壓縮器。
做爲通用部署產物的容器,老是應該在不一樣的環境中使用相同的部署產物並確保你所測試的也是你所部署的(更詳細的稍後展開說);但嘗試把本地開發和生產部署聯合起來是註定失敗的。
總之,要嘗試去理解你的 Docker 鏡像的角色。每個鏡像都應該扮演一個單獨的角色。若是把測試框架/庫放到生產環境那確定是錯的。你應該花些時間去學習並使用 多階段構建。
使用容器的最重要優點之一就是其不可變的屬性。這意味着一個 Docker 鏡像應該只被構建一次並依次部署在各類環境中(測試、預發佈)直至到達生產環境。
由於徹底相同的鏡像做爲單一的實體被部署,就能保證你在一個環境中所測試的和其它環境中徹底一致。
我見過不少企業將代碼版本或配置稍有差異的不一樣產出物,用於各類環境的構建。
這之因此有問題是由於沒法保證鏡像「足夠類似」,以便可以以相同方式驗證其行爲。同時也帶來了不少濫用的可能,開發者/運維人員 各自在非生產鏡像中使用額外的調試工具也形成了不一樣環境中鏡像的更大差別。
與其竭力確保不一樣鏡像儘量地相同,遠不如對全部軟件生命週期階段使用單一鏡像來得容易。
要注意不一樣環境使用不一樣設置(也就是密鑰和配置變量等)是特別正常的,本文後面也會談論這點。但除此以外,其它的全部東西,都應該如出一轍。
Docker registry(譯註:能夠理解爲相似 git 倉庫的實體,能夠是 DockerHub 那樣公有的,也能夠在私有數據中心搭建)起到的做用就是做爲那些能夠被隨時隨處從新部署的既有應用的一個目錄。它也是應用程序資源的中心位置,其中包含額外的元數據以及相同應用程序的之前的歷史版本。從它上面選擇一個 Docker 鏡像的指定 tag 很是容易,而且能將其部署到任意環境中。
使用 Docker registry 的最靈活的方式之一就是在 registries 之間推動鏡像。一個機構至少會有兩個 registries(開發/生產)。一個 Docker 鏡像應該被構建一次(參考以前的一個反模式)並被置於開發 registry 中。而後,一旦集成測試、安全檢查,及其自身的各類功能行質量驗證都正常後,該鏡像就能被推動到生產 registry 以供發送到生產服務器或 Kubernetes 集羣中了。
每一個地區/位置或每一個部門擁有不一樣的 Docker registries 機構一樣是可能的。這裏的要點是 Docker 部署的典型方式也會包含一個 Docker registry。Docker registries 同時起到了做爲應用資源 repository 和應用部署到生產環境以前中介存儲的兩個做用。
一種至關有問題的作法就是從生命週期中徹底移除了 Docker registries 並直接把源代碼推送到生產服務器。
生產服務器使用 git pull
以取得源碼,隨後 Docker 在線構建出一個鏡像並本地化地運行它(一般經過 Docker-compose 或其它編排工具)。這種「部署方法」簡直是反模式的集大成者!
這樣的部署作法形成一系列的問題,首先就是安全性問題。生產服務器不該該訪問 git 倉庫。若是一個企業嚴肅對待安全性問題,這種模式甚至不會被安全委員會批准。生產服務器安裝了 git 自己就莫名其妙。git(或其它版本管理系統)是一種開發者協做工具,而非一種產出物交付方案。
但其最嚴重的問題是這種「部署方法」徹底繞過了 Docker registries 的做用域。由於再也不有持有 Docker 鏡像的中心位置,你就沒法感知哪一個 Docker 鏡像被部署到了服務器上了。
起初這種部署方法可能工做正常,但隨着更大的安裝量將迅速變得低效。你須要去學習如何使用 Docker registries 及其帶來的好處(也包含相關的容器安全性檢查)。
Docker registries 有定義良好的 API,以及若干可被用來建立你的鏡像的開源和專有產品。
一樣要注意到,藉助 Docker registries,你的源碼安全性將老是能被防火牆擋在身後了。
對於前兩條反模式的一個推論是 -- 一旦採用了容器,你的 Docker registry 就應該成爲一切的真理,人們談論 Docker 的 tag 和鏡像的話題。開發者和運維人員應該使用容器做爲他們的通用語言,兩類團隊間的傳遞的實體應該是容器而非一個 git hash。
這與使用 git hash 做爲「推動產物」的舊方式背道而馳。源碼當然重要,但爲了推動它而反覆從新構建是一種對資源的浪費(參考反模式5)。不少企業認爲容器只應該被運維人員處理,而開發者只要弄好源碼就好了;這可能與正確作法相去甚遠。容器是一個讓開發者和運維人員協做的絕佳機會。
理想狀況下,運維人員甚至不該該關心到一個應用的 git 倉庫。他們須要知道的只是到手的 Docker 鏡像是否準備好了被推送到產品環境,而不是着眼於從新構建一個 git hash 以取得開發者已經在預發佈環境使用過的相同鏡像。
經過詢問你所在機構中的運維人員,就能知道你是否吃了這種反模式的虧。若是運維人員要熟悉構建系統或測試框架這些和實際運行時無關的應用內部細節,將很大地拖累其平常運維工做。
這個反模式和反模式 5 關係密切(每一個環境一種鏡像)。在大多數狀況下,當我問起一些企業爲什麼他們的 QA/預發/生產 環境須要不一樣的鏡像時,答案一般是它們包含了不一樣的配置和密鑰。
這不光破壞了對 Docker 的主要期待(部署你所測試過的),同時也讓全部 CI/CD 管道變得很是複雜 -- 它們不得不在構建時管理密鑰和配置。
固然對於熟悉 12-Factor(譯註:III - 在環境中存儲配置)的人來講,這個反模式不算新鮮事了。
應用應該在運行時而不是構建時請求配置。一個 Docker 鏡像應該是與配置無關的。只有在運行時配置才應該被「附加」到容器中。有不少對此的解決方案,而且大部分集羣化/部署系統都能集成一種運行時配置方案(如 configmaps、zookeeper、consul)和密鑰方案(vault、keywhiz、confidant、cerberus)。
若是你的 Docker 鏡像硬編碼了 IP 或憑證等,那你就中招了。
我讀到過一些文章,建議把 Dockerfile 應該被當成一種窮人版的 CI 解決方案去用。這就是一個那種 Dockerfile 的真實例子:
# Run Sonar analysis
FROM newtmitch/sonar-scanner AS sonar
COPY src src
RUN sonar-scanner
# Build application
FROM node:11 AS build
WORKDIR /usr/src/app
COPY . .
RUN yarn install \
yarn run lint \
yarn run build \
yarn run generate-docs
LABEL stage=build
# Run unit test
FROM build AS unit-tests
RUN yarn run unit-tests
LABEL stage=unit-tests
# Push docs to S3
FROM containerlabs/aws-sdk AS push-docs
ARG push-docs=false
COPY --from=build docs docs
RUN [[ "$push-docs" == true ]] && aws s3 cp -r docs s3://my-docs-bucket/
# Build final app
FROM node:11-slim
EXPOSE 8080
WORKDIR /usr/src/app
COPY --from=build /usr/src/app/node_modules node_modules
COPY --from=build /usr/src/app/dist dist
USER node
CMD ["node", "./dist/server/index.js"]
複製代碼
乍一看這個 Dockerfile 貌似很好的應用了 多階段構建,而實際上這一古腦兒的集合了以前的反模式。
就其自己而言,Docker 並非一個 CI 系統。容器化技術可被用做 CI/CD 管道的一部分,但這項技術某種程度上是徹底不一樣的。不要混淆須要運行在 Docker 容器中的命令和須要運行在 CI 構建任務中運行的命令。
某些文章提倡使用構建參數與 labels 交互並切換某些指定的構建階段等,但這隻會徒增複雜性。
修正以上 Dockerfile 的方法就是將其一分爲五。一個用來部署應用,其它用做 CI/CD 管道中不一樣的步驟。
一個 Dockerfile 只應該有一個單獨的 用途/目標。
由於容器也包含了其依賴,因此很適於爲每一個應用隔離庫和框架版本。開發者對於在工做站上嘗試爲相同工具安裝多個版本的問題不厭其煩。只須要在你的 Dockerfile 中精確描述應用所需,Docker 就能夠解決解決上述問題。
可是這種模式要用得對路纔有效。做爲一個運維人員,其實並不真的關心開發者在 Docker 鏡像中使用了什麼編程工具。運維人員應該在不用真的爲每種編程語言創建一個開發環境的前提下,建立一個 Java 應用的 Docker 鏡像,再建立一個 Python 的鏡像,緊接着再建立一個 Node.js 的。
然而不少企業仍將 Docker 視爲一種靜默打包格式,並只用其打包一個已經在容器以外完成了的 產出物/應用。
Java 的繁重組織形式是這種反模式的重災區,甚至官方文檔中也有出現。下面就是 「Spring Boot Docker guide」 官方文檔中推薦的 Dockerfile 寫法:
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
複製代碼
這個 Dockerfile 只是打包了一個既有的 jar 文件。這文件從哪來的?沒人知道。這事在 Dockerfile 中徹底沒有過描述。若是我是一名運維人員,還得專心安裝上全套 Java 本地化開發庫,就爲了構建這麼一個文件。若是你工做在一個使用了多種編程語言的機構中,不光是運維人員,對於整個構建節點,這個過程都會迅速變得脫離控制。
我用 Java 來舉例,但這個反模式也出如今其它情形下。Dockerfile 沒法工做,除非你先執行一句 npm install
,這也是經常發生的事情。
針對這個反模式的解決之道和對付反模式 2(不透明、不自包含的 Dockerfile)的辦法同樣。確保你的 Dockerfile 描述了某個過程的所有。若是你遵循這個方式,你的 運維/SRE 同事甚至會愛上你。
對於以上 Java 的例子,Dockerfile 應該被修改成:
FROM openjdk:8-jdk-alpine
COPY pom.xml /tmp/
COPY src /tmp/src/
WORKDIR /tmp/
RUN ./gradlew build
COPY /tmp/build/app.war /app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
複製代碼
這個 Dockerfile 明確描述了應用如何被建立,而且可以在不用安裝本地 Java 的狀況下被任何人在任何工做站上運行。做爲練習,你還能本身使用 多階段構建 來改進這個 Dockerfile。
不少企業在採用容器時遇到了麻煩,由於他們企圖把既有的虛擬機經驗硬塞進容器。最好先花費一些工夫從新思考容器具備的全部優點,並理解如何利用新習得的知識從頭建立你的過程。
在本文中,我列出了使用容器時若干錯誤的實踐,也爲每一條開出瞭解藥。
檢查你的工做流,和你的開發同事(若是你是運維人員的話)或運維同事(若是你是開發者)聊聊,試着找出企業是否踩了這些反模式的坑吧。
查看更多前端好文
請搜索 fewelife 關注公衆號
轉載請註明出處