Kubernetes 環境的維運,安全性和效能是兩大核心挑戰。本文除了探討如何透過 RBAC、網路策略、安全准入控制器等機制強化叢集安全,也深入程式碼安全層面,涵蓋容器映像掃描、最小許可權原則及供應鏈安全等實踐。此外,文章也介紹混沌工程和負載測試方法,藉由模擬真實錯誤場景和高負載情境,及早發現潛在問題並提升應用程式韌性。最後,文章也討論了實驗設計在服務最佳化中的重要性,並提供程式碼範例說明如何進行功能標記和 A/B 測試等實驗,以持續提升使用者經驗。
Kubernetes 安全最佳實踐與程式碼安全
在 Kubernetes 環境中,安全性是一個多層次且持續性的挑戰。為了確保叢集的安全,需要採取多種措施來保護控制平面、節點、容器和程式碼。本文將介紹 Kubernetes 安全最佳實踐和程式碼安全相關的工具和技術。
Kubernetes 安全最佳實踐
- 結合 Node 和 RBAC 授權器:使用 Node 和 RBAC 授權器,並結合 NodeRestriction 准入外掛,以確保叢集的安全性。
- 保護叢集控制平面:使用強大的身份驗證和授權機制來保護控制平面。
- 審查 Operator API 許可權:確保 Operator API 許可權遵循最小許可權原則。
- 實施最小許可權原則:限制使用者、Pod 和服務帳戶的存取和許可權。
- 網路策略:實施網路策略以限制 Pod 和名稱空間之間的流量。
- 啟用安全准入控制器:確保啟用建議的安全准入控制器。
- 使用 Seccomp、AppArmor 和 SELinux:最小化 Linux 核心攻擊面。
- 安全組態動態 Webhook 准入控制器:確保動態 Webhook 准入控制器的組態是安全的。
程式碼安全
- 非 Root 和 Distroless 容器:組態容器以非 Root 使用者執行,並使用 Distroless 或 Scratch 容器以減少攻擊面。
- 容器漏洞掃描:使用工具如 Trivy 對容器映像進行漏洞掃描。
- 程式碼倉函式庫安全:使用 SLSA(Supply-Chain Levels for Software Artifacts)框架和 OpenSSF Scorecard 來提高程式碼倉函式庫的安全性。
非 Root 和 Distroless 容器實作範例
# 使用非 Root 使用者執行容器
RUN useradd -ms /bin/bash appuser
USER appuser
內容解密:
RUN useradd -ms /bin/bash appuser:建立一個新的使用者appuser,並設定其預設 shell 為/bin/bash。USER appuser:將容器的執行使用者切換為appuser,確保應用程式以非 Root 使用者執行。
程式碼安全最佳實踐
- 審查 Operator API 許可權:確保 Operator API 許可權遵循最小許可權原則。
- 組態容器建置檔:設定容器建置檔以非 Root 使用者執行應用程式程式。
- 使用安全的容器基礎映像:使用如 Scratch 和 Distroless 的容器基礎映像。
- 進行容器漏洞掃描:對容器進行漏洞掃描,並根據掃描結果決定是否佈署容器。
- 檢視 OpenSSF Scorecard:檢視所依賴的開源專案的 OpenSSF Scorecard 評分。
- 實施 SLSA:實施 SLSA Level 1 以提供基線級別的透明度和完整性。
混沌測試的目標與前提條件
混沌測試的核心目標是將極端條件引入應用程式環境中,觀察應用程式在這些條件下的行為,特別是其失敗模式。雖然故意測試應用程式的失敗看似不尋常,但相較於在生產環境中發生故障,在測試環境中觀察到故障可以提供修復問題的機會,避免影響使用者或客戶。
當然,混沌測試的目標是引入真實的錯誤場景,以觀察應用程式的行為。引入過高程度的錯誤可能有助於強化應用程式在極端環境下的韌性,但如果這種極端情況永遠不會發生,那麼為此而投入的強化工作就是浪費。每個應用程式對於變異性和韌性的期望不同,例如行動遊戲的韌性要求遠低於飛機或汽車等關鍵系統。因此,瞭解應用程式的韌性要求和預期環境是進行高品質混沌測試的關鍵前提。
混沌測試的前提條件
要建立有效的混沌測試,必須瞭解應用程式可能遇到的環境條件,包括預期的錯誤頻率和錯誤型別。例如,如果已經使用了具備韌性的雲端儲存服務,那麼可能不需要測試應用程式對磁碟故障的容錯能力,但仍需要對與雲端儲存服務的通訊進行混沌測試。
在開始混沌測試之前,應思考應用程式中的風險,並找出想要引入錯誤的位置和頻率。思考頻率時,記住混沌測試不是針對平均情況進行測試,而是模擬可能一年或十年才會發生一次的極端環境。需要對應用程式有足夠的瞭解,才能描述出合理的場景。
另一個重要的前提條件是對應用程式的正確性和行為進行高品質的監控。除了引入混沌之外,還需要能夠觀察應用程式的操作,並有足夠的細節來判斷混沌的影響和找出需要強化的部分。高品質的監控對於任何生產級應用程式都是必要的,混沌測試也可以用來檢驗監控和日誌系統是否足以處理真實的故障。
對應用程式通訊進行混沌測試
對應用程式通訊進行混沌測試的一種簡單方法是,在客戶端和服務之間放置一個代理伺服器(proxy)。這個代理伺服器處理客戶端和伺服器之間的所有網路流量,並注入隨機故障,如額外的延遲、斷線或其他錯誤。其中一個流行的開源代理工具是ToxiProxy,由Shopify開發。使用ToxiProxy的方法之一是在每個實際服務前面執行一個ToxiProxy層。
ToxiProxy 組態範例
toxiproxy-cli create -l 0.0.0.0:8080 -u backend-real:8080 backend
內容解密:
此命令組態ToxiProxy監聽Pod內的8080埠,並將流量轉發到實際的後端服務(backend-real)。這裡:
-l 0.0.0.0:8080指定ToxiProxy監聽所有網路介面的8080埠。-u backend-real:8080指定將流量轉發到後端服務(backend-real)的8080埠。backend是建立的ToxiProxy名稱,用於後續管理和操作。
建立ToxiProxy Pod後,建立一個新的服務(backend)指向ToxiProxy Pods,這樣客戶端就會自動與混沌代理(ToxiProxy)進行通訊。然後,可以使用ToxiProxy命令列工具對應用程式新增混沌,例如:
kubectl exec $SomeToxiProxyPod -- toxiproxy-cli toxic add -t latency -a latency=2000 backend
內容解密:
此命令為backend這個ToxiProxy新增一個名為latency的有毒屬性,向所有透過此代理的流量新增2000毫秒的延遲。這裡:
kubectl exec $SomeToxiProxyPod在指定的ToxiProxy Pod中執行命令。toxiproxy-cli toxic add新增有毒屬性。-t latency指定有毒屬性型別為延遲。-a latency=2000設定延遲為2000毫秒。backend是目標ToxiProxy的名稱。
如果有多個ToxiProxy Pod,需要對每個Pod執行此命令,或使用指令碼自動化此過程。
混沌測試:強化應用程式的穩定性
在測試應用程式的運作時,除了模擬通訊不穩定的情況外,測試基礎設施不穩定或過載的情況也非常重要。最簡單的方法是刪除Pod。首先,你可以針對單一佈署(Deployment),使用簡單的bash指令碼根據標籤選擇器刪除隨機的Pod:
NAMESPACE="some-namespace"
LABEL=k8s-app=my-app
PODS=$(kubectl get pods --selector=${LABEL} -n ${NAMESPACE} --no-headers | awk '{print $1}')
for x in $PODS; do
if [ $[ $RANDOM % 10 ] == 0 ]; then
kubectl delete pods -n $NAMESPACE $x;
fi;
done
內容解密:
NAMESPACE和LABEL變數分別定義了名稱空間和標籤,用於篩選特定的Pod。kubectl get pods命令列出符合標籤選擇器的Pod,並擷取Pod名稱。- 迴圈遍歷所有Pod,並以10%的機率刪除每個Pod,模擬隨機的Pod故障。
當然,你也可以使用Kubernetes客戶端或現有的開源工具,如Chaos Mesh,來實作更完整的混沌測試。
擴充套件混沌測試
進一步地,你可以擴充套件測試範圍,同時刪除不同服務中的Pod,以模擬更大範圍的中斷。以下指令碼用於隨機刪除特定名稱空間中的Pod:
NAMESPACE="some-namespace"
PODS=$(kubectl get pods -n ${NAMESPACE} --no-headers | awk '{print $1}')
for x in $PODS; do
if [ $[ $RANDOM % 10 ] == 0 ]; then
kubectl delete pods -n $NAMESPACE $x;
fi;
done
內容解密:
- 該指令碼與前一個類別似,但刪除了標籤選擇器,從而對名稱空間中的所有Pod進行操作。
- 這模擬了更廣泛的中斷場景,有助於測試應用程式在極端情況下的還原能力。
模擬節點故障
你還可以模擬整個節點的故障。對於雲端Kubernetes叢集,可以使用雲端VM API關閉或重啟機器;對於實體基礎設施,可以直接斷電或重啟機器。無論是在實體還是虛擬硬體上,都可以透過執行特定命令使核心當機。
以下是一個簡單的指令碼,用於隨機使叢集中約10%的機器核心當機:
NODES=$(kubectl get nodes -o jsonpath='{.items[*].status.addresses[0].address}')
for x in $NODES; do
if [ $[ $RANDOM % 10 ] == 0 ]; then
ssh $x sudo sh -c 'echo c > /proc/sysrq-trigger'
fi
done
內容解密:
kubectl get nodes命令取得叢集中的節點列表,並擷取節點的IP地址。- 迴圈遍歷所有節點,並以10%的機率透過SSH連線到節點並執行命令,使核心當機。
Fuzz測試:強化安全與還原力
Fuzz測試與混沌測試類別似,但它關注的是輸入的極端情況,而不是基礎設施的故障。Fuzz測試透過引入技術上合法但極端的輸入(如重複欄位、超長資料或隨機值),來測試應用程式的還原力。這種方法常用於安全測試,因為它可以揭示潛在的漏洞或當機。
負載測試:評估應用程式效能
負載測試用於確定應用程式在負載下的行為。負載測試工具生成真實的應用程式流量,模擬實際生產環境下的使用情況。這種流量可以是人工生成的,也可以是從實際生產流量中錄製並重放的。
負載測試的目標
負載測試的核心目標是瞭解應用程式在負載下的行為。在構建應用程式時,通常只會接觸到少量使用者的流量,這不足以瞭解應用程式在真實負載下的表現。因此,負載測試對於瞭解應用程式在生產環境中的行為至關重要。
負載測試有兩個基本用途:估計當前容量和預防迴歸。預防迴歸是指使用負載測試來確保新版本的軟體能夠承受與前一版本相同的負載。每當我們推出新版本的軟體時,都會有新的程式碼和組態。雖然這些變更引入了新功能並修復了錯誤,但也可能引入效能迴歸。
負載測試的前提條件
負載測試用於確保應用程式在高負載下仍能正常運作。與混沌測試一樣,負載測試也需要應用程式具有可觀察性,以便在發生故障時能夠驗證應用程式的正確性並深入瞭解故障原因。
負載測試的挑戰與實踐
在進行負載測試之前,除了應用程式的核心可觀察性之外,另一個關鍵的前提是能夠為測試生成真實的負載。如果負載測試不能準確模擬真實世界的使用者行為,那麼它的價值將大打折扣。例如,假設負載測試不斷重複傳送相同的單一使用者請求,在許多應用程式中,這種流量將產生不切實際的快取命中率,從而使負載測試結果顯示出能夠處理大量負載的能力,但這在更真實的流量下是不可能的。
生成真實流量
根據應用程式的不同,生成真實世界流量模式的方法也有所不同。對於某些型別的只讀網站,例如新聞網站,重複存取每個不同的頁面使用某種機率分佈可能就足夠了。但對於許多應用程式,尤其是那些涉及讀寫操作的應用程式,生成真實負載測試的唯一方法是記錄真實世界的流量並重放它。最簡單的方法之一是將每個HTTP請求的完整詳細資訊寫入檔案,然後在稍後時間將這些請求重新傳送到伺服器。
挑戰與解決方案
然而,這種方法可能會遇到一些問題。首先,也是最重要的,是使用者隱私和安全問題。在許多情況下,對應用程式的請求包含私人資訊和安全令牌。如果將所有這些資訊記錄到檔案中以供重放,必須非常小心地處理這些檔案,以確保使用者隱私和安全得到尊重。
另一個挑戰與請求本身的及時性有關。如果請求中存在時間元件,例如有關最新新聞事件的搜尋查詢,那麼在這些事件發生後的幾周(或幾個月)後,這些請求的行為將會有很大不同。與舊新聞相關的訊息將會大大減少。及時性也會影回應用程式的正確行為。請求通常包含安全令牌,如果安全措施得當,這些令牌的壽命很短。這意味著記錄的令牌在驗證時可能無法正常工作。
最後,當請求將資料寫入後端儲存系統時,重放修改儲存的請求必須在生產儲存基礎架構的副本或快照中執行。如果不仔細設定,可能會對客戶資料造成重大問題。
建立負載模型
由於這些原因,簡單地記錄和重放請求雖然容易,但並不是最佳實踐。相反,更有用的方法是利用請求建立服務使用方式的模型。有多少讀取請求?針對哪些資源?有多少寫入請求?利用這個模型,可以生成具有真實特性的合成負載。
對應用程式進行負載測試
一旦生成了為負載測試提供動力的請求,剩下的就是將這些負載應用到服務上。不幸的是,這很少是簡單的事情。在大多數真實世界的應用程式中,都涉及到資料函式庫和其他儲存系統。為了正確模擬應用程式在負載下的行為,還需要寫入儲存系統,但不能寫入生產資料儲存。因此,為了正確地對應用程式進行負載測試,需要能夠啟動應用程式的真實副本及其所有依賴項。
一旦應用程式的克隆啟動並執行,就可以傳送所有請求。事實證明,大規模負載測試也是一個分散式系統問題。您會希望使用大量不同的Pod向應用程式傳送負載,以確保透過負載平衡器均勻分配請求,並使傳送超出單個Pod網路支援的負載成為可能。
選擇負載測試Pod的位置
您需要做出的選擇之一是是否在與應用程式相同的叢集中執行這些負載測試Pod,還是在單獨的叢集中執行。在相同叢集中執行Pod可以最大限度地提高可以向應用程式傳送的負載,但它確實會對將流量從網際網路引入應用程式的邊緣負載平衡器造成壓力。根據您希望測試應用程式的哪些部分,您可能希望在叢集內、叢集外或兩者都執行負載測試。
熱門工具
在Kubernetes中執行分散式負載測試的兩個熱門工具是JMeter和Locust。兩者都提供了描述要傳送到服務的負載的方法,並允許您將分散式負載測試機器人佈署到Kubernetes。
使用負載測試調優應用程式
除了使用負載測試來防止效能迴歸和預測未來效能問題之外,負載測試還可以用於最佳化應用程式的資源利用率。對於任何給定的服務,多個變數都可以進行調優,並且可以影響系統效能。出於本討論的目的,我們考慮三個變數:Pod數量、核心數量和記憶體。
資源調優的重要性
乍一看,似乎給定相同數量的副本乘以核心數,應用程式的效能應該相同。也就是說,具有五個Pod、每個Pod具有三個核心的應用程式應該與具有三個Pod、每個Pod具有五個核心的應用程式效能相同。在某些情況下,這是正確的,但許多情況下並非如此;服務的具體細節和瓶頸的位置通常會導致難以預測的行為差異。例如,使用Java、dotnet或Go等語言構建的提供垃圾回收的應用程式:具有一兩個核心時,應用程式對垃圾回收器的調優將與具有多個核心時大不相同。
同樣,更多的記憶體意味著更多的東西可以被儲存在快取中,這通常會帶來更好的效能,但這種好處有一個漸近極限。
實驗設計在服務最佳化中的重要性
在建構一個令人滿意的應用程式時,效能是一個至關重要的因素。負載測試確保了我們不會引入影響效能的迴歸問題,從而導致糟糕的使用者經驗。負載測試也可以作為一台時光機,使我們能夠預測應用程式在未來的行為,並對架構進行調整以支援額外的成長。同時,負載測試也有助於我們理解和最佳化資源使用,降低成本並提高效率。
實驗的目的
與混沌測試和負載測試不同,實驗的目的是找出改善使用者經驗的方法,而不是發現服務架構和運作中的問題。實驗是一種長時間對服務進行的變更,通常是在使用者經驗方面,其中一小部分使用者(例如,所有流量的1%)會接收到稍微不同的體驗。透過檢查對照組(沒有變更的組別)和實驗組(有不同體驗的組別)之間的差異,我們可以瞭解變更的影響,並決定是否繼續實驗或更廣泛地推出變更。
實驗目標
當我們建立一個服務時,我們通常會帶著一個目標。這個目標往往是提供一些對客戶或使用者有用的、易於使用且令人愉悅的東西。但是,我們如何知道是否達到了這個目標?相對容易看出我們的網站是否在混沌環境中當機,或者它只能處理少量的負載然後失敗,但是要了解使用者如何體驗我們的服務卻很難確定。
傳統方法
幾種傳統的方法可以用來瞭解使用者經驗,包括調查問卷,在調查問卷中,我們詢問使用者對目前服務的感受。雖然這可以幫助我們瞭解目前服務的效能,但是很難用調查問卷來預測未來變更的影響。就像效能迴歸一樣,在變更全面推出之前瞭解其影響要好得多。這是任何實驗的主要目標:以對使用者經驗的影響最小化的方式進行學習。
實驗的前提條件
就像我們小時候參加科學博覽會一樣,每一個好的實驗都始於一個好的假設,這也是我們服務實驗的自然前提。我們正在考慮進行一些變更,並且需要對其對使用者經驗的影響做出猜測。
衡量使用者經驗
當然,為了了解對使用者經驗的影響,我們還需要能夠衡量使用者經驗。這些資料可以來自前面提到的調查問卷,透過調查問卷可以收集到滿意度(“請給我們評分,一到五”)或淨推薦值(“你有多大可能將這項服務推薦給朋友?”)等指標。或者,它可以來自與使用者行為相關的被動指標(“他們在我們的網站上花了多長時間?”或“他們點選了多少頁面?”等等)。
一旦有了假設和衡量使用者經驗的方法,就可以開始實驗了。
設定實驗
設定實驗有兩種不同的方法。採用哪種方法取決於被測試的具體事物。可以將多個可能的體驗包含在單一服務中,也可以佈署兩個服務副本,並使用服務網格在它們之間導流。
方法一:單一服務中的多重體驗
第一種方法是將兩個版本的程式碼都納入釋出的二進位檔案中,並根據服務接收到的請求的一些屬性在實驗和對照之間進行切換。可以使用HTTP頭、cookie或查詢引數來讓使用者明確選擇加入實驗。或者,可以使用請求的特徵,例如源IP,隨機選擇使用者進行實驗。例如,可以選擇IP地址以1結尾的人進行實驗。
方法二:使用服務網格
常見的實作實驗的方法是使用明確的功能標記(feature flagging),其中使用者透過提供查詢引數或cookie來開啟實驗,從而決定是否選擇加入實驗。這是一種允許特定客戶試用新功能或展示新功能而不廣泛釋出的好方法。功能標記也可以用於在不穩定的情況下快速開啟或關閉功能。有許多開源專案,例如Flagger,可以用於實作功能標記。
程式碼實作範例
import random
def select_user_for_experiment(user_ip):
# 簡單的範例:根據使用者IP的最後一個位元決定是否加入實驗
return int(user_ip.split('.')[-1]) % 2 == 0
def feature_flagging(request):
# 簡單的功能標記實作
if 'experiment' in request.cookies:
return True
return False
# 使用範例
user_ip = '192.168.1.1'
if select_user_for_experiment(user_ip):
print("使用者被選入實驗組")
else:
print("使用者在對照組")
request = {'cookies': {'experiment': 'true'}}
if feature_flagging(request):
print("功能標記已開啟")
else:
print("功能標記未開啟")
內容解密:
select_user_for_experiment函式根據使用者IP的最後一個位元決定是否將使用者納入實驗,這是一種簡單隨機分配使用者到實驗組或對照組的方法。feature_flagging函式檢查請求中是否包含特定的cookie,以決定是否為使用者開啟實驗中的功能。- 這兩個函式展示瞭如何在服務中實作簡單的實驗控制邏輯,用於測試不同的功能或體驗對使用者行為的影響。