The KCNA Book — 重點摘要

Kubernetes (K8S) 入門基礎

Alan Wang
78 min readJun 24, 2024
Photo by zixuan Fu on Unsplash

本書 (2023 年初出版) 原用於準備 Kubernetes and Cloud Native Associate (KCNA) 認證測驗,但本身其實也可做為理解 Kubernetes (K8S) 與雲端技術的入門教材。本篇為重點翻譯,省去習題、插圖、廢話跟重複處。但請留意內容談論的技術可能部分已有變化或過時。

一:設定場景

虛擬化 (virtualization)

在很久很久以前,我們會把單一一個應用程式部署在實體伺服器上。這對實際資源跟公司資本而言其實是很浪費的,而且也會拖慢應用程式上線的速度,因為取得、裝設實體伺服器和把它們接上網路,然後再裝好作業系統,都是要時間的。

VMware 之類的虛擬化技術問世,開啟了在單一實體伺服器上執行多重應用程式的可能性。這表示我們不必再替每個新應用程式買一台伺服器了,而是以虛擬機 (virtual machines, VMs) 的方式在現有伺服器快速部署,並避開以下麻煩:

  • 不必再等財務部核准伺服器採購
  • 不必再等資料中心團隊架設和連接伺服器
  • 不必再等網路團隊開設權限讓伺服器接上網路
  • 不必再等系統管理者安裝作業系統

幾乎是一夜之間,我們就從浪費大筆時間金錢採購、安裝過度強大的伺服器的年代,轉換到能立即在既有伺服器產生虛擬機的地步。但業界的進步是從不止息的。

容器化 (containerization)

2010 年代早期,Docker 給了全世界一份大禮──簡單好用的容器 (containers)。

從高階角度來看,容器就是另一種形式的虛擬化,讓我們能在更少的伺服器上部署更多應用程式、而且加快部署速度。

硬體虛擬化將實體伺服器分割成多個虛擬機。每個 VM 看起來和用起來都像實體伺服器,這表示每個 VM 都有虛擬 CPU、虛擬記憶體、虛擬硬碟和虛擬網路卡。你在每個 VM 安裝作業系統,然後安裝應用程式,這表示若一台實體伺服器被分割成 10 個虛擬機,你就得安裝 10 次作業系統跟應用程式。

容器的分割是建立在作業系統層級,每個容器看起來和用起來都像正常的作業系統。這代表每個容器都有自己的進程樹 (process tree)、根檔案系統、網路介面等等。每個容器可以跑一個應用程式,因此若把一個伺服器的作業系統分割成 50 個容器,你就能跑 50 份應用程式。

這便是容器技術的概況。

容器 vs. VMs

容器和 VM 都是用來執行應用程式的虛擬化產物,但容器相對於 VM 有幾個重要優點。

首先是容器輕量得多,也比 VM 來得有效率。因此在同樣數量的實體伺服器上,企業能用容器運行的應用程式會多於用 VM 能跑的數量。

舉個例,一個組織擁有 10 台實體伺服器,也許能跑 100 個 VM (等於 100 個應用程式)。但若改用容器,數量就能增加到 500 個。之所以會這樣,原因之一是每個 VM 都需要自己的作業系統 (不管是 Windows 或 Linux),而每個作業系統都會消耗額外的 CPU、記憶體和磁碟空間。相對的,所有容器都會共用主機的作業系統,因此只有一個 OS 會消耗資源,使它有更多空間能運行應用程式。

此外,容器部署和執行也比 VM 快,因為 VM 自身包含龐大的作業系統,而且啟動時就得先啟動完整的 OS,耗去不少時間。

總歸來說,若將應用程式打包在容器中,你只打包應用程式跟其相依套件而已,不必把完整的 OS 塞進去。於是容器映像檔會更小和更易於分享──此外既然容器只需啟動應用程式,啟動的速度就會更快。

不過,在此我們仍得先承認 VM 相對於容器的幾個優點。VM 的專用 OS 對打包和應用程式啟動時間來說很不利,卻有安全性上的優勢。舉個例,如果容器主機的共享 OS 被鑽漏洞,那麼所有容器都會受其害。但既然 VM 的 OS 各自獨立,就無法彼此影響。

但撇開安全性不談,容器一般被認為是現代商業應用程式的最佳解決方案。

到這裡我們主要聚焦在伺服器之類的實體基礎設施,以及其最佳利用方式。現在我們則要著眼於應用程式的開發與管理。

單體應用程式 (monolithic app) vs. 微服務 (microservices)

過去我們會打造單體應用程式,在專用的實體伺服器上運作。單體應用程式指的是功能包山包海的應用程式,它可能會透過同一個應用程式打包並部署以下服務:

  • 網頁前端
  • 身分驗證
  • 購物車
  • 目錄
  • 持久性儲存區 (persistent store)
  • 回報問題

在此的重點是,這些服務都由單一一個團隊開發、包在單一應用程式內,安裝和更新也是單一應用程式。這意味著這種應用程式會極端複雜和難以應付。例如,若你只是要修補、更新回報服務或調整其規模,就得對整支應用程式這麼做。這使得所有變更都風險很高,導致開發團隊把更新濃縮成一年一度,在壓力爆表的周末加班之後來一次風險極高的更新作業。

這種開發模式在過去是可接受的,但對今日完全倚賴科技、需要應用程式對變遷迅速的市場做出及時回應的企業來說,在當代雲端時代這樣做已經行不通了。

微服務應用程式登場…

對於問題叢生的單體應用程式,解決之道便是轉換成雲原生微服務應用程式。

從高階角度來說,雲原生 (cloud native) 和微服務 (microservices) 是兩件不相關的東西。微服務是一種設計和開發應用程式的架構,雲原生則是一些應用程式功能 (feature),我們晚點會談到細節。

試想有同一個應用程式,需要包含網頁前端、驗證、購物車、目錄、持久性儲存區與報表產生功能。若要把它變成微服務應用程式,你得獨立開發、部署、更新以上每個功能和調整其規模,但它們仍會相互溝通和協作,好替使用者和客戶提供相同的整體應用程式體驗。

我們將每個功能稱為「服務」,而既然每個服務都很迷你,自然就變成「微」服務。因此新的應用程式會有一系列微服務,外加自身的獨立小程式。

這樣的微服務架構改變了許多事情,包括:

  • 每個微服務都能有自己的小型敏捷開發團隊
  • 每個微服務都能有自己的軟體發行與更新週期
  • 你可以在不影響其他服務的前提下調整微服務規模
  • 你可以在不影響其他服務的前提下修補與更新微服務

一般來說,採納微前端架構意味著比起單體應用程式,你能用更簡單、頻率更高的方式部署、修補、更新、調整規模與自我修復企業應用程式──微服務也因此成為雲原生的要件。許多組織都透過微服務化的應用程式天天推出安全、可靠的更新,甚至一天數次。

不過,微服務應用程式的分散化性質也帶來了一些挑戰。比如,每個微服務可以在完全不同的伺服器和虛擬機上運作,這表示它們得透過網路溝通,不僅增加網路複雜度,也會產生安全性問題。不同微前端的負責團隊亦得彼此溝通,並了解整個應用程式的狀況。但總歸來說,微服務架構的好處仍然遠遠超過它帶來的挑戰。

二:雲原生架構

定義雲原生架構

在了解雲原生架構時,第一個要了解的是雲原生包含一系列能力與實踐。沒錯──雲原生不只是在公用雲平台跑程式而已,這也描述了它能做的事和你得做的事。

雲原生能力包括具備適應力 (resilient)、規模可擴展 (scalable)、可觀察 (observable)、高度自動化且能輕易更新的應用程式和基礎設施。雲原生實踐則是關於如何打造團隊和程序,使之有利於協同合作、整合和公開治理。這產生的結果便是一個雲環境,能產生高品質的應用程式,可實現並回應現代企業的需求。

大多數雲原生應用程式也是微前端應用程式。如我們在前章解釋過的,微前端是一種軟體架構,將應用程式的每個功能以小型獨立程式的形式開發和部署。舉個簡單例子,一個應用程式若有網頁前端和資料庫後端,它就會有兩個微服務──前端後端各算一個。最常見的方式是將每個微服務以各自的一組容器部署,例如一開始前端有一個容器,後端則是另一個容器。若你需要調整前端的服務規模,加入更多相同的前端容器就好了。由此可見,微服務這種軟體架構,與雲原生講求的適應力、規模可調整性和軟體部署方式契合得非常好。

雖然雲原生的意義確實不只是在雲端跑程式,它的許多概念確實源自公用雲平台,在那裡也運作得最好。你當然可以在自家的資料中心實現雲原生,這樣也能替你的企業帶來諸多益處──讓自家基礎設施和應用程式可被觀察、自動化、甚至帶一部分的適應力和規模可調整性,要遷移到雲端、甚至做雲爆發 (cloud bursting) 也很簡單。然而,跟理論上資源無限的公用雲平台相比,自家平台也會永遠處於競爭劣勢。

我們來快速定義幾個關鍵名詞。下面沒有討論的詞彙將會在後續段落探討:

公用雲平台

第三方提供的隨付隨用基礎設施及服務,熱門的平台包括 Amazon Web Services、Microsoft Azure、Google Cloud Platform 和 Digital Ocean。其基礎設施和服務會與其他使用者共享,你無法控制其他人,只能使用其應用程式和依據你的使用量付費。多數雲平台都極為龐大,有些人會說其容量是無上限的。

自家資料中心

你自己擁有並管理的基礎設施及服務。你甚至也能擁有容納這些東西的實際建築,但這不是必要條件。這些平台跟公用雲相比通常都很小。

基礎設施

指伺服器、虛擬機、雲端執行個體、網路和儲存空間。

雲爆發

指將你的資料中心開到最大容量,並把雲端當成溢出區。舉個例,一個線上零售 app 可以在適逢節慶的周末將資料中心容量開到滿,並用公用雲來應付暫時的需求尖峰。app 使用者的請求也許是由自家資料中心或公用雲上的服務個體回應,但不會發現到有何區別。等到需求下降後,app 就會停止使用雲端。

下面我們來更深入各個主題。

適應力

有適應力的應用程式與基礎設施可以在故障時自我修復,事實上,有時我們會將適應力稱為「自我修復能力」(self-healing)。

一個簡單的基礎設施例子是節點池 (node pool);許多雲平台能讓你建立一群虛擬伺服器,即為所謂的 node pool。若你設定一個 node pool 有十台虛擬伺服器,而其中一台掛掉,系統可以自我修復、自動產生一個新 node 和加入節點池。

註:一個 node 在 Kubernetes 中代表一台實體或虛擬伺服器。每個 node 可執行若干 pods,而 pods 內可包含若干容器。

Kubernetes 同時提供應用程式以及基礎設施的自我修復能力。例如,若 Kubernetes 知道你的網頁前端微服務需要五個容器,而其中一個容器死掉,它就會產生新容器來確保你仍有五個容器在運作。

儘管以上的例子非常簡單,若將有適應力的應用程式結合有適應力的基礎設施,就會得到極為強大的結果。

低耦合及預期故障的建置方式

在設計有適應力的系統時,關鍵原則之一是預期東西一定會故障,驅使你打造具有容錯機制的東西。一個著名且歷久不衰的手法是磁碟鏡像處理,也就是將同樣的資料寫到兩個獨立的儲存空間內;當其中一套儲存空間失效時,你仍然能存取資料,因此就沒有停機或資料遺失問題。這個原則當中的關鍵字是何時而不是是否──東西何時會失效,而非東西是否會失效。

現代微服務應用程式的設計和建置,應該要充分體認到一個事實,就是元件每隔一段時間就會故障。應付這問題的簡單策略是讓微服務保持低耦合 (loose coupling),而且透過 API 相互存取。這兒有很多專業術語,所以下面我們打個非技術背景的比方。

