基礎設施即程式碼(Infrastructure as Code, IaC)的核心在於將基礎設施定義為可版本控制、可測試與可重複佈署的程式碼。本文將探討如何有效組織和構建基礎設施堆積積疊元件,以實作更高效、更可維護的基礎設施管理。

堆積積疊元件的語言選擇

基礎設施程式碼語言主要分為兩種風格:宣告式和命令式。這兩種語言風格適用於不同型別的程式碼,選擇正確的語言型別對於堆積積疊元件的實作至關重要。

宣告式與命令式語言的選擇考量

選擇哪種語言型別通常取決於你使用的基礎設施堆積積疊管理工具及其支援的語言。然而,更重要的是考慮你想透過特定堆積積疊及其元件達成的目標。根據語言型別,我們可以將堆積積疊元件分為兩類別:

  1. 宣告式模組:適用於定義變化不大的基礎設施元件
  2. 命令式程式函式庫:適用於需要動態佈建基礎設施資源的情境

使用模組重用宣告式程式碼

大多數具有宣告式語言的堆積積疊管理工具允許你使用相同的語言編寫分享元件。例如,CloudFormation 有巢狀堆積積疊,Terraform 有模組。你可以向這些模組傳遞引數,這些語言也具有一定的程式設計能力(如 Terraform 的 HCL 表示式子語言)。

然而,由於這些語言本質上是宣告式的,因此在其中編寫複雜邏輯往往會變得難以維護。因此,宣告式程式碼模組最適合定義變化不大的基礎設施元件。宣告式模組非常適合作為外觀模組(Facade Module),它包裝並簡化基礎設施平台提供的資源。

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

某些堆積積疊管理工具,如 Pulumi 和 AWS CDK,使用通用的命令式語言。你可以使用這些語言編寫可重用的程式函式庫,從堆積積疊專案程式碼中呼叫。程式函式庫可以包含更複雜的邏輯,根據使用方式動態佈建基礎設施資源。

例如,假設我們有不同的應用程式伺服器基礎設施堆積積疊。每個堆積積疊都為應用程式佈建伺服器和網路結構。有些應用程式導向公眾,有些則導向內部。

在這兩種情況下,基礎設施堆積積疊都需要為伺服器分配 IP 地址和 DNS 名稱,並從相關閘道建立網路由。導向公眾的應用程式和導向內部的應用程式的 IP 地址和 DNS 名稱會有所不同。此外,導向公眾的應用程式需要防火牆規則來允許連線。

// 導向公眾的結帳服務堆積積疊
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 物件,該物件佈建或參照必要的基礎設施元素:

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.xyz",
        this.public_ip_address
      )
    } else {
      // 類別似的程式碼,但用於私有 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)
    }
  }
}

這段程式碼將伺服器分配到已存在的公共 VLAN,並從 VLAN 的地址範圍設定其私有 IP 地址。它還為伺服器設定公共 DNS 條目,在我們的例子中將是 checkout.shopspinners.xyz。程式函式庫根據使用的 VLAN 找到閘道,因此對於導向內部的應用程式,這將是不同的。

堆積積疊元件的模式

以下是設計堆積積疊元件和評估現有元件的模式和反模式,作為思考這個主題的起點。

模式:外觀模組 (Facade Module)

外觀模組為堆積積疊工具語言或基礎設施平台的資源建立簡化的介面。該模組向呼叫程式碼公開少量引數:

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

模組使用這些引數呼叫它包裝的資源,並為資源需要的其他引數硬編碼值:

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

這個模組允許呼叫者建立虛擬伺服器,指定伺服器的名稱和記憶體量。使用該模組建立的每個伺服器都使用模組定義的來源映像、角色和網路。

動機:外觀模組簡化並標準化基礎設施資源的常見使用案例。使用外觀模組的堆積積疊程式碼應該更簡單、更易於閲讀。模組程式碼品質的改進可以迅速應用於使用它的所有堆積積疊。

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

後果:外觀模組限制了你使用底層基礎設施資源的方式。這可能很有用,簡化選項並圍繞更好、更安全的實作進行標準化。但它限制了靈活性,因此不適用於每個使用案例。

模組是堆積積疊程式碼和直接指定基礎設施資源的程式碼之間的額外層。這個額外層至少會增加維護、除錯和改程式式碼的開銷。它也可能使理解堆積積疊程式碼變得更困難。

反模式:混淆模組 (Obfuscation Module)

混淆模組包裝堆積積疊語言或基礎設施平台定義的基礎設施元素的程式碼,但不簡化它或增加任何特定價值。在最糟糕的情況下,模組使程式碼變得複雜:

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

