IaC Part 18-組織基礎設施代碼
基礎設施代碼庫可以包括各種類型的代碼,包括堆疊定義、伺服器配置、模組、函式庫、測試、配置和工具程式。
我們應該如何在專案之間和專案內組織這些代碼? 應該如何跨存儲庫組織專案? 基礎架構和應用程式代碼應該放在一起,還是應該分開? 應該如何為具有多個部分的資產組織代碼?
組織專案與儲存庫
在這種情況下,專案(Project)是用於建立系統的離散組件的代碼集合。 對於單一專案或其組件可以包含多少內容,沒有硬性規定。 例如,”IaC Part 5-將Infra Satck構建為代碼”一文中的「建構堆疊的模式和反模式」描述了基礎設施堆疊的不同層級範圍。
一個專案可能依賴代碼庫中的其他專案。 理想情況下,這些依賴關係和專案之間的邊界是明確定義的,並清楚地反映在專案程代碼的組織方式中。
康威定律指出,組織結構與其建構的系統之間存在直接關係。 團隊結構和系統所有權以及定義這些系統的代碼不一致,會造成摩擦和低效率。
在專案之間劃定邊界的另一面是當專案之間存在依賴關係時整合專案,如
IaC Part 17-使用堆疊作為組件一文中對堆疊的描述。
如何組織代碼的問題有兩個維度。 一是放置不同類型代碼的位置 — — 堆疊、伺服器配置、伺服器映像、配置、測試、交付工具和應用程式的代碼。 另一個是如何跨原始碼儲存庫安排專案。 最後一個問題比較簡單,所以讓我們從這裡開始。
一個或多個儲存庫?
鑑於組織會有多個代碼專案,我們應該將它們全部放在原始碼控制系統的單一儲存庫中,還是將它們分散到多個專案中? 如果使用多個儲存庫,是否每個專案都有自己的儲存庫,還是應該將一些專案分組到共用儲存庫中? 如果將多個專案安排到儲存庫中,應該如何決定將哪些專案分組以及將哪些專案分開?需要考量一些以下權衡因素:
- 將專案分離到不同的儲存庫中可以更容易地維護代碼層級的邊界。
- 讓多個團隊在單一儲存庫中處理代碼可能會增加管理精力並產生衝突。
- 跨多個儲存庫傳播代碼可能會使處理跨越它們的變更變得複雜。
- 保存在同一儲存庫中的代碼是版本化的並且可以分支在一起,這簡化了一些專案整合和交付策略。
- 不同的原始碼管理系統具有不同的效能和可擴展性特徵以及支援複雜場景的功能
讓我們看看根據這些因素跨儲存庫組織專案的主要選項。
只有一個儲存庫
一些團隊,甚至一些較大的組織,維護一個包含所有代碼的儲存庫。 這需要可以根據使用等級進行擴充的原始碼控制系統軟體。 隨著代碼庫規模、歷史記錄、使用者數量和活動層級的成長,一些軟體很難處理代碼庫。因此,拆分儲存庫成為管理效能的問題。
單一儲存庫可以更易於使用。 技術人員可以檢查他們需要處理的所有專案,確保他們擁有所有內容的一致版本。 某些版控軟體提供sparse-checkout等功能,讓使用者可以使用儲存庫的子集。
Monorepo — — 一個儲存庫,一個構建(Build)
單一儲存庫可以很好地與build-time整合配合使用。 monorepo 策略對在單一儲存庫中維護的專案使用建置時整合模式。 monorepo 的簡單版本會建立儲存庫中的所有專案,如下圖所示。
儘管這些專案是一起建構的,但它們可能會產生多個工件(artifacts),例如應用程式套件、基礎架構堆疊和伺服器映像。
一個儲存庫,多個構建(Build)
大多數將所有專案保存在單一儲存庫中的組織不一定在所有專案上運行單一構建。 他們通常有一些不同的構建來建立系統的不同子集(如下圖所示)。
通常,這些建置會共享一些專案。 例如,兩個不同的構建可能使用相同的共用函式庫(如下圖所示)。
管理多個專案的一個陷阱是它會模糊專案之間的界限。 技術人員可能會為一個專案編寫直接引用儲存庫中另一個專案中的檔案的代碼。 這樣做會導致更緊密的耦合和更少的依賴關係可見性。 隨著時間的推移,專案會變得混亂且難以維護,因為對一個專案中的檔案進行變更可能會與其他專案產生意外的衝突。
每個專案都有自己的儲存庫(Microrepo)
另一個極端是為每個專案擁有一個單獨的儲存庫(如下圖)。
此策略可確保專案之間的清晰分離,尤其是當擁有在整合每個專案之前單獨建置和測試每個專案的pipeline時。 如果有人檢查兩個專案並跨專案更改檔案,pipeline就會失敗,從而暴露問題。
從技術上講,可以透過先檢查所有構建(Build),在單獨儲存庫中管理的專案之間使用構建時整合(如下圖)。
在實踐中,在單一儲存庫中跨多個專案進行構建更為實用,因為它們的代碼是一起進行版本控制的。 將單一構建的變更推送到多個儲存庫會使交付過程變得複雜。 交付階段需要某種方式來了解要檢查的所有相關儲存庫的哪些版本以建立一致的構建。
當支援delivery-time與apply-time整合時,單一專案儲存庫效果最佳。 對任何一個儲存庫的變更都會觸發其專案的交付流程,從而將其與流程中的其他專案結合在一起。
多個儲存庫、多個專案
雖然有些組織走向一個極端或另一個極端 — — 所有東西都使用單一儲存庫,或者每個專案都有一個單獨的儲存庫 — — 但大多數組織都維護多個包含多個專案的儲存庫(如下圖)。
通常,將專案分組到儲存庫中是有機市的發生,而不是由像 monorepo 或 microrepo 這樣的策略驅動。 然而,有一些因素會影響事情的順利進行。
如同在其他儲存庫策略的討論中所看到的,一個因素是專案分組與其構建和交付策略的一致性。 當專案密切相關時,尤其是在建置時整合專案時,將專案保留在單一儲存庫中。 當專案的交付路徑未緊密整合時,請考量將專案分離到單獨的儲存庫。
另一個因素是團隊所有權。 儘管很多個人和團隊可以在同一個儲存庫中處理不同的專案,但這可能會分散注意力。 變更日誌將不同團隊的提交歷史記錄與不相關的工作流程混合在一起。 一些組織限制對代碼的存取。 原始碼控制系統的存取控制通常由儲存庫管理,這是決定哪些專案去哪裡的另一個驅動因素。
正如針對單一儲存庫所提到的,儲存庫中的專案更容易與檔案依賴項糾纏在一起。 因此,團隊可能會根據從架構和設計角度需要更強邊界的位置,在儲存庫之間劃分專案。
組織不同類型的代碼
基礎設施代碼庫中的不同專案定義了系統的不同類型元素,例如應用程式、基礎設施堆疊、伺服器設定模組和函式庫。 這些專案可能包含不同類型的代碼,包括宣告、命令式代碼、配置值、測試和實用腳本。 制定組織這些事情的策略有助於保持代碼庫的可維護性。
專案支援文件
一般來說,特定專案的任何支援代碼都應該與該專案的代碼相容。 堆疊的典型專案佈局可能類似於以下範例。
├── build.sh
├── src/
├── test/
├── environments/
└── pipeline/
該專案的資料夾結構包括:
- src/
基礎設施堆疊代碼,這是該專案的核心。 - test/
測試代碼 — 此資料夾可以再分為不同的子資料夾,用於在不同階段執行的測試,例如離線和線上測試。 使用不同工具的測試(例如靜態分析、效能測試和功能測試)可能也有專用的子資料夾。 - environments/
配置 — 此資料夾包含一個單獨的文件,其中包含每個堆疊實例的配置值。 - pipeline/
交付配置 — 此資料夾包含用於在delivery pipeline工具中建立交付階段的設定文件。 - build.sh/
執行構建(Build)活動的腳本
當然,這只是一個例子。 技術人員以不同的方式組織他們的專案,並且包括許多其他內容,而不是這裡顯示的內容。
關鍵要點是建議特定於專案的檔案與專案一起存在。 這確保了當有人檢查專案的版本時,他們知道基礎架構代碼、測試和交付都是相同的版本,因此應該一起作業。 如果測試儲存在單獨的專案中,則很容易使它們不匹配,從而為正在測試的代碼執行錯誤版本的測試。
但是,某些測試、配置或其他檔案可能並不特定於單一項目。 應該如何處理這些狀況呢?
跨專案測試
漸進式測試涉及在與其他專案整合進行測試之前單獨測試每個專案,如下圖所示。
我們可以輕易地將測試代碼放在該專案的每個專案的各個階段中運行。 但是整合階段的測試代碼呢? 我們可以將這些測試放在其中一個專案中,或為整合測試建立一個單獨的專案。
將整合測試保留在專案內
在多個整合多個專案的情況下,一個專案是某些類型測試的明顯切入點。 例如,許多功能測試連接到前端服務以證明整個系統正常運作。 如果後端元件(例如資料庫)配置不正確,則前端服務無法連接到它,因此測試失敗。
在這些情況下,整合測試代碼可以與提供前端服務的專案完美配合。 最有可能的是,測試代碼與該服務耦合。 例如,它需要知道該服務的主機名稱和連接埠。
將這些測試與早期交付階段運行的測試分開 — 例如,使用測試替身進行測試時。 我們可以將每組測試保存在專案中的單獨子資料夾中。
整合測試也非常適合使用其他項目的項目,而不是提供者項目。 ShopSpinner 範例包含一個定義應用程式基礎架構實例的堆疊項目,共用在不同堆疊中定義的網路結構。
將整合測試放入共享網路堆疊專案中與依賴關係的方向背道而馳。 網路專案需要了解應用程式堆疊以及使用它的任何其他堆疊的具體細節,以測試整合是否正常運作。 應用程式基礎架構堆疊已經了解共享網路堆疊,因此使用應用程式堆疊代碼進行整合測試可以避免專案之間的依賴循環。
專用整合測試專案
另一種方法是為整合測試建立一個單獨的專案,也許每個整合階段都有一個專案。 正如康威定律所說的那樣,當不同的團隊負責整合測試時,這種方法很常見。 當不清楚哪個專案符合整合測試時,其他團隊就會這樣做。
當將整合測試套件與其測試的代碼分開管理時,版控可能具有挑戰性。 技術人員可能會混淆針對特定版本的系統代碼執行哪個版本的整合測試。 為了緩解這種情況,請確保與代碼一起編寫和更改測試,而不是分離這些活動。 並實作一種關聯專案版本的方法; 例如,使用交付時間整合模式中所述的fan-in方法。
按領域概念組織代碼
單一專案中的代碼可以包含多個部分。 ShopSpinner 範例中的應用程式基礎架構專案定義了伺服器叢集和資料庫實例,以及每個實例的網路結構和安全性原則。 許多團隊在自己的檔案中定義網路結構和安全性策略,如以下範例所示。
└── src/
├── cluster.infra
├── database.infra
├── load_balancer.infra
├── routing.infra
├── firewall_rules.infra
└── policies.infra
firewall_rules.infra 檔案包含 cluster.infra 中建立的VM的防火牆規則以及database.infra 中定義的資料庫實例的規則。
以這種方式組織代碼的重點是功能元素而不是它們的使用方式。 當相關元素位於同一檔案中時,通常更容易理解、編寫、更改和維護相關元素的代碼。 想像一下,一個檔案有40種不同的防火牆規則,用於存取10種不同的服務,而一個檔案定義了1項服務以及與其相關的3個防火牆規則。這個概念遵循圍繞領域概念而不是技術概念進行設計的設計原則。
組織Configuration Value檔案
我們在"IaC Part 7-配置Stack Instance"一文中描述了用於管理堆疊專案不同實例的參數值的設定檔模式。 該描述提出了兩種不同的方法來組織多個專案中每個環境的配置。 一種是將它們儲存在相關專案中:
├── application_infra_stack/
│ ├── src/
│ └── environments/
│ ├── test.properties
│ ├── staging.properties
│ └── production.properties
│
└── shared_network_stack/
├── src/
└── environments/
├── test.properties
├── staging.properties
└── production.properties
另一種是擁有一個單獨的專案,其中包含所有堆疊的配置,按環境來組織:
├── application_infra_stack/
│ └── src/
│
├── shared_network_stack/
│ └── src/
│
└── configuration/
├── test/
│ ├── application_infra.properties
│ └── shared_network.properties
├── staging/
│ ├── application_infra.properties
│ └── shared_network.properties
└── production/
├── application_infra.properties
└── shared_network.properties
將配置值(configuration values)與專案代碼一起儲存會將通用的、可重複使用的代碼與特定實例的詳細資訊混合在一起。 理想情況下,更改環境配置不需要修改堆疊專案。
另一方面,當配置值靠近與其相關的專案時,可以說更容易追蹤和理解它們,而不是混合在一個整體的配置專案中。 像往常一樣,團隊所有權和一致性是一個因素。 將基礎設施代碼及其配置分開可能會阻礙兩者的所有權和責任。
管理基礎設施與應用程式代碼
應用程式和基礎設施代碼應該保存在單獨的儲存庫中還是放在一起? 每個答案對不同的人來說顯然都是正確的。 正確的答案取決於組織架構和所有權劃分。
在單獨的儲存庫中管理基礎架構和應用程式代碼支援單獨的團隊建立和管理基礎架構和應用程式的操作模型。 但如果應用程式團隊負責基礎設施,特別是特定於其應用程式的基礎設施,則會帶來挑戰。
即使應用程式團隊成員負責與其應用程式相關的基礎架構元素,分離代碼也會造成認知障礙。
如果代碼位於與技術人員最常使用的代碼庫不同的儲存庫中,那麼技術人員在深入研究代碼時就不會感到同樣的舒適度。 當技術人員不太熟悉的代碼以及與系統其他部分的基礎設施代碼混合時尤其如此。
位於團隊自己的代碼庫區域中的基礎設施代碼就不那麼令人生畏了。 很少有人認為更改可能會破壞其他人的應用程式甚至基礎設施的基本部分。
交付基礎設施與應用程式
無論是否一起管理應用程式和基礎架構代碼,最終都會將它們部署到同一個系統中。 對基礎設施代碼的變更應該在整個應用程式交付流程中與應用程式整合和測試(如下圖所示)。
作為一個反例,許多組織都將Production Infra視為獨立的環境。 通常,一個團隊擁有Production Infra,包括stage或pre-production環境,但不負責開發和測試環境(如下圖 )。
這種分離為應用程式交付以及基礎設施變更帶來了摩擦。 直到交付過程的後期,團隊才會發現系統兩個部分之間的衝突或差距。 如"IaC Part 8-持續測試與交付"一文中所述,在技術人員進行變更時不斷整合和測試系統的所有部分是確保高品質和可靠交付的最有效方法。
因此,交付策略應該在所有環境中交付對基礎架構代碼的變更。 基礎設施變更流程有幾種選擇。
使用基礎設施測試應用程式
如果您沿著應用程式交付路徑交付基礎架構更改,則可以利用自動化應用程式測試。 在每個階段,應用基礎架構變更後,都會觸發應用程式測試階段(如下圖所示)。
漸進式測試方法使用應用程式測試進行整合測試。 應用程式和基礎設施版本可以綁定在一起,並按照交付時間整合模式完成交付流程的其餘部分。 或者,可以使用應用程式時來整合將基礎架構變更推送到下游環境,而無需與正在進行的任何應用程式變更整合。
盡快推動應用程式和基礎架構的變更是理想的選擇。 但在實踐中,並非總是能夠消除組織中所有類型的變革所帶來的摩擦。 例如,如果利害關係人需要更深入地審查面向終端使用者的應用程式更改,可能需要更快地推動常規基礎架構變更。 否則,應用程式發布程序可能會佔用安全性修補程式等緊急變更和設定更新等較小變更。
整合之前測試基礎設施
將基礎架構代碼應用於共享應用程式開發和測試環境的風險是破壞這些環境會影響其他團隊。 因此,在將基礎設施代碼放到共享環境之前,最好先擁有用於測試基礎設施代碼的交付階段和環境(如下圖)。
這個想法是漸進式測試和delivery-time專案整合的具體實現。
使用基礎設施代碼部署應用程式
基礎設施代碼定義了伺服器上的內容。 部署應用程式涉及將東西放到伺服器上。 因此,編寫基礎設施代碼來自動化應用程式的部署過程似乎是明智的。 在實踐中,混合應用程式部署和基礎架構配置的問題會變得混亂。 應用程式和基礎設施之間的介面應該簡單明了。
RPM、.deb 檔案和 .msi 檔案等作業系統打包系統是用於打包和部署應用程式的定義明確的介面。 基礎架構代碼可以指定要部署的套件檔案,然後讓部署工具接手。
當部署應用程式涉及多項活動時,尤其是涉及多個移動部件時,就會出現問題。 例如,使用 Chef cookbook將 某個Java 應用程式部署到 Linux VM上。 這個cookbook需要執行以下步驟:
- 下載新的應用程式版本並將其解壓縮到新資料夾
- 如果應用程式的先前版本正在運行,停止這個process
- 如果需要更新設定檔
- 將指向應用程式目前版本的符號連結更新至新資料夾
- 運行資料庫架構遷移腳本
- 啟動新應用程式版本的流程
- 檢查新流程是否正常運作
這本cookbook對技術人員來說很麻煩,有時無法偵測到前一個Process何時尚未終止,或者New process在啟動後一分鐘左右崩潰。
從根本上講,這是宣告式基礎設施代碼庫中的代碼腳本。 如果將應用程式打包為 RPM 後,就不會有上述的這些問題,這意味著我們可以使用專門用於部署和升級應用程式的工具和腳本。 我們為 RPM 打包過程編寫了測試,該過程不依賴 Chef 代碼庫的其餘部分,因此我們可以深入研究導致部署不可靠的特定問題。
使用基礎架構代碼部署應用程式的另一個挑戰是部署過程需要編排多個部分。 將應用程式部署到單一伺服器時,流程運作會是良好的。 當我們跨多個伺服器對應用程式進行負載平衡時,它就不適合了。
即使在轉移到 RPM 套件之後,cookbook也沒有管理跨多個伺服器的部署順序。 因此,叢集將在部署作業期間運行混合應用程式版本。 而且資料庫模式遷移腳本應該只運行一次,因此我們需要實現鎖定以確保只有第一台伺服器的部署進程才會運行它。
解決方案是將部署操作從伺服器設定代碼中移出,並轉移到一個腳本中,該腳本將應用程式從中央部署位置(build server)推送到伺服器上。 此腳本管理伺服器部署順序和資料庫架構遷移,透過修改負載平衡器的滾動升級配置來實現零停機部署。
分散式雲端原生應用程式增加了編排應用程式部署的挑戰。 協調對數十個、數百個或數千個應用程式實例的更改確實會變得混亂。 團隊使用 Helm 或 Octopus Deploy 等部署工具來定義應用程式群組的部署。 這些工具透過專注於部署應用程式集來強制關注點分離,而將底層叢集的配置留給代碼庫的其他部分。
然而,最穩健的應用程式部署策略是保持每個元素鬆散耦合。 獨立於其他變更部署變更越容易、越安全,整個系統就越可靠。
結論
顧名思義,IaC從代碼庫驅動系統基礎架構的架構、品質和可管理性。 因此,需要根據業務需求和系統架構來建置和管理代碼庫。 它需要支援使團隊有著高效工作的工程原理和實踐。