試想你買了一樣商品,送來時缺了個零件。解法之一是聯絡客服部門,你得打一個通用客服號碼、讓客服專員填寫客服單並寄來遺失的零件。兩天後東西還是沒來,所以你再打一次,這回接的是另一個客服員。你問起之前那位客服員,得知她請了一星期的假跑去玩了,只得把客服單號碼交給現在這位處理。新客服員順利處理完客服單,因為整個客服部的資料儲存在外部共享空間,每個人都看得到資料。

在這個例子中,你跟客服部的關係就是低耦合,只靠一個電話號碼和一個客服單編號維繫。你的客服單儲存在一個分離的系統上,整個客服部都能存取。這表示不管你跟客服部的哪個人通電話,他們都能完成你的要求。

現在來舉個高耦合的例子,用一樣的範例,這回你是用客服專員的手機號碼聯絡對方。客服員把你的客服單號碼記在她自己的電腦裡,然後寄出遺失的零件。等到兩天後你還是沒收到,打電話去卻只聽到語音信箱。你找到另一位客服員的電話,但對方愛莫能助,因為他看不到你的客服單,資料連同另一位客服員的電腦被帶回家了。這就是高耦合的例子──你和客服部的某人以特定電話號碼維繫,而當對方不在場時,你需要的服務就垮了。

在雲原生微服務應用程式的世界中,你應該讓服務之間保持低耦合,並將資料儲存在應用程式之外,這樣單一微服務失效和改變時,整個應用程式才能維持運作。一個簡單範例是微服務 A 透過網路抽象層 (比如共享的 DNS 網域名稱) 和微服務 B 溝通,資料則儲存在外部倉儲裡。這樣若微服務 B 的任何執行個體 (instance) 掛掉,你還是能透過同樣的 DNS 名稱存取其他個體,並取得外部倉儲的相同資料狀態。

若微服務 B 的每個個體都把資料存在自己的本地環境,該個體失效時,資料狀態就會遺失,也沒有其他個體能接手。

自我修復能力

具適應力的應用程式與基礎設施的另一個重要特色,是能在沒有人類干預下恢復服務──系統自行治癒。

自我修復有三個關鍵概念:

  • 預期狀態 (desired state)
  • 觀察到的狀態 (observed state)
  • 協調 (reconciliation)

預期的狀態是你想要的狀態,觀察到的狀態是系統現有的狀態。調和性是試著讓觀察到的狀態符合預期狀態的過程。

試想以下例子。你告訴 Kubernetes 說你想要有五個 pod 運行版本 2.2 的網頁伺服器,這即為你的預期狀態。Kubernetes 會部署它們,並設立一個監看迴圈來觀察。此時預期狀態等於觀察到的狀態──五個版本 2.2 的網頁伺服器。但要是事情出錯,其中一個 pod 掛點,那麼觀察到的狀態就是 4 個 pod,而預期狀態仍然是 5 個。Kubernetes 會處理這個狀況,產生一個新的 pod (執行 2.2 版伺服器),好讓觀察到的狀態再度跟預期狀態一至。這個過程即稱為協調,也是它為何能在沒有人類干預下自我修復的關鍵。

健康狀態檢查

適應力的最後一個主題是健康狀態檢查 (health checks) 及探測 (probes)。這種額外檢查經常會包含所謂的應用程式智慧 (application intelligence) 中──後者這個詞代表檢查和測試你的應用程式是否如常運作。

例如,前面提到的預期狀態和協調例子,並沒有應用程式智慧可言。它只是確保永遠有五個執行 2.2 版伺服器的 pod 在運作,不會檢查 pod 內的應用程式是否正常運行。

舉個簡單的非技術性範例:你預期冰箱裡會有一瓶脫脂牛奶,打開冰箱看一下就能確定,但這樣無法檢查牛奶是否新鮮、或者紙盒裡是否還有牛奶。在這個情境下,應用程式健康狀態檢查做的就是拿起紙盒查看日期,也許聞一下味道… 重點是,快速的視覺檢查可以確保觀察到的狀態符合預期狀態 (你希望冰箱裡有一瓶牛奶),但這樣並不能確保牛奶的品質良好。

在雲原生世界中,健康狀態檢查通常是發出專門的請求,檢查應用程式是否運作如常。通常我們稱這些請求為探測

許多網頁應用程式會打開一個專門用於健康狀態檢查的端點,比如 /healthz。例如,若我們的應用程式網址為 knca-app.kubernetes.local,該端點就會位於 knca-app.kubernetes.local/healthz。對此端點送出請求,它就會傳回資料,讓你能用來判斷應用程式的健康狀況。

自動調整規模 (autoscaling)

自動調整規模是指應用程式和基礎設施能隨著需求而自動增減規模。它們若要真正符合雲原生特質,必須能做到三件事:

  • 應用程式以及基礎設施必須能夠調整規模
  • 調整規模必須是自動的
  • 調整規模方向可以是調高或調低

Kubernetes 有能力根據當前的工作負載量來調高或調低應用程式規模。它能用簡單的指標,像是 CPU 及記憶體用量,或者使用和應用程式相關的指標,如訊息佇列大小和請求回應時間等。

一個簡單的範例是一個應用程式的報表產生微服務。如果企業正在準備年終報告,報表微服務的請求量暴增到十倍,那麼 Kubernetes 可以啟動額外的容器來應付需求。等到需求跌落後,Kubernetes 即可將這些多餘容器砍掉。要是應用程式是部署在公用雲上,那麼 Kubernetes 更能自動調整基礎設施規模,以便能運行更多容器──常見的例子是 Kubernetes 能在節點池中加入更多 nodes。

關於以上例子,還有幾個重點得注意。規模增減之所以能自動化,這是拜 Kubernetes 持續監看你的應用程式的服務指標之賜。當它發現某些指標出問題,像是載入或回應時間變長,它就會擴大應用程式和基礎設施規模來因應,反之在需求降低時就會調低規模。後者對於控制營運成本是關鍵,特別是你在公用雲平台上得根據使用量來付費的時候。

微服務架構對這種行為也是關鍵,因為它允許 Kubernetes 只針對需要的微服務來調整規模。

最後,我們上述描述的規模調整是所謂的水平規模調整。從高階角度來說,這就是拿你已經有的東西產生更多執行個體。相對的,垂直規模調整是嘗試擴大既有資源的容量。以下的非技術性範例可以讓你更清楚瞭解。

回到年終報告的例子,如果一個小容器應付不來年終的需求,你可以水平加入更多小容器。替代方式是垂直擴大規模,也就是把小容器換成單一一個大容器。底下支援的基礎設施也可以如法炮製──你能水平加入多重小 node (伺服器),或是垂直地換成單一大 node。

一般說來,Kubernetes 與雲原生架構偏好水平規模調整。Kubernetes 支援以下兩種自動調整方式:

  • Horizontal Pod Autoscaler (HPA)
  • Cluster Autoscaler (CA)

HPA 會隨需求自動增減 pods 數量,而 CA 則得和公用雲平台整合,視需求增減 nodes 數量。而這兩個自動規模調整機制有個重要的地方,就是你能設定上下限。比如,你能要求 Kubernetes 限制節點池最大為 20 個 nodes。這能幫你控制成本,減少失控程序跟惡意攻擊讓節點池過度增長的風險。務必記得,nodes 的營運在雲端是要花錢的。

無伺服器 (Serverless)

Serverless 從高階角度來看,是一種事件驅動 (event-driven) 的計算模式,你撰寫一個小函式然後靠事件來處發其執行。這又是一堆術語,我們等等就會解釋,不過這裡要先探討兩件事:

  1. serverless 和 Function as a Service (FaaS) 大體上是指同一件事
  2. serverless 不是真的無伺服器

第二點想必會引起疑問:如果所謂的 serverless 還是會用伺服器,幹嘛這樣命名呢?

答案其實很簡單:伺服器被藏了起來,使得我們永遠不必去管它們。想想看,我們在雲端時代之前得花大量時間精力維護伺服器,你要購買、組裝和架設它們,支付維護費用,甚至在伺服器太老太舊時更換之。雲端的出現一除了大部分的伺服器維護作業。最後,serverless 則完全消除了伺服器作業。所以在 serverless 模式下,開發者可以完全專注在開發程式上,serverless 平台則肩負起該程式所需的運算和儲存空間。

Serverless 是事件驅動

事件驅動是 serverless 模式的關鍵。請看以下的 serverless 開發流程:

  1. 開發者撰寫程式
  2. 程式被打包成容器映像檔
  3. 程式被上傳到 serverless 平台
  4. 程式與一個事件連結起來
  5. 事件發生,使程式執行

在這種模式內,上傳到 serverless 平台的程式不會被執行,直到相關的事件發生為止。若事件從未發生,那程式就永遠不會跑,反之事件越常發生,程式被執行的頻率就越高。這在完全由第三方代管的雲平台上可是有財務上的重要性,因為你只需要替程式執行的那些時候付費;越常執行的程式,花的錢就會比不常執行的程式多。儘管許多 serverless 平台標榜低價,頻繁執行的程式其實仍能花上你可觀的費用。

大多現代程式語言都可用在 serverless 平台,但你應該依微前端模式來設計和撰寫 serverless 函式──只開發小型、專門、一次只做一件明確任務的函式。

serverless 函式也很常被包裝在容器內,因為這些應用程式小、輕量且符合微前端架構。但不是所有 serverless 平台都支援容器。

Serverless 平台

很多雲端服務商都有自己的私營 serverless 平台,通常易用且走隨用隨付的收費模式。不過,既然它們大多是私營平台,通常也缺乏治理功能和統一的技術標準。

熱門的雲端 serverless 平台包括:

  • AWS Lambda
  • AWS Fargate
  • Azure Functions
  • Google Cloud Functions

至於熱門、支援 Kubernetes 的開源 serverless 平台則包括:

  • Knative (發音 K-native)
  • OpenFaaS (發音 open faz)

在本書撰寫時,serverless 生態系仍欠缺充足的管理和標準,不過 CNCF (雲端原生運算基金會) 內發展中的 CloudEvents 計畫正在試著改善這點。

註:CloudEvents 定義了統一的事件格式 (可以是 HTTP、JSON、Kafka MQ 等格式) 來觸發 serverless functions。

Serverless 與規模調整

對 serverless 平台來說,規模調整是其至關重要的成分之一──因為平台得提供新的容器與基礎設施來執行你的函式,然後在執行完成後自動釋放資源。若一個函式每分鐘被執行一百萬次,平台照樣能提供服務容量,並根據你的使用量對你收費。

當 serverless 服務進入市場時,有許多人猜想它會成為主流的現代應用程式典範。幾年過去後,這種狂熱有所消褪,但仍然看得出來 serverless 會繼續與容器和 VM 一塊塑造雲端運算的未來。

社群與治理

包含 Kubernetes 在內,許多雲原生技術是開源的,使它們得以接觸並支援龐大、熱情的開發社群。但是,他們仍然需要強而有力的監督才能指引方向和帶來秩序。

為了因應這種需求,雲端原生運算基金會 (Cloud Native Computing Foundation, CNCF) 於 2015 年創立。它定義了三個成熟階段,專案得分別通過才能「畢業」:

  1. Sandbox (沙盒)
  2. Incubating (孵化中)
  3. Graduated (畢業)

這個三階段過程讓雲端技術專案能夠發展成熟,成為可正式投入業界的專案,以便繼續發展和替社群帶來價值。Kubernetes 就是 CNCF 的第一個專案,於 2018 年畢業。再那之後,有超過 15 個其他專案也陸續畢業、並有超過一百個專案處於沙盒或孵化中階段。

若專案想要通過這些階段,得對 CNCF 的技術監督委員會 (Technical Oversight Committee, TOC) 證明它夠成熟。每個階段都有一系列條件,比如下面是畢業的一部分條件:

  • 證明專案已經被真正的使用者主動使用
  • 有至少兩個組織的人員參與專案發展
  • 完成獨立的第三方安全稽核並發布結果
  • 獲得核心基礎架構聯盟 (Core Infrastructure Initiative) 的最佳實務標章 (Best Practices Badge)

