天天看點

如何編寫優雅的Dockerfile

導讀

Kubernetes要從容器化開始,而容器又需要從Dockerfile開始,本文将介紹如何寫出一個優雅的Dockerfile檔案。

文章主要内容包括:

  • Docker容器
  • Dockerfile
  • 使用多階建構

感謝公司提供大量機器資源及時間讓我們可以實踐,感謝在此專題上不斷實踐的部分項目及人員的支援。

如何編寫優雅的Dockerfile

一、Docker容器

1.1 容器的特點

我們都知道容器就是一個标準的軟體單元,它有以下特點:

  • 随處運作:容器可以将代碼與配置檔案和相關依賴庫進行打包,進而確定在任何環境下的運作都是一緻的。
  • 高資源使用率:容器提供程序級的隔離,是以可以更加精細地設定CPU和記憶體的使用率,進而更好地利用伺服器的計算資源。
  • 快速擴充:每個容器都可作為單獨的程序予以運作,并且可以共享底層作業系統的系統資源,這樣一來可以加快容器的啟動和停止效率。

1.2 Docker容器

目前市面上的主流容器引擎有Docker、Rocket/rkt、OpenVZ/Odin等等,而獨霸一方的容器引擎就是使用最多的Docker容器引擎。

Docker容器是與系統其他部分隔離開的一系列程序,運作這些程序所需的所有檔案都由另一個鏡像提供,從開發到測試再到生産的整個過程中,Linux 容器都具有可移植性和一緻性。相對于依賴重複傳統測試環境的開發管道,容器的運作速度要快得多,并且支援在多種主流雲平台(PaaS)和本地系統上部署。Docker容器很好地解決了“開發環境能正常跑,一上線就各種崩”的尴尬。

如何編寫優雅的Dockerfile

Docker容器的特點:

  • 輕量:容器是程序級的資源隔離,而虛拟機是作業系統級的資源隔離,是以Docker容器相對于虛拟機來說可以節省更多的資源開銷,因為Docker容器不再需要GuestOS這一層作業系統了。
  • 快速:容器的啟動和建立無需啟動GuestOS,可以實作秒級甚至毫秒級的啟動。
  • 可移植性:Docker容器技術是将應用及所依賴的庫和運作時的環境技術改造包成容器鏡像,可以在不同的平台運作。
  • 自動化:容器生态中的容器編排工作(如:Kubernetes)可幫助我們實作容器的自動化管理。

二、Dockerfile

Dockerfile是用來描述檔案的構成的文本文檔,其中包含了使用者可以在使用行調用以組合Image的所有指令,使用者還可以使用Docker build實作連續執行多個指令指今行的自動建構。

通過編寫Dockerfile生磁鏡像,可以為開發、測試團隊提供基本一緻的環境,進而提升開發、測試團隊的效率,不用再為環境不統一而發愁,同時運維也能更加友善地管理我們的鏡像。

Dockerfile的文法非常簡單,常用的隻有11個:

如何編寫優雅的Dockerfile

2.1 編寫優雅地Dockerfile

編寫優雅的Dockerfile主要需要注意以下幾點:

  • Dockerfile檔案不宜過長,層級越多最終制作出來的鏡像也就越大。
  • 建構出來的鏡像不要包含不需要的内容,如日志、安裝臨時檔案等。
  • 盡量使用運作時的基礎鏡像,不需要将建構時的過程也放到運作時的Dockerfile裡。

隻要記住以上三點就能寫出不錯的Dockerfile。

為了友善大家了解,我們用兩個Dockerfile執行個體進行簡單的對比:

FROM ubuntu:16.04
RUN apt-get update
RUN apt-get install -y apt-utils libjpeg-dev \     
python-pip
RUN pip install --upgrade pip
RUN easy_install -U setuptools
RUN apt-get clean           
FROM ubuntu:16.04
RUN apt-get update && apt-get install -y apt-utils \
  libjpeg-dev python-pip \
           && pip install --upgrade pip \
      && easy_install -U setuptools \
    && apt-get clean           

我們看第一個Dockerfile,乍一看條理清晰,結構合理,似乎還不錯。再看第二個Dockerfile,緊湊,不易閱讀,為什麼要這麼寫?

  • 第一個Dockerfile的好處是:當正在執行的過程某一層出錯,對其進行修正後再次Build,前面已經執行完成的層不會再次執行。這樣能大大減少下次Build的時間,而它的問題就是會因層級變多了而使鏡像占用的空間也變大。
  • 第二個Dockerfile把所有的元件全部在一層解決,這樣做能一定程度上減少鏡像的占用空間,但在制作基礎鏡像的時候若其中某個組編譯出錯,修正後再次Build就相當于重頭再來了,前面編譯好的元件在一個層裡,得全部都重新編譯一遍,比較消耗時間。

從下表可以看出兩個Dockerfile所編譯出來的鏡像大小:

$ docker images | grep ubuntu      
REPOSITORY      TAG     IMAGE ID    CREATED     SIZE                                                                                                                                   
ubuntu                   16.04       9361ce633ff1  1 days ago 422MB
ubuntu                   16.04-1   3f5b979df1a9  1 days ago  412MB           

呃…. 好像并沒有特别的效果,但若Dockerfile非常長的話可以考慮減少層次,因為Dockerfile最高隻能有127層。

三、使用多階建構

