宣告式基礎設施的根本不可能性:為何IaC將永遠不完整

宣告式基礎設施的根本不可能性:為何IaC將永遠不完整

深入探討基礎設施即程式碼的數學與哲學限制


簡介:承諾與現實

基礎設施即程式碼(IaC)曾向我們承諾一個世界,在那個世界裡,複雜的系統可以用簡單的宣告式清單來描述。我們被告知,只要我們能寫下我們想要的,雲端就會實現它。Terraform會塑造地貌,CloudFormation會形成雲朵,Kubernetes會在任何平台上無縫地進行協調。

但任何在生產環境中使用過IaC的人都知道那個骯髒的秘密:現實遠比我們的宣告來得殘酷複雜,而且時間永不倒流。

本文探討為何宣告式基礎設施管理面臨著任何工具都無法克服的根本數學限制,以及理解這些限制如何讓我們成為更好的工程師——尤其是在現代以應用為中心的基礎設施方法的背景下。


DocumentDB的悖論:一個不可能性的案例研究

思考一下每個基礎設施工程師都曾面臨過的這個看似簡單的情境:

  1. 您使用CloudFormation部署了一個使用t4g.medium執行個體的DocumentDB叢集。
  2. 後來,在一次效能危機中,您手動將執行個體升級為r6g.large以獲得更好的效能。
  3. 現在您想要更新您的CloudFormation範本以反映現實,並維持您的「基礎設施即程式碼」實踐。

接下來發生的事情揭示了宣告式IaC的根本缺陷:

// 原始範本
instanceType: new ec2.InstanceType("t4g.medium")

// 更新後以符合現實的範本
instanceType: new ec2.InstanceType("r6g.large")

CloudFormation看到此變更後,決定需要替換該執行個體以「修復」偏差。但它想要替換的執行個體已不復存在——您已經手動替換了它。CloudFormation現在正在追蹤一個幽靈資源,活在一個意圖狀態的平行宇宙中,而現實早已向前邁進。

您陷入了一個不可能的困境:

  • 無法更新範本 - CloudFormation會試圖替換不存在的資源
  • 無法匯入新資源 - 您無法將多個邏輯ID對應到同一個實體資源
  • 無法刪除並重新建立 - 其他資源依賴此資料庫
  • 無法置之不理 - CloudFormation會持續試圖「修復」這個偏差

這不是CloudFormation的錯誤。這是宣告式系統在試圖模型化動態、時間相關的現實時所面臨的根本限制。


CDK的安全檢查:對不可能性的承認

DocumentDB悖論說明了手動偏差的問題,其中人為干預創造了一個工具無法理解的狀態。但一個更常見的情境揭示了同樣的限制:初始部署失敗

思考一下這個事件序列,這是每位雲端工程師的成年禮:

  1. 您執行cdk deploy來建立一個新的堆疊。
  2. 由於任何數量的原因——短暫的網路錯誤、您忘記的IAM權限、無效的參數——部署中途失敗。
  3. AWS CloudFormation無法繼續,將堆疊置於CREATE_FAILED狀態。這不是一個乾淨的石板;這是一個由部分建立的資源組成的混亂集合的損壞狀態。
  4. 您在您的程式碼中修復了根本問題,並自信地再次執行cdk deploy

您得到的不是成功,而是一個硬生生的停止:

❌ MyStack failed: _ToolkitError: Stack is in a paused fail state (CREATE_FAILED)
and change includes a replacement which cannot be deployed...
terminal (TTY) is not attached so we are unable to get a confirmation from the user

這個錯誤不是一個程式錯誤;這是AWS CDK明確承認其自身的限制。這個工具在告訴您:

「我看到我的現實『宣告』是CREATE_FAILED。世界的實際狀態是破碎和不一致的。您的新程式碼要求我執行一個破壞性的替換,但我無法信任我所站立的基礎。自動進行太危險了。」