要滿足的條件其實不僅於此,但已經足以證明 CNCF 提供了什麼樣的治理──竭盡所能地將專案推向成熟且遵循最佳做法。但另一方面,CNCF 也盡可能讓監管維持在最低限度,好讓專案能學會監督自己。

角色與身分

想要走在雲端,關鍵不只是在使用的技術而已,也是在於創造協同合作的文化和運作正確的環境。後者有很大一部分來自安排適當的角色和職位,並讓他們獲得資深管理階層的全力支持。

下面是你在真實世界中會看到的典型雲端職稱與角色。

雲端架構師 (Cloud Architect)

雲端架構師的主要責任是選擇最適合企業與個人應用程式的雲端平台。選擇的結果可能會由公開雲、私有雲或混合雲組成。他們也負責把基礎設施和應用程式設計成符合雲原生的特質──具備適應力、規模可擴展性、可觀察性和自動化等等。

雲端架構師也必須了解成本多寡,以及資本支出 (capital expenditure) 跟營業費用 (operating expense) 有何差異。公開雲通常是營業費用,根據使用量來租用基礎設施,而自家環境或私有雲則通常是資本支出,你得先支付一大筆費用採購基礎設備。

DevOps 工程師

如其名稱所示,DevOps 工程師結合了開發人員和營運人員的技能,不僅有寫程式的經驗,也了解監看、日誌和部署等營運工具。通常,DevOps 工程師是 80% 開發人員加上 20% 營運人員。

一般對於 DevOps 工程師的期望如下:

  1. 了解整個應用程式與應用程式生命週期
  2. 對於合適的工具十分熟練
  3. 抱持積極態度,不會害怕改變會失敗

DevOps 工具包括持續整合、持續部署 (continuous integration, continuous development 或 CI/CD),以及能自動化應用程式開發、部署及測試的 GitOps 工具。

DevOps 這種角色之所以被創造出來,是要打破開發團隊與營運團隊的籓籬──他們經常會彼此拉鋸,這對企業或顧客都不是好事。最好的 DevOps 工程師能夠具備雙方的經驗,並願意透過彼此的角度去看事情。

DevOps 工程師也必須有足夠的自信,將團隊與組織推往雲原生文化,讓改變變成常態。

CloudOps 工程師

這是比較新的職位,很類似 DevOps 工程師,但管轄的範圍較窄,偏重在雲端基礎設施和工具。他們可能會更專注於公開雲技術,相對地 DevOps 工程師則需要更廣泛的技能來管理私有基礎設施跟應用程式。

安全性工程師 (Security Engineer)

安全性工程師必須了解雲原生基礎設施和應用程式,以及特定的弱點會出現在何處。這些威脅和傳統在自家資料中心運行的單體應用程式所面臨的問題相比,可能會有很大的不同。

舉個簡單範例:微服務應用程式開始越來越倚賴透過 IP 網路分享資料。以往單體應用程式把所有功能實作在同一個可執行檔內,以單一大程式的形式部署在單一實體或虛擬伺服器上,因此內部溝通只會透過該伺服器的安全管道進行。但微服務模型把所有應用程式功能打散成獨立小程式,分散在多重伺服器上。既然程式之間的通訊得通過 IP 網路,就會帶來新的風險。

另一個簡單範例是大多數容器平台建置時使用的共享 OS 模式。若主機遭到入侵,那麼在主機上跑的所有容器都會受害。VM 就不會遇到這種威脅,因為它們的 OS 是分離的。

從文化角度來看,安全性工程師必須讓自己融入其他團隊,好確保應用程式和基礎設施的每一步設計、開發與部署都將安全性視為重要優先,而非當成事後才想到的議題。

DevSecOps 工程師

DevSecOps 工程師就是 DevOps 工程師再加上注重安全性的思維。大多數人認為這職位和 DevOps 沒兩樣,畢竟許多組織現在都預期 DevOps 功能師能懂安全性的重要、具備安全技能並注重安全。無論如何,DevSecOps 這種職位通常被認為能確保安全性能被納入設計、部署及開發階段的所有討論。

你或許聽過一個說法:將安全性「左移」(left-shift),也就是把安全性挪到開發過程的最開端 (流程表的最左邊)。這是 DevSecOps 功能師能做到的事情。

資料工程師 (Data Engineer)

資料是許多組織的命脈──要是資料遺失或損壞,整個組織甚至有可能沒戲唱了。

資料工程師的角色是確保資料有正確地儲存和受到保護,並在有人需要時永遠可被存取。這包括資料局部性 (data locality,將資料存在正確的地方)、效能、可用性和資料保護。這也牽涉到對資料分析工具的知識與經驗。

全端工程師 (Full Stack Developer)

全端工程師是擁有多重技能的開發人員,能理解和開發系統的前端 (客戶端) 與後端 (伺服端) 元件。他們得具備多重程式語言的實際開發經驗,並操作過雲端平台、資料中心等基礎設施。

典型的全端工程師應該要至少熟悉兩種語言,懂得使用至少一種雲端平台和其工具,並操作過至少一種資料中心或資料庫技術。這類後端資料儲存技術通常是開源的。

人們預期全端工程師具備極高的生產力,並能憑著其廣泛技能快速做決策跟「搞定事情」。有時,萬能的全端工程師做事幾乎能比整個團隊快。

現場可靠性工程師 (Site Reliability Engineer, SRE)

現場可靠性工程師的主要角色是建置、維護與最佳化雲原生系統,有時被人們當成 DevOps 工程師 2.0,但比 DevOps 工程師會更注重在營運面。例如,典型的 SRE 可能是 20% 開發人員加上 80% 營運人員。

和 DevOps 工程師一樣,SRE 抱持著正向樂觀的態度,期待面臨持續改變。但他們做的不只是 DevOps,更會專注在重要的現場指標,如服務級別目標 (Service-Level Objective, SLO)、錯誤預算 (error budgets)、服務級別指標 (Service Level Indicator, SLI) 和服務級別協定 (Service-Level Agreement, SLA)。

有一種開玩笑的說法是:SRE 的工作就是把系統自動化,直到自己失業為止。

公開標準

公開標準是雲原生的一大重點,讓使用者免於被第三方廠商或產品套牢,也能讓開發人員有信心,知道他們打造的東西能與更廣大的生態系合作跟整合。

一個常見的非技術性範例是火車與鐵軌。全世界的主流軌距都相同 (1,435mm 標準距),讓運輸服務商能在不同的火車供應商之間切換,但知道這些火車都能在同樣的鐵道上跑。火車生產商同樣能確保他們建造的東西能用於世界任何角落。公開標準帶來信心,信心則帶動發展。

開放容器計畫 (Open Containers Initiative, OCI) 對雲原生生態系提出了以下重要標準:

  • 映像檔規格
  • 執行環境規格
  • 散佈規格

映像檔規格定義了建置和打包容器映像檔的標準方式;執行環境規格將容器的執行環境和容器生命週期標準化;散佈規格則統一了透過登錄服務散佈映像檔的方式。這三點結合起來,效果就會像四海皆準的火車軌距那樣,賦予開發者和使用者信心、催生出強勁的生態系。

試想以下例子:在 OCI 的標準之前,大家都在用 Docker 來打造容器映像檔。但有了 OCI 標準,開發者和新創公司就有信心打造自己的建置工具,或者從外面眾多選擇挑一個。對於容器執行環境和發佈也是如此,開發者和新創公司不必再只能仰賴 Docker runtime,或者總是透過 Docker Hub 來發佈映像檔。

以下是個和 OCI 標準相關的真實成功案例:Kubernetes 原本一開始就是使用 Docker runtime,但 Docker 逐漸從低階容器工具成長為對 Kubernetes 來說太複雜的東西。於是,Kubernetes 將容器引擎設計成可插拔式,讓使用者依自己的需求選擇最佳容器引擎 (runtime)。在這當中最棒的是,只要新的容器引擎符合 OCI 容器標準──而幾乎所有的新容器引擎都是如此──東西就仍能正常運作,也無須修改既有的映像檔。

OCI 目前是在 Linux 基金會底下運作,由主要的雲原生和容器企業的自願者組成,其目標是針對低階容器技術提供標準以及少量的監管。

目前對 Kubernetes 生態系來說很重要的標準包括:

  • 容器引擎介面 (Container Runtime Interface, CRI)
  • 容器網路介面 (Container Network Interface, CNI)
  • 容器儲存空間介面 (Container Storage Interface, CSI)
  • 服務網格介面 (Service Mesh Interface, SMI)

Kubernetes 的低階元件仰賴於以上這些標準,讓生態系內的夥伴能夠發展產品、並有信心產品能如常運作。這也能帶來可替換的環境跟產品,阻止被第三方廠商跟產品套牢的現象。我們在稍後各章會討論這些標準。

三:容器編排 (Container orchestration)

容器開發流程

下面是在容器上建置並執行應用程式的常見過程:

  1. 開發者撰寫應用程式
  2. 應用程式被打包成容器映像檔
  3. 映像檔被上傳到登錄服務分享
  4. 伺服器下載映像檔
  5. 伺服器以容器執行映像檔

我們來更仔細檢視這些步驟。

首先,開發人員用他們喜歡的語言撰寫應用程式──不需要為了容器特地學新語言。

接著,應用程式與其所有的相依套件會被打包成容器映像檔,我們通常直接簡稱為映像檔 (image)。既然所有相依套件都被打包在容器內,你就不必擔心應用程式能在開發人員的電腦上跑、到了實際上線環境就死翹翹,只因為實際上線環境的相依套件版本有出入。

常見的實務做法是將映像檔擺在中央登錄服務上,好讓它們能被不同環境 (開發、測試、部署環境) 和不同團隊存取。登錄服務很多,有些被設計成在你自己的私有網路 (在防火牆之內) 運作,有的則在公開網路上。Docker Hub 是第一個重要的登錄服務,也對整個網路公開,不過它也和所有登錄服務一樣分割成儲存庫,可以藏起來或限制存取。

一旦映像檔在登錄服務上分享,就可以被伺服器下載和以容器形式執行。你可以限制誰能下載映像檔,而伺服器可以是實體、虛擬甚至雲端執行個體伺服器。

映像檔被執行後就會產生容器,這表示容器是映像檔的執行個體。如果你用過 VMWare,你可以想成這就像用 VM 範本建立虛擬機一樣。不過請記住,映像檔和容器比 VM 範本跟 VM 輕量多了。

伺服器會用容器引擎來執行容器。我們稍後有專門的一節討論這部分。

容器與微服務

容器對於打造微服務架構十分重要。如我們在之前章節提過的,每個微服務通常負責應用程式的一個功能,且會有自己的容器。

舉個例,一個微服務應用程式會有前端、中介和後端程式,每個部分都能是一個容器化的微服務,並由專門的團隊負責打包跟部署。如果對應用程式某部分的使用需求增加,只要擴大該部分的容器數量即可。

容器架構

容器有時也被稱為虛擬作業系統。我們來比較一下它們和 VM 有何區別。

Hypervisor 虛擬化技術 (比如 VMware) 是將硬體虛擬化,這表示我們把實體伺服器分割成虛擬伺服器或虛擬機 (VM),每個都有自己的虛擬 CPU、儲存空間、網路卡等等。相對的,作業系統虛擬化 (如 Docker) 是將 Windows 或 Linux 這類 OS 分割成容器,每個容器有自己的虛擬程序樹、檔案系統和網路介面,可以執行自己的應用程式,比起 VM 就輕量許多。

容器引擎

容器可以在實體、虛擬或雲端執行個體伺服器上運行,這些伺服器從容器角度來看都是一樣的,因此我們可將它們統稱為節點 (node)。

每個 node 都需要用所謂的容器引擎來跑容器,包括從登錄服務下載映像檔,並建立、啟動、停止跟刪除 node 上的容器。Docker 是最著名的容器引擎,但並不是唯一一個。

