IaC Part 16-由組件中建立Stack

我們在"IaC Part 15 主要實踐:小而簡易"一文中解釋了精心設計的組件如何使基礎設施系統的變更變得更容易、更安全。 此訊息支持IaC系列文章的主題,即利用變化的速度來不斷提高系統的品質,並利用高品質來實現更快的變化。

本文重點討論Infra stack的模組化; 也就是說,將Stack分成更小的代碼片段。 考慮模組化堆疊(stack)有幾個原因:

重複利用
將如何實現(Implementation)特定構造的知識放入組件中,以便可以在不同的堆疊中重複使用它

成品
建立一種能力,該能力是能交換概念的不同實現,以便可以靈活地建立堆疊

可測試性
透過將堆疊分解為可以在整合之前單獨測試的片段,可以提高測試的速度和測試的焦點。 如果組件是可組合的,可以用測試替身替換它們,以進一步提高測試的隔離性和速度

分享
在團隊之間共享可組合、可重複使用且經過充分測試的組件,以便技術團隊可以更快地建立更好的系統

在IaC Part 15中「Stack Components VS. Stacks as Components」所述,將堆疊分解為模組和函式庫可以簡化代碼,但不會使Stack Instance變得更小或更簡單。 堆疊組件(Stack Components)可能會掩蓋它們添加到Stack Instance的基礎設施資源的數量和複雜性,讓事情變得更糟。

因此,我們應該確保了解我們使用的抽象、函式庫和平台背後的背景脈絡。 這些東西很方便,可以讓你專注於更高層次的任務。 但它們不應該取代完全理解系統是如何實現的。

堆疊組件的基礎設施語言

我們在”Iac Part 4–定義一切都是代碼”一文中討論了不同類型的基礎設施程式語言。 定義堆疊的兩種主要語言風格是宣告式(declarative)和命令式(imperative)。 該文中提到這些不同類型的語言適合不同類型的代碼。

當技術人員使用錯誤類型的語言編寫堆疊組件時,這些差異通常會與堆疊組件發生衝突。 使用錯誤類型的語言通常會導致宣告式語言和命令式語言的混合,正如前面所解釋的,這是一件壞事。

使用哪種語言的決策往往取決於我們使用的基礎設施堆疊管理工具及其支援的語言。

本文後面定義的模式應該鼓勵我們思考想要透過特定堆疊及其組件來實現什麼目標。 要使用它來考量我們應該使用的語言類型以及可能的堆疊工具的類型,而根據語言類型考慮兩類堆疊組件。

透過模組重複使用宣告式代碼

大多數具有宣告式語言的堆疊管理工具都允許我們使用相同的語言編寫共用組件。 CloudFormation 有巢狀堆疊(nested stacks),Terraform 有模組(module)。 我們可以將參數傳遞給這些模組,並且這些語言至少具有一定的可程式性(例如 Terraform 的 HCL 表達式子語言)。 但這些語言從根本上來說是宣告式的,因此幾乎所有用它們編寫的複雜邏輯都是原始的。

因此,宣告式代碼模組最適合定義變化不大的基礎架構元件。 宣告式模組非常適合外觀模組(Facade module),它包裝並簡化了基礎設施平台提供的資源。 當我們將這些模組用於更複雜的情況時(例如建立spaghetti modules),它們會變得麻煩。

正如"IaC 8 -持續測試與交付"一文中討論到的「挑戰一: 測試宣告代瑪的價值通常很低」中所提到的,測試宣告式模組應該相當簡單。 應用宣告式模組的結果變化不大,因此我們不需要全面的測試覆蓋率。 這並不意味著我們不應該為這些模組編寫測試。 當模組組合多個宣告來建立更複雜的實體時,我們應該測試它是否符合其要求。

使用函式庫動態建立堆疊元素

一些堆疊管理工具(例如 Pulumi 和 AWS CDK)使用通用命令式語言。 我們可以使用這些語言編寫可從堆疊專案代碼中呼叫的可重複使用函式庫。 函式庫可以包含更複雜的邏輯,根據其使用方式動態配置基礎架構資源。

例如,團隊的基礎架構包括不同的application server infrastructure stacks。 每個堆疊都為該應用程式提供應用程式伺服器和網路結構。 有些應用程式面向一般大眾,有些則面向內部。

無論哪種情況,基礎設施堆疊都需要為伺服器分配 IP 位址和 DNS 名稱,並從相關Gateway建立網路路由。 面向一般大眾的應用程式與面向內部的應用程式的 IP 位址和 DNS 名稱將有所不同。 面向一般大眾的應用程式需要防火牆規則來允許連線。

