原文地址:Docker Images : Part III - Going Farther To Reduce Image Size
介紹
在本系列的前兩部分中双妨,我們介紹了優(yōu)化Docker鏡像大小的最常用方法寓盗。我們看到了多階段構(gòu)建,結(jié)合基于Alpine的鏡像以及有時是靜態(tài)構(gòu)建的方式望侈,通秤∈撸可以為我們帶來最大的空間優(yōu)化。在最后一部分中脱衙,我們將進行更深一步探討侥猬。我們將討論標準化基礎(chǔ)鏡像例驹,剝離二進制文件,項目優(yōu)化以及其他構(gòu)建系統(tǒng)或附加組件退唠,例如DockerSlim或Bazel鹃锈,以及NixOS發(fā)行版。
我們還將討論一些我們早先遺漏的小細節(jié)瞧预,但這些細節(jié)很重要屎债,例如時區(qū)文件和證書。
公共基礎(chǔ)配置
如果我們的節(jié)點并行運行許多容器(甚至只有幾個)垢油,那么有一件事也可以節(jié)省大量資源盆驹。
Docker鏡像由layer組成。每一個layer都可以添加滩愁,刪除或更改文件躯喇。就像代碼存儲庫中的提交代碼或從另一個類繼承的類一樣。當我們執(zhí)行時docker build硝枉,Dockerfile的每一行都會生成一個layer廉丽。傳輸鏡像時,僅傳輸目標上尚不存在的layer檀咙。
layer不僅節(jié)省了網(wǎng)絡(luò)帶寬逛裤,還節(jié)省了存儲空間:如果多個鏡像共享layer隧魄,則Docker只需要存儲一次這些layer审丘。并且榕堰,根據(jù)所使用的存儲驅(qū)動程序足删,layer還可以節(jié)省磁盤I / O和內(nèi)存忽洛,因為當多個容器需要從layer讀取相同的文件時纪蜒,系統(tǒng)將僅讀取和緩存這些文件一次瓤漏。(overlay2和aufs驅(qū)動程序就是這種情況凿将。)
這意味著校套,如果我們在運行多個容器的節(jié)點中嘗試優(yōu)化網(wǎng)絡(luò)和磁盤訪問以及內(nèi)存使用率,則可以通過確保這些容器運行的鏡像具有盡可能多的公共layer來節(jié)省大量資源牧抵。
這可能直接違反我們之前給出的一些準則笛匙!例如,如果我們使用靜態(tài)二進制文件構(gòu)建超級優(yōu)化的鏡像犀变,則這些二進制文件可能比其動態(tài)等效文件大10倍妹孙。讓我們假設(shè)一個場景,運行10個容器获枝,每個容器使用帶有這些二進制文件之一的不同鏡像蠢正。
方案1:scratch鏡像中的靜態(tài)二進制文件
每個鏡像的占用:10 MB
10個鏡像的占用:100 MB
方案2:使用ubuntu鏡像的動態(tài)二進制文件(64 MB)
每個鏡像的單個占用:65 MB
每個鏡像的細目:64 MB用于ubuntu+ 1 MB用于特定的二進制文件
磁盤總使用量:74 MB(單個layer10x1 MB +共享層64 MB)
方案3:使用alpine鏡像的動態(tài)二進制文件(5.5 MB)
每個鏡像的單個占用:6.5 MB
每個鏡像的細目:alpine 5.5 MB + 特定的二進制文件1 MB
磁盤總使用量:15.5 MB
最初,這些靜態(tài)二進制文件看起來不錯省店,但是在這種情況下嚣崭,它們會適得其反笨触。鏡像將需要更多的磁盤空間,需要更長的傳輸時間并使用更多的RAM雹舀!
但是芦劣,為了使這些方案起作用,我們需要確保所有鏡像實際上都使用完全相同的標準说榆。如果我們某些使用centos鏡像持寄,而其他使用debian,這是行不通的娱俺。即使我們使用比如 ubuntu:16.04和ubuntu:18.04稍味,兩個不同版本的ubuntu!這意味著在更新基礎(chǔ)鏡像時荠卷,我們應(yīng)該重建所有鏡像模庐,以確保所有容器中的鏡像都是一致的。
這也意味著我們需要良好的管理和團隊之間的良好溝通油宜。你可能會想掂碱,“這不是個技術(shù)問題!”慎冤,那么你是對的疼燥!這不是技術(shù)問題。這意味著對于某些人來說蚁堤,解決起來會困難得多醉者,因為你無法自己無法解決的工作量很多:你將不得不讓其他人參與進來!也許你堅持使用Debian披诗,但是另一個團隊堅持使用Fedora撬即。如果你想使用通用基礎(chǔ),則必須說服其他團隊呈队。這也意味著你必須接受他們也可以說服你的結(jié)果剥槐。結(jié)論:在某些情況下,最有效的解決方案是需要溝通能力而非技術(shù)能力宪摧!
最后粒竖,在特定情況下,靜態(tài)鏡像仍然有用:當我們知道我們的鏡像將被部署在異構(gòu)環(huán)境中時几于;或它們將是在給定節(jié)點上運行的唯一對象蕊苗。在這種情況下,無論如何都不會發(fā)生任何共享孩革。
剝離和轉(zhuǎn)換
還有一些并非特定于容器的其他技術(shù)岁歉,這些技術(shù)可以從我們的鏡像中刪除幾兆字節(jié)(有時甚至是千兆字節(jié))。
剝離二進制文件
默認情況下,大多數(shù)編譯器會生成帶有標記的二進制文件锅移,這些標記對于調(diào)試定位問題很有用熔掺,但執(zhí)行時并不是必選項。strip工具可以刪除這些標記非剃。這不太可能改變程序本身的運行方式置逻,但是如果你處在每個字節(jié)都很重要的情況下,那這肯定會有所幫助备绽。
資源處理
如果我們的容器鏡像包含媒體文件券坞,是否可以縮小這些文件,例如通過使用不同的文件格式或解碼器肺素?我們可以將它們托管在其他地方恨锚,以使我們發(fā)送的鏡像更小嗎?如果代碼經(jīng)常更改倍靡,而資源卻不更改猴伶,那后者特別有用。在這種情況下塌西,我們應(yīng)盡量避免在每次發(fā)布新版本的代碼時都重新生成這些資源他挎。
壓縮:不是一個好主意
如果要減小鏡像的大小,為什么不壓縮文件捡需?HTML办桨,JavaScript,CSS之類的資源使用zip或gzip應(yīng)該可以很好地壓縮站辉。還有更有效的方法呢撞,例如bzip2、7z庵寞,lzma狸相。首先,它看起來像是一種減小鏡像大小的簡單方法捐川。但是,如果我們的計劃是在使用之前先解壓縮這些資源逸尖,那么我們最終將浪費資源古沥!
Layer在傳輸之前已經(jīng)被壓縮,因此提取鏡像不會更快娇跟。而且岩齿,如果我們需要解壓縮文件,則磁盤使用率將比以前更高苞俘,因為在磁盤上盹沈,我們現(xiàn)在將同時擁有文件的壓縮版本和未壓縮版本!更糟糕的是:如果這些文件位于共享Layer上吃谣,那么共享將不會帶來任何好處乞封,因為在運行容器時我們將解壓縮的這些文件將不會被共享做裙。
那么UPX怎么樣?如果你不熟悉UPX肃晚,那么它是一個出色的工具锚贱,可以減少二進制文件的大小。如果我們想減少容器的占用空間关串,UPX缺會適得其反拧廊。首先,磁盤和網(wǎng)絡(luò)的使用不會減少晋修,因為無論如何Layer都是壓縮的吧碾。因此UPX不會在這里給我們?nèi)魏螏椭?/p>
當運行普通的二進制文件時,它會映射到內(nèi)存中墓卦,以便僅在需要時才加載(或“分頁”)所需的字節(jié)滤港。運行使用UPX壓縮的二進制文件時,必須在內(nèi)存中解壓縮整個二進制文件趴拧。這會導(dǎo)致更高的內(nèi)存使用率和更長的啟動時間溅漾,尤其是對于像Go運行時,它往往會生成更大的二進制文件著榴。
(我曾經(jīng)嘗試在hyperkube二進制文件上使用UPX添履,嘗試在KVM中構(gòu)建優(yōu)化的節(jié)點鏡像并運行在本地Kubernetes集群。結(jié)果卻并不順利脑又,因為雖然它減少了我的VM的磁盤使用量暮胧,但它們的內(nèi)存使用量卻上升了,很多N属铩)
一些其他小技巧
還有其他工具可以幫助我們獲得較小的圖像尺寸往衷。這將不是一個詳盡的清單...
DockerSlim
DockerSlim使用了一種幾乎不可思議的技術(shù)來減小鏡像的大小。我不知道它到底是如何工作的(除了自述文件中的設(shè)計說明)严卖,因此我將進行有根據(jù)的猜測席舍。我想DockerSlim運行我們的容器,并檢查容器中運行的程序訪問了哪些文件哮笆。然后刪除其他文件来颤。基于這一猜測稠肘,在使用DockerSlim之前福铅,我會非常小心,因為許多框架會動態(tài)或延遲地(即首次需要它們時)加載文件项阴。
為了驗證該假設(shè)滑黔,我嘗試使用一個簡單的Django應(yīng)用程序來測試DockerSlim。DockerSlim將其從200 MB減少到30 MB,表現(xiàn)的非常好略荡!但是庵佣,盡管該應(yīng)用程序的首頁運行正常,但許多鏈接卻被破壞了撞芍。我想這是因為DockerSlim尚未檢測到它們的模板秧了,并且它們也沒有包含在最終鏡像中。錯誤報告本身也被破壞序无,可能是因為用于顯示和發(fā)送異常的模塊也被忽略了验毡。任何可以動態(tài)地import
的模塊,python代碼都會在運行時才進行加載帝嗡。
不過請不要誤會我的意思:在許多情況下晶通,DockerSlim仍然可以為我們創(chuàng)造奇跡!與往常一樣哟玷,當有這樣一個非常強大的工具時狮辽,了解它的內(nèi)部結(jié)構(gòu)將非常有幫助,因為它可以幫助我們對它的工作方式有一個很好的理解巢寡。
Distroless
Distroless鏡像是使用外部工具構(gòu)建的最小鏡像的集合喉脖,無需使用經(jīng)典的Linux分發(fā)程序包管理器。它產(chǎn)生的鏡像非常小抑月,但是沒有基本的調(diào)試工具树叽,也沒有簡單的安裝方法。
就個人喜好而言谦絮,我更喜歡擁有一個軟件包管理器和一個熟悉的發(fā)行版题诵,因為誰知道我可能需要什么額外的工具來解決容器問題?Alpine只有5.5 MB层皱,它允許我能夠安裝所需的幾乎所有東西性锭。我不知道是否要放棄這點!但是叫胖,如果你有全面的方法來對容器進行故障排查草冈,無需依賴鏡像中的工具,那么你確實可以通過Distroless節(jié)省一些額外的空間臭家。
此外疲陕,基于的Alpine鏡像通常會比其Distroless鏡像小。所以你可能想知道:既然如此為什么我們還要去了解Distroless钉赁?至少有兩個原因。
首先携茂,從安全角度考慮你踩,Distroless使你獲得的鏡像非常小。更少的內(nèi)容意味著更少的潛在漏洞。
其次带膜,Distroless圖像是使用Bazel構(gòu)建的吩谦,因此,如果你想學習或試驗或使用Bazel膝藕,它們是非常不錯的入門示例的集合式廷。Bazel到底是什么?很高興你提出這個問題芭挽,我將在下一部分中介紹滑废!
Bazel(和其他替代)
有些構(gòu)建系統(tǒng)甚至不使用Dockerfile。Bazel是其中之一袜爪。Bazel的強大在于它可以表達我們的源代碼和它所構(gòu)建的目標之間的復(fù)雜依賴關(guān)系蠕趁,有點像Makefile。這樣就可以只重建需要重建的東西辛馆。無論是在我們的代碼中(在進行小的本地更改時)還是在我們的基本鏡像中(以便修補或升級庫都不會觸發(fā)所有鏡像的整個重建)俺陋。它還可以運行等效的單元測試,并且僅對受代碼更改影響的模塊運行測試昙篙。
這在非常大的代碼庫上特別有效腊状。在某個時候,我們的構(gòu)建和測試系統(tǒng)可能需要幾個小時才能運行苔可,有時甚至幾天缴挖。我們可以花費數(shù)小時部署并行構(gòu)建服務(wù)器場和測試環(huán)境,但這需要大量資源硕蛹,并且無法再次在本地環(huán)境中運行醇疼。這種場景下才是Bazel之類的真正發(fā)光時刻,因為它將能夠在幾分鐘內(nèi)構(gòu)建并測試所需的內(nèi)容法焰,而不是幾小時或幾天秧荆。
很棒!那我們應(yīng)該馬上跳到Bazel嗎埃仪?沒那么快乙濒。使用Bazel需要學習完全不同的構(gòu)建系統(tǒng),即使擁有上面提到的所有漂亮的多階段構(gòu)建以及靜態(tài)和動態(tài)庫的精妙之處卵蛉,使用Dockerfile都可能比普通的Dockerfile復(fù)雜得多颁股。維護此構(gòu)建系統(tǒng)和相關(guān)配置將需要大量工作。盡管我本人沒有使用Bazel的第一手經(jīng)驗傻丝,但根據(jù)我周圍的經(jīng)驗甘有,至少需要安排一名專職高級或總工程師來承擔配置和維護Bazel的工作。
如果我們的組織有數(shù)百名開發(fā)人員葡缰;建造或測試時間正在成為我們發(fā)展的主要障礙亏掀;那么選擇Bazel可能是一個好主意忱反。否則,如果我們是一家處于起步階段的初創(chuàng)企業(yè)或小型組織滤愕,那么這可能不是個好選擇温算。除非我們有幾位工程師非常了解Bazel并想為其他所有人去配置它。
Nix
我決定增加一個有關(guān)Nix軟件包管理器的部分间影,因為在第1部分和第2部分發(fā)布之后注竿,有些人對它充滿了熱情。
劇透警報:是的魂贬,Nix可以幫助您獲得更好的構(gòu)建巩割,但是學習曲線陡峭。也許不像Bazel那樣陡峭随橘,但是也很接近了喂分。你需要學習Nix,其概念机蔗,其自定義表達語言蒲祈,以及如何使用它為你喜歡的語言和框架打包(有關(guān)示例,請參見nixpkgs手冊)萝嘁。
盡管如此梆掸,我還是想談?wù)凬ix,這有兩個原因:它的核心概念非常強大(可以幫助我們總體上對軟件打包有更好的理解)牙言,還有一個名為Nixery的特殊項目可以在部署容器時幫助我們酸钦。
什么是Nix?
我第一次聽說Nix大約是10年前咱枉,當時我參加了一場會議演講卑硫。那時,它已經(jīng)功能齊全且穩(wěn)定蚕断。這不是一個時髦的新鮮事物欢伏。
一點專業(yè)的解釋:
- Nix是一個程序包管理器,可以在任何Linux機器以及macOS上安裝亿乳;
- NixOS是基于Nix 的Linux發(fā)行版硝拧。
-
nixpkgs
是Nix的軟件包集合; - “派生(derivation)”是Nix構(gòu)建的秘訣葛假。
Nix是功能性包管理器障陶。“功能性”是指每個程序包都由其輸入(源代碼聊训,依賴項...)及其派生(構(gòu)建方式)定義抱究。如果我們使用相同的輸入和相同的構(gòu)建,我們將獲得相同的輸出带斑。如果它使我們想起Docker構(gòu)建緩存媳维,那是完全正常的:因為他們是完全相同的想法酿雪!
在傳統(tǒng)系統(tǒng)上遏暴,當程序包依賴于另一個程序包時侄刽,該依賴關(guān)系通常表示得不是很精確。例如朋凉,在Debian中州丹, python3.8依賴于,python3.8-minimal (= 3.8.2-1)
而python3.8-minimal依賴于libc6 (>= 2.29)
杂彭。另一方面墓毒,ruby2.5依賴于libc6 (>= 2.17)
。因此亲怠,我們安裝單個版本的libc6
所计,大多數(shù)情況下都能正常工作。
在Nix上团秽,程序包取決于庫的確切版本主胧,并且有一個非常巧妙的機制,每個程序都將使用自己的庫而不與其他庫沖突习勤。(如果你對此感到疑惑:動態(tài)鏈接程序使用鏈接器踪栋,該鏈接器被設(shè)置為使用來自特定路徑的庫。從概念上講图毕,這與指定#!/usr/local/bin/my-custom-python-3.8
使用特定版本的Python解釋器運行Python腳本沒有什么不同夷都。)
例如,當程序使用C庫時予颤,在傳統(tǒng)系統(tǒng)上囤官,它引用/usr/lib/libc.so.6
,但是對于Nix蛤虐,它可能引用了/nix/store/6yaj...drnn-glibc-2.27/lib/libc.so.6
党饮。
看到那個/nix/store
路徑了嗎?那是Nix倉庫笆焰。存儲在其中的東西是不可變的文件和目錄劫谅,由哈希標識。從概念上講嚷掠,Nix存儲類似于Docker使用的層(layer)捏检,但有一個很大的區(qū)別:Docker中各層相互疊加,而Nix存儲中的文件和目錄是不相交的不皆。它們永遠不會相互沖突(因為每個對象都存儲在不同的目錄中)贯城。
在Nix上,“安裝軟件包”意味著在Nix倉庫中下載大量文件和目錄霹娄,然后設(shè)置配置文件(實際上是一堆符號鏈接能犯,以便我們現(xiàn)在可以使用剛剛安裝的程序$PATH
)鲫骗。
Nix實踐
上面的聽起來很理論吧?讓我們看看Nix的實踐踩晶。
我們可以使用在容器中運行Nix docker run -ti nixos/nix
执泰。
然后,我們可以使用nix-env --query
或檢查安裝的軟件包nix-env -q渡蜻。
它只會顯示給我們nix和nss-cacert术吝。很奇怪,難道我們還沒有像Shell ls這樣的工具以及其他工具嗎茸苇?是的排苍,但是在這個特定的容器鏡像中,它們是由靜態(tài)busybox可執(zhí)行文件提供的学密。
好了淘衙,我們該如何安裝?我們可以nix-env --install redis
或niv-env -i redis
腻暮。該命令的輸出向我們表明彤守,它正在獲取新的“路徑”并將其放置在Nix倉庫中。它至少會為redis獲取一條“路徑”西壮,很可能為glibc獲取另一條路徑遗增。碰巧的是,Nix本身(例如nix-env二進制文件和其他一些文件)也使用glibc款青,但它可能與redis使用的版本不同做修。如果運行ls -ld /nix/store/*glibc*/
我們將看到兩個目錄,分別對應(yīng)于glibc的兩個不同版本抡草。在編寫這些行時饰及,我得到了以下兩個版本glibc-2.27:
ef5936ea667f:/# ls -ld /nix/store/*glibc*/
dr-xr-xr-x ... /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/
dr-xr-xr-x ... /nix/store/6yaj6n8l925xxfbcd65gzqx3dz7idrnn-glibc-2.27/
你可能會想:“等等,這不是同一版本嗎康震?” 是的燎含,沒有!它們是相同的版本號腿短,但可能是用不同的選項構(gòu)建的屏箍。結(jié)果會有些許不同,因此從Nix的角度來看橘忱,這是兩個不同的對象赴魁。就像當我們構(gòu)建相同的Dockerfile但在某處更改一行代碼時一樣,Docker構(gòu)建器會跟蹤這些微小差異并為我們提供兩個不同的鏡像钝诚。
我們可以通過nix-store --query --references
或nix-store -qR
命令颖御,展示Nix倉庫中任何文件的依賴關(guān)系。例如凝颇,要查看我們剛剛安裝的Redis二進制文件的依賴性潘拱,我們可以這樣做 nix-store -qR $(which redis-server)
疹鳄。
在我的容器中,輸出如下所示:
/nix/store/6yaj6n8l925xxfbcd65gzqx3dz7idrnn-glibc-2.27
/nix/store/mzqjf58zasr7237g8x9hcs44p6nvmdv7-redis-5.0.5
這些目錄是我們在任何地方運行Redis所需要的芦岂。是的瘪弓,其中包括scratch。我們不需要任何額外的庫盔腔。(也許只是為了方便我們對$PATH進行了調(diào)整杠茬,但這不是必要的。)
我們甚至可以使用Nix 配置文件來實現(xiàn)該過程弛随。配置文件包含我們需要添加到$PATH目錄中的bin文件夾(以及其他一些內(nèi)容;為方便起見宁赤,我將其簡化)舀透。這意味著,如果我執(zhí)行 nix-env --profile myprof -i redis memcached
决左, myprof/bin將包含Redis和Memcached的可執(zhí)行文件愕够。
更好的是,配置文件也是Nix倉庫中的對象佛猛。因此惑芭,我可以使用nix-store -qR列出其依賴關(guān)系。
使用Nix創(chuàng)建最小鏡像
使用上一節(jié)中看到的命令继找,我們可以編寫以下Dockerfile:
FROM nixos/nix
RUN mkdir -p /output/store
RUN nix-env --profile /output/profile -i redis
RUN cp -va $(nix-store -qR /output/profile) /output/store
FROM scratch
COPY --from=0 /output/store /nix/store
COPY --from=0 /output/profile/ /usr/local/
第一階段使用Nix將Redis安裝到新的“配置文件”中遂跟。然后,我們要求Nix列出該配置文件的所有依賴項(即nix-store -qR命令)婴渡,然后將所有這些依賴項復(fù)制到/output/store幻锁。
第二階段將這些依賴項復(fù)制到/nix/store(即它們在Nix中的原始位置),并復(fù)制配置文件边臼。(主要是因為配置文件目錄包含一個bin目錄哄尔,并且我們希望該目錄位于我們的$PATH目錄中!)
鏡像的最終大小是35MB柠并,只帶有Redis岭接,僅此而已。如果你想要一個shell臼予,只需更新Dockerfile為-i redis bash
鸣戴。
如果你想重寫所有Dockerfile來使用它,請稍等瘟栖。首先葵擎,該鏡像缺少關(guān)鍵的元數(shù)據(jù),例如VOLUME半哟,EXPOSE以及ENTRYPOINT酬滤。其次签餐,在下一節(jié)中,我為你提供了更好的選擇盯串。
Nixery
所有軟件包管理器都以相同的方式工作:他們下載(或生成)文件并將其安裝在我們的系統(tǒng)上氯檐。但是Nix有一個重要的區(qū)別:安裝的文件被設(shè)計為不可變。當我們使用Nix安裝軟件包時体捏,不會改變我們以前的版本冠摄。Docker層可以互相影響(因為一個層可以更改或刪除在上一層中添加的文件),但是Nix存儲對象則不能几缭。
看一下我們先前運行的Nix容器(或者重新開始一個新容器docker run -ti nixos/nix
)河泳。特別要注意/nix/store
。有很多這樣的目錄:
b7x2qjfs6k1xk4p74zzs9kyznv29zap6-bzip2-1.0.6.0.1-bin/
cinw572b38aln37glr0zb8lxwrgaffl4-bash-4.4-p23/
d9s1kq1bnwqgxwcvv4zrc36ysnxg8gv7-coreutils-8.30/
如果我們使用Nix來構(gòu)建容器鏡像(如上一節(jié)末尾在Dockerfile中所做的那樣)年栓,則我們需要的只是一堆目錄拆挥,/nix/store
以及一些鏈接以方便使用。
想象一下某抓,我們將Nix存儲的每個目錄上傳為Docker注冊表中的鏡像層纸兔。
現(xiàn)在,當我們需要使用包X否副,Y和Z生成鏡像時汉矿,我們可以:
- 使用符號鏈接集生成一個小的層,以輕松調(diào)用X备禀,Y和Z中的任何程序(這對應(yīng)于上面Dockerfile
COPY
中的最后一行)洲拇, - 詢問Nix,對應(yīng)的存儲對象是什么(對于X痹届,Y和Z呻待,以及它們的依賴關(guān)系),以及相應(yīng)的層队腐,
- 生成引用所有層的Docker鏡像清單蚕捉。
這就是Nixery所做的。Nixery是一個“神奇”的容器注冊表柴淘,它動態(tài)地生成容器鏡像清單迫淹,并引用作為Nix存儲對象的層。
具體來說为严,如果執(zhí)行docker run -ti nixery.dev/redis/memcached/bash bash
敛熬,我們將在具有Redis,Memcached和Bash的容器中獲得shell第股;并且該容器的鏡像是即時生成的应民。(請注意,我們最好執(zhí)行docker run -ti nixery.dev/shell/redis/memcached sh
,因為當鏡像以shell
開頭時诲锹,Nixery在外殼頂部為我們提供了一些基本的程序包繁仁;例如coreutils
。)
Nixery中還有一些額外的優(yōu)化归园;如果你有興趣的話黄虱,可以查看這篇博客文章或NixConf的演講。
使用Nix的其他方法
Nix還可以直接生成容器鏡像庸诱。這個博客文章中有一個很好的例子捻浦。但是請注意,博客文章中使用的技術(shù)需要kvm并且在大多數(shù)云實例的構(gòu)建環(huán)境(除了嵌套虛擬化的實例除外桥爽,這種情況仍然非常罕見)中或在容器中都無法使用朱灿。顯然,你將不得不放棄上面的示例并使用buildLayeredImage聚谁,但是我還沒有進行進一步探索母剥,所以我不知道需要多少工作量。
要不要使用Nix形导?
在像這樣的簡短(甚至不是那么簡短)的博客文章中,我無法教你如何通過書本來使用Nix习霹,并生成完美的容器鏡像朵耕。但是我至少可以演示一些基本的Nix命令,并演示如何在多階段Dockerfile中使用Nix淋叶,以全新的方式生成自定義容器鏡像阎曹。我希望這些例子可以幫助你確定Nix是否對你的應(yīng)用程序有幫助。
就個人而言煞檩,我希望在需要臨時容器鏡像(尤其是在Kubernetes上)時使用Nixery处嫌。讓我們假設(shè),例如斟湃,我需要的鏡像包含curl熏迹,tar以及AWS CLI。我的傳統(tǒng)方法是使用alpine凝赛,執(zhí)行apk add curl tar py-pip
注暗,然后pip install awscli
。但是使用Nixery墓猎,我可以簡單地使用鏡像 nixery.dev/shell/curl/gnutar/awscli
捆昏!
還有一些小細節(jié)
如果我們使用非常小的鏡像(例如scratch,或某種程度上alpine甚至使用distroless毙沾,Bazel或Nix生成的鏡像)??骗卜,我們可能會遇到意想不到的問題。我們通常不會考慮某些文件在容器文件系統(tǒng)中可以找到,但是有些程序可能希望在UNIX系統(tǒng)上找到寇仓。
我們到底在談?wù)撌裁次募倩В亢冒桑@是一個簡短但不詳盡的清單:
- TLS證書
- 時區(qū)文件焚刺,
- UID / GID映射文件敛摘。
讓我們看看這些文件到底是什么,為什么以及何時需要它們乳愉,以及如何將它們添加到鏡像中兄淫。
TLS證書
當我們建立到遠程服務(wù)器的TLS連接時(例如,通過HTTPS向Web服務(wù)或API發(fā)出請求)蔓姚,該遠程服務(wù)器通常會向我們顯示其證書捕虽。通常,該證書已由知名證書頒發(fā)機構(gòu)(比如CA)簽名坡脐。通常我們要檢查此證書是否有效泄私,并且我們確實知道對其進行了簽名。
(我之所以說“通潮赶校”晌端,是因為在一些非常罕見的場景中,這無關(guān)緊要恬砂,或者我們以不同的方式驗證咧纠;但是,如果你處于其中一種情況泻骤,則應(yīng)該知道漆羔。如果你不知道,請假設(shè)你必須驗證證書狱掂!安全第一Q菅鳌)
在此過程中,密鑰位于這些知名的證書頒發(fā)機構(gòu)中趋惨。要驗證所連接服務(wù)器的證書鸟顺,我們需要證書頒發(fā)機構(gòu)的證書。這些通常安裝在下/etc/ssl希柿。
如果使用的是scratch或其他小鏡像诊沪,在連接到TLS服務(wù)器,則可能會收到證書驗證錯誤曾撤。使用Go端姚,返回的信息應(yīng)該類似:x509: certificate signed by unknown authority
。如果發(fā)生這種情況挤悉,我們要做的就是將證書添加到你的鏡像中渐裸。我們可以從幾乎任何常見的圖像(例如ubuntu或alpine)中獲取它們巫湘。我們使用哪一個并不重要,因為它們都附帶幾乎相同的證書包昏鹃。
下面這個命令可以解決問題:
COPY --from=alpine /etc/ssl /etc/ssl
順便說一句尚氛,這表明如果我們要從鏡像中復(fù)制文件,即使它不是構(gòu)建階段洞渤,也可以用--from來引用阅嘶!
時區(qū)
如果我們的代碼操作時間,尤其是本地時間(例如载迄,如果我們在本地時區(qū)中顯示時間讯柔,而不是日期或內(nèi)部時間戳記),則需要時區(qū)文件护昧。你可能會想:“等等魂迄,什么?如果我想管理時區(qū)惋耙,我只需要知道UTC的偏移量即可捣炬!” 嗯,但這不算夏時制绽榛!夏時制(DST)很棘手湿酸,因為并非所有地方都有DST。在具有DST的地方中灭美,標準時間和DST之間的更改不會在同一日期發(fā)生稿械。多年來,有些地方在實施(或取消)DST冲粤,或更改其使用期限。
因此页眯,如果要顯示本地時間梯捕,則需要描述所有這些信息的文件。在UNIX上窝撵,則是tzinfo
或zoneinfo
文件。它們通常存儲在/usr/share/zoneinfo
。
一些鏡像(例如centos或debian)確實包含時區(qū)文件缤至。其他鏡像(例如alpine或ubuntu)則沒有碍论。包含相關(guān)信息的軟件包通常命名為tzdata
。
要在我們的鏡像中安裝時區(qū)文件赐劣,我們可以執(zhí)行例如:
COPY --from=debian /usr/share/zoneinfo /usr/share/zoneinfo
或者嫉拐,如果我們已經(jīng)在使用alpine,我們可以簡單地進行apk add tzdata
魁兼。
要檢查時區(qū)文件是否已正確安裝婉徘,我們可以在容器中運行以下命令:
TZ=Europe/Paris date
如果顯示比如Fri Mar 13 21:03:17 CET 2020
這樣的信息,則表示安裝完成。如果顯示UTC盖呼,則表明未找到時區(qū)文件儒鹿。
UID / GID映射文件
我們的代碼可能還需要做的另一件事:查找用戶和組ID。這是通過在/etc/passwd
和/etc/group
中查找來完成的几晤。就個人而言约炎,我唯一需要提供這些文件的場景是在容器中運行桌面應(yīng)用程序(使用clink或Jessica Frazelle的dockerfiles之類的工具。
如果需要將這些文件安裝在容器中蟹瘾,則可以在本地或在多階段容器的一個階段中生成它們圾浅,或通過主機綁定安裝它們(取決于你要實現(xiàn)的目標)。
這篇博客文章顯示了如何將用戶添加到構(gòu)建容器中热芹,然后復(fù)制/etc/passwd
和/etc/group
到運行的容器贱傀。
結(jié)論
如你所見,有很多方法可以減小鏡像的大小伊脓。如果你想知道“減小鏡像尺寸的絕對最佳方法是什么府寒?”,壞消息:沒有絕對最佳的方法报腔。像往常一樣株搔,答案是“看情況”。
基于Alpine的多階段構(gòu)建將在許多情況下提供出色的結(jié)果纯蛾。
但是有些庫在Alpine上不可用纤房,構(gòu)建它們可能需要比我們想要的更多的工作。因此在這種情況下翻诉,使用經(jīng)典發(fā)行版進行多階段構(gòu)建會非常有用炮姨。
Distroless或Bazel之類的機制可能更好,但需要大量的前期調(diào)研和準備碰煌。
在像嵌入式系統(tǒng)這樣的空間非常小的環(huán)境中進行部署時舒岸,靜態(tài)二進制文件和scratch
鏡像可能會很有用。
最后芦圾,如果我們構(gòu)建并維護許多鏡像蛾派,我們最好堅持使用一種技術(shù),即使那并非是最好的个少。使用相同的結(jié)構(gòu)來維護數(shù)百個鏡像可能要容易一些洪乍,而不是針對某種基場景使用過多的變體和一些特殊的構(gòu)建系統(tǒng)或Dockerfile。