開放容器計畫 (OCI)

Docker 是最早的容器引擎,至今也仍被廣泛使用,但 OCI 制定了一系列業界標準,讓開發者和企業能建立自己的容器引擎。事實上,Docker 就是 OCI 的創始成員之一,也大量參與這些標準的制定。

一些最熱門的容器引擎包括:

  • Docker
  • containerd (發音 container-D)
  • runC (發音 run-C)
  • CRI-O (發音 cry-oh,RedHat OpenShift cluster 的預設引擎)
  • gVisor (發音 G-visor)
  • Kata (在輕量 VM 內執行容器)

可以想見,每個引擎都有各自的優缺點。

Docker 存在已久,也已經不再只能做低階容器管理。它可以做類似 Kubernetes 的高階排程與實作,這導致在很多專案跟情境使用 Docker 就變成殺雞用牛刀。從 Kubernetes 1.20 版起,Docker 就不再是其預設引擎。

containerd 就是純粹的低階容器引擎,符合 OCI 規格且適用於其他 OCI 映像檔,是從 Docker 拆出來和於 2017 年捐給 CNCF 的。多虧其輕量特質,它已經成為 Kubernetes 上最常用的引擎。

容器引擎類型

容器與容器引擎主要有三類:

  • 以 namespace 隔離 (共用 OS)
  • 沙盒化 (代理 OS)
  • 虛擬化 (專用 OS)

以上術語其實可以用容器大小和安全性來區分。

以 namespace 隔離的容器是最快和最輕量的,但也最不安全。Docker、containerd 和 CRI-O 是這類容器的範例。

虛擬化容器相當於 VM,啟動最慢且耗費最多資源,但因為有自己的 OS,因此最為安全。Windows 容器和 Kata 容器即為此類範例。

沙盒容器嘗試在以上兩者之間取得平衡,速度、大小與安全性都介於兩者。這裡我們不討論它們如何實作,但通常是在容器跟主機 OS 之間建置一個代理層。gVisor 就是沙盒容器的範例之一。

Kubernetes 與容器

雖然 Kubernetes 的角色是容器編排器 (container orchestrator, 簡單說就是容器的管理和營運,這晚點解釋),它仍然需要容器引擎才能執行容器。

如前所提,Kubernetes 早期使用 Docker 為容器引擎,很多人也仍在這麼用,但現在 Kubernetes 實作了容器引擎介面 (CRI),讓使用者能選擇自己最喜歡的引擎。只要引擎符合 OCI 規格,就可以隨意抽換。在本文撰寫時,Kubernetes 預設的引擎是 containerd。

容器編排器基礎

我們一旦開始使用容器,就會發現需要幫手來管理它們,因為我們已經將大型應用程式打散成低耦合的微服務,以便隨需求調整規模、並能隨時發行新版本等等。於是原本極少變動的單體應用程式會被不斷改變的大量微服務取代,勢必得有人來看管它們。

Docker Swarm、Kubernetes 及 Mesosphere DCOS 等工具的問世都是要解決這種問題。長話短說,Kubernetes 成了當中最受歡迎的,如今也是全球最重要的雲原生技術。

從高階角度來說,Kubernetes 是所謂的容器編排器 (container orchestrator)。這個詞表示它能執行和管理容器化的應用程式,這之中包括一部分雲原生功能,如排程、自我修復和自動調整規模等。

試想以下簡單範例:你有個企業網站由前端和後端組成,包裝成兩個微服務容器。你挑選 Kubernetes 來執行應用程式和提供雲原生功能,要它用 containerd 引擎在三個 node 上分別跑三個容器──前端兩個,後端一個。這即為你的期望狀態

現在假設其中一個 node 有個前端容器失效。Kubernetes 的監看迴圈會注意到觀察到的狀態不等於期望狀態,於是自動啟動一個新的前端容器。或者若其中一個 node 掛掉,Kubernetes 就會建立一個新 node。這種特質我們就稱之為自我修復能力適應力

再來,試想你有設定應用程式的規模調整指標,而 Kubernetes 注意到前端有點難以滿足當前的使用量,於是自行將前端容器從兩個增加到四個。等到需求跌回去後,Kubernetes 就會將前端容器數量恢復成兩個。

以上我們展示的 Kubernetes 雲原生能力,就稱為 container orchestration

Kubernetes 架構基礎

一個 Kubernetes 環境叫做叢集 (cluster),並由兩種 node 組成:control plane nodes 及 worker nodes。

control plane nodes 是 Kubernetes 的骨幹──它執行所有雲原生功能的實作產物。worker nodes 則是使用者應用程式執行的地方。

我們來打個非技術背景的比方。Amazon 包含網站的網路服務和實體送貨服務,網路服務負責產品目錄、搜尋、購物車、願望清單等功能,也就是讓 Amazon 之所以為 Amazon 的那些東西,相當於 Kubernetes 的 control plane nodes。送貨服務則只是用來把商品交給使用者,有點像 worker nodes。

control plane nodes 務必維持高效能且隨時可以取用。例如,在不同的資料中心或雲平台區域各擺一個 control plane nodes 是實務上的良好做法;這表示若某個資料中心或雲區域因為停電斷線,也不至於導致所有的 control plane nodes 失效。一般來說,建議是提供三到五個 control plane nodes 來維持高度可用性。

容器安全性

容器會面臨幾種相關風險,我們將會逐一討論:

  • 共用 OS kernel
  • 根權限容器
  • 未加密網路
  • 未受信任的程式碼

容器和共用 OS kernel

kernel 是指作業系統的核心功能,比如 Windows OS 的 kernel 是 Windows NT kernel,而 Linux OS 則是 Linux kernel。通常 OS 和 kernel 可以視為同義詞,如我們前面所做的一樣。

以 namespace 隔離的容器 (namespaced containers) 是最普遍的容器形式,在同一個 node 上跑的容器都會共用同一個 OS kernel。這便是為何容器可以又小又快的原因,但這也代表一種安全性風險。VM 就沒有這種問題,而沙盒容器則嘗試在共用 OS kernel 的前提下保護個別容器。

根權限容器

root 是 Linux 系統權限最大的帳號,因此若你用 root 帳號來執行容器,就會賦予太多權限並帶來安全性風險。所以,你應該避免寫出需要用 root 權限執行的應用程式。

如果真的非這麼做不可,你應該考慮使用使用者命名空間 (user namespace),把容器內的 root 帳號自動對應到共享 node 上的其他使用者名稱。

未加密的網路

前面提過容器化的微服務應用程式高度分散化、仰賴網路來溝通,使得網路成為潛在的弱點和最常被攻擊之處。因此你務必將容器間的通訊加密,而做法之一就是使用服務網格 (service mesh)。

你也應該考慮用 Kubernetes 網路策略 (Network Policies) 來限制哪些微服務可以跟其他微服務通訊。

未受信任的程式碼

容器發展的時候,公開雲還是相當新的概念,因此人們得透過各種錯誤來學習教訓。例如,一個常見的習慣是用所謂的公開映像檔來執行容器,但這些映像檔是由未知且未受信任的第三方提供。

來快速看個例子。你負責一個專案,要用你之前沒接觸過的技術部署一個新應用程式。與其自己搞懂和建置你信任的映像檔,你發現有人已經在登錄服務上丟了個「能用」的映像檔。你決定下載它來節省大量力氣。這麼做的顯著風險在於,公開映像檔有可能包含惡意程式碼,進而危害到你自己的專案。

簡單來說,雲原生安全性通常被認為由四個 C 組成:

  • Code (程式碼)
  • Containers (容器)
  • Clusters (叢集)
  • Cloud (雲端)

在此我們只說,你應該永遠確保你能信任你執行的程式碼,包括程式碼是哪來的、包含什麼和會做那些事。你應該確保容器安全,選擇合適的容器引擎,並避免以 root 權限執行之。你得考慮是否要減輕 namespaced 容器和共用 OS Kernel 帶來的風險,你得套用 Kubernetes cluster 最新的安全性修補,並套用 RBAC (user accounts and access control,以角色為基礎的存取控制) 來確保操作安全。最後,你應該盡全力鞏固你的資料中心和雲端基礎設施。

容器網路通訊

容器和微服務帶來了下列網路通訊挑戰:

  • 內網通訊的增加
  • 發現服務 (service discovery)
  • 請求追溯
  • IP 變動 (IP churn)
  • 安全性

內網通訊代表同一個應用程式的容器在同一個網路內交談,有別於應用程式跟外部網路服務溝通。例如,應用程式的網頁前端容器會跟後端購物車容器在內網通訊,或者跟外部網路的使用者手機 app 連線。由於微服務架構將複雜的應用程式切成眾多小容器,這使得需要加密的內網通訊量大大增加。

此外,微服務應用程式很常部署在 Kubernetes 這類平台,具備如自我修復、規模調整和零停機升級等,使得 Kubernetes 會不斷在 IP 網路上新增或移除容器,以比一般網路設計更頻繁的方式改變 IP 位址──這有時被稱為 IP churn。

Kubernetes 與容器網路通訊

Kubernetes 實作一個公開的扁平網路,稱為 pod network。容器就是在此執行,Kubernetes cluster 內的 nodes 也是在此產生出來。我們來更仔細看看它能做些什麼。

Kubernetes pod network 是扁平和公開的,意思是所有容器都能看到彼此和輕易通訊。然而,你應該使用 Kubernetes 的 network policies 來控制通訊,包括限制哪些容器跟服務能彼此交談──畢竟放任所有東西自由通訊,就會是一大安全性風險。

Kubernetes 也實作了服務發現功能,並且將之自動化。所有新應用程式會自動登錄其名稱和 IP 在 Kubernetes 的內部 DNS 內,並用內部 DNS 來尋找其他應用程式。既然已經自動化了,應用程式就不必管實作面。

Kubernetes 實作了 IP 位址管理 (IPAM),負責指派 IP 給新容器,並將終結或失效容器的 IP 收回。這能保證 DNS 維持在最新狀態,永遠不會對失去回應的容器發送請求。

最後,Kubernetes 用容器網路介面 (CNI) 實作 pod network,這使得你能根據需求抽換網路介面,因為並非所有網路介面都支援 IPv4 和 IPv6。這原理就透過 CRI 抽換容器引擎是一樣的。

服務網格

服務網格 (service meshes) 能自動替雲原生應用程式帶來網路通訊安全、可觀察性和可靠性,對大規模正式上線環境來說也是不可或缺的。

服務網格的重點之一在於它能自動加入這些功能,因為這些是在平台層實作,而非在應用程式層實作。這表示你只要在平台上安裝服務網格,平台上的所有應用程式都能自動「升級」。這樣的強大模式使得開發者和個別應用程式根本不必顧慮服務的存在。

試想有個大型的微服務應用程式需要加密通訊。在沒有服務網路的情況下,你需要替每個微服務設定相關資料和憑證,不僅增加複雜度,也讓開發人員更難專注在發展有助於顧客和企業的功能上。相對的,服務網格能自動完成微服務間的通訊加密和解密,應用程式甚至不會知道這些事情的存在──服務網格以服務的形式提供通訊加密。

我們來看看 Kubernetes 上的服務網格是怎麼實作的。

Kubernetes 通常會用所謂的 pod 來執行一個或多個容器。實際上,pod 就只是一個輕量的包覆層,讓 Kubernetes 能把多個容器當成一個單位來執行。服務網格利用這種特質,在所有應用程式的 pod 中各注入一個輕量的服務容器。這個服務容器會接管 pod 內的進出通訊,套上加密和解密作業,並將進階的網路效能數據傳回,好用於除錯和最佳化用途。重點在於,應用程式容器並不會知道服務容器的存在,只會照正常的方式運作。

熱門的服務網格包括 Istio、Linkerd 和 Consul,其實遠遠不僅於此,但功能大同小異。Linkerd 是處於畢業狀態的 CNCF 專案,而服務網格介面 (SMI) 標準意旨在追求 Kubernetes 服務網格的標準化。

