文章摘要: 旨在使應用程式更容易在 Kubernetes Engine 和一般的容器中執行你為應用程式的邊車容器新增一個日誌代理(在同一pod中
本文介紹了一組使容器更易於運維的最佳實踐。這些實踐涉及安全性、監控和日誌記錄等廣泛的主題,旨在使應用程式更容易在 Kubernetes Engine 和一般的容器中執行。這裏討論的許多實踐都受到 12因子方法 的啓發 ,12因素方法是一個構建雲原生應用程式的優質資源。
這些最佳實踐的重要等級不一樣。例如,對於有些實踐,你可能在缺少他們的情況下在生產環境中成功執行,但另外一些實踐是不可或缺的。特別是,與安全相關的最佳實踐的重要性是主觀的,是否實現它們取決於你的環境和約束。
這些實踐並不適合零基礎的讀者,你需要事先了解Docker 和Kubernetes 的一些知識。此處討論的一些最佳實踐也適用於Windows容器,但大多情況下數假設你使用的是Linux容器。有關構建容器的建議,請參閱 構建容器的 最佳實踐 。
使用容器的原生日誌記錄機制
重要性:高
作為應用程式管理的一部分,日誌中包含寶貴的資訊,可讓人瞭解應用程式中發生的事件。Docker 和Kubernetes 致力於簡化日誌管理。
在傳統伺服器上,你可能需要將日誌寫入特定檔案並處理日誌輪換以避免填滿磁碟。如果有高階日誌系統,則可以將這些日誌轉發到遠端伺服器來集中它們。
通過容器可以將日誌寫入stdout 和stderr,因而容器提供了一種簡單且標準化的方式來處理日誌。Docker 捕獲這些日誌行,並允許你使用docker logs 命令訪問它們。作為應用程式開發人員,你不需要實現高階日誌記錄機制,試試用原生的日誌記錄機制吧。
平臺運營商必須提供一個系統來集中日誌並進行搜尋,你可以使用Kubernetes Engine 提供的 fluentd 和 Stackdriver Logging 。其他常見方法包括使用 EFK (Elasticsearch,Fluentd,Kibana)棧。
圖1.Kubernetes 中典型的日誌管理系統圖
JSON 日誌
大多數日誌管理系統實際上是時序資料庫,用於儲存時間索引文件。這些文件通常以JSON 格式提供。在Stackdriver Logging 和EFK 中,單個日誌行和一些後設資料(容器組、容器、節點等相關資訊)一起被儲存為一個文件。
你可以直接通過將不同欄位以JSON 格式進行日誌記錄。然後,可以根據這些欄位更有效地搜尋日誌。
例如,考慮將以下日誌轉換為JSON格式:
[2018-01-01 01:01:01] foo - WARNING - foo.bar - There is something wrong.
這是轉換後的日誌:
{ "date": "2018-01-01 01:01:01", "component": "foo", "subcomponent": "foo.bar", "level": "WARNING", "message": "There is something wrong." }
通過這種轉換,你可以在日誌中輕鬆搜尋所有 WARNING 級別日誌或foo.bar 元件中的所有日誌。
如果你決定記錄JSON 格式的日誌,請注意必須在每一行上加入事件才能正確解析。在實際中,它看起來是下面這樣:
{"date":"2018-01-01 01:01:01","component":"foo","subcomponent":"foo.bar","level": "WARNING","message": "There is something wrong."}
如你所見,結果遠不如正常的日誌可讀。如果決定使用此方法,請確保你的團隊不會嚴重依賴手動日誌檢查。
邊車模式的記錄聚合器
某些應用程式(如 Tomcat )無法通過簡單配置來生成日誌。這些應用程式在磁碟上寫入不同的日誌檔案,所以在Kubernetes 中處理它們的最佳方法是使用邊車模式進行日誌記錄。邊車是一個小容器,與應用程式在同一個pod中執行。有關邊車的更詳細資訊,請參閱 Kubernetes 官方文件 。
在這種解決方案中,你為應用程式的邊車容器新增一個日誌代理(在同一pod中,)並在兩個容器之間共享 emptyDir 卷,示例: GitHub 上的這個YAML 示例 。然後,配置應用程式將日誌寫入共享卷,接著配置日誌代理進行讀取,並轉發到需要的地方。
在此模式中,因為沒有使用Docker 和Kubernetes 原生的日誌記錄機制,所以必須處理日誌輪換。如果你的日誌代理程式不處理日誌輪換,則同一pod中的另一個邊車容器會處理。
圖2.日誌管理的邊車模式
確保容器是無狀態且不可變的
重要性:高
如果你是第一次嘗試容器,請不要將它們視為傳統伺服器。比如,你可能想要在正在執行的容器內更新應用程式,或者在出現漏洞時給正在執行的容器打補丁。從根本上 說 ,容器 不是 以這種方式工作的。它們被設計成了 無狀態且不可改變 。
無狀態
無狀態意味著任何狀態(任何型別的持久資料)都儲存在容器之外。這個外部儲存可以採取多種形式,具體取決於你的需求:
- 要儲存檔案,我們建議使用 Cloud Storage 等物件儲存。
- 要儲存使用者會話等資訊,我們建議使用外部的低延遲鍵值儲存,例如 Redis 或Memcached。
- 如果需要塊級儲存(例如資料庫),則可以使用連線到容器的外部磁碟。對於Kubernetes Engine,我們建議使用 持久化 磁碟 。
通過以上方法將資料從容器本身中移出,這意味著可以隨時乾淨地關閉和銷燬容器,而不必擔心資料丟失。如果建立了一個新容器來替換舊容器,則只需將新容器連線到同一資料儲存區或將其繫結到同一磁碟即可。
不變性
不可變 意味著容器在其生命週期內不會被修改:沒有更新,沒有補丁,沒有配置更改。如果必須更新應用程式程式碼或打補丁,則需要構建新映象並重新部署。不變性使部署更安全,更可重複。如果需要回滾,只需重新部署舊映象即可。此方法允許你在每個環境中部署相同的容器映象,使它們儘可能一致。
爲了在不同環境中使用相同的容器映象,我們建議你外部化容器配置(偵聽埠,執行時選項等)。容器通常配置有環境變數或掛載到特定路徑上的配置檔案。在Kubernetes中,你可以使用 保密字典 和 配置集 作為環境變數或檔案將配置注入到容器中。
如果需要更新配置,請使用更新的配置部署新容器(基於相同的映象)。
圖3. 如何更新將配置集掛載為容器組中的配置檔案的部署中的配置
無狀態和不變性的結合是基於容器的基礎設施的賣點之一。這種組合允許你自動化部署並提高其頻率和可靠性。
避免使用特權容器
重要性:高
在虛擬機器或裸機伺服器中,你會避免使用root 使用者執行應用程式,原因很簡單:如果應用程式受到攻擊,攻擊者就可以完全訪問伺服器。出於同樣的原因,請避免使用特權容器。特權容器是一個容器,可以訪問主機的所有裝置,繞過容器的幾乎所有安全功能。
如果你認為需要使用特權容器,請考慮以下備選方案:
- 通過 Kubernetes 的 securityContext選項 或Docker 的–cap-add 標誌為容器提供特定功能 。該 Docker 文件 同時列出了預設啟用和必須明確啟用的功能。
- 如果你的應用程式必須修改主機設定才能執行,請在邊車容器或 初始化容器 中修改這些設定 。與你的應用程式不同,這些容器不需要暴露於內部或外部流量,更加獨立。
- 如果需要在Kubernetes 中修改sysctls,請使用 專用註解 。
在Kubernetes 中,特權容器可以被特定的Pod 安全策略 禁止 。Pod安全策略是叢集管理員配置和管理的Kubernetes 物件,它強制執行對pod的特定要求。在Kubernetes 叢集中,你無法建立違反這些要求的pod。
使應用程式易於監控
重要性:高
與日誌一樣,監控是應用程式管理的一個組成部分。在許多方面,監控容器化應用的原則與非容器化應用的監控相同。但是,由於容器化的基礎架構往往是高度動態的,伴隨著頻繁建立或刪除的容器,你無法每次都去重新配置監控系統。
你可以區分兩種主要的監控型別: 黑盒監控 和 白盒監控 。黑盒監控是指從外部檢查應用程式,你就是終端使用者。如果你想要最終提供的服務可用且有效,則黑盒監控非常有用。由於它位於基礎設施外部,因此 黑盒 監控在傳統基礎設施和容器化基礎設施之間沒有區別。
白盒監控是指使用某種特權訪問檢查應用程式,並收集終端使用者無法檢視的度量指標。由於白盒監控必須檢查基礎架構的最深層,因此傳統基礎架構和容器化基礎架構的差異很大。
Prometheus是Kubernetes 社羣中用於白盒監控的一個流行選擇,可以自動發現必須監控的容器。Prometheus 以期望的特定格式獲取容器組的指標。 Stackdriver 能夠監控Kubernetes叢集,應用也可以執行自己的的Prometheus 。瞭解如何 在Kubernetes Engine上啟用Stackdriver Kubernetes Monitoring 。
以下是一個Stackdriver Kubernetes Monitoring的演示例項:
圖4.Stackdriver Kubernetes Monitoring中的儀表板
要從Prometheus 或Stackdriver Kubernetes Monitoring 中受益,應用程式需要按照Prometheus 的格式公開指標。你可以按照以下兩種方法來做。
HTTP端點度量
HTTP 端點度量的工作方式與後面提到的 公開應用程式的執行狀況 的端點類似 。它通常在/metrics URI 上公開應用程式的內部指標。響應如下:
http_requests_total{method="post",code="200"} 1027 http_requests_total{method="post",code="400"} 3 http_requests_total{method="get",code="200"} 10892 http_requests_total{method="get",code="400"} 97
在這個例子中,http_requests_total 是度量,method 和code 是標籤,最右邊的數字是該指標對於這些標籤的值。上圖中所示,自啟動以來,該應用程式已使用400錯誤碼響應了97次HTTP 的GET 請求。
通過已有的多種語言的 Prometheus客戶端庫 ,可以輕鬆生成此HTTP 端點 。 OpenCensus 還可以使用此格式(以及許多其他功能)匯出指標。不要將此端點暴露給公共網路。
Prometheus 官方 文件 詳細介紹了該主題。你還可以閱讀 站點可靠性工程的 第6章 ,以瞭解有關白盒(和黑盒)監控的更多資訊。
用於監控的邊車模式
並非所有應用程式都可以使用/metrics HTTP 端點進行檢測。爲了保持標準化監控,我們建議使用邊車模式以正確的格式匯出指標。
日誌聚合邊車模式 部分介紹如何使用邊車容器來管理應用程式日誌。你可以使用相同的模式進行監控:邊車容器託管監控代理程式,該代理程式將應用程式公開的度量標準轉換為全域性監控系統可以理解的格式和協議。
考慮一個具體示例:Java 應用程式和Java Management Extensions(JMX)。許多Java 應用程式使用JMX 公開指標。利用 jmx_exporter ,你可以不必重寫應用程式就公開Prometheus 格式的指標。jmx_exporter 通過JMX 從應用程式收集指標,並通過Prometheus 可以讀取的/metrics 端點公開它們。這種方法還具有限制JMX 端點暴露的優點,因為它可以用來修改應用程式設定。
圖5.用於監控的邊車模式
暴露應用程式的健康狀況
重要性:中等
爲了便於在生產中進行管理,應用程式必須將其狀態傳達給整個系統:應用程式是否正在執行?它健康嗎?它準備好接收流量嗎?它是如何表現的?
Kubernetes 有兩種型別的健康檢查:活性探針(liveness probes )和就緒探針(readiness probes)。如下文所述,每個都有特定的用途。你可以通過多種方式實現這兩種方式(包括在容器內執行命令或檢查TCP埠),但首選方法是使用此最佳實踐中描述的HTTP 端點。有關此主題的更多資訊,請參閱 Kubernetes文件 。
注意:本節中給出的路徑只是一種約定。HTTP 端點的實際路徑可能因應用程式而異。
活性探針
實現 活性探針 的推薦方法是讓應用程式公開/health HTTP 端點。在此端點上收到請求後,如果認為健康,應用程式應傳送「200 OK」響應。在Kubernetes 中,健康意味著容器不需要被殺死或重新啟動。影響健康的因素因應用程式而異,但通常意味著以下內容:
- 應用程式正在執行。
- 它的主要依賴性得到滿足(例如,它可以訪問其資料庫)。
就緒探針
實現 就緒探針 的推薦方法是讓應用程式公開/ready HTTP 端點。在此端點上收到請求後,如果應用程式已準備好接收流量,則應傳送「200 OK」響應。準備接收流量意味著以下內容:
- 該應用程式是健康的。
- 完成任何潛在的初始化步驟。
- 傳送到應用程式的任何有效請求都不會導致錯誤。
Kubernetes 使用就緒探針來編排應用程式的部署。如果更新 部署 ,Kubernetes 將對屬於該部署的pod進行滾動更新。預設更新策略是一次更新一個pod:Kubernetes 在更新下一個pod之前等待新pod準備就緒(如就緒探針所示)。
注意:在許多應用程式中,/health 和/ready 端點合併爲一個/health 端點,因為它們的健康狀態和就緒狀態之間沒有真正的區別。
避免以root 身份執行
重要性:中等
容器提供隔離:使用預設設定,Docker 容器內的程序無法訪問來自主機或其他並置容器的資訊。但是,由於容器共享主機的核心,因此隔離不像虛擬機器那樣完整。攻擊者可以找到未知的漏洞(在Docker 或Linux 核心本身中),這些漏洞將允許攻擊者從容器中逃脫。如果攻擊者確實發現了漏洞並且你的程序在容器內以root 身份執行,則他們將獲得對主機的root訪問許可權。
圖6.左側,虛擬機使用虛擬化硬體。右側,容器中的應用程式使用主機核心。
為避免這種可能性,最佳做法是不在容器內以root 身份執行程序。你可以使用 PodSecurityPolicy 在Kubernetes中強制執行此行為 。在Kubernetes中建立pod時,使用 runAsUser 選項 指定正在執行該程序的Linux 使用者。這種方法會覆蓋Dockerfile 中的USER指令。
實際上,存在挑戰。許多軟體包都以root 身份執行其主程序。如果要避免以root 使用者身份執行,設計你的容器使用未知的非特權使用者執行。這種做法通常意味著你必須調整各種資料夾的許可權。在容器中,如果按照 一個容器一個應用 的最佳實踐,並且一個應用一個使用者( 最好不是root 使用者 ),則授予所有使用者對資料夾和檔案的讀寫許可權不是問題 。
檢查容器是否符合此最佳實踐的一種簡單方法是在本地使用隨機使用者執行容器並測試是否正常工作。替換[YOUR_CONTAINER]為你的容器名稱。
docker run --user $((RANDOM + 1))[YOUR_CONTAINER]
如果容器需要外部卷,則可以配置 fsGroup Kubernetes選項 以將此卷的所有權授予給特定的Linux 組。此配置解決了外部檔案所有權的問題。
如果你的程序由非特權使用者執行,則它將無法繫結到1024以下的埠。這不是什麼大問題,因為你可以配置Kubernetes 服務將流量從一個埠路由到另一個埠。例如,你可以配置HTTP 伺服器繫結到8080埠,並通過Kubernetes 服務從80埠將流量重定向回來。
仔細選擇映象版本
重要性:中等
當你使用Docker 映象時,無論是作為Dockerfile 中的基礎映象,還是作為Kubernetes中部署的映象,你都必須選擇正在使用的映象的標籤。
大多數公共和私有映象都遵循 構建容器最佳實踐 中所述的標籤系統 。如果映象使用 語義版本控制 的系統 ,則必須考慮一些標籤細節。
最重要的是,「latest」標籤可以在映象之間頻繁移動。結果是你無法依賴此標籤進行可預測或可重現的構建。例如,採用以下Dockerfile:
FROM debian:latest RUN apt-get -y update && apt-get -y install nginx
如果你在不同的時間使用這個Dockerfile 構建兩次映象,你最終會得到兩個不同版本的Debian 和NGINX。相反,考慮這個修訂版:
FROM debian:9.4 RUN apt-get -y update && apt-get -y install nginx
通過使用更精確的標籤,你可以確保生成的映象始終基於Debian 的特定子版本。因為特定的Debian 版本還附帶了特定的NGINX 版本,所以你可以更好地控制正在構建的映象。
這個結果不僅適用於構建時,也適用於執行時。如果你在Kubernetes 清單中引用「latest」標籤,則無法保證Kubernetes 將使用的版本。叢集的不同節點可能會在不同時刻拉取相同的「latest」標籤。如果標籤已經在拉動之間的某個點更新,則最終可能會在不同的節點執行不同的映象(這是因為同時打上了「latest」標籤)。
理想情況下,你應始終在FROM 行中使用不可變標籤。此標籤允許你重現構建。但是,存在一些安全性權衡:你固定使用的版本越多,安全補丁在映象中的自動化程度就越低。如果你使用的映象使用正確的語義版本控制,則補丁版本(即「X.Y.Z」中的「Z」)不應具有向後不相容的更改:你可以使用「X.Y」標籤並自動修復錯誤。
注意:標籤在Docker 中不是真正不變的。只要映象的所有者決定更改標籤。但是,「X.Y.Z」標籤實際上幾乎總是不變的。
設想一下名為「SuperSoft」的軟體。假設SuperSoft 的安全過程是通過新的 補丁 版本來修復漏洞。你想自定義SuperSoft,並編寫了以下Dockerfile:
FROM supersoft:1.2.3 RUN a-command
一段時間後,供應商發現了一個漏洞,併發布了SuperSoft 的1.2.4版本來解決這個問題。在這種情況下,你可以隨時瞭解SuperSoft 的補丁並相應地更新Dockerfile。如果你在Dockerfile 中使用 FROM supersoft:1.2 進行替換,則會自動拉取新版本。
最後,你必須仔細檢查正在使用的每個外部映象的標籤系統,判定你對構建這些映象的人員的信任程度,並確定要使用的標籤。
原文連結
感謝張嬋對本文的審校。