Docker已迅速成為本人最喜歡的基礎工具之一,以便構建可重復軟件產品,從而帶來盡可能靜態的服務器環境。
我在本文中將概述我在使用Docker的過程中開始反復出現的幾種模式。我不指望它們會帶來多少新奇或驚喜,但希望其中一些有用,我也很想聽聽各位在使用Docker過程中遇到的模式。
我試用Docker的基礎是保持在卷中持續的狀態,那樣Docker容器本身可以隨意重建,而不會丟失數據(除非我改動容器狀態,而不更新Docker文件(Dockerfile)的狀態,而經常重建容器有助于改掉這個壞習慣)。
下面的示例Docker文件都專注于此:構建容器――在這種環境下,容器本身可以隨時更換,沒必要考慮它。
1. 共享基礎容器
Docker鼓勵“繼承”,所以這應該并不奇怪――繼承是高效使用Docker的一個基本方面,尤其是由于它有助于減少構建新容器所需的時間,因為沒必要那么頻繁地重新執行步驟。Docker會試圖將中間步驟放入到緩存,它在這方面做得很好――有時太好了,不過要是沒有明確注明,也很容易錯過共享的機會。
將我的各種容器遷移到Docker上時明顯出現的事情之一是,存在太多的冗余設置。
我為預計部署到任何地方的大多數項目運行單獨的容器,至少它需要任何長時間運行的進程,或者需要“標準”程序包集之外的任何特定程序包時,是這樣,因而我有好多容器,而程序包迅速變得越來越多。
等到我考慮遷移時,就試圖在Docker中運行“一切”(包括我依賴的少數幾個桌面應用程序),以便讓我的mybase環境完全可以隨意使用。
于是我很快開始將我的基本設置提取到基礎容器,用于眾多用途。下面是我當前的“devbase”Docker文件:
這里沒有什么需要特別說明的――它安裝了我往往喜歡隨時可用的一些特定工具。這些工具對大多數人來說恐怕不一樣。選擇什么樣的發行版很隨意。值得考慮的是,如果/當你重建容器時,就要指定一個特定的標記以避免意外。
它在默認情況下暴露了端口8080,因為那是我通常暴露Web應用程序的端口,我通常將這些容器用于這些Web應用程序。
它為我添加了一個用戶,將userid設置為服務器上的用戶ID,并不創建/home目錄。之所以不創建/home目錄,是由于我從主機綁定掛載共享/ home,這就引出了下一種模式。
2. 共享卷開發容器
我的所有開發容器與主機至少共享一個卷:/ home,這么做是為了便于開發。就許多應用程序而言,它讓我可以讓與合適的基于文件-系統-變更的代碼重載器一起運行的應用程序處于開發模式,那樣容器就可以封裝操作系統/發行版層面的依賴項,并且幫助證實捆綁的應用程序在原始環境中運行,我用不著針對每處代碼變更,需要完全重啟/重建虛擬機。與此同時,我可以相當頻繁地重啟虛擬機,確保沒有什么錯失。
至于其他,它讓我可以只要重啟(而不是重建)容器,即可接受代碼變更。
對于測試/試運行容器和生產容器,我在大多數情況下會避免通過卷共享代碼,而是使用“ADD”命令,將相應代碼添加到Docker容器本身中。
比如說,下面是我“homepage”開發容器的Docker文件,它含有我自主開發的個人維基,可利用來自“devbase”容器的已經共享的/home卷,并展示了共享基礎容器和我如何使用共享/home卷:
(注意:我確實應該對我的devbase容器加上版本標記)
至于我博客的開發版本:
因為它們從共享軟件庫獲取代碼,而且基于共享的基礎容器,當我添加/修改/刪除依賴項時,這些容器通常可以極其迅速地重建,我覺得這很重要,以便確保我沒有忍不住采用疏忽未記錄依賴項的變通方法。
即便如此,肯定有些方面是我想改進的。盡管上述基礎容器是輕量級,但它們肯定不止這樣:這些容器中的大多數內容仍然未使用。由于Docker采用寫時拷貝(copy-on-write)覆蓋,這不會導致龐大開銷,但確實仍意味著我并沒有真正體現最基本需求,也沒有盡可能減少攻擊或出錯風險(我倒不是很擔心這些特定情況的攻擊風險,因為我的博客并不在“實時”版本中含有任何重要狀態。)。
3. 開發工具容器
這對像我們這些喜歡依靠通過ssh連接至屏幕會話來編寫代碼的人來說可能最有吸引力,而對IDE人群來說不太有吸引力;但對我來,上述方案的一個好處就是,它讓我可以將編輯和測試執行部分代碼與運行開發中的應用程序分離開來。
過去開發系統方面很煩人的問題之一是,開發及生產依賴項與開發工具依賴項很容易混在一起。你可以試著將它們分開來,但除非這些設置真正做到了分離開來,否則很容易建立未記錄依賴項。
在過去,我花了幾周對應用程序的依賴項進行“反向工程”后,總算搞清楚了這個問題。由于開發環境、測試和初始原型部署環境混在一起,這個應用程序積累了各種各樣的未記錄依賴項。
雖然有很多方法可以解決這個問題:只要確保你進行定期的測試部署,結合上述模式,但我還是有一種個人很喜歡的解決方案,因為它可以從根本上防止問題出現:
我有一個單獨的容器含有Emacs安裝環境,還有我喜歡隨時可用的其他各種工具。我仍試圖保持精簡,但問題是,我的屏幕會話可以駐留在這個容器中,結合我那臺筆記本電腦上設置的“autossh”,幾乎總是有一條連接與容器相連,那樣我就可以編輯與我的其他開發容器“實時”共享的代碼。
在這個容器,我還允許偶爾出錯:直接安裝程序包,因為它只影響調試和開發。
目前,它看起來如下:
結合共享“/ home“,這給了我一個足夠實用的小地方可以通過ssh連入。我確信,用它用得越多,我會補充它,但眼下它證明完全能滿足我的需要。
4. 不同環境下測試容器
我特別喜歡Docker的一個方面是,可以在不同環境下輕松測試代碼。比如說,我升級Ruby編譯項目以便處理Ruby 1.9(早就該有了)后,創建了這個小小的Docker文件,好讓我在將主開發環境遷移到1.9之后,在Ruby 1.8環境中生成一個外殼。
當然,你可以用rbenv等獲得類似的效果。但我總是覺得這些工具很煩人,因為我更喜歡盡量使用發行版程序包來部署,尤其是由于,如果我確保這順利開展,它讓其他人更容易使用我的代碼。
擁有這樣一個Docker容器:當我暫時需要不同的環境時,只要運行“docker run”,圓滿地解決了這個問題,而且還有這個好處:它并不受制于像Ruby這種有預包裝自定義工具來處理版本的編程語言。
我還可以使用標準虛擬機來達到目的,但是可以在短得多的時間內啟動上述docker容器。
5. 構建容器
如今我編寫的代碼大多是用解釋語言編寫的,但即使那樣,還是常常會有實用的“構建”(build)步驟需要花很大的開銷,我寧可不是一直執行它們。
一個例子是為Ruby應用程序運行“捆綁工具”(bundler)。捆綁工具可為Rubygem更新緩存的依賴項(還可視情況更新全部的gem文件,甚至更新未打包的內容),針對較大的應用程序運行捆綁工具要花一段時間。
它還常常需要應用程序運行時并不需要的依賴項。比如說,安裝依賴原生擴展的gem常常依賴眾多的程序包――常常沒有記錄到底是哪些程序包,通過獲取所有的build-essential程序包及其依賴項,就更容易啟動。與此同時,雖然你可以事先讓捆綁工具做所有的工作,但我真的不想在主機環境中運行它,主機環境可能與容器兼容,也可能不兼容。
這方面的解決辦法就是創建構建容器。如果依賴項不同的話,你可以創建單獨的Docker文件,也可以重復使用主應用程序Docker文件,只要覆蓋命令來運行你所需要的構建命令。比如說,Docker文件看起來如下:
然后,只要你更新了依賴項,將你的build/source目錄掛載到容器的”build”目錄下,就可以運行這段命令。只要更換合適的項就行。
關鍵在于,你可以將應用程序的構建或者其一部分與最后的包裝分開來,同時仍封裝Docker容器中的進程和依賴項,只要將進程細分到兩個或多個容器中。
6.安裝容器
這種方法雖非我原創,但確實值得一提。出色的nsenter和docker-enter工具隨帶一個安裝選項,這與流行的,但又令人畏懼的“curl [你無法控制的某個URL] | bash”模式相比是個很大的進步。它通過提供實現上述“構建容器”模式的Docker容器來做到這一點,不過更進了一步。它值得關注。
這是Docker文件的最后部分,之后下載并構建了一個合適的nsenter版本(我要提醒的一點是,對下載文檔沒有進行完整性檢查):
雖然惡意攻擊者仍有可能企圖利用容器可能存在的特權提升問題大做手腳,但是攻擊風險至少要小得多。
但這種模式最可能立即吸引我們大多數人的地方在于,避免了這一風險:本意良好的開發人員偶爾在安裝腳本方面犯下很危險的錯誤。
我確實很喜歡這種方法。但愿它會有助于減少大家對“curl [something] | bash”的厭惡感(但即使沒有起到這個作用,至少我們可以輕松控制容器里面的東西)。
7. 盒子中默認服務容器
如果我“認真對待”某個應用程序,會比較迅速地準備好合適的容器,為開發項目處理數據庫等服務,但我覺得擁有一系列“基本”的基礎設施容器非常重要,我可以進行合適的調整/改動,就能啟動所選擇的數據庫,或者所選擇的有合適默認設置的隊列系統。
當然你也可以“基本上如愿以償”,只要試一試“docker run [某個應用程序名稱]”,祈禱Docker索引中有一個出色的替代者,而且這個替代者常常就在索引中。但我喜歡先審查,比如弄清楚它們如何處理數據,然后我更有可能將自己的修改后版本添加到自己的“庫”中。
比如說,我有一個Beanstalkd的Docker文件:
我的目標是能夠“OK,我將需要memcached、postgres和beanstalk”,運行三個快速“docker run”命令后,會啟動并運行三個容器,它們都針對我的環境和默認環境個人偏好已經過了改動,隨時可以使用。設置“標準”基礎設施部件應該只需要1分鐘,而不是占用開發本身的時間。
8. 基礎設施/粘合劑容器
許多這種模式專注于開發環境(這意味著生產環境方面還有更多需要討論的),但缺少了一大類別:粘合劑容器。
其目的是把你的環境組合成統一整體的容器。這是到目前為止有待我進一步研究的領域,不過我會提到一個特別的例子:
為了能夠輕松訪問我的容器,我有一個小小的haproxy容器。我有一個指向主服務器的通配符DNS項,一個iptable項允許訪問我的haproxy容器。Docker文件沒什么特別之處:
這里有趣的部分是haproxy.cfg,它由一段從“docker ps”的輸出結果生成后端部分(就像這里)的腳本和前端定義中的一批acls和use_backend語句共同生成,前端定義將[hostname].mydomain發送到右邊的backend.backend測試。
如果我想玩點花樣,可以部署像AirBnB的Synapse這樣的產品,但它擁有的選項數量之多超出了我對開發使用的要求。
就我的家用環境而言,這滿足了我的大多數基礎設施要求。我仍在不斷推出了一系列基礎設施容器,其目的是讓實際應用程序部署起來輕而易舉,就像我將一個完整的私有云系統向Docker遷移那樣。