註:Istio 已經成為 CNCF 畢業專案。

容器儲存空間

容器被設計成短暫存在 (ephemeral) 和無狀態 (stateless),可以隨時被新容器取代,也從不打算儲存資料。若一個容器失效,裡面就算有資料也會隨之遺失。

因此,容器應該將資料儲存於外部空間,而這種模式也很單純。比如,在 AWS 上執行的容器化企業應用程式或許會將資料存在 Amazon Elastic Block Store (EBS),如此一來就算某個容器掛掉,資料也仍能保存在 EBS 內。或者若容器是在私有環境運行,可能會將資料存在共用的 NetApp 資料庫。這些範例都是在應用程式碼與應用程式資料之間減少耦合。

Kubernetes 同樣透過容器儲存空間介面 (CSI) 支援可抽換的儲存空間。例如,若你手上有 NetApp 資料庫,你可以在 Kubernetes 安裝 NetApp 的 CSI 外掛,使得容器得以連接該資料庫。

四:Kubernetes 基礎

很多年以來,Google 已經在數以億計的容器上運行 Google 搜尋、GMail 等服務,這可是無法等閒視之的龐大規模,也需要找個辦法幫忙應付。因此他們先後開發了兩個內部工具,分別叫做 Borg 與 Omega。而在 Docker 使容器流行起來後,其餘世人也面臨了同樣的問題。就算我們的營運規模不及 Google,挑戰仍是一樣的。於是,就在 Docker 開始流行的同時,Google 的一群技術人員打造了一個新的容器管理工具,以他們從 Borg 和 Omega 學到的經驗為基礎。2014 年,該開源專案以「Kubernetes」的名稱釋出。

因此 Kubernetes 能以龐大的規模調度和管理容器,帶來自我修復、自動規模調整、零停機更新等功能。它從 2014 年起就成為業界的容器管理標準,是 Github 上第二大的專案,也是第一個自 CNCF 畢業的專案。

Kubernetes 目前處於一個蓬勃發展的生態系的核心,並有眾多專案建構在它之上、擴展其功能和提供額外的服務。儘管 Kubernetes 的主要角色是容器編排器,它其實也能管理虛擬機 (如 KubeVirt 計畫) 和 serverless 函式 (如 Knative 和 OpenFaaS)。一些以 Kubernetes 為基礎的專案包括:

  • KubeVirt
  • Knative
  • OpenFaaS
  • The Container Network Interface (CNI)
  • Prometheus
  • Linkerd
  • Harbor

最後,Kubernetes 這個名稱是希臘文「舵手」的意思,因此 Kubernetes 的標誌是船上的舵輪。此外,你也會看到人們將 Kubernetes 簡稱為 K8S,因為 8 代表將中間的八個字母省略 (K ubernete s → K8S)。

簡單的 Kubernetes 工作流程

應用程式開發者撰寫程式,將之打包成容器映像檔,並儲存在登錄服務上。

Kubernetes 為了執行容器,必須先把它們包成 pod。你可以撰寫 pod 的 manifest 檔案,描述 pod 內要包含的容器,並將 manifest 檔透過合法的登入管道傳給 Kubernetes API。若檔案檢查通過,pod 的定義就會永久儲存於 Kubernetes cluster 的資料庫內,並透過排程將 pod 指派給一個 node。被指派的 worker node 上會有叫做 Kubelet 的服務,它會負責要 node 的容器引擎下載所需的映像檔、並以容器形式啟動。

這段話塞了很多術語,我們會在本章逐一解說。

容器和 pod

這裡要先釐清一些名詞。我們有時會混用容器與 pod 來指同一件事,但如前面所提,pod 其實是一個以上的容器的包覆層,也是 Kubernetes 用來執行容器的必要條件。這表示容器若要在 Kubernetes 上跑,就一定得包在 pod 中。

下面是一個簡單的 YAML 宣告式 (declarative) 檔案,定義了要在 Kubernetes 中產生的一個 Pod,當中會包含單一一個容器:

apiVersion: v1
kind: Pod
metadata:
name: kcna-pod
labels:
project: kcna-book
spec:
containers: # <<=== 容器定義在這底下
- name: kcna-container
image: <container-image-goes-here>

註:YAML 是一種常見的結構化資料表達形式。其名稱原意是 Yet Another Markup Language,但後來改成 YAML Ain’t a Markup Language。Kubernetes 也可接受 JSON 格式,但 YAML 對人類來說比較容易整理。

上述定義 (即 manifest) 指定類形是 Pod、名稱為 kcna-pod 而標籤是 project (其值為 kcna-book)。可見容器被包在 pod 的定義內,這就是我們所說的「pod 包覆容器」。

pod 除了讓 Kubernetes 能夠執行容器,也替容器帶來下列的額外功能與能力:

  • 協同排程 (co-scheduling)
  • 共享記憶體
  • 共享儲存區
  • 共享網路
  • 容器管理策略
  • Kubernetes intelligence

協同調度讓你能在同一個 pod 執行多個容器,並確保它們永遠會在同一個 node 上運行。這麼做的常見模式是有一個主應用程式容器跟一個幫手容器,後者通常被稱為「邊車」(sidecar)。我們來看底下兩個例子。

logging sidecar 容器會在應用程式容器的同一個 pod 內執行,收集應用程式容器的日誌和傳到中央位置儲存。這麼一來,應用程式能專注在自身運作,logging sidecar 則負責收集日誌。

之前提到的服務網格也使用協同排程,對 pod 中的每個應用程式容器掛上一個 proxy sidecar (代理伺服器容器)。這個新容器會攔截進出 pod 的通訊,做加密和解密、傳回詳細的網路表現數據,並且最佳化網路流量。就和前面一樣,應用程式容器能專注在自身作業,讓服務網格容器提供進階網路功能。

pod 也允許你套上管理策略,如資源請求和資源上限。前者像是最低的 CPU 與記憶體需求,後者則是可用的 CPU 與記憶體上限。這兩者非常重要,因為這能讓排程器尋找有足夠資源的 node 來指派 pod。

假想有兩個 pods,其中一個 pod 的兩個容器能存取同樣的記憶體、儲存區和網路,這表示這些容器會共享同一個 IP,並可透過專門的 port 來存取。這兩個容器也一定會被排程在同一個 worker node 上。

下面是個 YAML 檔,定義了個有多重容器的 pod,附帶一些進階功能。如果覺得看起來很複雜也不必緊張──你不需要搞懂全部細節,只要懂概念即可。

apiVersion: v1
kind: Pod # 定義一個 Pod
metadata:
name: kcna-pod # Pod 名稱
spec:
volumes: # 定義一個容器可存取的儲存區
- name: html # 儲存區名稱
emptyDir: {} # 空儲存區
runtimeClassName: level1 # 指定排程到有特定引擎的 node
containers: # 定義容器
- name: main-container # 主應用程式容器
image: <app-image> # 主應用程式容器映像檔
volumeMounts: # 將儲存區掛載到主容器
- name: html # 要掛載的儲存區名稱
mountPath: /usr/share/nginx/ # 在主容器的掛載路徑
- name: sidecar # sidecar 容器
image: <sidecar-image> # sidecar 容器映像檔
volumeMounts: # 將儲存區掛載到 sidecar
- name: html # 要掛載的儲存區名稱
mountPath: /tmp/git # 在 sidecar 的掛載路徑

上面第 1 至 9 行定義了 Pod,第 10 至 20 行則定義了兩個容器。和之前的範例一樣,容器都被包在 pod 的定義底下。

第 6~8 行定義了一個空的儲存區叫 html。由於它屬於 pod 的環境,兩個容器都能存取它。第 9 行要 Kubernetes 將這個 pod 排程到擁有某名稱之容器引擎的 node,這個細節其實不重要,你只需要了解 pod 可以替容器加上原本沒有的功能。

第 11~15 行定義了主應用程式容器,將 html 儲存區掛載在該容器的 /usr/share/nginx 下。第 16~20 行則定義 sidecar 容器,將 html 儲存區掛載在 /tmp/git 下。

關於 pod 我們還有很多很多東西可以介紹,但目前只需要知道以上這些。

增強 pod

前面我們學到 pod 如何包覆一個以上的容器並加入新功能。然而,pod 並不具自我修復能力,也不會做零停機更新這種事。因此,Kubernetes 提供了高階控制器 (controllers) 來包覆在 pod 之外,好實作出這些功能。這也是為什麼你幾乎總是應該用 deploymentsstatefulsets 之類的更高階控制器來部署 pod。

本章稍後會談什麼是控制器。現在我們先來看個簡單的範例。

試想你的開發團隊寫出一個無狀態網路應用程式,並包裝在容器內。你知道你需要五個執行個體來應付需求,而由於你使用 Kubernetes 環境,這些容器會包在 pod 中,並在一個 node 上執行。但 pod 本身不具適應力,如果其中兩個 pod 掛了,Kubernetes 並沒有機制會重新啟動兩個 pod、好確保隨時有五個在運行。

對於這種問題,Kubernetes 提供了更高階的 deployments 控制器,不僅能接受 pod 定義 (pod 被包覆在 deployments 中),還能確保它在任何時間的執行個體數量。下面的 YAML 檔展示了簡單的 deployment 定義。

apiVersion: apps/v1
kind: Deployment
metadata:
name: kcna-deployment
spec:
replicas: 5
# ...略
template:
spec:
runtimeClassName: level1
containers:
- name: kcna-pod
image: <container-image>

上面是一個 Deployment 的定義,replicas 定義了預期狀態是要有 5 個 pod 的複本,最後 template 和之前一樣定義了容器引擎跟容器映像檔。

註:在 Kubernetes 中,一個 deployment 或一個 pod 之類的東西稱為一個物件或資源。

Kubernetes 架構

這節會討論很多東西,因此我們會分成許多小節:

  • Kubernetes 高階架構
  • Control plane
  • Controllers
  • Worker nodes
  • kubectl
  • Kubernetes 代管服務

Kubernetes 高階架構

從高階角度來看,Kubernetes 是由一群 nodes 組成,用來執行容器化應用程式。它會執行一系列控制器 (controllers) 來實現像是自我修復、自動釋放資源等功能。

如前面提過的,Kubernetes 的 node 分為兩種:

  • Control plane nodes
  • Worker nodes

這兩種 node 都可以是實體伺服器、虛擬伺服器或雲端執行個體伺服器,甚至可以在同一個 cluster 內混用。

至於 Kubernetes 的命令列工具,則是叫做 kubectl。

Control plane

Kubernetes 的 control plane 相當於 Kubernetes 的大腦,會運行一系列特殊的獨立服務,並透過 controllers 實作 Kubernetes 的獨特行為。

既然 control plane 對 Kubernetes 至關重要,它必須具備高效能且隨時可用。真實世界的正式營運 Kubernetes 環境通常會有三到五個 control plane nodes 安裝在故障網域 (failure domain,可能會故障的網域),好確保有任何 control plane 或支援它的基礎設施失效時,不至於拖垮所有 control plane nodes。你也應該確保它們能夠使用低延遲的網路連線。如果一個網域內的 control plane 失效,已經在執行的應用程式其實不會受影響,只是你沒辦法做任何修改或更新而已。

Control plane 內的主要服務如下:

  • API 伺服器
  • 排程器 (scheduler)
  • Cluster 資料儲存庫 (store)
  • 控制器 (controllers)

API 伺服器是進出 Kubernetes 的主要門口,當你送指令給 Kubernetes 就是透過它。所有 worker node 的 kubelet 服務也會監看 API 伺服器,轉達它的工作指派,並回報狀態報告。

Scheduler 負責決定要在哪個 worker node 上執行 pod,把 pod 安排到有足夠 CPU 和記憶體資源的 node 或分攤至多個 node,甚至可以指定或排除特定的 node。