checkout_service stack託管一個面向一般大眾的應用程式:

application_networking = new ApplicationServerNetwork(PUBLIC_FACING, "checkout")

virtual_machine:
name: appserver-checkout
vlan: $(application_networking.address_block)
ip_address: $(application_networking.private_ip_address)

堆疊代碼從 application_networking 函式庫建立一個 ApplicationServerNetwork object,該物件提供或引用必要的基礎設施元素:

class ApplicationServerNetwork {

def vlan;
def public_ip_address;
def private_ip_address;
def gateway;
def dns_hostname;

public ApplicationServerNetwork(application_access_type, hostname) {
if (application_access_type == PUBLIC_FACING) {
vlan = get_public_vlan()
public_ip_address = allocate_public_ip()
dns_hostname = PublicDNS.set_host_record(
"${hostname}.shopspinners.jason",
this.public_ip_address
)
} else {
// 類似的東西,但用於 private VLAN
}

private_ip_address = allocate_ip_from(this.vlan)
gateway = get_gateway(this.vlan)
create_route(gateway, this.private_ip_address)

if (application_access_type == PUBLIC_FACING) {
create_firewall_rule(ALLOW, '0.0.0.0', this.private_ip_address, 443)
}
}
}

上面的虛構碼將伺服器指派給已存在的public VLAN,並從 VLAN 的位址範圍設定其private IP 位址。 它還為伺服器設定一個public DNS entry,在我們的範例中為 checkout.shopspinners.jason。 該函式庫根據所使用的 VLAN 來尋找Gateway,因此這對於面向內部的應用程式來說會有所不同。

堆疊組件的模式

以下一組模式和反模式提供了設計堆疊組件和評估現有組件的思考。 這不是我們應該或不應該建立模組和函式庫的完整清單; 相反,它是思考這個主題的起點。

模式:外觀模組(Facade module)

也稱為wrapper module。

外觀模組會建立一個到堆疊工具語言或基礎設施平台的資源的簡化介面。 該模組會向呼叫代碼公開一些參數(如下範例)。

use module: shopspinner-server
name: checkout-appserver
memory: 16GB

此模組使用這些參數來呼叫它所包裝的資源,並對資源所需的其他參數的值進行hardcode(如下範例)。

declare module: shopspinner-server
virtual_machine:
name: ${name}
source_image: hardened-linux-base
memory: ${memory}
provision:
tool: servermaker
maker_server: maker.shopspinner.jason
role: application_server
network:
vlan: application_zone_vlan

此範例模組允許呼叫者建立VM,並指定伺服器的名稱和記憶體容量。 使用該模組建立的每個伺服器都使用該模組定義的source image、角色和網路。

動機
外觀模組簡化並標準化了基礎設施資源的常見用例。 使用外觀模組的堆疊代碼應該更簡單且更易於閱讀。 所有使用模組代碼的堆疊都可以快速獲得模組代碼品質的改進。

適用性
外觀模組最適合簡單的使用案例,通常涉及基本的基礎設施資源。

後果
外觀模組限制了使用底層基礎設施資源的方式。 這樣做很有用,可以簡化選項並有其實現是標準化的。 但它限制了靈活性,因此不適用於所有狀況。

模組是堆疊代碼和直接指定基礎設施資源的代碼之間的額外代碼層。 這個額外的層次至少增加了一些維護、除錯和改進代碼的維護資源(人力/時間)。 它還會使堆疊代碼更難理解。

實現(Implementation)
實現外觀模組通常涉及使用多個hardcodes value以及從使用該模組的帶碼傳遞的少量value來指定基礎設施資源。 宣告式基礎設施語言適用於外觀模組。

相關模式
混淆(Obfuscation)模組是一個外觀模組,不會隱藏太多內容,增加複雜性,但沒有增加太多值(value)。 一個bundle模組宣告了多個相關的基礎設施資源,所以就像一個包含更多部分的外觀模組。

反模式: 混淆(Obfuscation)模組

混淆模組包裝著由堆疊語言或基礎設施平台定義的基礎設施元素的代碼,但不會簡化它或添加任何特定值。 在最糟的情況下,該模組會使代碼變得複雜(如以下範例)。

use module: jason_server
server_name: checkout-appserver
ram: 16GB
source_image: base_linux_image
provisioning_tool: servermaker
server_role: application_server
vlan: application_zone_vlan

模組本身將參數直接傳遞給堆疊管理工具的代碼,如下範例所示。