為什麼危險?因為CDK可能會盲目地嘗試替換,可能再次失敗並讓堆疊處於一個更加損壞的狀態,使得清理工作變得更加困難。這是一個程式化的認知,即在一個破碎的基礎上執行一個主要操作是災難的根源。

這個安全檢查是一個內建的「逃生艙口」,證明了本文的論點。宣告式模型已經失敗。工具本身認識到,清單與現實之間的鴻溝已經變成它無法跨越的深淵。它被迫停止,並要求一個手動的、帶外的人為干預——登入AWS主控台刪除失敗的堆疊——以將現實重置到一個足夠簡單的狀態,讓宣告式模型能夠再次變得有用。

工具本身在告訴您:我的宣告式能力在面對累積的、失敗的歷史時,是不可能應用的。

自訂名稱資源的陷阱:當身份變得不可變時

另一個常見的情境揭示了同一個不可能性的不同面向——自訂名稱資源的陷阱

UPDATE_FAILED | AWS::CloudFront::KeyValueStore | basicAuthus-west-2/solo-dev-auth
CloudFormation cannot update a stack when a custom-named resource requires replacing.
Rename soloinfragarybasicAuthuswest2solodevauth93819E8D and update the stack again.

這個錯誤暴露了宣告式系統中的一個根本矛盾:身份 vs 可變性。事情是這樣發生的:

  1. 您建立了一個帶有自訂名稱(非自動生成)的CloudFront KeyValueStore。
  2. 後來,您做了一個變更,需要CloudFormation替換該資源。
  3. CloudFormation發現它無法替換一個自訂名稱的資源,因為:
    • 它需要先刪除舊的資源。
    • 但在舊的資源消失之前,它無法用相同的名稱建立新的資源。
    • 這會產生一個該名稱不存在的短暫時刻。
    • 依賴該確切名稱的其他資源將在過渡期間中斷。

CloudFormation基本上是在說:

「您要求我以宣告方式管理此資源,但您也給了它一個固定的身份,這使得替換變得不可能。我無法調和您對穩定身份宣告式可變性的雙重渴望。這些在數學上是不相容的要求。」

CloudFormation建議的「解決方案」——手動重命名資源——證明了本文的觀點:您必須跳出宣告式模型,執行手動的、指令式的操作來解決宣告式系統無法處理的矛盾。

這不是CloudFormation的限制;這是一個邏輯上的不可能性。您無法同時保證:

  • 穩定、可預測的資源名稱(用於依賴)
  • 無縫的資源替換(用於更新)
  • 零停機的轉換(用於可用性)
  • 宣告式管理(用於可重現性)

必須有所取捨,而當取捨發生時,宣告式模型就會崩潰,需要人為干預來解決這個矛盾。


資訊理論問題

核心問題是宣告與現實之間的資訊不對稱——這個問題比任何單一的IaC工具都還要根深蒂固:

我們所宣告的(簡單)

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    image: nginx:1.20
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"

實際存在的(複雜)

{
  "metadata": {
    "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "creationTimestamp": "2024-01-15T10:30:00Z",
    "resourceVersion": "12345678",
    "generation": 3,
    "managedFields": [/* 200+ 行的欄位管理歷史 */]
  },
  "status": {
    "phase": "Running",
    "hostIP": "10.0.1.45",
    "podIP": "172.16.0.23",
    "startTime": "2024-01-15T10:30:15Z",
    "containerStatuses": [{
      "containerID": "containerd://abc123...",
      "imageID": "sha256:def456...",
      "lastState": { "terminated": { "exitCode": 0, "reason": "Completed" }},
      "restartCount": 2,
      "ready": true,
      "started": true
    }],
    "qosClass": "Burstable",
    "conditions": [
      { "type": "Initialized", "status": "True", "lastTransitionTime": "2024-01-15T10:30:10Z" },
      { "type": "Ready", "status": "True", "lastTransitionTime": "2024-01-15T10:30:20Z" },
      { "type": "ContainersReady", "status": "True", "lastTransitionTime": "2024-01-15T10:30:20Z" },
      { "type": "PodScheduled", "status": "True", "lastTransitionTime": "2024-01-15T10:30:05Z" }
    ]
    // ... 還有數百個追蹤資源歷史、網路狀態、效能指標、
    // 安全上下文和執行期決策的欄位
  }
}