資料儲存庫用來儲存 cluster 狀態,也是整個 control plane 中有狀態的地方,它會建立跟 cluster 有關的資料和避免讓它遺失。例如,若你部署一個應用程式,需求是跑五個 pod,這個資訊就會被存在儲存庫並被控制器跟其他元件參考。目前 Kubernetes 的 cluster 儲存庫是使用 etcd 分散式資料庫。

Controllers 負責監看 cluster 狀態,並採取行動來確保狀態永遠符合預期狀態,例如在 pod 數量不足時啟動額外的 pod 來滿足之。

Controllers

Kubernetes 會執行一系列專門的 controllers,實現 Kubernetes 的獨特特性。這些 controllers 會跑所謂的協調迴圈 (reconciliation loop) 或控制迴圈 (control loop),以便持續監看 cluster 內可觀察到的狀態是否符合預期狀態。

我們前面短暫提過一些 controllers,這裡來一一檢視:

Deployment controller:用來監看 deployment 的狀態,例如確保隨時有五個容器複本在運行。它也實現了零停機更新等功能。例如,你的五個 pod 在執行 1.3 版伺服器,而你想要更新到 1.4 版。你只要更新宣告式 manifest 檔,deployment controller 會發現預期狀態是 5 個 v1.4 容器,觀察到的狀態卻是 5 個 v1.3 容器。控制器會啟動新版容器,並逐漸關掉舊版容器,於是在不停機的情況下完成更新。

StatefulSet controller:很類似 deployment controller,但監看的是 statefulset 物件。statefulset 和 deployment 一樣會部署並維持一定數量的 pod,但也定義了啟動順序關閉順序,這對於有狀態、且會對共用資料庫讀寫的應用程式來說可能很重要。

DaemonSet controller:DaemonSet 確保 cluster 內每一個 node 都會跑一個一個特定 pod 的執行個體。常見的例子是日誌功能,你需要每個 node 都有一個代理程式來收集和送出日誌。DaemonSet controller 能在新加入的 node 內自動啟動 pod,並在 pod 失效時重啟或更換之。

Worker nodes

Worker nodes 是使用者應用程式跑的地方。所有 worker node 都有以下三個重要服務:

  • kubelet
  • 容器引擎
  • kube-proxy

kubelet 是該 node 上主要的 Kubernetes 代理服務,監看 API 伺服器來轉達任務、要容器引擎啟動或停止容器等。它也能產生容器日誌,讓 kubectl logs 指令能夠取用。

藉由容器引擎介面 (CRI),使用者得以抽換不同的引擎,這在前面已經討論過。

最後是 kube-proxy,實作了網路功能,讓外部流量能接觸到 pods。

kubectl

kubectl 是 Kubernetes 的命令列工具,在大多數平台 (Windows、macOS 及 Linux) 都可用。只要安裝好和經過設定,就能用來控制遠端的 Kubernetes 環境。它能做以下事情:

  • 取得 cluster 資訊
  • 部署 pods 和 services (服務) 之類的物件
  • 查詢物件狀態
  • 讀取日誌
  • 執行容器內的指令

它有一個叫做 config 的設定檔,通常被稱為 kubeconfig,位在你的本機 $HOME/.kube 隱藏目錄下。它會列出已知的 cluster 和使用者憑證 (包括密碼)。例如若你有兩個 cluster,那麼它們連同使用者名稱、憑證都會列在你的 kubeconfig 內。

以下是一些常見的 kubectl 指令:

  • kubectl get <物件>
  • kubectl describe <物件>
  • kubectl apply -f <檔名>
  • kubectl delete <-f 檔名 | 物件>
  • kubectl api-resources

kubectl get 會傳回特定資源的簡短資訊。比如,下列指令會查詢稱為 web-fe 的 pod:

$ kubectl get pod web-fe

NAME READY STATUS RESTARTS AGE
web-fe 1/1 Running 0 29m

kubectl describe 則會傳回比 kubectl get 更詳細的資訊:

$ kubectl describe pod web-fe

Name: web-fe
Namespace: default
Priority: 0
Node: prod-cluster/192.168.65.4
Start Time: Wed, 9 Feb 2022 21:43:01 +0000
Labels: project=kcna-book
Annotations: <none>
Status: Running
IP: 10.1.0.107
...(略)

雖然我們能直接透過 kutectl 命令來部署特定物件,最好的方式還是使用 YAML 格式的 manifest 檔,並透過 kubectl applykubectl delete 指令來套用或移除資源。下列指令即透過一個檔案 web-deploy.yml 來部署物件 (檔案中的 deployment 叫做 deployment.apps/kcna-deploy):

$ kubectl apply -f web-deploy.yml

deployment.apps/kcna-deploy created

kubectl delete 指令則能刪除物件,可以指定物件名稱或原本部署用的 YAML 檔名。如果是後者,那麼 manifest 內定義的所有物件都會被移除。

$ kubectl delete pod web-fe

pod "web-fe" deleted

---

$ kubectl delete -f web-deploy.yml

deployment.apps "kcna-deploy" deleted

最後,kubectl api-resources 會列出你能在這個 cluster 上部署的所有資源類型:

$ kubectl api-resources

NAME APIVERSION NAMESPACED KIND
pods v1 true Pod
deployments apps/v true Deployment
daemonsets appsv1 true DaemonSet
jobs batch/v1 true Job
...(略)

務必注意你的 kubectl 版本不能比 Kubernetes API 伺服器的次版本多或少超過一個版本。例如,若你在使用 Kubernetes 1.23,那麼你可用的 kubectl 版本就介於 v1.22 至 v1.24 之間。

Kubernetes 代管服務

許多雲端平台都提供 Kubernetes 代管服務,也就是它們擁有和管理 Kubernetes cluster,並讓你租用之,不必自己架設。

大多 Kubernetes 代管服務會掌控自己的 control plane,但允許你管理自己的 worker node。在這種模式下,雲端平台負責維持 control plane 的效能及可用性,還有在需要時升級它們。你則負責新增或移除 worker nodes,以及管理節點池。

Kubernetes 代管服務非常熱門,因為這去掉了自己打造和管理高效能、高可用性 cluster 的痛苦,但儘管成本一開始看似不高,若管理不善的話,也是有可能成長到可觀鉅額的。

熱門的 Kubernetes 代管服務包括但不限於:

  • AWS Elastic Kubernetes Service (EKS)
  • Azure Kubernetes Service (AKS)
  • Google Kubernetes Engine (GKE)
  • Linode Kubernetes Engine (LKE)

排程

Kubernetes 有內建的排程器 (scheduler),在 control plane 中運作,透過先進的邏輯來安排將 pod 指派到適當的 worker node。

當 Kubernetes 要求執行一個新 pod 時,排程作業就開始了。這個時機可能是你將一個新的 manifest 檔傳給 API 伺服器、要求建一個新的 pod,或是系統自動調整規模的事件,也有可能是要產生 pod 來替補失效容器的自我修復行為。無論如何,一旦有新的 pod 被要求產生,該 pod 就會進入等待 (pending) 狀態,排程器則尋找執行它的最佳 node。node 找到之後,pod 就進入排程了。若合適的 node 遲遲找不到的話,pod 就會繼續處於等待狀態。

排程器也能做一些進階功能:

  • 親和性和反親和性 (affinity and anti-affinity)
  • Pod 拓撲分布約束 (topology spread constraints)
  • 將工作負擔對應到容器引擎
  • 將工作排程到有合適儲存庫的 node

親和性規則讓你能將 pod 排程到特定的 node,或者有在執行特定 pod 的 node。反親和性則相反,讓你要求排程器不要把 pod 指派給特定 node 或有特定 pod 的 node 上。

Pod 拓撲分布約束讓你將 pod 平均排程到使用者定義的故障網域內。這是很重要的工具,能替應用程式帶來高度可用性。

排程器也能將 pod 安排到有特定容器引擎的 node。例如,你能要 Kubernetes 只將特定的工作放在 gVisor 引擎上跑。或者,你也能要排程器選擇能存取特定資料儲存庫的 node。

Kubernetes 命名空間

Kubernetes 命名空間 (namespace) 可以將一個 Kubernetes cluster 分割成多個虛擬 clusters。每個命名空間能有自己的資源配額和使用者帳號,這使得你能讓不同團隊或開發環境共用一個 cluster。

舉個例,你也許會用命名空間將 cluster 分割成 dev (開發)、test (測試) 和 qa (品管)。但你不該用命名空間來當成工作負載邊界,好試著將有惡意或危險的作業隔離起來。命名空間無法阻止出問題的容器從一個命名空間影響其他空間的容器。目前保證唯一能讓工作負擔彼此不受影響的方式,就是把它們擺在不同的 cluster 內。

Kubernetes API 與 API 伺服器

如果你對 API (應用程式介面) 的概念還不熟,你可以把 Kubernetes API 想成一個目錄,列出所有 Kubernetes 物件與其屬性,而這個目錄得透過 API 伺服器來存取。API 伺服器會透過 RESTful HTTPs 端點來開放 API 存取,而所有對 API 伺服器的請求都得經過驗證 (authN) 和授權 (authZ)。

Kubernetes 定義了許多物件,如 pods、deployments、replicasets、statefulsets、cronjobs、services、ingresses、network policies 等等不及備載,這些資源的屬性都可以用 API 來設定。API 伺服器會驗證、授權並排程你要求的資源,讓它在 cluster 被建立起來。當然,要是你嘗試建立的資源或屬性不存在,這個過程就會失敗。

API 新加入的功能會處於 alpha 階段,然後進展到 beta,最後變成 stable (穩定)。我們有時將 stable 稱為 generally available (GA) 或 v1,但處於 stable 的物件也有可能是 v2 v3

你可以用 kubectl api-resources 來查詢 Kubernetes API 支援的物件。底下的 Deployment 處於 stable v1,而且歸在稱為 apps 的群組底下;CSIStorageCapacity 則是 beta,歸在 storage.k8s.io 群組下。

$ kubectl api-resources

NAME APIVERSION NAMESPACED KIND
deployments apps/v1 true Deployment
csistoragecapacities storage.k8s.io/v1beta1 true CSIStorageCapacity
...(略)

Kubernetes 有個政策,保證所有被廢棄的穩定物件規格仍然會被支援 12 個月或接下來 2 次發行之內 (看哪個時間較長)。

Kubernetes 網路連接

這節我們將討論以下網路相關主題:

  • Kubernetes 服務
  • pod 網路
  • 服務註冊與發現

Kubernetes 服務

你已經知道 pod 通常會透過 deployment 或 statefulset 之類的更高階控制器來部署,而這些高階控制器會實現雲原生功能,如自我恢復、自動規模調整、零停機更新和版本復原等。但這些功能會導致個別 pod 變得非常不可靠。

試想以下例子。每次有 node 或 pod 失效時,遺失的 pod 就會被一個新的 pod 取代,附帶一個新 IP 位址。如果客戶端之前直接連接到那個失效的 pod,連線就會逾時或失敗,也無法自動連接到新的 pod。此外,Kubernetes 調高容器規模時也會加入有新 IP 的 pod,並在調低規模時移除 pod,或者在更新 pod 版本時將舊版 pod 砍掉並產生新的。這都使得 pod 的連線極為不可靠,這也是為什麼其他應用程式不該直接連接到 pod。

為了解決這個問題,Kubernetes 提供了服務 (services) 物件,擺在 pods 前面來提供可靠的網路連接。例如,你定義一個服務物件和要它將所有網路流量轉給帶有特定標籤 (label) 的 pods。服務本身有穩定的名稱和 IP,Kubernetes 也保證永遠不會改變它。客戶端連接到服務,服務再將流量導至 pods。服務也夠聰明,知道什麼時候 pods 有新增或移除,於是這也保證服務能實現負載平衡 (load-balancing),將流量平均分配給運行中的最新 pods。

pod 網路