declare module: jason_server
virtual_machine:
name: ${server_name}
source_image: ${origin_server_image}
memory: ${ram}
provision:
tool: ${provisioning_tool}
role: ${server_role}
network:
vlan: ${server_vlan}

動機
混淆模組可能是出現問題的外觀模組。 有時技術人員編寫這種模組的目的是遵循 DRY 原則。 他們發現定義通用基礎設施元素(例如虛擬伺服器、負載平衡器或安全群組)的代碼在函式庫的多個位置使用。 因此,他們建立了一個模組,該模組宣告該元素類型一次並在任何地方使用它。 但由於元素在代碼的不同部分的使用方式不同,因此它們需要在模組中公開大量參數。

其他人建立混淆模組是為了設計自己的語言來引用基礎設施元素,「改進」他們的堆疊工具提供的語言。

適用性
沒有人會故意寫出混淆模組。 我們可能會爭論特定的模組是混淆還是外觀,這種爭論是有用的。 應該考量模組是否增加了真正的價值,如果沒有,則將其重構為直接使用堆疊語言的代碼。

後果
編寫、使用和維護模組代碼而不是直接使用堆疊工具提供的結構會增加overhead。 它增加了更多需要維護的代碼、需要學習的認知精力以及建置和交付過程中額外的移動部件。 組件應該增加足夠的價值以使overhead是有價的。

實行
如果模組既沒有簡化其定義的資源,也沒有為底層堆疊語言代碼增加價值,請考慮直接使用堆疊語言來取代。

相關模式
混淆模組類似外觀模組,但並沒有明顯簡化底層代碼。

反模式: 非共享模組

非共享模組只在代碼庫中被使用一次,而不是被多個堆疊重複使用。

動機
技術人員通常建立非共享模組作為在堆疊專案中組織代碼的一種方式。

適用性
隨著堆疊專案代碼的增長,我們可能會想要將代碼劃分為模組。 如果劃分代碼來為每個模組編寫測試,則可以更輕鬆地使用代碼。 否則,可能有更好的方法來改進代碼庫。

後果
將單一堆疊的代碼組織成模組會增加代碼庫的overhead,可能包括版控和其他移動組件。 當我們不需要重複使用時建立一個可重複使用的模組是 YAGNI(「You Aren’t Gonna Need It」)的一個例子,現在投入精力以取得將來可能需要也可能不需要的效益。

實行
當堆疊專案變得太大時,有多種選擇可以將其代碼移至模組中。 使用適當的堆疊結構模式將堆疊拆分為多個堆疊通常會更好。 如果堆疊相當有凝聚力(cohesive),我們可以簡單地將代碼組織到不同的檔案中,如有必要,還可以組織到不同的資料夾中。 這樣做可以使代碼更易於導航和理解,而無需其他選項的overhead。

軟體重用的三法則說明,當我們在三個地方找到需要使用同一個東西時,我們應該將其轉變為可重複使用組件。

相關模式
非共享模組可以緊密地對應到較低層級的基礎設施元素(如外觀模組)或較高層級的實體(如基礎設施領域實體)。

模式: 綑綁模組(Bundle Module)

捆綁模組透過簡化的介面來宣告相關基礎設施資源的集合。 堆疊代碼使用模組來定義它需要提供的內容:

use module: application_server_jason
service_name: checkout_service
application_name: checkout_application
application_version: 4.56
min_cluster: 3
max_cluster: 9
ram_required: 8GB

模組代碼宣告多個基礎設施資源,通常以核心資源為中心。 在下面範例中,資源是伺服器集群,但也包括負載平衡器和 DNS entry。

server_cluster:
id: "${service_name}-cluster"
min_size: ${min_cluster}
max_size: ${max_cluster}
each_server_node:
source_image: base_linux
memory: ${ram_required}
provision:
tool: servermaker
role: appserver
parameters:
app_package: "${checkout_application}-${application_version}.war"
app_repository: "repository.shopspinner.jason"

load_balancer:
protocol: https
target:
type: server_cluster
target_id: "${service_name}-cluster"

dns_entry:
id: "${service_name}-hostname"
record_type: "A"
hostname: "${service_name}.shopspinner.jason"
ip_address: {$load_balancer.ip_address}

動機
捆綁模組對於定義基礎設施資源的內聚集合(cohesive collection)非常有用。 它避免了冗長且冗餘的代碼。 這些模組可用於獲取有關所需各種元素以及如何將它們連接在一起以實現共同目的的知識。