模組本身將引數直接傳遞給堆積積疊管理工具的程式碼:

declare module: any_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 原則(避免重複)。他們看到定義常見基礎設施元素(如虛擬伺服器、負載平衡器或安全組)的程式碼在程式碼函式庫的多個地方使用。因此,他們建立一個模組,在一個地方宣告該元素型別,並在所有地方使用它。但由於元素在程式碼的不同部分被不同地使用,他們需要在模組中公開大量引數。

其他人建立混淆模組是為了設計自己的語言來參照基礎設施元素,“改進"堆積積疊工具提供的語言。

適用性:沒有人故意編寫混淆模組。你可能會爭論給定的模組是混淆還是外觀,這種爭論是有用的。你應該考慮模組是否增加真正的價值,如果沒有,則將其重構為直接使用堆積積疊語言的程式碼。

後果:編寫、使用和維護模組程式碼而不是直接使用堆積積疊工具提供的建構會增加開銷。它增加了更多需要維護的程式碼,學習的認知開銷,以及構建和交付過程中的額外移動部分。元件應該增加足夠的價值,使開銷值得。

反模式:未分享模組 (Unshared Module)

未分享模組在程式碼函式庫中只使用一次,而不是被多個堆積積疊重用。

動機:人們通常建立未分享模組作為組織堆積積疊專案內程式碼的方式。

適用性:隨著堆積積疊專案程式碼的增長,你可能會傾向於將程式碼分成模組。如果你分割程式碼以便為每個模組編寫測試,這可以使處理程式碼變得更容易。否則,可能有更好的方法來改程式式碼函式庫。

後果:將單個堆積積疊的程式碼組織成模組會增加程式碼函式庫的開銷,可能包括版本控制和其他移動部分。在不需要重用時構建可重用模組是 YAGNI(“你不會需要它”)的一個例子,現在投入努力以獲得將來可能需要也可能不需要的好處。

實作:當堆積積疊專案變得太大時,有幾種替代方案可以將其程式碼移入模組。通常更好的做法是將堆積積疊分成多個堆積積疊,使用適當的堆積積疊結構模式。如果堆積積疊相當有凝聚力,你可以將程式碼組織到不同的檔案中,如果需要,還可以組織到不同的資料夾中。這樣做可以使程式碼更容易導航和理解,而不會增加其他選項的開銷。

軟體重用的三次法則建議,當你發現三個地方需要使用某物時,應該將其變成可重用元件。

模式:捆綁模組 (Bundle Module)

捆綁模組宣告一組相關的基礎設施資源,並提供簡化的介面。堆積積疊程式碼使用該模組定義需要佈建的內容:

use module: application_server
service_name: checkout_service
application_name: checkout_application
application_version: 1.23
min_cluster: 1
max_cluster: 3
ram_required: 4GB

模組程式碼宣告多個基礎設施資源,通常圍繞一個核心資源。在下面的例子中,資源是伺服器叢集,但也包括負載平衡器和 DNS 條目:

declare module: application_server
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.xyz"
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.xyz"
  ip_address: {$load_balancer.ip_address}

動機:捆綁模組用於定義一組有凝聚力的基礎設施資源。它避免冗長、重複的程式碼。這些模組對於捕捉有關所需的各種元素以及如何將它們連線起來以達到共同目的知識很有用。

適用性:捆綁模組適用於使用宣告式堆積積疊語言時,以及當涉及的資源在不同使用案例中不會變化時。如果你發現需要模組根據使用情況建立不同的資源或不同地設定它們,那麼你應該建立單獨的模組,或者切換到命令式語言並建立基礎設施領域實體。

後果:捆綁模組在某些情況下可能會佈建比你需要的更多資源。模組的使用者應該瞭解它佈建的內容,如果模組對他們的使用案例來説過於複雜,則避免使用該模組。