每個 Kubernetes ckuster 會實作一個網路叫做 pod network,所有 pods 都會在這裡面運行。這是個龐大、扁平且開放的網路,意味著所有 pod 都能彼此溝通。這點讓部署變得很簡單,但也是重大的安全性風險。這便是為何你應該實作 Kubernetes 網路策略,好控制 pods 之間的通訊。

從幕後來看,pod 網路其實是由一個網路外掛來實作的。Kubernetes 使用容器網路介面 (CNI) 來開放一個可抽換的網路層,並由第三方廠商的外掛套件來實際實作。這使得 Kubernetes 網路生態系獲得了大量的進展。一些最知名的 CRI 外掛包括:

  • Cilium
  • Canal
  • Flannel
  • Weave

以 eBPF 為基礎的 Cilium 目前是 Kubernetes 最受歡迎的 CRI 外掛。

服務註冊與發現

Kubernetes 也實作了以 DNS 為基礎的原生服務登錄及服務發現。前者是將服務名稱及 IP 註冊到 DNS,後者則是透過服務名稱來取得 IP。

Kubernetes 服務發現會依據兩個原則來運作:

  1. 所有新服務能自動透過 DNS 註冊
  2. 所有 pods 和容器會自動被設定成使用 DNS 來發現 IP

為了讓這上面的一切能運作,Kubernetes cluster 會執行一個叫做 cluster DNS 的內部網域服務,它會監看 API 伺服器有無建立新服務物件的需求。這代表服務不必實作任何邏輯就能自行註冊──cluster DNS 永遠會自動監看 cluster 並註冊服務。

同時,Kubernetes 內所有容器會自動被設定來使用 cluster DNS 發現新服務──任何執行中的容器不需要實作任何特殊邏輯就能這麼做。

五:雲原生應用程式交付

本章涵蓋雲原生應用程式交付的主題,分成以下小節:

  • 導論
  • CI/CD
  • GitOps

導論

雲原生應用程式交付通常被稱為是 code to cloud 或 code to production,我們通常也會用持續整合 (continuous integration, CI) 及持續交付 (continuous delivery, CD) 管線和 GitOps 工具來自動化這個過程。我們前面稍微談過這部分,但現在你該了解自動化的主要三大步驟:

  1. 建置
  2. 測試
  3. 發佈

只要將這些步驟自動化,就能快速且頻繁地發佈高品質的成果。發佈 (release) 是指推出新版本的應用程式或環境。在很多時候 rollout、update 是和 release 同義的。

CI/CD

如前所提,CI/CD 是一組自動化軟體建置、測試與發佈的工具,也是 DevOps 的一環。

CI 通常是關於開發人員將程式碼合併到一個中央程式庫,軟體會在那裡進行建置和廣泛測試。建置和測試必須是全自動的過程,以便將高品質的軟體交給 CD 過程,且能夠在軟體發佈至正式環境之前很有效地找出問題。

CD 則負責將軟體發佈到正式環境,包括開發人員、測試人員和客戶使用的環境。它通常會在 CI 成功測試完畢後被觸發執行。

整個 CI/CD 過程通常也被稱作「管線」(pipeline),而其自動化的性質意味著開發人員只要對程式庫做出改變,管線就能生出建置好、測試過和部署完成的軟體產品。

CI/CD 的自動化會促成以下好處:

  • 更快的軟體交付
  • 更頻繁的軟體交付
  • 更少的 bug (因為有自動化測試)
  • 開發人員生產力更高 (可以專注在開發工作)

不過,CD 有時會被分成兩種稍微不同的類型:

  • 持續交付 (continuous delivery)
  • 持續部署 (continuous deployment)

持續交付需要人類批准才能將軟體部署到正式環境,而持續部署的部署部分則是自動化的。

CI/CD 的良好慣例是越頻繁進行越好,讓開發和管理人員都習慣,這不僅能壓低改變和更新幅度、連帶減少更新後的衝擊,也能加速軟體交付、開發新功能還有修正錯誤的時間。反過來說,若你將大量變動保留到每年一兩次的大型更新,這樣不僅風險高,也會拖延推出功能或修補問題的時程。

熱門的 CI/CD 工具包括:

  • Jenkins
  • CircleCI
  • GitLab CI
  • Travis CI

GitOps

2017 年,Alexi Richardson 在 Kubernetes 管理工具開發公司 Weaveworks 的部落格寫了一篇名為「靠 PR 進行營運」 (Operations by Pull Request) 的文章後,使 GitOps 的概念流行起來。

從高階角度來說,這是將我們多年來於軟體開發領域用過、身經百戰的 DevOps 實務帶進基礎設施和 Kubernetes 的世界。如果你還記得前面的 CI/CD 介紹,GitOps 就是 CI/CD 加上版本控制和 pull request (PR)。

因此,你先將基礎設施想要的狀態以設定檔形式存在 Git 程式庫中,賦予版本控制。每次你修改環境,就從 Git 取出檔案 (建立一個 branch)、修改後送個 PR (pull request) 來將之合併 (merge) 到程式庫。PR 是用來告知你的團隊成員,你的變更已經準備好接受審核和合併,其本身也提供了討論的論壇空間,並能儲存評論以供日後復原版本或稽核的參考。PR 一旦合併,變更就會被套用到你的線上環境。

雖然外頭有很多 GitOps 工具如 Argo、Flux 和 Jenkins X,所謂的 GitOps 其實是一群工具、工作流程和最佳實踐的合稱。這當中包括:

  • 將基礎設施當成程式碼
  • PR 和 merge
  • 持續交付
  • 對手動變更保持零容忍

我們來仔細看看每個項目。

將基礎設施當成程式碼 (Infrastructure as Code, IaC)

IaC 是一個將基礎設施定義成宣告式檔案的實務做法──與其用圖形介面和草稿碼部署虛擬機、網路和存取政策之類的東西,你將之寫在定義檔中和輸入給 GitOps 工具,讓它們代勞設定環境的苦工。這是讓你得以快速和可重複部署基礎設施的關鍵。

在雲端平台上若有一系列你可要求和設定的基礎設施,這種使用設定檔的做法就很方便。IaC 設定檔可能會定義雲端個體、網路設定、防火牆、存取控制串列 (ACL) 規則、路由表、網路政策等等。你把設定檔傳給雲端平台讓它處理,但這樣不若在自家環境那樣強大,因為私有環境可設定的資源更多。另一方面,IaC 無法在自家環境做現場準備,比如架設實體的伺服器主機、連接實體網路線等等。

IaC 最強大的地方之一就是在 Kubernetes,因為 Kubernetes 將所有資源部署透過設定檔來描述,比如 pods、服務、ingress、網路政策和負載平衡伺服器等,大多都可以用 YAML 檔形式傳給 Kuberentes 的 API 伺服器來處理。

下面即為 IaC 的範例之一,以 YAML 檔描述一個 Kubernetes 負載平衡服務,會監聽 port 9000 並將流量傳給位在 port 8080、標籤為 env=prod 的 pods。你能用 kubectl apply 指令將它套用到 Kubernetes 叢集,Kubernetes 會向你的雲端平台要求並建立一個負載平衡器。

apiVersion: v1
kind: Service
metadata:
name: test-lb
spec:
type: LoadBalancer
ports:
- port: 9000
targetPort: 8080
selector:
env: prod

PR 與 Merge

GitOps 將 IaC 與行之有年的 Git 工作流程結合,創造出自動化的 CD 管線。過去幾年來,這已經成為管理 Kubernetes 環境最受歡迎的辦法,也催生了新一代的 CD 工具問世。

試想以下範例:你建了個 Kubernetes cluster,安裝一個 GitOps 工具 (如 Flux、Argo 或 Jenkins X),設定它監看一個 Git 程式庫,裡面存有 Kubernetes 的設定檔。然後你建立一個新的 Kubernetes YAML 檔,描述了一些物件,並用 PR 將該檔案合併到 Git 庫中。PR 會描述改變的地方和任何測試的結果,你的同事會提出評論和核可它,完成 merge 作業。你的 GitOps 工具會注意到合併行為,並展開 CD 程序,在你的 Kubernetes cluster 內加入新的應用程式或基礎設施。

由此可見 PR 和 merge 對 GitOps 來說是至關重要的。事實上重要的 GitOps 規則有四個:

  1. 所有應用程式和基礎設施設定存在 Git 庫中
  2. 用 PR 和 merge 來取得核可並觸發部署
  3. 自動化部署
  4. 停止做手動變更

持續交付

GitOps 主要專注在 CD 這塊,不處理 CI 的部分。這表示你仍需要一個 CI 工具來做應用程式建置、測試和分享。GitOps 會在 CI 結束後加入版本控制、變更管理和 CD。

對手動變更的零容忍

一旦你採納 GitOps,你就再也不能對正式環境做手動變更了,因為這會在每次 PR 被成功合併後被覆蓋掉。

試想以下例子:你用 GitOps 來管理一個正式營運中的 Kubernetes 系統,但某個團隊新成員還沒搞懂 GitOps 的意義,對該環境做了手動變更。這是一種反模式 (anti-pattern),也會讓環境跟 Git 庫內描述的預期狀態脫節。然後當天稍晚你依照 GitOps 的流程,在一個新功能的 branch 內測試變更、送出 PR 並合併之,CD 管線也自動將變更部署到正式環境。整個設定檔一套用下去,之前的手動變更就被覆蓋掉了。

重要的 GitOps 工具與專案

我們前面提過 GitOps 刺激了新一代的 CD 工具問世,能夠搭配 Git 工作流程。下面我們就來簡短介紹它們。

Flux (有時稱為 FluxCD 或 Flux CD) 是容易使用的 Kubernetes 原生應用程式,一開始是 Weaveworks 公司的專案,現在則是 CNCF 孵化專案。它的運作就如前面所提,可以監看一個 Git 庫的變更和同步套用變化到 Kubernetes。Flux 素以極為易用、容易安裝著稱,但完全專注在 CD (不做 CI),可設定的地方也不若其他工具多。

Argo (有時稱為 ArgoCD 或 Argo CD) 很像 Flux,原本由 Intuit 發展,現在同樣是 CNCF 孵化專案。Argo 相對於 Flux 的優勢之一是單一一個 Argo 應用程式可以監控多個 Git 庫,而且能部署在多個 Kubernetes 命名空間內,感覺像是 Flux 的進階版。

Jenkins X 則是以 Kubernetes 為目標的 GitOps 工具,涵蓋整個 CI/CD 程序。它底下是由一系列其他開源工具組成,包裝得相當容易安裝,但用起來也比 Argo 或 Flux 複雜得多。

六:雲原生可觀察性

導論

雲原生應用程式會被部署成多個活動零件、透過網路相互連接,且會經常被更新或置換。這使得監控應用程式是如何連接、或者追蹤請求如何在應用程式分散的微服務當中流動,就變得比以往更加重要。

為了輔助這點,每個微服務都得輸出高品質的量測數據 (telemetry, 指標和日誌的形式)。你接著需要合適的工具來收集這些數據、集結起來和分析,好找出問題和做決策。這個過程是個回饋迴路:觀察 (Observe) → 分析 (Analyse) → 調整 (Adjust)。

即使我們在傳統單體式應用程式做這件事已經行之有年,以下這些對雲原生應用程式來說,若不是新的領域,就是比以往更加重要:

  • 追蹤請求
  • 與應用程式相關的指標
  • 網路延遲時間
  • 規模擴張決策
  • 日誌與指標的容量
  • 收集和分析量測數據

量測數據與觀察性

量測數據是指日誌 (logs)、指標 (metrics) 和追蹤請求 (trace),讓一個遠端系統來監看並了解系統如何運作、診斷問題、衡量並最佳化效能等。一個具備高度可觀察性的系統,能夠產出高品質的量測數據。例如,你可能會有個自家的雲原生應用程式,內含眾多小型微服務,並會在旁邊執行一個中央監看平台。每個微服務都會產生量測數據,並被監看平台收集、儲存和分析。