根本問題:您無法在沒有大量資訊損失的情況下,將1000多個複雜的執行期狀態欄位壓縮到20個宣告式意圖的欄位中。這不是我們工具的限制——這是一個數學上的不可能性,就像試圖在保留所有音樂資訊的情況下,將一首交響樂壓縮成一個單音符一樣。


長尾效應:系統複雜度的80/20法則

我稱之為「長尾效應」的現象描述了真實系統的複雜度如何遵循冪次法則分佈:

  • 20%的系統狀態可以輕易地被宣告(映像檔、副本數、基本設定)
  • 80%的系統狀態是從執行期行為、平台決策和歷史事件中浮現出來的

這80%包括:

  • 建立時間戳 - 無法宣告,只能觀察
  • 資源版本 - 由平台透過操作歷史生成
  • 效能指標 - 透過實際使用隨時間累積
  • 網路分配 - 基於可用性的平台特定分配
  • 安全補丁 - 基於弱點資料庫自動應用
  • 重啟歷史 - 操作事件和失敗模式的結果
  • 服務間關係 - 從實際的流量模式和依賴關係中浮現
  • 資源使用模式 - 從應用程式行為隨時間學習
  • 平台特定優化 - 由雲端供應商根據工作負載特性應用

薛丁格的組態問題

思考一下在每個生產系統中都會發生的這個量子態悖論:

// 這個宣告應該是什麼?
engineVersion: "4.0.0"  // 原始宣告的版本
engineVersion: "4.0.1"  // 自動修補後實際運行的版本
engineVersion: "4.0.2"  // 安全團隊手動升級後的版本

這三個值根據您的觀點和您觀察系統的時刻,同時都是「正確的」和「錯誤的」。宣告式模型無法捕捉這種時間上的複雜性,因為它在一個多維的現實中假設了一個單一的真理來源。


Terraform的狀態檔案:不可能性的紀念碑

Terraform的狀態檔案代表了彌合宣告式意圖與執行期現實之間鴻溝的最精密嘗試之一。然而,它完美地說明了為何這座橋樑永遠無法完整:

狀態檔案的悖論

# 狀態檔案聲稱知道現實
terraform show
# 但現實已經獨立地改變了
aws ec2 describe-instances

# 現在我們有三個版本的「真相」:
# 1. 在.tf檔案中宣告的內容
# 2. 在狀態檔案中記錄的內容
# 3. 在AWS中實際存在的內容

狀態檔案是Terraform試圖維護現實的「影子副本」的嘗試,但它註定永遠無法同步,因為:

  • 雲端供應商做出決定 - 超出Terraform的控制
  • 其他工具修改資源 - 以Terraform無法追蹤的方式
  • 手動變更發生 - 在事件和操作工作中
  • 時間流逝 - 系統獨立演進

狀態刷新的幻覺

當您執行terraform refresh時,您可能會認為您正在與現實同步,但您實際上是在:

  1. 在單一時間點抽樣現實
  2. 將其投影到Terraform有限的資料模型上
  3. 遺失不符合其結構描述的資訊
  4. 在刷新完成的那一刻創造一個新的鴻溝

狀態檔案在被寫入的那一刻就成為了歷史文物——一張已經向前邁進的系統的照片。


為何「匯入」在根本上是有缺陷的

大多數IaC工具提供「匯入」功能,作為將現有資源納入管理的解決方案:

terraform import aws_instance.web i-1234567890abcdef0
kubectl apply -f pod.yaml  # 針對已存在的pod

但匯入功能試圖解決的是一個無法解決的數學問題——從最終產物反向工程出原始意圖。

反向工程的謬誤

匯入功能試圖進行這樣的轉換:

複雜的執行期狀態 → 簡單的宣告

這相當於要求一位廚師從一道完成的菜餚反向工程出食譜,或要求一位考古學家從廢墟中確定古代建築者的確切思想。這在數學上等同於:

已編譯的二進位檔案 → 原始原始碼 + 開發者意圖
MP3檔案 → 原始錄音室錄音 + 藝術視野
烤好的蛋糕 → 食譜 + 廚師的技巧 + 食材的來源
分散式系統 → 原始架構 + 所有歷史決策

資訊已經被不可逆轉地遺失了。 從意圖到最終產物的壓縮是有損的,無論多麼精密的工具都無法恢復那些從未被保存下來的資訊。

匯入偏差循環

在匯入過程中實際發生的情況如下:

  1. 匯入功能將資源ID對應到一個邏輯名稱。
  2. 根據目前觀察到的狀態,猜測90%的組態。
  3. 忽略無法宣告的執行期狀態(時間戳、生成的ID、計算值)。
  4. 假設的預設值可能與實際建立時的參數不符。
  5. 期望下一次的計畫不會因為這些假設而想要「修復」一切。

不可避免的結果是:

terraform plan
# Plan: 0 to add, 47 to change, 0 to destroy

# 您未曾預期的變更:
# ~ aws_instance.web
#   + monitoring                 = true -> false  # AWS的預設值 vs Terraform的預設值
#   + ebs_optimized             = true -> null    # AWS推斷了它,Terraform沒有
#   + instance_initiated_shutdown_behavior = "stop" -> "terminate"  # 不同的假設

您成功地匯入了一個資源,卻弄壞了其他47個——這不是因為工具不好,而是因為這個問題從根本上就是不可能解決的。


Kubernetes跨平台的謊言

Kubernetes的承諾是「一次編寫,到處執行」——一個單一的YAML清單,在所有平台上都能同樣運作。這或許是宣告式抽象化最雄心勃勃的嘗試,而它的限制揭示了關於平台無關宣告之不可能性的更深層真相。

平台現實:同一個YAML,不同的宇宙

同一個Kubernetes清單在不同平台上會產生根本不同的結果:

AWS EKS

apiVersion: v1
kind: Service
spec:
  type: LoadBalancer
# 結果是:一個具有AWS特定宇宙的應用程式負載平衡器:
# - Route53整合(AWS DNS生態系)
# - ACM憑證自動化(AWS PKI)
# - VPC原生網路(AWS網路模型)
# - CloudWatch日誌整合(AWS可觀測性)
# - ELB健康檢查(AWS特定演算法)
# - 安全群組整合(AWS防火牆模型)

Google GKE

apiVersion: v1
kind: Service  
spec:
  type: LoadBalancer
# 結果是:一個具有GCP特定宇宙的Google Cloud負載平衡器:
# - Cloud DNS整合(Google DNS生態系)
# - Google管理的憑證(Google PKI)
# - VPC原生網路(Google網路模型)
# - Cloud Logging整合(Google可觀測性)
# - Google健康檢查(Google特定演算法)
# - 防火牆規則整合(Google防火牆模型)

Azure AKS

apiVersion: v1
kind: Service
spec:
  type: LoadBalancer  
# 結果是:一個具有Azure特定宇宙的Azure負載平衡器:
# - Azure DNS整合(Microsoft DNS生態系)
# - Key Vault憑證整合(Microsoft PKI)
# - Azure CNI網路(Microsoft網路模型)
# - Azure Monitor整合(Microsoft可觀測性)
# - Azure健康探測(Microsoft特定演算法)
# - 網路安全群組整合(Microsoft防火牆模型)

平台互補問題