Docker在更新到Docker 17.05之後就能支援多階建構了,為了使鏡像更加小巧,我們采用多階建構的方式來打包鏡像。在多階建構出現之前我們通常使用一個Dockerfile或多個Dockerfile來建構鏡像。

3.1單檔案建構

在多階建構出來之前使用單個檔案進行建構,單檔案就是将所有的建構過程(包括項目的依賴、編譯、測試、打包過程)全部包含在一個Dockerfile中之下:

FROM golang:1.11.4-alpine3.8 AS build-env
ENV GO111MODULE=off
ENV GO15VENDOREXPERIMENT=1
ENV BUILDPATH=github.com/lattecake/hello
RUN mkdir -p /go/src/${BUILDPATH}
COPY ./ /go/src/${BUILDPATH}
RUN cd /go/src/${BUILDPATH} && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install –v

CMD [/go/bin/hello]           

這種的做法會帶來一些問題:

  • Dockerfile檔案會特别長,當需要的東西越來越多的時候可維護性指數級将會下降;
  • 鏡像層次過多,鏡像的體積會逐漸增大,部署也會變得越來越慢;
  • 代碼存在洩漏風險。

以Golang為例,它運作時不依賴任何環境,隻需要有一個編譯環境,那這個編譯環境在實際運作時是沒有任務作用的,編譯完成後,那些源碼和編譯器已經沒有任務用處了也就沒必要留在鏡像裡。

如何編寫優雅的Dockerfile

上表可以看到,單檔案建構最終占用了312MB的空間。

3.2 多檔案建構

在多階建構出來之前有沒有好的解決方案呢?有,比如采用多檔案建構或在建構伺服器上安裝編譯器,不過在建構伺服器上安裝編譯器這種方法我們就不推薦了,因為在建構伺服器上安裝編譯器會導緻建構伺服器變得非常臃腫,需要适配各個語言多個版本、依賴,容易出錯,維護成本高。是以我們隻介紹多檔案建構的方式。

多檔案建構,其實就是使用多個Dockerfile,然後通過腳本将它們進行組合。假設有三個檔案分别是:Dockerfile.run、Dockerfile.build、build.sh。

  • Dockerfile.run就是運作時程式所必須需要的一些元件的Dockerfile,它包含了最精簡的庫;
  • Dockerfile.build隻是用來建構,建構完就沒用了;
  • build.sh的功能就是将Dockerfile.run和Dockerfile.build進行組成,把Dockerfile.build建構好的東西拿出來,然後再執行Dockerfile.run,算是一個排程的角色。

Dockerfile.build

FROM golang:1.11.4-alpine3.8 AS build-env
ENV GO111MODULE=off
ENV GO15VENDOREXPERIMENT=1
ENV BUILDPATH=github.com/lattecake/hello
RUN mkdir -p /go/src/${BUILDPATH}
COPY ./ /go/src/${BUILDPATH}
RUN cd /go/src/${BUILDPATH} && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install –v           

Dockerfile.run

FROM alpine:latest
RUN apk –no-cache add ca-certificates
WORKDIR /root
ADD hello .
CMD ["./hello"]           

Build.sh

#!/bin/sh
docker build -t –rm hello:build . -f Dockerfile.build
docker create –name extract hello:build
docker cp extract:/go/bin/hello ./hello
docker rm -f extract
docker build –no-cache -t –rm hello:run . -f Dockerfile.run
rm -rf ./hello           

執行build.sh完成項目的建構。

如何編寫優雅的Dockerfile

從上表可以看到,多檔案建構大大減小了鏡像的占用空間,但它有三個檔案需要管理,維護成本也更高一些。

3.3 多階建構

最後我們來看看萬衆期待的多階建構。

完成多階段建構我們隻需要在Dockerfile中多次使用FORM聲明,每次FROM指令可以使用不同的基礎鏡像,并且每次FROM指令都會開始新的建構,我們可以選擇将一個階段的建構結果複制到另一個階段,在最終的鏡像中隻會留下最後一次建構的結果,這樣就可以很容易地解決前面提到的問題,并且隻需要編寫一個Dockerfile檔案。這裡值得注意的是:需要確定Docker的版本在17.05及以上。下面我們來說說具體操作。

在Dockerfile裡可以使用as來為某一階段取一個别名”build-env”:

FROM golang:1.11.2-alpine3.8 AS build-env           

然後從上一階段的鏡像中複制檔案,也可以複制任意鏡像中的檔案:

COPY –from=build-env /go/bin/hello /usr/bin/hello            

看一個簡單的例子:

FROM golang:1.11.4-alpine3.8 AS build-env
 
ENV GO111MODULE=off
ENV GO15VENDOREXPERIMENT=1
ENV GITPATH=github.com/lattecake/hello
RUN mkdir -p /go/src/${GITPATH}
COPY ./ /go/src/${GITPATH}
RUN cd /go/src/${GITPATH} && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -v
 
FROM alpine:latest
ENV apk –no-cache add ca-certificates
COPY --from=build-env /go/bin/hello /root/hello
WORKDIR /root
CMD ["/root/hello"]           

執行docker build -t –rm hello3 .後再執行docker images ,然後我們來看鏡像的大小:

如何編寫優雅的Dockerfile

多階建構給我們帶來很多便利,最大的優勢是在保證運作鏡像足夠小的情況下還減輕了Dockerfile的維護負擔,是以我們極力推薦使用多階建構來将你的代碼打包成Docker 鏡像。

作者:王聰

内容來源:宜信技術學院