IaC Part 19-交付基礎設施代碼
軟體交付生命週期是我們IT產業中的一個重要概念。 在地端機房時代背景下,基礎設施交付通常遵循不同類型的流程,每個公司的IT部門還因為不同的人有不同的交付方式。 在沒有先進行測試的情況下對生產基礎設施進行變更是很常見的; 例如,硬體異動。
但使用代碼來定義基礎架構創造了透過更全面的流程來管理變更的機會。 在開發環境中複製手動建置的系統的變更可能看起來很愚蠢,例如更改伺服器中的 RAM 大小。 但是,當在代碼中實做變更時,我們可以使用流水線(pipeline)的方式輕易的放到生產路徑。 這樣做不僅會發現變更本身的問題,而且還會發現應用變更過程中的任何問題。 它還保證生產路徑上的所有環境都配置一致。
交付基礎設施代碼
流水線隱喻描述了基礎設施代碼的變更如何從更改人員進程到生產實例。 此交付過程所需的活動會影響組織代碼庫的方式。
用於交付代碼版本的流水線具有多種類型的活動,包括build、Promote、Apply和Validate。 流水線中的任何特定階段都可能涉及多個活動,如下圖所示。
Building
準備一個代碼版本來使用,並使其可用於其他階段。每次原始代碼更改時, Build通常在流水線中做完一次。
Promoting
在交付階段之間移動程式碼版本,如我們在"IaC Part 8-持續測試與交付"一文中的「漸進測試」所述。例如,一旦堆疊專案版本通過了其堆疊測試階段,它可能會被promotex來呈現它已準備好進入系統整合測試階段。
Applying
運行相關工具以將代碼應用到相關基礎設施實例。 此實例可以是用於測試活動的交付環境或生產實例。
建置基礎設施專案
建置基礎設施專案準備使用代碼。 活動可包括:
- 檢索build-time依賴項,例如函式庫,包括來自代碼庫和外部函式庫中其他項目的依賴項
- 處裡build-time configuration,例如提取在多個專案之間共用的配置值
- 編譯或轉換代碼,例如從模板產生配置檔案
- 執行測試,包括離線和線上測試
- 準備代碼以供使用,將其轉換為相關基礎設施工具用於應用它的格式
有幾種不同的方法可以準備基礎設施代碼並使其可供使用。 有些工具直接支援特定的方法來執行此操作,例如標準工件包(standard artifact package)格式或儲存庫。 其他人則將其留給使用該工具實現自己的代碼交付方式的團隊。
將基礎設施代碼打包為工件(Artifact)
對於某些工具,「準備程式碼以供使用」涉及將檔案組裝成具有特定格式的套件檔案(工件)。 此過程對於 Ruby (gems)、JavaScript (NPM) 和 Python(與 pip 安裝程式一起使用的 Python 套件)等通用程式語言來說是常態。 用於為特定作業系統安裝檔案和應用程式的其他套件格式包括 .rpm、.deb、.msi 和 NuGet (Windows)。
沒有太多基礎設施工具為其代碼專案提供套件格式。 然而,一些團隊為此建立了自己的工件,將堆疊代碼或伺服器程式碼捆綁到 ZIP 檔案或「tarball」(使用 gzip 壓縮的 tar 檔案)中。 例如,有些團隊使用作業系統打包格式,建立將 Chef Cookbook 檔案解壓縮到伺服器上的 RPM 檔案。 其他團隊會建立包含堆疊專案代碼以及堆疊工具可執行檔的 Docker 映像。
其他團隊不會將其基礎設施代碼打包到工件中,特別是對於沒有本機套件格式的工具。 他們是否這樣做取決於他們發布、共享和使用代碼的方式,這取決於他們使用哪種類型的儲存庫。
使用儲存庫交付基礎架構代碼
團隊使用原始碼儲存庫來儲存和管理對其基礎設施原始碼的變更。 他們通常使用單獨的儲存庫來儲存準備交付到環境和實例的代碼。 正如我們將看到的,有些團隊將同一個儲存庫用於這兩個目的。
從概念上講,Build階段將這兩種儲存庫類型分開,從原始碼儲存庫中取得代碼,組裝它,然後將其發佈到交付儲存庫(如下圖)。
交付儲存庫(delivery repository)通常儲存特定專案代碼的多個版本。 如稍後所述,Promotion階段標記專案代碼的版本以呈現它們已進展到哪個階段; 例如,是否已準備好進行整合測試或生產。
應用程式活動從交付儲存庫中提取專案代碼的一個版本,並將其應用到特定實例,例如 SIT 環境或 PROD 環境。
有幾種不同的方法來實現交付儲存庫。 特定的系統可以針對不同類型的專案使用不同的儲存庫實作。 例如,他們可能會為使用該工具的專案使用特定於工具的儲存庫,如 Chef Server。 對於使用 Packer 等沒有套件格式或專用儲存庫的工具的項目,同一系統可能會使用通用檔案儲存服務(例如 S3 儲bucket)。 有多種類型的交付代碼儲存庫實作,以下為幾種類型說明。
1.專門的工件儲存庫
上面討論的大多數套件格式都有一個套件儲存庫產品、服務或多個產品和服務可以實現的標準。 .rpm、.deb、.gem 和 .npm 檔案有多種儲存庫產品和託管服務。
一些儲存庫產品(例如 Artifactory 和 Nexus)支援多種套件格式。 運行其中一個的企業中的團隊有時會使用它們來儲存其工件,例如 ZIP 檔案和 tarball。 許多雲端平台都包含專門的工件儲存庫,例如server image storage。
ORAS(OCI Registry As Storage)專案提供了一種使用最初為 Docker image設計的工件儲存庫作為任意類型工件的儲存庫的方法。
如果工件儲存庫支援tags或labels,我們可以使用它們進行Promotion。 例如,要將堆疊專案工件提升至系統整合測試階段,可以使用 SIT_STAGE=true 或 Stage=SIT 對其進行標記。
或者,可以在儲存庫伺服器中建立多個儲存庫,每個交付階段使用一個儲存庫。 若要promote工件,可以將工件複製或移至相關儲存庫。
2.特定於工具的儲存庫
許多基礎設施工具都有一個專門的儲存庫,不涉及打包的工件。 相反,執行一個工具將專案的代碼上傳到伺服器,並為其指派一個版本。 這與專用工件儲存庫的工作方式幾乎相同,但沒有套件檔案。其中的範例包括 Chef Server(自架)、Chef Community Cookbooks(public)和 Terraform Registry(public module)。
3.通用檔案儲存庫
許多團隊,尤其是那些使用自己的格式儲存基礎架構代碼專案來提供給交付團隊,將它們儲存在通用檔案儲存服務或產品中。 這可能是檔案伺服器、Web 伺服器或物件儲存服務。 這些儲存庫不提供處理工件的特定功能,例如發布版本編號。 因此,可以自行指定版本號,也許可以將其包含在檔案名稱中(例如,my-stack-v1.3.45.tgz)。 要promote工件,可以將其複製或連結到相關交付階段的資料夾。
4.從原始碼儲存庫交付代碼
鑑於原始代碼(source code)已經儲存在原始碼儲存庫中,並且許多基礎設施代碼工具沒有將其代碼視為發布的套件格式和工具鏈,因此許多團隊只是將原始程式碼儲存庫中的程式碼應用到環境中。
將main branch (trunk)中的代碼套用到所有環境將導致管理不同版本的代碼變得困難。 因此,大多數執行此操作的團隊都使用branches,通常為每個環境維護一個單獨的branches。 他們透過將代碼版本合併到相關branches來將其promote到環境。 GitOps 將這種實踐與持續同步結合。
使用branches來promote代碼可能會模糊編輯代碼和交付代碼之間的差異。 CD 的核心原則是在build階段之後永不更改代碼。 雖然團隊可能承諾永遠不會在branches中編輯代碼,但維持這項紀律通常很困難。
整合專案
如我們在"IaC Part 18-組織基礎設施代碼"一文中所提到的,代碼庫中的專案之間通常具有依賴關係。 下一個問題是何時以及如何組合相互依賴的不同版本的專案。
作為範例, ShopSpinner 團隊代碼庫中的幾個專案。 他們有兩個堆疊專案。 其中一個 application_infrastruction-stack 定義了特定於應用程式的基礎架構,包括VM pool和應用程式流量的負載平衡器規則。 另一個堆疊專案 shared_network_stack 定義了 application_infrastruction-stack 的多個實例共享的公共網路,包括address block(VPC 和子網)和允許流量流向應用程式伺服器的防火牆規則。
團隊還有兩個伺服器配置專案:tomcat-server,用於設定和安裝應用程式伺服器軟體;monitor-server,用於設定和安裝監控代理。
第五個基礎架構項目 application-server-image 使用 tomcat-server 和 monitor-server 設定模組來建構server image(如下圖)。
application_infrastruct-stack專案在shared_network_stack所建立的網路結構中建立其基礎架構。 它還使用 application-server-image 專案建立的server images在其應用程式伺服器叢集中建立伺服器。 application-server-image透過套用tomcat-server和monitor-server中的伺服器設定定義來建構伺服器映像。
當有人對這些基礎設施代碼專案之一進行變更時,它會建立該專案程代碼的新版本。 該專案版本必須與其他每個專案的版本整合。 專案版本可以在建置時、交付時或應用時整合。
特定系統的不同專案可以在不同點整合,如以下模式描述中的 ShopSpinner 範例所示。
模式:Build-time專案整合
Build-time專案整合模式跨多個專案執行建置活動。 這樣做涉及整合它們之間的依賴關係並設定跨專案的代碼版本。
構建過程通常涉及先建置和測試每個組成專案,然後再一起構建和測試它們(如下圖)。 此模式與其他模式的差異在於,它要麼為所有專案產生一個工件,要麼產生一組作為一個群組進行版本控制、promote和apply的工件。
在這個範例中,單一構建階段使用多個伺服器配置專案產生server image。 建置階段可能包括多個步驟,例如建置和測試各個伺服器配置模組。 但輸出(server image)由其所有組成專案的代碼組成。
動機
一起建立專案可以儘早解決任何依賴關係問題。 這樣做可以快速反饋衝突,並在整個代碼庫到生產的交付過程中建立高度的一致性。 在構建時整合的專案代碼在整個交付週期中是一致的。 相同版本的代碼應用於整個生產過程的每個階段。
適用性
使用這種模式或其他替代模式主要是一個偏好問題。 這取決於我們喜歡哪一組權衡,以及團隊管理跨專案建立複雜性的能力。
後果
在Runtime中構建和整合多個專案非常複雜,尤其是對於大量專案而言。 根據實施構建的方式,這可能會導致回饋時間變慢。
大規模使用build-time專案整合需要複雜的工具來編排建置。 在大型代碼庫中使用此模式的大型組織(例如 Google 和 Facebook)擁有專門負責維護內部工具的團隊。
如同實行中所討論的,一些構建工具可用於建立大量軟體專案。 但這種方法在業界的應用並不像單獨建構專案那麼廣泛,因此沒有那麼多工具和參考資料可以提供幫助。
由於專案是一起構建的,因此它們之間的界限比其他模式更不明顯。 這可能會導致專案之間的耦合更緊密。 發生這種情況時,很難在不影響代碼庫的許多其他部分的情況下進行小量變更,繼而增加變更的時間和風險。
實行
將Build的所有專案儲存在單一儲存庫(通常稱為monorepo)中,透過在它們之間整合代碼版控來簡化將它們一起建置。
大多數軟體建置工具(例如 Gradle、Make、Maven、MSBuild 和 Rake)用於協調少量專案的建置。 在大量專案中運行建置和測試可能需要很長時間。
並行化(Parallelization)可以透過在不同執行緒、進程(Process)甚至跨運算網格中建置和測試多個專案來加速此過程。 但這需要更多的運算資源。
優化大規模構建的更好方法是使用有向圖(directed graph)來限制代碼庫已更改部分的構建和測試。 如果做得好,這應該會減少提交後構建和測試所需的時間,這樣它只比為單獨的專案運行構建花費的時間稍長一些。
有幾種專門的構建工具旨在處理超大規模的多專案建置。 其中大部分都是受到Google和 Facebook 創建的內部工具的啟發。 其中一些工具包括 Bazel、Buck、Pants 和 Please。
相關模式
在build time整合專案版本的替代方法是在delivery time或apply time整合。 在單一儲存庫中管理多個專案的策略雖然不是一種模式,但支援此模式。用於此模式的範例(如上圖 )在建立server image時套用伺服器設定碼。 immutable server模式是build-time integration優於delivery-time integration的另一個範例。
許多專案構建都會在建置時解決對第三方代碼庫的依賴關係,下載它們並將它們與可交付成果捆綁在一起。 不同之處在於,這些依賴項不是在使用它們的專案中構建和測試的。 當這些依賴項來自同一組織內的其他專案時,這是delivery-time integration的範例。
模式:Delivery-time專案整合
特定多個專案之間存在依賴關係,Delivery-time專案整合會在組合專案之前單獨建構建和測試每個專案。 此方法比build-time整合晚於整合代碼版本。
一旦專案被合併和測試,它們的代碼就會在交付週期的其餘部分一起進行。
例如,ShopSpinner application-infrastruct-stack 專案使用 application-server-image 專案中定義的Server image定義VM叢集(如下圖)。
當有人對基礎設施堆疊代碼進行變更時,交付流水線(delivery pipeline)會自行構建和測試堆疊專案,如“IaC Part 9 測試Infra Stacks”一文中所述。
如果堆疊專案的新版本通過了這些測試,它將進入整合測試階段,該階段測試與透過其自身測試的最後一個server image整合的堆疊。 此階段是兩個專案的整合點。 然後,專案的版本一起進入後續階段。
動機
在整合專案之前單獨構建和測試專案是在它們之間強制執行清晰邊界和鬆散耦合的一種方法。
例如,ShopSpinner 團隊的成員在 application-infrastruction-stack 中實作防火牆規則,該規則會開啟 application-server-image 中的設定檔中定義的 TCP 連接埠。 他們編寫直接從該設定檔讀取連接埠號的代碼。 但是,當他們推送代碼時,堆疊的測試階段失敗,因為來自其他專案的設定檔在build agent上不可用。
這次失敗是一件好事。 它暴露了兩個專案之間的耦合。 團隊成員可以變更其代碼以使用要開啟的連接埠號碼的參數值,並稍後設定該值。 該代碼比跨專案直接引用檔案的代碼庫更易於維護。
適用性
當我們需要代碼庫中的專案之間有明確的界限,但仍希望一起測試和交付每個專案的版本時,Delivery-time integration非常有用。 但該模式很難擴展到大量專案。
後果
Delivery-time integration將解決和協調不同專案的不同版本的複雜性放入交付過程中。 這需要複雜的交付實現,例如流水線。
實行
交付流水線使用「fan-in」流水線設計整合不同的項目。 將不同專案聚集在一起的階段稱為fan-in階段或專案整合階段。
階段(Stage)如何整合不同的專案取決於它所整合的專案類型。 在使用server image的堆疊專案範例中,將套用堆疊代碼並傳遞Image的相關版本的參考。 基礎設施相依性是從代碼交付儲存庫中檢索的。
在交付過程的後期階段需要套用同一組組合專案版本。 有兩種常見的方法來處理這個問題。
一種方法是將所有專案代碼捆綁到一個工件中,以便在後續階段使用。 例如,當整合和測試兩個不同的堆疊專案時,整合階段可以將兩個專案的代碼壓縮到單一檔案中,並將其promote到下游流水線階段。 GitOps 流程會將專案合併到整合階段分支,然後將它們從該分支合併到下游分支。
另一種方法是建立一個包含每個專案的版本號的描述符檔案(descriptor file)。 例如:
descriptor-version: 2.7.1
stack-project:
name: application-infrastructure-stack
version: 4.5.67
server-image-project:
name: application-server-image
version: 3.2.1
交付過程將描述符檔案視為工件。 應用基礎架構代碼的每個階段都會從交付儲存庫中提取單一專案工件。 第三種方法是使用聚合版本號來標示相關資源。
相關模式
Build-time專案整合模式在開始時整合專案,而不是在單獨的專案上已經發生一些交付活動之後。 Apply-time專案整合模式在每個交付階段整合專案,將它們一起使用,但不會「鎖定」版本。
模式: Apply-time專案整合
也稱為:去耦合交付或去耦合流水線。
Apply-time專案整合涉及將多個專案分別推進到交付階段。 當有人更改專案的代碼時,流水線會將更新的代碼套用到該專案交付路徑中的每個環境。 此版本的專案代碼可以與每個環境中的上游或下游專案的不同版本整合。
在 ShopSpinner 範例中,應用程式基礎設施堆疊專案依賴共享網路堆疊專案所建立的網路結構。 每個專案都有自己的交付階段,如圖 19–6 所示。
透過將applicationin-frastructure-stack code應用到環境中來進行專案之間的整合。 此操作建立或變更使用共用網路中的網路結構(例如子網路)的伺服器叢集。無論在特定環境中使用哪個版本的共用網路堆疊,都會發生這種整合。 因此,每次應用代碼時,版本的整合都是單獨發生的。
動機
在Apply-time整合專案可以最大限度地減少專案之間的耦合。 不同的團隊可以將對系統的變更推送到生產中,而無需協調,並且不會因對另一個團隊的專案進行更改而出現問題而受阻。
適用性
這種程度的解耦適合具有自治團隊結構的組織。 它還有助於較大規模的系統,在這種系統中,協調發布並在數百或數千名工程師之間同步交付它們是不切實際的。
後果
此模式將去除專案間依賴關係的風險轉移到了 apply-time 操作。 它不能確保整個流水線的一致性。 如果有人透過流水線推送對一個專案的變更比對其他專案的變更更快,那麼他們將在生產中與測試環境中不同的版本整合。
這需要仔細管理專案之間的介面,以最大限度地提高任何給特定依賴項每一方不同版本之間的相容性。 因此,這種模式在設計、維護和測試依賴項和介面方面需要更多的複雜性。
實行
在某些方面,使用Apply-time整合來設計和實現解耦的構建和流水線比替代模式更簡單。 每個流水線都會構建、測試和交付一個專案。
我們在"IaC Part 17-使用堆疊作為組件"一文中討論了整合不同基礎設施堆疊的策略。 例如,當一個Stage apply應用程式基礎設施堆疊時,它需要引用共享網路堆疊所建立的網路結構。
無法保證在任何特定環境中使用了另一個專案代碼的哪個版本。 因此,團隊需要清楚地識別專案之間的依賴關係,並將它們視為合約。 Shared-network-stack公開其他專案可能使用的網路結構的識別碼。 它需要使用"IaC Part 17-使用堆疊作為組件"中描述的模式之一以標準化的方式公開這些內容。
如”IaC Part 9 測試Infra Stacks”一文中的「使用Test Fixtures處理依賴關係」所述,我們可以使用Test Fixtures單獨測試每個堆疊。 透過 ShopSpinner 範例,團隊希望在不使用共用網路堆疊實例的情況下測試application-infrastructure-stack專案。 網路堆疊定義了支援測試用例不需要的冗餘且複雜的基礎設施。 因此,團隊的測試設定可以創建一組精簡的網路。 這樣做還可以降低應用程式堆疊演變為承擔網路堆疊實作細節的風險。
擁有其他專案所依賴的專案的團隊可以實施合約測試來證明其代碼符合預期。 共用網路堆疊可以驗證網路結構(子網路)是否已創建,以及它們的識別碼是否使用其他專案使用它們的機制公開。
確保合約測試有明確的標籤。 如果有人進行了導致測試失敗的代碼更改,他們應該明白他們可能會破壞其他專案,而不是認為他們只需要更新測試以匹配他們的更改。
許多組織發現CDC(consumer-driven contract)測試很有用。 透過此模型,開發依賴提供者專案中創建的資源的使用者專案的團隊可以編寫在提供者專案的流水線中運行的測試。 這有助於提供者團隊了解使用者團隊的期望。
相關模式
Build-time專案整合模式與此模式處於光譜的兩端。 此模式在交付週期開始時整合專案一次,而不是每次。 Delivery-time專案整合也可以整合專案一次,但是是在交付週期的某個時刻而不是在開始時。 IaC Part 11 -Building Servers as Codeu 一文中的「Frying a Server Instance」說明了用於伺服器配置的Apply-time整合。 每次建立新伺服器實例時都會套用伺服器設定模組等依賴項,通常會採用升級到相關階段的最新版本的伺服器模組。
使用腳本封裝基礎設施工具
大多數管理基礎架構代碼的團隊都會建立自訂腳本來編排和執行其基礎架構工具。 有些使用 Make、Rake 或 Gradle 等軟體建置工具。 其他人則使用 Bash、Python 或 PowerShell 編寫腳本。 在許多情況下,這種支援代碼至少變得與定義基礎架構的代碼一樣複雜,導致團隊花費大量時間來debug和維護它。
團隊可以在build-time、delivery-time或apply-time時執行這些腳本。 通常,腳本會處理多個專案階段。 這些腳本處理各種任務,其中可能包括:
Configuration
組合配置參數值,可能解決值的層次結構。
Dependencies
解析和檢索函式庫、提供者和其他代碼。
Packaging
準備交付代碼,無論是將其打包到工件中還是建立或合併分支。
Promotion
將代碼從一個階段移動到下一階段,無論是透過tag或移動工件還是建立或合併分支。
Orchestration
根據不同的堆疊和其他基礎設施元素的依賴性,以正確的順序應用它們。
Execution
運行相關的基礎設施工具,根據代碼應用的實例組裝命令列參數和設定檔。
Testing
設定和運行測試,包括配置測試裝置和資料,以及收集和發布結果。
組裝配置值(Configuration Values)
封送和解析配置值可能是更複雜的包裝器腳本任務之一。 考慮像 ShopSpinner 範例這樣的系統,該系統涉及多個交付環境、多個生產客戶實例和多個基礎設施元件。
一組簡單的單級配置值(每個元件、環境和客戶組合都有一個文件)需要相當多的檔案。 而且許多值都是重複的。
想像一下每個客戶的 store_name 值,必須為每個組件的每個實例設定該值。 團隊很快就決定在一個具有共享值的位置設定該值,並將代碼新增到其包裝器腳本中,以從共用配置和每個組件的配置中讀取值。
他們很快就發現需要在特定環境中的所有實例之間共用一些值,從而創建第三組配置。 當一個配置項在多個設定檔中具有不同的值時,腳本必須按照優先權層次結構來解析它。
這種類型的參數層次結構編碼起來有點混亂。 當引入新參數、配置正確的值以及追蹤和debug任何特定實例中使用的值時,技術人員更難理解。
使用配置註冊表(configuration registry)會帶來不同的複雜性。 不是在array of file中追蹤參數值,而是透過註冊表的各個subtrees追蹤它們。 包裝器腳本可能會處理來自註冊表不同部分的解析值,就像設定檔一樣。 或者,可以使用腳本預先設定註冊表值,因此它擁有解析預設值層次結構以設定每個實例的最終值的邏輯。 這兩種方法都會給設定和追蹤任何給定參數值的來源帶來麻煩。
簡化包裝腳本
通常來說團隊花費更多時間處理包裝腳本中的錯誤,而不是改進基礎設施代碼。 這種情況是由混合和耦合問題引起的,這些問題可以而且應該分開。 需要考量的一些領域:
拆分專案生命週期
單一腳本不應處理專案生命週期的build、promotion和apply階段的任務。 為每項活動編寫並使用不同的腳本。 確保清楚地了解訊息需要從這些階段之一傳遞到下一階段的位置。 與任何 API 或合約一樣,在這些階段之間實現明確的界限。
單獨的任務
分解管理基礎架構所涉及的各種任務,例如組裝配置值、打包程代碼和執行基礎架構工具。 再次定義這些任務之間的整合點並保持它們鬆散耦合。
解耦專案
跨多個專案編排操作的腳本應與在專案內執行任務的腳本分開。 並且應該可以獨立執行任何專案的任務。
讓包裝器代碼保持無知
腳本不應該了解它們支援的專案的任何資訊。 避免將依賴基礎設施代碼的操作hardcode到包裝器腳本中。 理想的包裝腳本是通用的,可用於特定形式的任何基礎設施專案(例如,使用特定堆疊工具的任何專案)。
將包裝器腳本視為“真實”代碼有助於解決所有這些問題。 使用 shellcheck 等工具測試和驗證腳本。 將良好的軟體設計規則應用於腳本,例如組合規則、單一職責原則以及圍繞領域概念進行設計。