反模式:義大利麵模組 (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
    ...
  ...

這個例子程式碼將它建立的伺服器分配到三個不同的網路段之一,並可選地建立資料函式庫叢集並將連線字元串傳遞給伺服器設定。在某些情況下,它建立一組容器例項而不是虛擬伺服器。這個模組相當複雜。

動機:與其他反模式一樣,人們意外地建立義大利麵模組,通常是隨著時間的推移。你可能建立一個外觀模組或捆綁模組,隨著複雜性增加以處理表面上看似相似的不同使用案例。

義大利麵模組通常是嘗試使用宣告式語言實作基礎設施領域實體的結果。

後果:做太多事情的模組比範圍更緊密的模組更難維護。模組做的事情越多,它可以建立的基礎設施變化越多,在不破壞某些東西的情況下更改它就越困難。

這些模組更難測試。如果你在編寫自動化測試和構建管道以隔離測試模組時遇到困難,這是你有一個義大利麵模組的跡象。

實作:義大利麵模組的程式碼通常包含在不同情況下應用不同規範的條件。例如,資料函式庫叢集模組可能接受一個引數來選擇要佈建的資料函式庫。

當你意識到你有一個義大利麵模組時,你應該重構它。通常,你可以將其分成不同的模組,每個模組都有更集中的職責。例如,你可以將單個應用程式基礎設施模組分解為應用程式基礎設施不同部分的不同模組。

模式:基礎設施領域實體 (Infrastructure Domain Entity)

基礎設施領域實體透過組合多個低階基礎設施資源來實作高階堆積積疊元件。高階概念的一個例子是執行應用程式所需的基礎設施。

以下是實作 Java 應用程式基礎設施例項的程式函式庫如何從堆積積疊專案程式碼中使用的例子:

use module: application_server
service_name: checkout_service
application_name: checkout_application
application_version: 1.23
traffic_level: medium

程式碼定義要佈署的應用程式和版本,以及流量級別。領域實體程式函式庫程式碼可能類別似於捆綁模組範例,但包括根據 traffic_level 引數佈建資源的動態程式碼:

...
switch (${traffic_level}) {
  case ("high") {
    $appserver_cluster.min_size = 3
    $appserver_cluster.max_size = 9
  } case ("medium") {
    $appserver_cluster.min_size = 2
    $appserver_cluster.max_size = 5
  } case ("low") {
    $appserver_cluster.min_size = 1
    $appserver_cluster.max_size = 2
  }
}
...

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

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

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

我建議從軟體架構和設計中吸取經驗教訓。基礎設施領域實體模式源自領域驅動設計(DDD),它為軟體系統的業務領域建立概念模型,並使用它來驅動系統本身的設計。

基礎設施,特別是設計和構建為軟體的基礎設施,應該被視為自己的領域。該領域是構建、交付和執行軟體。

一種特別強大的方法是組織使用 DDD 設計業務軟體的架構,然後將領域擴充套件到包括用於構建和執行該軟體的系統和服務。

構建抽象層

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

例如,應用程式團隊可能需要定義包括應用程式伺服器、資料函式庫例項和存取訊息佇列的環境。該團隊可以使用抽象路由規則和基礎設施資源許可權組裝細節的元件。

即使對於具有技能和經驗實作低階資源的團隊,元件也可能有用。抽象有助於分離不同的關注點,使人們能夠專注於特定詳細級別的問題。他們還應該能夠深入瞭解並可能改進或擴充套件底層元件。

你可能夠使用更靜態的元件(如外觀模組或捆綁模組)為某些系統實作抽象層。但更常見的是,你需要層的元件更靈活,因此像基礎設施領域實體這樣的動態元件更有用。

抽象層可能隨著人們構建程式函式庫和其他元件而有機地出現。但有一個更高階別的設計和標準是有用的,這樣層的元件可以很好地協同工作並適合系統的凝聚檢視。

抽象層的元件通常使用低階基礎設施語言構建。許多團隊發現為他們的抽象層定義堆積積疊構建高階語言很有用。結果通常是一種更高階別的宣告式語言,它指定應用程式執行時環境部分的需求,它呼叫用低階命令式語言編寫的動態元件。

從元件構建堆積積疊在有多個人和團隊處理和使用基礎設施時非常有用。但要警惕抽象層和元件函式庫帶來的複雜性,並確保根據系統的大小和複雜性調整這些建構的使用。

在選擇基礎設施程式碼語言和組織堆積積疊元件時,關鍵是理解你的需求並選擇最適合的方法。宣告式語言適合定義變化不大的基礎設施元件,而命令式語言則更適合需要動態邏輯的情況。

透過遵循本文中討論的模式和避免反模式,你可以建立更易於維護、更靈活與更有效的基礎設施程式碼。記住,好的設計是關於平衡簡單性、靈活性和功能性,以滿足你的特定需求。

基礎設施程式碼的組織與整合策略

堆積積疊設定案的組織方式

在管理不同環境的堆積積疊專案引數值時,有兩種主要的組織方式。第一種是將設定案存放在相關專案內部:

├── 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

將設定值與專案程式碼存放在一起,會將通用、可重複使用的程式碼與特定例項的細節混合。理想情況下,變更環境設定不應該需要修改堆積積疊專案本身。

另一方面,當設定值靠近相關專案時,可能更容易追蹤和理解,而不是混合在一個單一的設定專案中。團隊所有權和協作也是一個因素。分離基礎設施程式碼和其設定可能會降低團隊對兩者的所有權和責任感。

管理基礎設施與應用程式碼

應用程式和基礎設施程式碼應該分開存放還是放在一起?答案取決於組織結構和所有權分配。

將基礎設施和應用程式碼分開存放在不同儲存函式庫中,支援由不同團隊建立和管理基礎設施與應用程式的運作模式。但當應用程式團隊負責基礎設施時,特別是與其應用程式相關的基礎設施,這種分離會帶來挑戰。

程式碼分離會形成認知障礙,即使應用程式團隊成員被賦予與其應用程式相關的基礎設施元素的責任。如果這些程式碼位於他們不常使用的儲存函式庫中,他們不會有相同程度的舒適感去深入研究。當這些程式碼不太熟悉,與與系統其他部分的基礎設施程式碼混合時,這種情況尤為明顯。

位於團隊自己程式碼區域的基礎設施程式碼較不令人生畏。團隊成員較少擔心變更可能會破壞其他人的應用程式或基礎設施的基本部分。

DevOps與團隊結構

DevOps運動鼓勵組織嘗試傳統開發和營運分工的替代方案。Matthew Skelton和Manuel Pais關於團隊拓撲的著作提供了更深入的思考,探討如何構建應用程式和基礎設施團隊。

交付基礎設施與應用程式

無論是否將應用程式和基礎設施程式碼放在一起管理,最終都會將它們佈署到同一個系統中。基礎設施程式碼的變更應該在整個應用程式交付流程中與應用程式整合和測試。

許多組織將生產基礎設施視為獨立的孤島,這是一種傳統觀點。通常,一個團隊擁有生產基礎設施(包括預生產環境),但不負責開發和測試環境。

這種分離為應用程式交付和基礎設施變更創造了摩擦。團隊直到交付過程後期才發現系統兩部分之間的衝突或差距。持續整合和測試系統所有部分是確保高品質和可靠交付的最有效方式。

因此,交付策略應該將基礎設施程式碼的變更應用於所有環境。基礎設施變更流程有幾種選擇。

使用應用程式測試基礎設施

如果沿著應用程式交付路徑交付基礎設施變更,可以利用自動化應用程式測試。在每個階段,應用基礎設施變更後,觸發應用程式測試階段。

漸進式測試方法使用應用程式測試進行整合測試。應用程式和基礎設施版本可以繫結在一起,按照交付時間整合模式透過交付流程的其餘部分進行。或者,基礎設施變更可以推播到下游環境,而不與正在進行的任何應用程式變更整合,使用應用時間整合。

盡可能快速推播應用程式和基礎設施變更是理想的。但實際上,並非總是能夠消除組織中所有型別變更的摩擦。例如,如果利益相關者要求對導向使用者的應用程式變更進行更深入的審查,可能需要更快地推播常規基礎設施變更。否則,應用程式發布流程可能會阻礙安全補丁等緊急變更和設定更新等小變更。

在整合前測試基礎設施

將基礎設施程式碼應用於分享的應用程式開發和測試環境的風險是,破壞這些環境會影響其他團隊。因此,最好有專門的交付階段和環境,用於獨立測試基礎設施程式碼,然後再將其提升到分享環境。

使用基礎設施程式碼佈署應用程式

基礎設施程式碼定義了伺服器上的內容。佈署應用程式涉及將內容放到伺服器上。因此,編寫基礎設施程式碼來自動化應用程式的佈署過程似乎是合理的。但在實踐中,混合應用程式佈署和基礎設施設定的關注點會變得混亂。應用程式和基礎設施之間的介面應該簡單明確。

作業系統封裝系統(如RPM、.deb檔案和.msi檔案)是封裝和佈署應用程式的明確定義介面。基礎設施程式碼可以指定要佈署的包檔案,然後讓佈署工具接管。

當佈署應用程式涉及多個活動,特別是涉及多個移動部分時,問題就會出現。例如,玄貓曾經編寫了一個Chef cookbook來將團隊的Dropwizard Java應用程式佈署到Linux虛擬機器上。這個cookbook需要:

  1. 下載並解壓新應用程式版本到新資料夾
  2. 如果先前版本的應用程式正在執行,停止其程式
  3. 如果需要,更新設定檔案
  4. 更新指向應用程式當前版本的符號連結到新資料夾
  5. 執行資料函式庫架構遷移指令碼
  6. 啟動新應用程式版本的程式
  7. 檢查新程式是否正常工作

這個cookbook對團隊來説很麻煩,有時無法檢測先前程式是否已終止,或新程式在啟動後一分鐘左右當機。

從根本上説,這是宣告式基礎設施程式碼函式庫中的一個程式性指令碼。在決定將應用程式封裝為RPM後,團隊取得了更大的成功,這意味著可以使用專門用於佈署和升級應用程式的工具和指令碼。團隊為RPM封裝過程編寫了測試,這些測試不依賴於Chef程式碼函式庫的其餘部分,因此可以專注於使佈署不可靠的特定問題。

使用基礎設施程式碼佈署應用程式的另一個挑戰是當佈署過程需要協調多個部分時。團隊的流程在將Dropwizard應用程式佈署到單個伺服器時運作良好,但不適用於在多個伺服器之間負載平衡應用程式的情況。

即使在轉向RPM包後,cookbook也無法管理跨多個伺服器的佈署順序。因此,在佈署操作期間,叢集會執行混合的應用程式版本。而與資料函式庫架構遷移指令碼應該只執行一次,所以需要實作鎖定機制,確保只有第一個伺服器的佈署過程會執行它。

解決方案是將佈署操作從伺服器設定程式碼移出,放入從中央佈署位置(建置伺服器)將應用程式推播到伺服器的指令碼中。這個指令碼管理伺服器佈署的順序和資料函式庫架構遷移,透過修改負載平衡器的設定實作零停機佈署,進行滾動升級。

分散式、雲原生應用程式增加了協調應用程式佈署的挑戰。協調對數十、數百或數千個應用程式例項的變更可能變得非常複雜。團隊使用像Helm或Octopus Deploy這樣的佈署工具來定義應用程式組的佈署。這些工具透過專注於佈署應用程式集,將底層叢集的設定留給程式碼函式庫的其他部分,從而強制關注點分離。

然而,最穩健的應用程式佈署策略是保持每個元素鬆散耦合。佈署變更獨立於其他變更越容易和安全,整個系統就越可靠。

交付基礎設施程式碼

管道隱喻描述了基礎設施程式碼的變更如何從進行變更的人員到生產例項。這個交付過程所需的活動影響程式碼函式庫的組織方式。

交付程式碼版本的管道有多種活動型別,包括建置、提升、應用和驗證。管道中的任何給定階段可能涉及多種活動。

建置

準備程式碼版本供使用,並使其可用於其他階段。建置通常在管道中只進行一次,每次原始碼變更時。

提升

在交付階段之間移動程式碼版本。例如,一旦堆積積疊專案版本透過了堆積積疊測試階段,它可能被提升以表明它已準備好進入系統整合測試階段。

應用

執行相關工具將程式碼應用於相關基礎設施例項。例項可以是用於測試活動的交付環境或生產例項。

建置基礎設施專案

建置基礎設施專案準備程式碼供使用。活動可能包括:

  • 檢索建置時依賴項,如程式函式庫,包括來自程式碼函式庫中其他專案的程式函式庫和外部程式函式庫
  • 解析建置時設定,例如提取跨多個專案分享的設定值
  • 編譯或轉換程式碼,如從範本生成設定檔案
  • 執行測試,包括離線和線上測試
  • 準備程式碼供使用,將其放入相關基礎設施工具用於應用的格式
  • 使程式碼可用

有幾種不同的方式來準備基礎設施程式碼並使其可用。一些工具直接支援特定的方式,如標準成品包格式或儲存函式庫。其他工具則讓使用該工具的團隊實作自己的程式碼交付方式。

將基礎設施程式碼封裝為成品

對於某些工具,“準備程式碼供使用"涉及將檔案組裝成特定格式的包檔案,即成品。這個過程在通用程式設計語言(如Ruby的gems、JavaScript的NPM和Python的pip安裝程式包)中很典型。其他用於安裝特定作業系統檔案和應用程式的包格式包括.rpm、.deb、.msi和NuGet(Windows)。

不多基礎設施工具有其程式碼專案的包格式。然而,一些團隊為這些工具建立自己的成品,將堆積積疊程式碼或伺服器程式碼封裝成ZIP檔案或"tarballs”(用gzip壓縮的tar檔案)。一些團隊使用作業系統封裝格式,例如建立將Chef Cookbook檔案解壓到伺服器上的RPM檔案。其他團隊建立Docker映像,其中包含堆積積疊專案程式碼和堆積積疊工具可執行檔案。

使用儲存函式庫交付基礎設施程式碼

團隊使用原始碼儲存函式庫來儲存和管理基礎設施原始碼的變更。他們通常使用單獨的儲存函式庫來儲存準備交付到環境和例項的程式碼。一些團隊同時使用同一個儲存函式庫來達到這兩個目的。

從概念上講,建置階段分離了這兩種儲存函式庫型別,從原始碼儲存函式庫取得程式碼,組裝它,然後將其發布到交付儲存函式庫。

交付儲存函式庫通常儲存給定專案程式碼的多個版本。提升階段將專案程式碼的版本標記為顯示它們已進展到哪個階段;例如,是否已準備好進行整合測試或生產。

應用活動從交付儲存函式庫中提取專案程式碼的版本,並將其應用於特定例項,如SIT環境或PROD環境。

有幾種不同的方式來實作交付儲存函式庫。給定系統可能為不同型別的專案使用不同的儲存函式庫實作。例如,他們可能為使用該工具的專案使用特定於工具的儲存函式庫,如Chef Server。同一系統可能為使用沒有包格式或專用儲存函式庫的工具(如Packer)的專案使用通用檔案儲存服務,如S3儲存桶。

交付程式碼儲存函式庫實作有幾種型別:

  1. 專用成品儲存函式庫:大多數前面討論的包格式都有包儲存函式庫產品、服務或標準,多個產品和服務可能實作這些標準。.rpm、.deb、.gem和.npm檔案有多個儲存函式庫產品和託管服務。一些儲存函式庫產品,如Artifactory和Nexus,支援多種包格式。

  2. 工具特定儲存函式庫:許多基礎設施工具有一個專用儲存函式庫,不涉及封裝成品。相反,執行一個工具將專案的程式碼上載到伺服器,分配一個版本。這與專用成品儲存函式庫的工作方式幾乎相同,但沒有包檔案。

  3. 通用檔案儲存函式庫:許多團隊,特別是那些使用自己的格式來儲存基礎設施程式碼專案進行交付的團隊,將它們儲存在通用檔案儲存服務或產品中。這可能是檔案伺服器、Web伺服器或物件儲存服務。

  4. 從原始碼儲存函式庫交付程式碼:鑑於原始碼已經儲存在原始碼儲存函式庫中,與許多基礎設施程式碼工具沒有將其程式碼視為發布的包格式和工具鏈,許多團隊直接從原始碼儲存函式庫將程式碼應用於環境。

整合專案

程式碼函式庫內的專案通常彼此之間有依賴關係。下一個問題是何時以及如何組合相互依賴的不同版本專案。

以ShopSpinner團隊程式碼函式庫中的幾個專案為例。他們有兩個堆積積疊專案。其中一個,application_infrastructure-stack,定義了特定於應用程式的基礎設施,包括虛擬機器池和應用程式流量的負載平衡器規則。另一個堆積積疊專案,shared_network_stack,定義了由多個application_infrastructure-stack例項分享的通用網路,包括地址塊(VPC和子網)和允許流量到應用程式伺服器的防火牆規則。

團隊還有兩個伺服器設定專案,tomcat-server設定並安裝應用程式伺服器軟體,monitor-server為監控代理做同樣的事情。

第五個基礎設施專案,application-server-image,使用tomcat-server和monitor-server設定模組構建伺服器映像。

application_infrastructure-stack專案在shared_network_stack建立的網路結構內建立其基礎設施。它還使用application-server-image專案構建的伺服器映像來建立其應用程式伺服器叢集中的伺服器。而application-server-image透過應用tomcat-server和monitor-server中的伺服器設定義來構建伺服器映像。

當有人對這些基礎設施程式碼專案之一進行變更時,它會建立該專案程式碼的新版本。該專案版本必須與每個其他專案的版本整合。專案版本可以在建置時、交付時或應用時整合。

給定系統的不同專案可以在不同點整合,如以下模式描述中的ShopSpinner範例所示。

模式:建置時專案整合

建置時專案整合模式跨多個專案執行建置活動。這涉及整合它們之間的依賴關係並設定跨專案的程式碼版本。

建置過程通常涉及在一起建置和測試它們之前建置和測試每個組成專案。將此模式與替代方案區分開來的是,它為所有專案生成單一成品,或作為一組版本化、提升和應用的成品組。

在這個例子中,單一建置階段使用多個伺服器設定專案生成伺服器映像。

建置階段可能包括多個步驟,如建置和測試單個伺服器設定模組。但輸出——伺服器映像——由來自所有組成專案的程式碼組成。

動機

一起建置專案可以早期解決任何依賴問題。這提供了關於衝突的快速反饋,並透過交付過程到生產環境建立了程式碼函式庫的高度一致性。在建置時整合的專案程式碼在整個交付週期中保持一致。相同版本的程式碼在過程的每個階段都被應用,直到生產環境。

適用性

使用此模式與替代方案之一主要是偏好問題。這取決於您偏好哪組權衡,以及您團隊管理跨專案建置複雜性的能力。

後果

在執行時建置和整合多個專案很複雜,特別是對於大量專案。根據您如何實作建置,它可能導致反饋時間變慢。

在規模上使用建置時專案整合需要複雜的工具來協調建置。在大型程式碼函式庫中使用此模式的大型組織,如Google和Facebook,有專門的團隊維護內部工具。

有一些建置工具可用於建置大量軟體專案,如實作部分所討論的。但這種方法在業界的使用不如單獨建置專案廣泛,因此沒有那麼多工具和參考資料可以幫助。

因為專案是一起建置的,所以它們之間的邊界比其他模式更不明顯。這可能導致跨專案的更緊密耦合。當這種情況發生時,很難在不影響程式碼函式庫許多其他部分的情況下進行小變更,增加了變更的時間和風險。

實作

將建置的所有專案儲存在單一儲存函式庫中,通常稱為monorepo,透過整合跨專案的程式碼版本控制來簡化一起建置它們的過程。

大多數軟體建置工具,如Gradle、Make、Maven、MSBuild和Rake,用於協調跨適量專案的建置。跨大量專案執行建置和測試可能需要很長時間。

平行化可以透過在不同執行緒、程式甚至跨計算網格中建置和測試多個專案來加速這個過程。但這需要更多計算資源。

最佳化大規模建置的更好方法是使用有向圖來限制建置和測試到程式碼函式庫中已更改的部分。做得好的話,這應該減少提交後建置和測試所需的時間,使其只比執行單獨專案的建置稍長一些。

有幾種專門設計用於處理大規模、多專案建置的建置工具。這些工具大多受到Google和Facebook建立的內部工具的啟發。這些工具包括Bazel、Buck、Pants和Please。

模式:交付時專案整合

給定多個相互依賴的專案,交付時專案整合在組合它們之前單獨建置和測試每個專案。這種方法比建置時整合晚整合程式碼版本。

一旦專案被組合和測試,它們的程式碼一起透過交付週期的其餘部分進展。

例如,ShopSpinner的application-infrastructure-stack專案使用application-server-image專案定義的伺服器映像定義了虛擬機器叢集。

當有人對基礎設施堆積積疊程式碼進行變更時,交付管道單獨建置和測試堆積積疊專案,如第9章所述。

如果堆積積疊專案的新版本透過這些測試,它將進入整合測試階段,該階段測試堆積積疊與透過自己測試的最後一個伺服器映像的整合。這個階段是兩個專案的整合點。然後,專案的版本一起進入管道的後續階段。

動機

在整合專案之前單獨建置和測試它們是強制它們之間明確邊界和鬆散耦合的一種方式。

例如,ShopSpinner團隊的成員在application-infrastructure-stack中實作了一個防火牆規則,該規則開啟了application-server-image設定檔案中定義的TCP連線埠。他們編寫了直接從該設定檔案讀取連線埠號的程式碼。但當他們推播程式碼時,堆積積疊的測試階段失敗了,因為建置代理上沒有其他專案的設定檔案。

這種失敗是好事。它暴露了兩個專案之間的耦合。團隊成員可以更改他們的程式碼,使用連線埠號的引數值,稍後設定該值(使用第7章中描述的模式之一)。程式碼將比具有跨專案檔案直接參照的程式碼函式庫更易於維護。

適用性

當您需要程式碼函式庫中專案之間的明確邊界,但仍然希望一起測試和交付每個專案的版本時,交付時整合很有用。該模式難以擴充套件到大量專案。

後果

交付時整合將解析和協調不同專案的不同版本的複雜性放入交付過程中。這需要複雜的交付實作,如管道。

實作

交付管道使用"扇入"管道設計整合不同專案。將不同專案整合在一起的階段稱為扇入階段或專案整合階段。

階段如何整合不同專案取決於它組合的專案型別。在使用伺服器映像的堆積積疊專案的例子中,堆積積疊程式碼將被應用並傳遞對相關映像版本的參照。基礎設施依賴項從程式碼交付儲存函式庫中檢索。

相同的組合專案版本集需要在交付過程的後續階段中應用。處理這個問題有兩種常見方法。

一種方法是將所有專案程式碼封裝成單一成品,以便在後續管道階段使用。例如,當兩個不同的堆積積疊專案被整合和測試時,整合階段可以將兩個專案的程式碼壓縮成單一檔案,將其提升到下游管道階段。GitOps流程會將專案合併到整合階段分支,然後從該分支合併到下游分支。

另一種方法是建立一個包含每個專案版本號的描述符檔案。例如:

descriptor-version: 1.9.1
stack-project:
  name: application-infrastructure-stack
  version: 1.2.34
server-image-project:
  name: application-server-image
  version: 1.1.1

交付過程將描述符檔案視為成品。應用基礎設施程式碼的每個階段從交付儲存函式庫中提取單個專案成品。

第三種方法是用聚合版本號標記相關資源。

模式:應用時專案整合

也稱為:解耦交付或解耦管道。

應用時專案整合涉及將多個專案分別推播透過交付階段。當有人更改專案的程式碼時,管道將更新後的程式碼應用於該專案交付路徑中的每個環境。該專案程式碼的版本可能在每個這些環境中與上游或下游專案的不同版本整合。

在ShopSpinner範例中,application-infrastructure-stack專案依賴於shared-network-stack專案建立的網路結構。每個專案都有自己的交付階段。

專案之間的整合透過將application-infrastructure-stack程式碼應用於環境來進行。此操作建立或更改使用網路結構(例如,子網)的伺服器叢集。

無論給定環境中分享網路堆積積疊的哪個版本,這種整合都會發生。因此,版本的整合在每次應用程式碼時都會單獨發生。

動機

在應用時整合專案最小化了專案之間的耦合。不同團隊可以將其系統的變更推播到生產環境,而無需協調,也不會被另一個團隊專案變更的問題阻礙。

適用性

這種程度的解耦適合具有自主團隊結構的組織。它也有助於更大規模的系統,在這些系統中,跨數百或數千工程師協調發布並鎖步交付是不切實際的。

後果

這種模式將跨專案破壞依賴關係的風險轉移到應用時操作。它不確保透過管道的一致性。如果有人將一個專案的變更推播透過管道的速度快於其他專案的變更,它們將在生產環境中與測試環境中的不同版本整合。

專案之間的介面需要仔細管理,以最大化任何給定依賴關係兩側不同版本之間的相容性。因此,這種模式在設計、維護和測試依賴關係和介面方面需要更多複雜性。

實作

在某些方面,使用應用時整合設計和實作解耦建置和管道比替代模式更簡單。每個管道建置、測試和交付單個專案。

基礎設施即程式碼的團隊工作流程

使用程式碼來建立和變更基礎設施,與傳統方法相比是一種根本性的不同工作方式。我們不再透過在命令提示符輸入指令或直接編輯線上設定來修改虛擬伺服器和網路設定,而是編寫程式碼並將其推播到自動化系統中應用。這種轉變比學習新工具或技能更為深遠。

基礎設施即程式碼(Infrastructure as Code,IaC)改變了所有參與設計、建構和管理基礎設施的人員的工作方式,無論是個人還是團隊層面。本文旨在解釋不同角色如何在基礎設施程式碼上協作,以及團隊如何設計、定義和應用程式碼的流程。

有效的基礎設施即程式碼團隊流程特徵

高效的基礎設施即程式碼團隊流程具有以下特點:

  • 自動化流程是團隊成員進行變更的最簡單、最自然的方式
  • 人員有明確的方法來確保品質、可操作性和政策一致性
  • 團隊能輕鬆保持系統更新,適當地保持一致性,並清晰管理必要的變化
  • 團隊對系統的知識嵌入在程式碼中,工作方式體現在自動化流程中
  • 錯誤能快速被發現並容易修正
  • 變更定義系統的程式碼以及測試和交付該程式碼的自動化流程既簡單又安全

總體而言,良好的自動化工作流程足夠快速,能在緊急情況下及時將修復推播到系統,使人們不會傾向於手動進行變更。同時,它足夠可靠,使人們更信任它而非自己手動調整線上系統設定。

衡量工作流程的有效性

根據Accelerate研究中提到的四個關鍵指標(佈署頻率、變更前置時間、服務還原時間和變更失敗率),是評估團隊效能的良好基礎。研究表明,在這些指標上表現良好的組織往往在核心組織目標(如盈利能力和股價)上也表現良好。

團隊可以使用這些指標來建立服務水平指標(SLIs)、服務水平目標(SLOs)和服務水平協定(SLAs)。具體測量的內容取決於團隊的環境和改進高層次成果的具體方式。