每個平台都必須「填補」Kubernetes未宣告的「空白」,而這些空白構成了實際系統行為的大部分:

  • 網路實作 - CNI外掛程式在效能、安全模型和偵錯能力上差異巨大。
  • 儲存類別 - 完全是平台特定的,具有不同的IOPS、耐用性和一致性保證。
  • 安全策略 - 不同的RBAC整合、身份提供者和合規性框架。
  • 監控與日誌 - 平台原生的解決方案,具有不相容的資料模型和查詢語言。
  • 憑證管理 - 因雲端供應商而異,具有不同的信任鏈和更新流程。
  • 負載平衡器行為 - 不同的演算法、健康檢查機制和容錯轉移策略。
  • 自動擴展決策 - 平台特定的指標、演算法和資源分配策略。

那個「可攜式」的Kubernetes清單只是冰山一角——或許只佔實際系統定義的10%。剩下的90%是由無法在Kubernetes YAML中宣告的平台特定實作所決定的,這使得真正的可攜性成為一個美麗卻不可能實現的夢想。


ONDEMANDENV的回應:擁抱以應用為中心的合約

傳統的IaC在清單與現實的鴻溝中掙扎,而像ONDEMANDENV這樣的平台則採取了不同的方法:擁抱這個鴻溝,而不是與之對抗

合約勝於組態

ONDEMANDENV不是試圖宣告基礎設施的每一個面向,而是專注於合約——關於應用程式需要什麼的明確協議,而不是基礎設施應該如何組態:

// 傳統IaC:試圖宣告一切
const database = new rds.DatabaseInstance(this, 'DB', {
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
  engine: rds.DatabaseInstanceEngine.postgres({
    version: rds.PostgresEngineVersion.VER_13_7
  }),
  allocatedStorage: 20,
  storageEncrypted: true,
  backupRetention: cdk.Duration.days(7),
  // ... 其他50多個試圖捕捉每個細節的參數
});

// ONDEMANDENV合約:宣告意圖與邊界
export const DatabaseContract = {
  needs: {
    storage: { type: 'relational', consistency: 'strong' },
    performance: { tier: 'standard', scaling: 'vertical' },
    backup: { retention: '7d', pointInTime: true },
    security: { encryption: 'at-rest', access: 'private' }
  },
  provides: {
    endpoint: { type: 'postgresql', version: '^13.0' },
    schema: { migrations: './db/migrations' }
  }
}

以應用為中心的環境

ONDEMANDENV認識到,部署的基本單位不是基礎設施,而是帶有其上下文的應用程式。它不是試圖完美地宣告基礎設施狀態,而是專注於:

  1. 上下文邊界 - 這個應用程式需要什麼才能運作?
  2. 合約履行 - 在當前限制下如何滿足這些需求?
  3. 環境版本控制 - 我們如何追蹤應用程式上下文隨時間的演變?
  4. 隔離保證 - 我們如何確保環境之間不會互相干擾?

這種方法承認基礎設施總是會包含無法宣告的元素,但確保無論這些實作細節如何,應用程式都能被可靠地部署和測試。


人類的相似之處:認知限制與心智模型

這個限制反映了人類如何理解和應對複雜性。我們只能感知和處理可用資訊的一小部分:

可觀察的現實 vs 實際的現實

人類資訊處理的瓶頸揭示了為何宣告式系統面臨類似的限制:

  • 人類感官輸入:約每秒1100萬位元
  • 意識處理:約每秒40位元
  • 宇宙的資訊內容:約10^120位元
  • 基礎設施系統狀態:約每秒10^9位元(且在增長中)

我們建立簡化的心智模型來應對複雜性:

現實:無限的複雜性與持續的變化
心智模型:簡化的、靜態的抽象
行動:基於不完整、過時的資訊
結果:通常與預期不同

正如人類無法完全理解現實,必須使用簡化的模型來操作一樣,IaC也無法完全宣告複雜的系統,必須使用不完整的抽象來工作。

基礎設施中的地圖-領域問題

我們的基礎設施清單就像地圖——幫助我們導航的有用抽象,但從根本上說,它們是對領域不完整的表述:

地圖 領域 基礎設施的相似之處
顯示道路 不顯示交通、天氣、施工 顯示資源,不顯示執行期行為
靜態快照 動態、變化的現實 固定的宣告 vs 不斷演進的系統
簡化的符號 複雜的實體現實 YAML/JSON vs 實際的雲端供應商內部
有限的規模 無限的細節可用 選定的欄位 vs 完整的系統狀態

地圖和清單之所以有價值,正是因為它們省略了細節,但當我們忘記它們的限制,將模型誤認為現實時,兩者都會變得危險。


哲學意涵:決定論 vs 湧現

決定論的假設

宣告式IaC建立在一個根本上是決定論的世界觀之上:

相同的輸入 → 相同的輸出(總是)
相同的清單 → 相同的基礎設施(總是)
相同的宣告 → 相同的執行期行為(總是)

這假設複雜的系統僅僅是繁雜的——只要有足夠的規格,我們就能達到完美的預測性。

湧現的現實

但複雜系統展現出無法從其組成部分預測的湧現行為

相同的輸入 → 不同的輸出(取決於上下文、時間、歷史)
相同的清單 → 不同的基礎設施(取決於平台、時間、操作歷史)
相同的宣告 → 不同的行為(取決於流量模式、安全更新、網路狀況)

基礎設施中湧現的例子:

  • 資料庫效能取決於資料分佈、查詢模式和硬體磨損。
  • 網路延遲隨路由決策、擁塞和地理因素而變化。
  • 安全態勢隨弱點的發現和補丁的部署而改變。
  • 自動擴展行為根據學習到的使用模式和平台演算法進行調整。
  • 服務網格路由根據觀察到的失敗模式和效能指標演進。

今天部署的資料庫叢集,其行為將與上個月部署的「相同」叢集不同,即使清單完全相同,因為:

  • 安全補丁已經發布並應用。
  • 網路拓撲已隨其他工作負載演進。
  • 效能特性已隨使用模式改變。
  • 平台演算法已從操作資料中學習。
  • 合規性要求已經轉變。

時間之箭:為何重新宣告會失敗

或許最根本的是,時間是不可逆的。一旦一個系統:

  • 在事件中被手動修改過
  • 經歷了自動修補過程
  • 在實際工作負載下經歷了效能瓶頸
  • 被安全掃描修改過

…它就再也無法回到其原始的宣告狀態。您無法抹去歷史。重新執行terraform apply並不能消除過去發生的湧現學習、熵的累積或手動干預。

這是因為複雜的系統是其歷史、環境和互動的產物。清單只是一個意圖的陳述,而不是系統隨時間演進的豐富而複雜的故事。


結論:擁抱不可能性

IaC和宣告式基礎設施是強大的工具,但它們並非魔法。它們受到時間、熵和湧現複雜性等現實基本力量的約束。

認識到這種不可能性並非失敗主義,而是一種解放。它將我們從以下束縛中解放出來:

  • 追求完美宣告的幻想
  • 不斷與清單與現實的鴻溝作鬥爭
  • 試圖在程式碼中捕捉每一個細節

相反,我們可以專注於:

  1. 以應用為中心的合約:定義應用程式需要什麼,讓平台處理細節。
  2. 有彈性的實踐:預期系統會失敗,並建立工具來快速恢復。
  3. 可觀測性:投資於理解系統的實際狀態,而不是試圖宣告一個單一的真理來源。
  4. 擁抱湧現:設計允許系統隨時間學習和適應的架構。

真正有效的基礎設施管理,並非來自試圖將現實強加於清單,而是來自承認清單永遠不完整,並建立穩健的系統來管理這個鴻溝。當我們停止試圖控制我們無法控制的力量,轉而專注於在我們必須導航的複雜動態現實中,建立有彈性、能適應、可觀測的系統時,真正的進步才會到來。

最終,IaC的目標不是創造現實的完美複製品,而是在應用程式的需求和基礎設施的能力之間,創造一個可靠、可重現且可演進的合約。擁抱那個合約,才是前進的道路。

</rewritten_file>

📝
Source History
🤖
Analyze with AI