IaC Part 17-使用堆疊作為組件
堆疊(Stack)通常是基礎設施系統中的最高層級組件。 它是可以獨立定義、配置和變更的最大單元。 可重複使用的堆疊模式讓我們將堆疊視為共用和重複使用基礎架構的主要單元。
由小型堆疊(Stack)組成的基礎設施比由模組和函式庫組成的大型堆疊更靈活。 與大型堆疊相比,我們可以更快、更輕鬆、更安全地變更小型堆疊。 因此,該策略支持利用變化速度提高質量,利用高品質實現快速變化的良性循環。
從多個堆疊建置系統需要保持每個堆疊的大小合適、設計良好、內聚且鬆散耦合(cohesive and loosely coupled)。IaC Part 15 主要實踐:小而簡易中的建議與堆疊以及其他類型的基礎設施組件相關。 堆疊的具體挑戰是在不產生緊密耦合的情況下實現它們之間的整合。
堆疊之間的整合通常涉及一個堆疊管理另一個堆疊使用的資源。 有許多流行的技術可以實現堆疊之間的資源發現和整合,但其中許多技術會產生緊密耦合,從而使更改變得更加困難。 因此,本文從不同的方法如何影響耦合的角度來探討它們(發現依賴關係)。
探索跨堆疊的依賴關係
電商系統包括一個應用程式基礎設施堆疊,它與另一個管理網路的sharednetwork-stack整合。 網路堆疊宣告一個 VLAN:
vlan:
name: "appserver_vlan"
address_range: 10.3.0.0/8
應用程式堆疊定義了一個應用程式伺服器,它被指派給VLAN:
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: "appserver_vlan"
此範例對兩個堆疊之間的依賴關係是hardcode,所以是非常緊密的耦合。 如果沒有網路堆疊的instance,就不可能測試應用程式堆疊代碼的變更。 對網路堆疊的變更受到其他堆疊對 VLAN 名稱的依賴的限制。
如果某個工程師決定加入更多 VLAN 以實現韌性,他們需要consumer stack更改其代碼實現以及對consumer stack的變更。 否則,他們可能會保留原來的名稱,混亂會增加,使代碼和基礎設施更難以理解和維護:
vlans:
- name: "appserver_vlan"
address_range: 10.3.0.0/8
- name: "appserver_vlan_2"
address_range: 10.3.1.0/8
- name: "appserver_vlan_3"
address_range: 10.3.2.0/8
對於不同的環境,Hardcode整合點可能會使維護多個Infra instance變得更加困難。 這可能取決於基礎設施平台針對特定資源的 API。 例如,我們可能會在不同的雲端帳號中為每個環境建立Infra stack instance,因此我們可以在每個環境中使用相同的 VLAN 名稱。 但更多時候,我們需要為多個環境整合不同的資源名稱。因此應該避免hardcode的依賴關係。 相反,應使用以下模式之一來探索依賴關係。
模式:資源匹配
Consumer stack使用資源匹配來尋找與名稱、標籤或其他識別特徵相符的基礎設施資源,從而發現依賴關係。 例如,provider stack可以根據屬於 VLAN 的資源類型和 VLAN 的環境來命名 VLAN(如下圖)。
在上範例中,vlan-appserver-staging 適用於stage環境中的應用程式伺服器。 應用程式基礎設施堆疊代碼透過匹配命名模式來尋找此資源:
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: "vlan-appserver-${ENVIRONMENT_NAME}"
動機
使用大多數堆疊管理工具和語言可以輕鬆實現資源匹配。 此模式主要消除了hardcode的依賴關係,減少了耦合。 資源匹配也避免了工具的耦合。 基礎設施的提供者和堆疊使用者可以使用不同的工具來實現。
適用性
當管理"Infra提供者和使用者"代碼的團隊都清楚地了解哪些資源應用作依賴項時,使用資源匹配來發現依賴項。 如果遇到打破團隊之間依賴關係的問題,可以可慮切換到替代模式。
資源匹配在較大的組織或跨組織中非常有用,其中不同的團隊可能使用不同的工具來管理其基礎設施,但仍需要在基礎設施層級進行整合。 即使目前每個人都使用單一工具,資源匹配也可以減少對該工具的鎖定,繼而為系統的不同部分提供使用新工具的選項。
後果
一旦consumer stack進行資源匹配以從另一個堆疊發現資源,匹配模式就成為一種契約。 如果有人更改shared networking stack中 VLAN 的命名模式,consumer stack的依賴性就會中斷。
因此,consumer stack團隊應該只透過以Infra提供者團隊明確支持的方式匹配資源來發現依賴關係。 Infra提供者團隊應清楚傳達他們支援哪些資源匹配模式,並確保以合約形式維護這些模式的完整性。
實行
透過配對發現基礎設施資源的方法有多種。 最直接的方法是在資源名稱中使用變量,如前面的範例代碼所示:
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: "vlan-appserver-${ENVIRONMENT_NAME}"
字串 vlan-appserver-${ENVIRONMENT_NAME} 將與環境的相關 VLAN 相符。 大多數堆疊語言都具有匹配資源名稱以外的其他屬性的功能。 Terraform有Data source,AWS CDK支援resource importing 在此範例中(使用偽代碼),Infra提供者為其 VLAN 指派標籤:
vlans:
- appserver_vlan
address_range: 10.3.0.0/8
tags:
network_tier: "application_servers"
environment: ${ENVIRONMENT_NAME}
Consumer code使用這些標籤發現它需要的 VLAN:
external_resource:
id: appserver_vlan
match:
tag: name == "network_tier" && value == "application_servers"
tag: name == "environment" && value == ${ENVIRONMENT_NAME}
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: external_resource.appserver_vlan
相關模式
資源匹配模式類似於堆疊資料查找模式。 主要區別在於資源匹配不依賴跨Infra提供者堆疊和使用者堆疊的相同堆疊工具的實作。
模式: 堆疊資料查找
也稱為: remote state file lookup、 stack reference lookupc或stack resource lookup。堆疊資料查找使用由管理Infra提供者堆疊的工具所維護的資料結構來尋找Infra提供者的資源(如下圖)。
許多堆疊管理工具維護每個stack instance的資料結構,其中包括堆疊代碼導出的值。 範例包括 Terraform 和 Pulumi remote state files。
動機
堆疊管理工具供應商可以使用其堆疊資料查找功能來整合不同的專案。 大多數跨堆疊資料共享的實作都要求Infra提供者堆疊明確宣告要發布哪些資源以供其他堆疊使用。 這樣做可以阻止堆疊使用者在Infra提供者不知情的情況下創建對資源的依賴關係。
適用性
當系統中的所有基礎設施都使用相同工具進行管理時,可以使用堆疊資料查找功能來發現堆疊之間的依賴關係。
後果
堆疊資料查找往往會有vendor lockin的狀況。 可以將該模式與不同的工具一起使用,如該模式的實作中所述。 但這會增加實施的複雜性。
這種模式有時會跨越同一個堆疊工具的不同版本。 該工具的升級可能涉及更改堆疊資料結構。 將Infra提供者堆疊升級到該工具的新版本時,這可能會導致問題。 在將堆疊使用者也升級到該工具的新版本之前,舊版本的工具可能無法從升級的堆疊提供者中提取資源值。 這會讓我們在跨堆疊逐步推進行堆疊工具升級,可能會迫使在整個資產中進行破壞性的協調升級。
實行
堆疊資料查找的實作使用堆疊管理工具及其定義語言的功能。 Terraform 將輸出值儲存在remote state file中。 Pulumi 也會將資源詳細資訊儲存在state file中,可以使用 StackReference 在堆疊使用者中引用該狀態檔案。
CloudFormation 可以跨堆疊匯出和匯入堆疊輸出值,AWS CDK 也可以存取這些值。 Infra提供者通常明確宣告其向使用者提供的資源:
stack:
name: shared_network_stack
environment: ${ENVIRONMENT_NAME}
vlans:
- appserver_vlan
address_range: 10.3.0.0/8
export:
- appserver_vlan_id: appserver_vlan.id
使用者宣告對提供者堆疊的引用,並使用它來引用該堆疊導出的 VLAN 識別碼:
external_stack:
name: shared_network_stack
environment: ${ENVIRONMENT_NAME}
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: external_stack.shared_network_stack.appserver_vlan.id
此範例將對外部堆疊的參考嵌入到堆疊代碼中。 另一種選擇是使用dependency injection,以便堆疊代碼與依賴項發現的耦合程度較低。 編排腳本從提供者堆疊中尋找輸出值,並將其作為參數傳遞給堆疊代碼。
儘管堆疊資料查找與管理提供者堆疊的工具相關聯,但通常可以在腳本中提取這些值,以便可以將它們與其他工具一起使用,如以下範例所示。
#!/usr/bin/env bash
VLAN_ID=$(
stack value \
--stack_instance shared_network-staging \
--export_name appserver_vlan_id
)
此代碼執行stack 指令,指定要尋找的堆疊實例 (shared_network-staging) 以及要讀取和列印的匯出變數 (appserver_vlan_id)。 shell 指令將指令的輸出(即 VLAN 的 ID)儲存在名為 VLAN_ID 的 shell 變數中。 然後腳本可以以不同的方式使用該變數。
相關模式
主要的替代模式是資源匹配和註冊表查找。
模式:整合註冊表查找
也稱為: integration registry
Consumer stack可以使用整合註冊表查找來探索提供者堆疊發布的資源(如下圖所示)。 兩個堆疊都引用註冊表,使用已知位置來儲存和讀取值。
許多堆疊工具支援在定義代碼中儲存和檢索來自不同類型註冊表的值。shared-networking-stack代碼設定此值:
vlans:
- appserver_vlan
address_range: 10.3.0.0/8
registry:
host: registry.shopspinner.jason
set:
/${ENVIRONMENT_NAME}/shared-networking/appserver_vlan: appserver_vlan.id
然後,application-infrastructure-stack帶碼檢索並使用該值:
registry:
id: stack_registry
host: registry.shopspinner.jason
values:
appserver_vlan_id: /${ENVIRONMENT_NAME}/shared-networking/appserver_vlan
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: stack_registry.appserver_vlan_id
動機
使用配置註冊表可以解耦不同基礎設施堆疊的堆疊管理工具。 不同的團隊可以使用不同的工具,只要他們同意使用相同的配置註冊表服務和在其中儲存值的命名約定。 這種解耦也使得增量升級和更改工具變得更加容易,一次一個堆疊。
使用配置註冊表可以使堆疊之間的整合點變得明確。堆疊使用者只能使用Infra提供者堆疊明確發布的值,因此Infra提供者團隊可以自由更改他們實現資源的方式。
適用性
整合註冊表查找模式對於大型企業或擔心被vendor lockin的組織也很有用,因為不同的團隊可能使用不同的技術。
例如,如果系統已使用配置註冊表來按照堆疊參數註冊表模式向堆疊實例提供配置值,則使用相同的註冊表來整合堆疊是有意義的。
後果
當採用整合註冊表查找模式時,配置註冊表將成為一項關鍵服務。 當註冊表掛掉時,可能無法設定或復原資源。
實行
配置註冊表是使用此模式的先決條件。 有關註冊表的討論,請參閱本部落格"IaC Part 7-配置Stack Instance"一文中的「Configuration Registry」。 一些基礎設施工具供應商提供註冊表伺服器,如IaC Part 7-配置Stack Instance的「基礎設施自動化工具註冊表」中所述。對於任何註冊表產品,請確保它得到使用的工具以及將來可能考慮使用的工具的良好支援。
建立明確的參數命名約定至關重要,尤其是在使用註冊表跨多個團隊整合基礎架構時。 許多組織使用類似於目錄或資料夾結構的分層命名空間,即使註冊表產品實現了簡單的key/value機制。 該結構通常包括架構單元(例如服務、應用程式或產品)、環境、地理位置或團隊的組件。
例如,ShopSpinner 可以使用基於地理區域的分層路徑:
/infrastructure/
├── au/
│ ├── shared-networking/
│ │ └── appserver_vlan=
│ └── application-infrastructure/
│ └── appserver_ip_address=
└── asia/
├── shared-networking/
│ └── appserver_vlan=
└── application-infrastructure/
└── appserver_ip_address=
在本例中,亞洲區域的應用程式伺服器的 IP 位址位於位置 /infrastruct/asia/application-infrastruct/appserver_ip_address。
相關模式
與此模式類似,堆疊資料查找模式在登錄中儲存和檢索vlaue。 此模式使用特定於堆疊管理工具的資料結構,而該模式使用通用註冊表實作。 參數註冊表模式本質上與此模式相同,因為堆疊從註冊表中提取值以在給定堆疊實例中使用。 唯一的區別是,使用此模式時,value來自另一個堆疊,並明確用於整合堆疊之間的基礎設施資源。
Dependency Injection
上述的這些模式描述了使用者發探索由提供者堆疊管理的資源的策略。 大多數堆疊管理工具都支援在堆疊定義程代碼中直接使用這些模式。 但是,有一個論點認為將定義堆疊資源的代碼與發現要整合的資源的代碼分開。 考慮依賴項匹配模式的早期實作範例中的這個片段:
external_resource:
id: appserver_vlan
match:
tag: name == "network_tier" && value == "application_servers"
tag: name == "environment" && value == ${ENVIRONMENT_NAME}
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: external_resource.appserver_vlan
這段代碼的核心部分是VM的宣告。 代碼片段中的其他所有內容都是外圍的,是用於組裝VM配置值的實作細節。
混合依賴和定義代碼的問題
將依賴關係探索與堆疊定義代碼結合會增加閱讀或使用代碼的認知精力。 雖然這並不會阻止把事情做好,但這些不方便性會累積起來。
可以透過將代碼拆分為堆疊專案中的單獨檔案來消除認知精力。 但是,在堆疊定義專案中包含探索代碼的另一個問題是,它將堆疊與依賴機制耦合。
與依賴機制的耦合類型和深度以及耦合所產生的影響類型將因不同的機制以及實現它們的方式而異。 我們應該避免或盡量減少與提供者堆疊以及配置註冊表等服務的耦合。
依賴管理和定義的耦合可能會導致創建和測試堆疊實例變得困難。 許多測試方法都使用臨時實例或測試替身(test doubles)等實踐來實現快速、頻繁的測試。 如果設定依賴關係涉及太多工作或時間,這可能會很有挑戰性。
將有關依賴項探索的特定假設hardcode到堆疊代碼中可能會降低其可重複使用性。 例如,如果建立一個核心應用程式伺服器基礎架構堆疊供其他團隊使用,則不同的團隊可能想要使用不同的方法來設定和管理其相依性。 有些團隊甚至可能想要更換不同的提供者堆疊。 例如,他們可能對面向外部和內部的應用程式使用不同的網路堆疊。
將依賴關係與其探索解耦
DI(Dependency injection)是一種組件接收其依賴項而不是自己發現它們的技術。 基礎設施堆疊專案會將其依賴的資源宣告為參數,與”IaC Part 7-配置Stack Instance”一文中描述的實例配置參數相同。編排堆疊管理工具的腳本或其他工具將負責探索依賴項並將它們傳遞到堆疊。
考量本文前面用來說明依賴關係探索模式的應用程式基礎架構堆疊範例在 DI 中的呈現:
parameters:
- ENVIRONMENT_NAME
- VLAN
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: ${VLAN}
此代碼宣告了兩個在將代碼套用至實例時必須設定的參數。 ENVIRONMENT_NAME 參數是一個簡單的堆疊參數,用於命名應用程式伺服器虛擬機器。 VLAN 參數是將虛擬機器指派到的 VLAN 的識別碼。
要管理此堆疊的實例,需要探索 VLAN 參數並提供一個值。 編排腳本可以使用本文中所述的任何模式來完成此操作。 它可以透過使用堆疊管理工具來尋找提供者堆疊專案的輸出來根據標籤設定參數,也可以在登錄中尋找值。
使用堆疊資料尋找的範例腳本可能會使用堆疊工具從提供者堆疊實例中檢索 VLAN ID,如以下範例所示,然後將該值傳遞給使用者堆疊實例的堆疊命令:
ENVIRONMENT_NAME=$1
VLAN_ID=$(
stack value \
--stack_instance shared_network-${ENVIRONMENT_NAME} \
--export_name appserver_vlan_id
)
stack apply \
--stack_instance application_infrastructure-${ENVIRONMENT_NAME} \
--parameter application_server_vlan=${VLAN_ID}
第一個指令從名為shared_network-${ENVIRONMENT_NAME}的提供者堆疊實例中擷取appserver_vlan_id值,然後將其作為參數傳遞給使用者堆疊application_infrastruct-${ENVIRON MENT_NAME}。
這種方法的好處是堆疊定義代碼更簡單並且可以在不同的背景脈絡中使用。 當我們變更筆電腦上的堆疊代碼時,可以傳遞任何 VLAN 值。 可以使用本機 API 模擬應用代碼,或套用到基礎架構平台上的個人實例。 在這些情況下提供的 VLAN 可能非常簡單。
在更類似於生產的環境中,VLAN 可能是更全面的網路堆疊的一部分。 這種交換不同提供者實現的能力使得實現漸進式測試變得更加容易,其中早期的pipeline stages快速運行並隔離測試使用者組件,而後期階段則測試更全面的整合系統。
結論
由設計良好、規模適當、鬆散耦合的堆疊組成基礎設施,可以更輕鬆、更快速、更安全地對系統進行變更。 做到這一點需要遵循模組化基礎設施的一般設計指南。 它還需要確保堆疊在共享和提供資源時不會過於緊密地耦合在一起。