雲原生的量測數據有時也被稱為信號類型 (signal types)、垂直側錄 (verticals) 或類別 (classes)。日誌、指標和追蹤請求各有不同目的,但合起來說都能提供詳細的見解,有助於理解、除錯和最佳化微服務應用程式。

OpenTelemetry 計畫是一個孵化中的 CNCF 專案,目標是將我們收集量測數據和匯出給中央系統分析的方式加以標準化。該計畫包含了技術標準以及 instrumentation (在應用程式中加入量測數據程式碼)。

最後,雲原生應用程式吐出的量測數據量可能會大爆炸,因為所有微服務都會噴數據出來,導致雲原生應用程式的數據經常能達到傳統程式的十或百倍之譜。這點推動了新一代的雲原生監看工具及相關產物──當中包括 Prometheus、Fluentd、Grafana、Jaeger 等等。

日誌資料

日誌資料通常代表跟應用程式相關的事件 (events),非常適合用於除錯,也是有東西出包的時候應該最先檢查的地方。每個事件 (比如連線中斷、服務重啟) 都會產生一筆帶有時間戳記的日誌,且通常會格式化成 JSON 字串以利查詢。它們也會依據嚴重性分級:從最低的 info 往上到 warning、error,最嚴重的則是 critical。此外還有 debug 層級,通常會包含其他層級的所有內容,也就是更為詳盡。

大多數應用程式會將日誌寫入到標準輸出 (stdout) 和標準錯誤輸出 (stderr) 串流,通常會顯示在命令列或終端機內。不過,也有的應用程式會將日誌寫入到其他地方。

註:很久以前的電腦使用輸出入串流通道,輸入串流接收鍵盤輸入,輸出串流將內容顯示在螢幕,錯誤輸出串流則能將錯誤資訊寫入到跟螢幕無關的裝置。Linux 繼續實作了這種架構,使得日誌資料慣例上仍會被輸出給 stdout 和 stderr。Kubernetes 內的容器至今也仍然沿用此種架構。

Kubernetes 環境內每個節點上的 kubelet 都會監看所有容器的 stdout 與 stderr 串流,你也能使用 kubectl logs 指令來存取這些訊息。即使是最近掛掉的容器,其日誌也會被保留下來以便查看,前提是容器有把日誌輸出到 stdout 與 stderr 串流。

例如,以下指令會從名為 kcna 的 pod 取回日誌:

$ kubectl logs kcna

若該 pod 已經失效或被終止,你可以用 -p--previous 旗標。以下指令是從 kcna 這個 pod 中讀取名為 ctrl 的容器的日誌:

$ kubectl logs -p -c ctr1 kcna

但這樣的缺點是若某個 node 掛掉,所有日誌訊息就遺失了。因此常見的實務是將所有 kubelet 的日誌收集到一個中央儲存庫,好讓日誌的生命週期脫離容器和 node 的生命週期。Kubernetes 將此稱為叢集層級日誌 logging,並提供以下選項:

  • 在 node 層級使用 logging agent
  • 透過 sidecar 搭配 logging agent

第一個是在每個 node 安裝 logging agent,收集 kubelet 的所有容器日誌和找地方存起來。一般做法是在 Kubernetes 透過一個 DaemonSet 來部署和管理每個 node 上的 agent。

第二個方式是在應用程式的 pod 內多加入一個支援容器 (叫做「邊車」),它會擷取應用程式容器的日誌,然後看是透過自己的 stdout 與 stderr 輸出,或者直接傳到中央儲存區。後者會繞過 kubelet,使得你沒辦法用 kubectl logs 指令來查看日誌。

還有一種模式是應用程式層級 logging,也就是設定應用程式本身將日誌直接送到中央儲存區。這對於雲原生應用程式來說是種反模式,你通常也不應該在應用程式中寫死跟環境有關的設定。

指標

指標是跟效能有關的資料,在一段時間內被收集和測量。每筆指標資料都有時間戳記、名稱和能用於分組/查詢的標籤。它們很適合用來觀察歷史趨勢和預測未來的變化。

指標的範例包括 CPU 使用率、佇列長度、請求數量、錯誤率等等。例如,要是過去六個月的請求平均時間為每分鐘 150 次,現在突然暴增到每分鐘一萬次,你就知道狀況有所改變、系統可能得做出因應。同樣的,如果連線逾時率本來是每分鐘 2 次,現在卻突然跳到 5000 次,就表示事情大條了。

CPU 使用率和記憶體使用率這類通用指標被認為太基本了,欠缺應用程式所需的智慧。自訂指標會比較有用,像是佇列長度、回應時間、每秒請求次數,族繁不及備載。重點是這些指標是針對特定應用程式量身打造,因此更能衡量應用程式效能、以及該不該調整其規模。

說到調整規模,雲原生微服務通常能根據需求自動調整,但你必須觀察指標才能讓自動規模調整正確運作。所以你不只該監看適當的指標,還得快速分析它們來決定如何調整規模。

以下是個例子:你部署了一個雲原生應用程式,有三個會自動調整規模的微服務,被設定來監看記憶體使用量。三個微服務都會接收請求,但突然你發現其回應變得異常緩慢。你查看儀表板,看到微服務都處於合理記憶體用量內,但其中一個的請求佇列膨脹到極度龐大。你和開發人員討論後,意識到記憶體用量並不能有效反映應用程式效能,所以不能算是好的指標。因此你首先解決當務之急,多加入一個微服務個體,並新增自訂的指標來監看佇列效能,這樣系統以後才能針對適當的指標來自動增減規模。

網路相關的效能(如延遲時間)也日益重要,因為有越來越多服務請求仰賴於網路。

追蹤請求

Tracing 是指追蹤一個請求在分散式系統中穿越不同元件的完整歷程。

各別請求是可以追蹤的,而每個追蹤報告──包括那些系統元件處理過請求,包括哪些事件,以及每個事件的處理時間。在分散式的微服務環境裡,能夠理解請求如何流動、潛在的瓶頸又為何,已經變得比以往更為重要。以前只會連到單一伺服器和在本地端處理掉的請求,現在會傳送給多重伺服器、在微服務之間來回傳遞。這便是為何你得有辦法觀察每個請求的完整歷程,好清楚了解狀況。

Jaeger 是儲存和分析追蹤資料的熱門工具。

Prometheus

Prometheus 被視為是 Kubernetes 與雲原生環境內監看工具的業界標準。它是在 2012 年由 SoundCloud 開發的,當時他們意識到現有的監看工具實在不符需求。Prometheus 在 2016 年以開源專案形式捐贈給 CNCF,並在 2018 年末成為第二個畢業的專案。

現在業界的慣例便是在 Kubernetes 上運行雲原生環境,使用 Prometheus 來監看和提出警示,並用 Grafana 當成儀表板和視覺化工具。

Prometheus 如何運作

在最高的層級上,每個微服務都應該以日誌和指標形式產生高品質的量測資料。Prometheus 會收集和儲存它們,使之能被查詢和分析,好協助儲存跟最佳化目的。Prometheus 也能將資料輸入 Grafana 來產生高品質的儀表板跟視覺圖表。

讓我們來更深入探究…

Prometheus 預期應用程式會透過 /metrics 這個 HTTP 端點來提供指標資料。例如,如果一個微服務的位址是 store.myapp.k8s.local,那麼 Prometheus 可存取的指標會透過 store.myapp.k8s.local/metrics 來提供。Prometheus 會定期存取該端點來收集指標跟儲存它們。這是一種 pull model,也就是應用程式只負責產生指標資料,Prometheus 負責收集。Prometheus 收集資料的頻率稱為 scrape interval。

Prometheus 會將資料存入一個時間序列資料庫;每個事件以純文字儲存成一列,而時間序列意味著事件帶有時間戳記。事實上,每筆 Prometheus 指標有一個名稱、一個時間戳記和多重標籤,使其非常容易分組、查詢和分析。你或許會聽人將 Prometheus 的資料模型形容成「多維度」,因為事件可以用多重條件分組和查詢。Prometheus 甚至擁有自己的強大查詢語言 PromQL。

雖然 Prometheus 有自己的基本儀表板,人們通常會搭配 Grafana 來產生更強大的視覺圖表。

不過,某些應用程式──比如存活時間短的批次作業──就不適用於 pull model,因為它們可能會在 Prometheus 能收集資料之前就執行結束。這種狀況下,你可以設定應用程式主動將指標資料「推向」Prometheus 的 pushgateway,資料會暫存在該處、直到 Prometheus 有時間讀取為止。

原生上不支援 Prometheus 指標的應用程式

當你的應用程式原生上無法透過 /metrics 產生 Prometheus 的時間序列指標時,你有兩個主要選項:

  • 客戶端函式庫
  • 匯出服務

若你是應用程式的擁有者,你可以用客戶端函式庫來加入對 Prometheus 的支援,這在諸如 Python、Java 和 Go 等語言都有提供。該函式庫實作了所需的時間序列指標,並通常能透過上述端點來提供之。這麼做稱為 direct instrumentation。

若你使用的是第三方應用程式,沒辦法直接加入客戶端函式庫,那麼最佳選項就是使用匯出服務 (exporter)。這會在應用程式的 pod 旁邊掛上一個邊車 pod,收集前者的原生指標跟日誌、轉換成 Prometheus 格式的時間序列資料,然後透過 /metrics 端點匯出。一個常見例子是 Nginx 伺服器:這是極度受歡迎的應用程式,但本身不支援 Prometheus 也沒有 /metrics 端點,但這點可以透過 Prometheus exporter 來克服。

Prometheus 與觸發警示

Prometheus 也可以觸發警示,比如發訊息到你的手機報告某個狀況發生了。

這若要能作用,該狀況必須持續夠久,以便能觸發警示。警示會傳到 Prometheus 的警示管理中心,後者負責處理如何將警示送到某處。你可以使用的設定包括傳送到呼叫器、寄送 email 或簡訊、甚至發 Slack 訊息等等。

成本控管

有效的成本控管是管理雲原生應用程式及基礎設施的關鍵之一。你應該永遠考量到以下三點:

  1. 選擇合適的基礎設施
  2. 規模最適化
  3. 關閉未使用的基礎設施

選擇合適的基礎設施

選擇合適的雲平台是至關重要的決策。但就算你已經做出選擇,還是有很多決策會對成本及成本控管帶來巨大影響。

比如,大多雲平台提供下列的應用程式執行個體類型:

  • On-demand
  • Reserved
  • Spot

On-demand 是最常見也最有彈性的──你在需要時創造執行階段,它們也隨你使用,然後等到不需要時再刪掉。然而,這種服務形式也是最昂貴的。

Reserved 是以長期租約來換取優惠,例如若你在 AWS EC2 選擇租用 3 年並先繳清部分成本,你之後就可以用 50%-70% 的折扣使用 on-demand 式服務。這樣的缺點是你會被租約綁死,就算你已經不需要使用應用程式個體也一樣。

Spot 通常是最便宜的,但也可以隨時刪除。所有大型雲平台都有多餘容量來應付短期增加的消費者需求,而為了因應這些平常擺著沒用的容量,他們會以極高的優惠 (有時折扣可達 90%) 提供所謂的 spot instance 服務。這樣的缺點是當雲平台需要額外容量時,就會直接刪除你的執行個體。有些平台雖然會對使用者發出通知,但不能保證給你足夠的時間來反應。

規模最適化

規模最適化 (rightsizing) 是確保你使用合適程度的基礎設施來滿足需求,而這也得在效能與成本之間取得微妙平衡。若你納入太多基礎設施,效能會變得很好,但成本也會水漲船高。若基礎設施不足,你雖然省了荷包,但效能也會慘兮兮。幸好這點可以透過自動規模調整來輔助,而這則有賴於監看適當的指標。

另一個相關的議題是找出未使用的資源,然後將它們關掉或刪除。這同樣可靠自動規模調整來協助,不然你就得時常檢查自己是不是在付錢租用閒置的基礎設施跟服務了。

--

--