您好,登錄后才能下訂單哦!
本篇內容介紹了“怎么正確且快速構建Docker優質的安全鏡像”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
緩存以加快構建速度
鏡像的構建時間大都花在系統軟件包和應用程序依賴包的下載和安裝。但是,這些通常不會經常變更,因此推薦進行緩存。
從系統包和工具開始——通常在FROM后運行,以確保已將其緩存。無論您使用哪個Linux發行版作為基本鏡像,都應該得到如下所示的結果:
FROM ... # any viable base image like centos:8, ubuntu:21.04 or alpine:3.12.3 # RHEL/CentOS RUN yum install ... # Debian RUN apt-get install ... # Alpine RUN apk add ... # Rest of the Dockerfile (COPY, RUN, CMD...)
另外,您甚至可以將這些相關命令提取到獨立的Dockerfile以構建自己的基礎鏡像。然后可以將該鏡像推送到鏡像倉庫,以便您和其他人可以在其他的Dockerfile中引用。
這樣,您無需再去擔心系統包以及相關的依賴項,除非您需要升級它們或添加與刪除某些內容。
在系統包之后,我們通常要安裝應用程序依賴項。這些可能是來自Maven存儲庫中的Java庫(默認存儲在.m2目錄中),JavaScript模塊node_modules或Python庫venv。
與系統依賴項相比,這些更改的頻率更高,但不足以保證每次構建都能進行完整的重新下載和重新安裝。但是如果對應Dockerfile寫得不好,您會注意到,即使未修改依賴項,也不會使用緩存:
FROM ... # any viable base image like python:3.8, node:15 or openjdk:15.0.1 # Copy everything at once COPY . . # Java RUN mvn clean package # Or Python RUN pip install -r requirements.txt # Or JavaScript RUN npm install # ... CMD [ "..." ]
這是為什么?問題出在COPY . .,Docker在構建的每個步驟中都使用緩存,直到它遇到新的或已修改的命令/層。
在這種情況下,當我們將所有內容復制到鏡像中時—包括未更改的依賴關系列表以及已修改的源代碼。
Docker會繼續進行并重新下載且重新安裝所有依賴關系。因為修改過源碼文件,它不再能夠在該層使用緩存。為避免這種情況,我們必須分兩個步驟復制文件:
FROM ... # any viable base image like python:3.8, node:15 or openjdk:15.0.1 COPY pom.xml ./pom.xml # Java COPY requirements.txt ./requirements.txt # Python COPY package.json ./package.json # JavaScript RUN mvn dependency:go-offline -B # Java RUN pip install -r requirements.txt # Python RUN npm install # JavaScript COPY ./src ./src/ # Rest of Dockerfile (build application; set CMD...)
首先,我們添加列出所有應用程序依賴項的文件并安裝它們。如果此文件沒有更改,則將緩存所有更改。只有這樣,我們才能將其余(修改過的)源碼復制到鏡像中,并運行應用程序代碼的測試和構建。對于更多的“高級”方法,我們使用Docker的BuildKit及其實驗功能進行相同的操作:
# syntax=docker/dockerfile:experimental FROM ... # any viable base image like python:3.8, openjdk:15.0.1 COPY pom.xml ./pom.xml # Java COPY requirements.txt ./requirements.txt # Python RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline -B # Java RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt # Python
上面的代碼顯示了如何使用命令--mount選項RUN來選擇緩存目錄。如果您要顯式使用非默認緩存位置,這將很有幫助。
但是,如果要使用此功能,則必須包括指定語法版本的標題行(如上所述),并使用來運行構建,比如:DOCKER_BUILDKIT=1 docker build name:tag .。
在這些文檔(https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md#run---mounttypecache)中可以找到有關實驗功能的更多信息。
到目前為止,所有內容僅適用于本地構建—對于CI,情況則不同,并且通常每個工具/提供程序都會有所不同,但對于其中的任何一個,您將需要一些持久性卷來存儲緩存/依賴項 。例如,對于Jenkins,您可以在代理中使用存儲。
對于在Kubernetes上運行的Docker構建(無論是使用JenkinsX,Tekton還是其他),您將需要Docker守護進程,該守護進程可以在Docker(DinD)中使用Docker進行部署,DinD是在Docker容器中運行的Docker守護進程。
至于構建本身,您將需要一個連接到DinD socket的pod(容器)來運行docker build命令。
為了演示和簡化操作,我們可以使用以下pod進行操作:
apiVersion: v1 kind: Pod metadata: name: docker-build spec: containers: - name: dind # Docker in Docker container image: docker:19.03.3-dind securityContext: privileged: true env: - name: DOCKER_TLS_CERTDIR value: '' volumeMounts: - name: dind-storage mountPath: /var/lib/docker - name: docker # Builder container image: docker:19.03.3-git securityContext: privileged: true command: ['cat'] tty: true env: - name: DOCKER_BUILDKIT value: '1' - name: DOCKER_HOST value: tcp://localhost:2375 volumes: - name: dind-storage emptyDir: {} - name: docker-socket-volume hostPath: path: /var/run/docker.sock type: File
上面的容器由2個容器組成—一個用于DinD,一個用于鏡像構建。要使用構建容器運行構建,可以訪問其shell,克隆一些存儲庫并運行構建流程:
~ $ kubectl exec --stdin --tty docker-build -- /bin/sh # Open shell session ~ # git clone https://github.com/username/reponame.git # Clone some repository ~ # cd reponame ~ # docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t name:tag --cache-from username/reponame:latest . ... => importing cache manifest from martinheinz/python-project-blueprint:flask ... => => writing image sha256:... => => naming to docker.io/library/name:tag => exporting cache => => preparing build cache for export
最終docker build使用了一些新選項—--cache-from image:tag,來告訴Docker它應該使用(遠程)倉庫中的指定鏡像作為緩存源。這樣,即使緩存的層未存儲在本地文件系統中,我們也可以利用緩存的優點。
另一個選項----build-arg BUILDKIT_INLINE_CACHE=1用于在創建緩存元數據時將其寫入鏡像。這必須用于--cache-from工作,有關更多信息,請參閱文檔(https://docs.docker.com/engine/reference/commandline/build/#specifying-external-cache-sources)。
最小鏡像
快速構建確實很讓人高興,但是如果您擁有真正的“thick”圖像,則仍然需要花費很長的時間才能push/pull它們,而且胖鏡像很可能還包含許多無用的庫,工具以及諸如此類的東西,這些都使鏡像變得更加臃腫。
易受攻擊,因為它會造成更大的攻擊面。
制作更小的鏡像的最簡單方法是使用Alpine Linux之類的基礎鏡像,而不是基于Ubuntu或RHEL的鏡像。另一個好的方法是使用多步驟Docker構建,其中您使用一個鏡像進行構建(第一個FROM命令),而使用另一個更小的鏡像來運行應用程序(第二個/最后一個FROM),例如:
# 332.88 MB FROM python:3.8.7 AS builder COPY requirements.txt /requirements.txt RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt # only 16.98 MB FROM python:3.8.7-alpine3.12 as runner # copy only the dependencies installation from the 1st stage image COPY --from=builder /venv /venv COPY --from=builder ./src /app CMD ["..."]
上面顯示了我們首先在基本的Python 3.8.7鏡像中準備了應用程序及其依賴項,該鏡像很大,為332.88 MB。在此處,我們安裝了應用程序所需的虛擬環境和庫。
然后,我們切換到更小的基于Alpine的鏡像,該鏡像僅為16.98 MB。我們將先前創建的整個虛擬環境以及源代碼復制到該鏡像。這樣,我們最終得到的圖像要小得多,鏡像層更少,同時也有更少的不必要的工具和二進制文件。
要記住的另一件事是我們在每次構建過程中產生的層數。FROM,COPY,RUN以及CMD是都會生成新的層。至少在RUN的情況下,我們可以通過將所有RUN命令合并成這樣的一個命令來輕松地減少它創建的層的數量:
# Bad, Creates 4 layers RUN yum --disablerepo=* --enablerepo="epel" RUN yum update RUN yum install -y httpd RUN yum clean all -y # Good, creates only 1 layer RUN yum --disablerepo=* --enablerepo="epel" && \ yum update && \ yum install -y httpd && \ yum clean all -y
我們可以更進一步,完全擺脫可能很重的基礎鏡像。為此,我們將使用特殊的FROM scratch信號通知Docker應使用最小的基本鏡像,而下一個命令將是最終鏡像的第一層。
這對于以二進制文件運行且不需要大量工具的應用程序特別有用,例如Go,C ++或Rust應用程序。但是,這種方法要求二進制文件是靜態編譯的,因此它不適用于Java或Python之類的語言。FROM scratchDockerfiles的示例可能像這樣:
FROM golang as builder WORKDIR /go/src/app COPY . . # Static build is required so that we can safely use 'scratch' base image RUN CGO_ENABLED=0 go install -ldflags '-extldflags "-static"' FROM scratch COPY --from=builder /go/bin/app /app ENTRYPOINT ["/app"]
很簡單,對吧?借助這種Dockerfile,我們可以生成僅約3MB的鏡像!
鎖定版本
速度和大小是大多數人關注的兩件事,而鏡像的安全性成為人們的事后考慮。有幾種簡單的方法可以將鏡像鎖定下來,并限制攻擊者可以利用的攻擊面。
最基本的建議是鎖定所有庫、包、工具和基本鏡像的版本,這不僅對安全性很重要,而且對鏡像的穩定性也很重要。如果您對鏡像使用最新標記,或者您沒有在Python的requirements.txt或JavaScript的package.json中指定版本,您在構建期間下載的鏡像/庫可能與應用程序代碼不兼容,或者使容器暴露于漏洞中。
當您想將所有內容鎖定到特定版本時,還應該定期更新所有這些依賴項,以確保您擁有所有可用的最新安全補丁程序和修補程序。
即使您真的很努力地避免所有依賴中的任何漏洞,仍然會有一些您錯過或尚未修復/發現的漏洞。所以,為了減輕任何可能的攻擊的影響,最好避免以根用戶身份運行容器。
因此,應該在Dockerfiles中包含用戶1001,以表示從Dockerfiles創建的容器應該并且可以作為非根用戶(理想情況下是任意用戶)運行。當然,這可能需要您修改應用程序并選擇正確的基本鏡像,因為一些常見的基本映像(如nginx)需要根權限(例如,由于特權端口)。
通常很難在Docker鏡像中找到與避免漏洞,但是如果鏡像僅包含運行應用程序所需的最低限度,則可能會更容易一些。Google發行的Distroless(https://github.com/GoogleContainerTools/distroless)是一個這樣的鏡像。
將Distroless鏡像修剪到甚至沒有shell或軟件包管理器的程度,這使得它們比Debian或基于Alpine的鏡像在安全性方面要好得多。如果您使用的是多步驟Docker構建,那么大多數情況下,切換到Distroless runner映像非常簡單:
FROM ... AS builder # Build the application ... # Python FROM gcr.io/distroless/python3 AS runner # Golang FROM gcr.io/distroless/base AS runner # NodeJS FROM gcr.io/distroless/nodejs:10 AS runner # Rust FROM gcr.io/distroless/cc AS runner # Java FROM gcr.io/distroless/java:11 AS runner # Copy application into runner and set CMD... # More examples at https://github.com/GoogleContainerTools/distroless/tree/master/examples
除了最終鏡像及其容器中可能存在的漏洞外,我們還必須考慮用于構建鏡像的Docker守護程序和容器運行時。因此,與我們的所有鏡像一樣,我們不應允許Docker與root用戶一起運行,而應使用所謂的rootless模式。
“怎么正確且快速構建Docker優質的安全鏡像”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。