適用性
當我們使用宣告式堆疊語言,並且所涉及的資源在不同的使用案例中沒有變化時,捆綁模組是合適的。 如果我們發現需要模組根據使用情況建立不同的資源或以不同的方式配置它們,那麼您應該建立單獨的模組,或者切換到命令式語言並建立基礎結構領域實體(infrastructure domain entity)。

後果
在某些情況下,捆綁模組可能會提供比我們實際所需要的更多的資源。 該模組的使用者應該了解它提供的內容,並避免使用該模組(如果該模組對於他們的使用案例來說太多了)。

實行
以宣告式定義模組,包括與宣告的目的密切相關的基礎設施元素。

相關模式
外觀模組包裝單一基礎設施資源,而捆綁模組包含多個資源,儘管兩者本質上都是宣告式的。 基礎設施領域實體類似於捆綁模組,但動態產生基礎設施資源。 spaghetti module是一個捆綁模組,它希望它是一個領域實體,但由於其宣告式語言的限制而陷入錯亂。

反模式: Spaghetti Module

Spaghetti module是可配置的,可以根據特定的參數建立顯著不同的結果。 這個模組的實作很混亂且難以理解,因為它有太多的活動部件(如下範例)。

declare module: application-server-infrastructure
variable: network_segment = {
if ${parameter.network_access} = "public"
id: public_subnet
else if ${parameter.network_access} = "customer"
id: customer_subnet
else
id: internal_subnet
end
}

switch ${parameter.application_type}:
"java":
virtual_machine:
origin_image: base_tomcat
network_segment: ${variable.network_segment}
server_configuration:
if ${parameter.database} != "none"
database_connection: ${database_instance.my_database.connection_string}
end
...
"NET":
virtual_machine:
origin_image: windows_server
network_segment: ${variable.network_segment}
server_configuration:
if ${parameter.database} != "none"
database_connection: ${database_instance.my_database.connection_string}
end
...
"php":
container_group:
cluster_id: ${parameter.container_cluster}
container_image: nginx_php_image
network_segment: ${variable.network_segment}
server_configuration:
if ${parameter.database} != "none"
database_connection: ${database_instance.my_database.connection_string}
end
...
end

switch ${parameter.database}:
"mysql":
database_instance: my_database
type: mysql
...
...

上面代碼範例將其建立的伺服器指派給三個不同網段之一,並可選擇建立資料庫叢集並將連接字串傳遞到伺服器配置。 在某些情況下,它會建立一組容器實例而不是虛擬伺服器。

動機
與其他反模式一樣,技術人員通常會隨著時間的推移偶然建立spaghetti module。 我們可以建立一個外觀模組或捆綁模組,它們的複雜性會增加​​,以處理表面上看起來相似的不同使用案例。Spaghetti modules通常是由於嘗試使用宣告式語言實現基礎設施領域實體(infrastructure domain entity)而產生的。

後果
做太多事情的模組比範圍更窄的模組更難維護。 模組做的事情越多,它可以建立的基礎設施的變化越多,在不破壞某些東西的情況下改變它就越困難。 這些模組更難測試。 正如我們在"IaC Part 8-持續測試與交付"一文中所討論的,設計得更好的代碼更容易測試,因此,如果我們正在努力編寫自動化測試並建立pipeline來單獨測試模組,那麼這表示我們有一個spaghetti module。

實行
spaghetti module的代碼通常包含在不同情況下應用不同規範的條件。 例如,資料庫叢集模組可能採用一個參數來選擇要設定的資料庫。

當我們意識到手上有一個spaghetti module時,我們應該重構它。 通常,可以將其分成不同的模組,每個模組都有更集中的職責。 例如,可以將單一應用程式基礎架構模組分解為應用程式基礎架構不同部分的不同模組。 以這種方式使用分解模組(而不是使用上面範例中的 spaghetti module)的堆疊範例可能類似於以下範例。

use module: java-application-servers
name: checkout_appserver
application: "shopping_app"
application_version: "5.20"
network_segment: customer_subnet
server_configuration:
database_connection: ${module.mysql-database.outputs.connection_string}

use module: mysql-database
cluster_minimum: 3
cluster_maximum: 9
allow_connections_from: customer_subnet

每個模組都比原始的 spaghetti module更小、更簡單,因此更容易維護和測試。

相關模式
spaghetti module通常是使用宣告式代碼建立基礎設施領域實體(infrastructure domain entity)的嘗試。 它也可能是技術人員試圖擴展以處理不同使用案例的外觀模組或捆綁模組。

模式:基礎設施領域實體(infrastructure domain entity)

基礎設施領域實體透過組合多個較低層級的基礎設施資源來實現高階堆疊組件。 更高等級概念的一個例子是運行應用程式所需的基礎設施。

以下範例展示如何從堆疊專案代碼中使用 Java 應用程式基礎結構實例的函式庫:

use module: application_server
service_name: checkout_service
application_name: checkout_application
application_version: 4.56
traffic_level: high

該代碼定義了要部署的應用程式和版本,以及流量等級。 Domain entity library code看起來與捆綁模組範例類似,但包含根據traffic_level參數配置資源的動態代碼:

...
switch (${traffic_level}) {
case ("high") {
$appserver_cluster.min_size = 6
$appserver_cluster.max_size = 12
} case ("medium") {
$appserver_cluster.min_size = 3
$appserver_cluster.max_size = 6
} case ("low") {
$appserver_cluster.min_size = 1
$appserver_cluster.max_size = 3
}
}
...

動機
領域實體通常是抽象層的一部分,技術人員可以使用它來根據更高層級的需求定義和建立基礎設施。 基礎設施平台團隊建立其他團隊可以用來組裝堆疊的組件。

適用性
因為基礎設施領域實體動態地提供基礎設施資源,所以它應該用命令式語言而不是宣告式語言編寫。

實行
在具體層面上,實行基礎設施領域實體就是編寫代碼的問題。 但建立易於工程師學習和維護的高品質代碼庫的最佳方法是採用設計主導的方法。

我們應該吸取軟體架構和設計的經驗教訓。 基礎設施領域實體模式源自DDD(Domain Driven Design),它為軟體系統的業務域建立概念模型,並使用該模型來驅動系統本身的設計。 基礎設施,尤其是作為軟體設計和建構的基礎設施,應該被視為一個獨立的領域。 該領域是建構、交付和運行軟體。

對於組織來說,一種特別強大的方法是使用 DDD 來設計業務軟體的架構,然後擴展領域以包含用於建置和運行該軟體的系統和服務。

相關模式
捆綁模組類似於領域實體(domain entity),因為它建立基礎設施資源的內聚集合。 但是捆綁模組通常會創建一組相當靜態的資源,沒有太多變化。 捆綁模組的思維模式通常是自下而上的,從要創建的基礎設施資源開始。 領域實體是一種自上而下的方法,從使用案例的需求開始。

大多數基礎設施堆疊的spaghetti modules都是推動宣告式代碼來實現動態邏輯的結果。 但有時基礎設施領域實體變得過於複雜。 內聚性(cohesion)較差的領域實體會變成spaghetti modules。

建立一個抽象層

抽象層為較低層級的資源提供簡化的介面。 一組可重複使用、可組合的堆疊組件可以充當基礎設施資源的抽象層。 組件可以實現如何將基礎設施平台公開的低階資源組裝成對專注於更高層級任務的人員有用的實體知識。

例如,開發團隊可能需要定義一個包括應用程式伺服器、DB instance和message queues存取的環境。 團隊可以使用抽象路由和基礎設施資源權限組裝具有規則細節的組件。

即使對於具有實施低階資源的技能和經驗的團隊來說,組件也很有用。 抽像有助於分離不同的關注點,以便技術人員可以在特定的細節層級上關注問題。 他們還應該能夠深入了解並根據需要改進或擴展底層組件。

我們也許能夠使用更多靜態組件(如外觀模組或捆綁模組)為某些系統實作抽象層。 但更多時候我們需要該層的組件更加靈活,因此像基礎設施領域實體這樣的動態元件更有用。

當技術人員建立函式庫和其他組件時,抽象層可能會有機地出現。 但擁有更高層次的設計和標準是有用的,這樣該層的組件就能很好地協同作業,並融入系統的凝聚力視圖中。

抽象層的組件通常使用低階基礎設施語言建構。 許多團隊發現建立一種高階語言來使用抽象層定義堆疊非常有用。 結果通常是一種更高層級的宣告式語言,它指定了部分application runtime environment的要求,該環境呼叫以低階命令式語言編寫的動態組件。

結論

當我們有多個人員和團隊處理和使用基礎設施時,從組件建立堆疊會很有用。 但要小心抽象層和函式庫帶來的複雜性,並確保調整這些結構的使用以符合系統的大小和複雜性。

--

--

運用"雲端服務"加速企業的數位轉型願景
運用"雲端服務"加速企業的數位轉型願景

Written by 運用"雲端服務"加速企業的數位轉型願景

我們協助您駕馭名為"雲端運算"的怪獸,馴服它為您所用。諮詢請來信jason.kao@suros.com.tw. https://facebook.com/jason.kao.for.cloud

No responses yet