在現代雲端運算環境中,函式即服務(FaaS)作為無伺服器架構的核心元件,正逐漸改變開發者與基礎設施互動的方式。本文將探討FaaS的本質、實作方式以及如何透過基礎設施即程式碼(IaC)的方法來建構和管理FaaS環境。

理解函式即服務(FaaS)的本質

函式即服務代表了一種全新的運算資源提供模式。與傳統的持續執行容器或伺服器不同,FaaS模式下的應用程式碼僅在需要時執行,通常是回應特定事件或按照預定排程。

FaaS與無伺服器的概念澄清

「無伺服器」這個術語其實有些誤導性,因為程式碼最終仍然在伺服器上執行。真正的區別在於:

  • 開發者視角:伺服器對開發者來説是「不可見的」
  • 執行模式:FaaS是短暫執行的處理程式,而非長時間執行的服務
  • 資源管理:平台自動處理資源擴充套件,開發者無需考慮伺服器容量規劃

許多專業人士更傾向使用「FaaS」而非「無伺服器」這個術語,以避免與其他無伺服器概念混淆,例如後端即服務(BaaS)等外部託管服務。

FaaS的理想應用場景

FaaS特別適合以下場景:

  • 定義明確的短期任務
  • 需要快速啟動的程式碼
  • 處理HTTP請求
  • 回應訊息佇列中的錯誤事件
  • 需求波動較大的工作負載

當需求出現峰值時,平台會自動啟動多個程式碼例項平行處理;而在不需要時,則完全不執行,從而實作高效率的資源利用。

FaaS執行時環境的佈署模式

與應用程式叢集類別似,FaaS執行時環境也有兩種主要佈署模式:

1. 平台提供的FaaS服務

主要雲端供應商提供的代管FaaS服務包括:

  • AWS Lambda
  • Azure Functions
  • Google Cloud Functions

這些服務大幅減少了需要定義和管理的基礎設施數量。通常,開發者不需要指定程式碼執行的主機伺服器的大小和性質,平台會自動處理這些細節。

2. 自行佈署的FaaS解決方案

如果需要更多控制或有特殊需求,也可以選擇自行佈署FaaS執行時環境:

  • Fission
  • Kubeless
  • OpenFaaS
  • Apache OpenWhisk

對於這些解決方案,需要自行佈建和設定基礎設施及管理工具,可以使用前面章節描述的伺服器池和管理服務等策略。

自行佈署FaaS解決方案時,需要深入瞭解選擇的FaaS解決方案如何工作,特別是資料隔離和安全性方面。例如,某些FaaS實作可能會在臨時位置留下檔案或其他殘留物,這些位置可能被其他FaaS程式碼存取,從而產生安全性和合規性問題。FaaS解決方案對資料隔離的支援程度以及其擴充套件能力,應該成為決定是否執行多個FaaS執行時例項的關鍵因素。

FaaS基礎設施的程式碼定義

即使用雲端供應商提供的FaaS服務,仍然需要定義和管理一些基礎設施資源:

網路設定需求

FaaS程式碼通常需要與其他服務和資源互動,因此需要定義:

  • 入站請求的網路設定(觸發FaaS應用程式的請求)
  • 出站請求的網路設定(程式碼發出的請求)
resource "aws_security_group" "lambda_sg" {
  name        = "lambda-security-group"
  description = "Security group for Lambda functions"
  vpc_id      = aws_vpc.main.id

  # 允許出站流量
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_lambda_function" "example_function" {
  function_name = "example-function"
  role          = aws_iam_role.lambda_role.arn
  handler       = "index.handler"
  runtime       = "nodejs14.x"
  filename      = "function.zip"
  
  vpc_config {
    subnet_ids         = aws_subnet.private.*.id
    security_group_ids = [aws_security_group.lambda_sg.id]
  }
}

上面的Terraform程式碼展示瞭如何為AWS Lambda函式設定網路設定。首先建立一個安全群組,允許所有出站流量。然後在Lambda函式設定中,透過vpc_config區塊將函式與VPC中的私有子網路和安全群組關聯。這樣,Lambda函式就能夠安全地與VPC內的資源進行通訊,同時透過安全群組控制流量。

資源存取與整合

FaaS程式碼經常需要讀寫資料和訊息到:

  • 儲存裝置
  • 資料函式庫
  • 訊息佇列

這些都需要定義和測試相應的基礎設施資源。以下是一個AWS Lambda函式與DynamoDB整合的例子:

resource "aws_iam_role_policy" "lambda_dynamodb_policy" {
  name = "lambda-dynamodb-policy"
  role = aws_iam_role.lambda_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem",
          "dynamodb:DeleteItem",
          "dynamodb:Query",
          "dynamodb:Scan"
        ]
        Effect   = "Allow"
        Resource = aws_dynamodb_table.example_table.arn
      }
    ]
  })
}

resource "aws_dynamodb_table" "example_table" {
  name           = "example-table"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "id"
  
  attribute {
    name = "id"
    type = "S"
  }
}

這段程式碼示範瞭如何設定Lambda函式與DynamoDB表的整合。首先建立一個IAM策略,授予Lambda函式對特定DynamoDB表的讀寫許可權。然後定義DynamoDB表本身,使用按需計費模式和"id"作為主鍵。這種方式確保了Lambda函式只能存取它需要的資源,遵循最小許可權原則,同時也使資源之間的關係在基礎設施程式碼中明確可見。

持續交付流程

與任何其他程式碼一樣,FaaS程式碼也應該使用管道進行交付和測試:

# GitLab CI/CD 管道範例
stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - npm install
    - npm run build
    - zip -r function.zip .

test:
  stage: test
  script:
    - npm test
    - terraform init
    - terraform plan -var-file=test.tfvars

deploy:
  stage: deploy
  script:
    - terraform init
    - terraform apply -var-file=prod.tfvars -auto-approve
  only:
    - main

這個GitLab CI/CD管道設定展示了FaaS應用程式的完整交付流程。在build階段,安裝依賴項、構建應用程式並建立佈署包。test階段執行單元測試並執行Terraform計劃,驗證基礎設施變更。最後,deploy階段僅在主分支上執行,應用Terraform設定將函式佈署到生產環境。這種方法確保了程式碼和基礎設施變更一起被測試和佈署,保持一致性。

模組化設計與基礎設施即程式碼

在設計FaaS基礎設施時,應遵循基礎設施即程式碼的核心實踐,特別是"小而簡單的元件"原則。

模組化FaaS基礎設施的優勢

將FaaS基礎設施分解為小型、可重用的模組有以下好處:

  1. 降低耦合度:函式之間的依賴關係更清晰
  2. 提高內聚性:每個模組專注於單一職責
  3. 簡化測試:可以獨立測試每個元件
  4. 加速變更:小元件更容易理解和修改

實作模組化的策略

以下是一個模組化AWS Lambda基礎設施的Terraform範例:

# modules/lambda/main.tf
variable "function_name" {}
variable "handler" {}
variable "runtime" {}
variable "source_dir" {}
variable "environment_variables" {
  default = {}
}

resource "aws_lambda_function" "function" {
  function_name = var.function_name
  handler       = var.handler
  runtime       = var.runtime
  
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  
  role = aws_iam_role.lambda_role.arn
  
  environment {
    variables = var.environment_variables
  }
}

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = var.source_dir
  output_path = "${path.module}/files/${var.function_name}.zip"
}

# 主專案中使用模組
module "order_processor" {
  source        = "./modules/lambda"
  function_name = "order-processor"
  handler       = "index.handler"
  runtime       = "nodejs14.x"
  source_dir    = "${path.module}/functions/order-processor"
  environment_variables = {
    TABLE_NAME = aws_dynamodb_table.orders.name
  }
}

這個範例展示瞭如何將Lambda函式定義封裝為可重用的Terraform模組。模組接受函式名稱、處理程式、執行時和原始碼目錄等引數,並處理封裝和佈署細節。在主專案中,只需提供特定函式的引數即可建立新的Lambda函式。這種方法使得增加新函式變得簡單,同時確保所有函式遵循一致的佈署模式。環境變數引數允許將外部資源(如DynamoDB表名)傳遞給函式,實作鬆散耦合。

設計考量與最佳實踐

安全性與資料隔離

在設計FaaS基礎設施時,必須特別注意安全性和資料隔離:

# 為每個函式建立單獨的執行角色
resource "aws_iam_role" "function_specific_role" {
  name = "${var.function_name}-role"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

# 僅授予必要的最小許可權
resource "aws_iam_role_policy" "function_specific_policy" {
  name = "${var.function_name}-policy"
  role = aws_iam_role.function_specific_role.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:logs:*:*:*"
      },
      {
        Action = var.required_actions
        Effect = "Allow"
        Resource = var.resource_arns
      }
    ]
  })
}

這段程式碼展示了FaaS安全性的最佳實踐。為每個函式建立專用的IAM角色,而不是分享角色,這樣可以實作更精細的許可權控制。政策只授予函式所需的最小許可權集,包括基本的日誌記錄許可權和特定資源的操作許可權。這種方法遵循最小許可權原則,減少了潛在的安全風險,同時使許可權管理更加透明和可稽核。

效能與成本最佳化

FaaS環境的效能和成本最佳化需要考慮以下因素:

  1. 記憶體設定:根據函式需求調整記憶體大小
  2. 超時設定:設定合理的超時間
  3. 冷啟動最佳化:考慮預熱策略或使用佈建平行性
  4. 程式碼最佳化:減小佈署包大小,最佳化依賴項
resource "aws_lambda_function" "optimized_function" {
  function_name = "optimized-function"
  handler       = "index.handler"
  runtime       = "nodejs14.x"
  filename      = "function.zip"
  
  # 根據函式需求調整記憶體
  memory_size = 256
  
  # 設定合理的超時
  timeout = 10
  
  # 使用佈建平行性減少冷啟動
  provisioned_concurrency = 5
  
  # 設定環境變數
  environment {
    variables = {
      NODE_OPTIONS = "--enable-source-maps"
      STAGE = "production"
    }
  }
}

這個Lambda函式設定展示了幾個效能和成本最佳化策略。memory_size設定為256MB,這是根據函式的實際需求選擇的適當值,既能確保效能又不會過度設定資源。timeout設定為10秒,為函式提供足夠的執行時間,同時避免長時間執行導致的不必要費用。provisioned_concurrency設定為5,這意味著系統會預先初始化5個函式例項,減少冷啟動延遲,適用於需要快速回應的關鍵函式。環境變數設定了Node.js的源對映支援,有助於除錯,並指定了生產環境標識。

監控與可觀測性

FaaS環境的監控和可觀測性對於維護系統健康至關重要:

# 設定CloudWatch日誌組和指標過濾器
resource "aws_cloudwatch_log_group" "function_logs" {
  name              = "/aws/lambda/${aws_lambda_function.function.function_name}"
  retention_in_days = 14
}

resource "aws_cloudwatch_log_metric_filter" "error_metric" {
  name           = "${aws_lambda_function.function.function_name}-errors"
  pattern        = "ERROR"
  log_group_name = aws_cloudwatch_log_group.function_logs.name
  
  metric_transformation {
    name      = "${aws_lambda_function.function.function_name}-error-count"
    namespace = "CustomLambdaMetrics"
    value     = "1"
  }
}

# 設定警示
resource "aws_cloudwatch_metric_alarm" "function_error_alarm" {
  alarm_name          = "${aws_lambda_function.function.function_name}-error-alarm"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "${aws_lambda_function.function.function_name}-error-count"
  namespace           = "CustomLambdaMetrics"
  period              = 60
  statistic           = "Sum"
  threshold           = 5
  alarm_description   = "This alarm monitors for errors in the Lambda function"
  alarm_actions       = [aws_sns_topic.alarm_topic.arn]
}

這段程式碼展示瞭如何為Lambda函式設定完整的監控和警示系統。首先建立一個CloudWatch日誌組,保留函式日誌14天。然後設定一個指標過濾器,搜尋日誌中的"ERROR"模式並建立自定義指標。最後,設定一個CloudWatch警示,當在1分鐘內檢測到超過5個錯誤時觸發,並透過SNS主題傳送通知。這種方法提供了對函式錯誤的即時可見性,使團隊能夠快速回應問題,同時透過保留適當的日誌資料支援事後分析。

結合基礎設施即程式碼與無伺服器架構

FaaS與基礎設施即程式碼的結合代表了雲端運算的一個重要發展方向。這種結合帶來了幾個關鍵優勢:

  1. 更快的創新速度:小型、獨立的函式更容易開發、測試和佈署
  2. 更高的資源效率:按需執行模式減少了資源浪費
  3. 更低的營運負擔:減少了伺服器管理的工作量
  4. 更好的可擴充套件性:自動擴充套件能力使系統能夠應對流量波動

然而,這種方法也帶來了新的挑戰,如函式間協調、冷啟動延遲和分散式系統複雜性。透過將基礎設施即程式碼的實踐應用於FaaS環境,可以更好地管理這些挑戰,實作更可靠、更高效的系統。

在設計FaaS基礎設施時,應遵循小型、簡單元件的核心實踐,將系統分解為可獨立測試和佈署的模組。這種方法不僅提高了系統的可維護性和可靠性,還使團隊能夠更快地適應變化,持續改進系統。

隨著雲端技術的不斷發展,FaaS和基礎設施即程式碼的結合將繼續演化,為開發者提供更強大、更靈活的工具,以構建下一代雲端應用程式。透過掌握這些技術和實踐,組織可以在數位轉型的道路上取得更大的成功。

模組化基礎設施設計:小而簡單的元件實踐

在基礎設施即程式碼(IaC)的世界中,系統隨著時間的推移必然會成長。隨著更多使用者、更多開發者加入,以及更多功能的增加,系統變得越來越複雜。這種成長往往導致變更風險增加,流程變得更加繁瑣,最終使得系統改進變得困難,技術債務積累,系統品質下降。

本文將探討如何透過基礎設施即程式碼的第三個核心實踐——「構建小而簡單的元件」,來維持系統的高速變更能力,同時在系統成長過程中持續提升品質。

模組化設計的目標與原則

模組化設計的主要目標是使系統變更加容易與安全。這可以透過幾種方式實作:

模組化的核心目標

  1. 消除實作重複:減少需要修改的程式碼數量
  2. 簡化實作:提供可以不同方式組合的元件
  3. 隔離變更影響:設計系統使得可以修改較小的元件而不影響其他部分

良好設計元件的特性

設計元件的藝術在於決定哪些元素應該組合在一起,哪些應該分開。這涉及理解元素之間的關係和依賴性。兩個重要的設計特性是耦合度和內聚性:

  graph TD
    A[良好的元件設計] --> B[低耦合]
    A --> C[高內聚]
    B --> D[元件間變更獨立]
    C --> E[元件內元素緊密相關]

耦合度描述一個元件的變更需要另一個元件也變更的頻率。零耦合通常不是現實目標,因為這可能意味著它們根本不屬於同一個系統。相反,我們追求低耦合或鬆散耦合。

內聚性描述元件內元素之間的關係。內聚性也與變更模式有關。低內聚性的堆積積疊中,對一個資源的變更通常與堆積積疊中的其他資源無關。

這個圖表説明瞭良好元件設計的兩個核心特性:低耦合和高內聚。低耦合意味著不同元件之間的依賴關係最小化,使得可以獨立變更一個元件而不影響其他元件。高內聚意味著元件內的元素緊密相關,通常一起變更,有明確的共同目的。這兩個特性共同促進了系統的可維護性和可擴充套件性。

簡單設計的四條規則

Kent Beck(極限程式設計和測試驅動開發的創始人)提出了四條使元件設計簡單的規則。根據他的規則,簡單的程式碼應該:

  1. 透過測試(做它應該做的事)
  2. 揭示意圖(清晰易懂)
  3. 沒有重複
  4. 包含最少的元素

設計元件的原則

軟體架構和設計包含許多原則和指導方針,用於設計低耦合和高內聚的元件。

避免重複(DRY原則)

DRY(Don’t Repeat Yourself)原則指出:「系統中的每一部分知識必須有單一、明確、權威的表示。」重複迫使人們在多個地方進行變更。

例如,ShopSpinner的所有堆積積疊都使用一個設定使用者帳戶來應用伺服器例項的設定。最初,該帳戶的登入詳細資訊在每個堆積積疊中都有指定,也在構建基礎伺服器映像的程式碼中指定。當需要變更登入詳細資訊時,需要在程式碼函式庫中的所有這些位置找到並變更它。因此,團隊將登入詳細資訊移至中央位置,每個堆積積疊和伺服器映像構建器都參照該位置。

重複的有用性考量

DRY原則反對重複概念的實作,這與重複字面程式碼行不同。讓多個元件依賴分享程式碼可能會建立緊密耦合,使變更變得困難。

玄貓曾見過團隊堅持集中任何看起來相似的程式碼;例如,讓所有虛擬伺服器使用單一模組建立。實際上,為不同目的建立的伺服器,如應用伺服器、Web伺服器和構建伺服器,通常需要不同的定義。需要建立所有這些不同型別伺服器的模組可能變得過於複雜。

考慮程式碼是否重複並應集中時,考慮程式碼是否真正代表相同的概念。變更一個程式碼例項是否意味著另一個例項也應該變更?

也考慮將兩個程式碼例項鎖定在同一變更週期中是否是個好主意。強制組織中的每個應用伺服器同時升級可能不現實。

重用增加耦合。因此,重用的一個好經驗法則是在元件內部保持DRY,在元件之間保持濕潤(WET)。

組合規則

要建立可組合的系統,需要建立獨立的部分。依賴關係的一方應該可以輕鬆替換,而不會干擾另一方。

ShopSpinner團隊最初有一個單一的Linux應用伺服器映像,從不同堆積積疊中設定。後來他們增加了Windows應用伺服器映像。他們設計從任何給定堆積積疊設定伺服器映像的程式碼,使他們可以根據特定應用的需要輕鬆在這兩個伺服器映像之間切換。

單一責任原則

單一責任原則(SRP)指出任何給定元件應該對一件事負責。這個想法是保持每個元件專注,使其內容具有內聚性。

基礎設施元件,無論是伺服器、設定函式庫、堆積積疊元件還是堆積積疊,都應該圍繞單一目的組織。該目的可能是分層的。為應用提供基礎設施是一個單一目的,可以由基礎設施堆積積疊實作。可以將該目的分解為應用的安全流量路由(由堆積積疊函式庫實作)、應用伺服器(由伺服器映像實作)和資料函式庫例項(由堆積積疊模組實作)。每個元件在每個層級都有一個單一的、易於理解的目的。

圍繞領域概念設計元件,而非技術概念

人們經常傾向於圍繞技術概念構建元件。例如,建立一個用於定義伺服器的元件,並在任何需要伺服器的堆積積疊中重用它,這似乎是個好主意。實際上,任何分享元件都會耦合所有使用它的程式碼。

更好的方法是圍繞領域概念構建元件。應用伺服器是一個可能想要為多個應用重用的領域概念。構建伺服器是另一個領域概念,可能想要重用以給不同團隊自己的例項。因此,這些比伺服器(可能以不同方式使用)更好的元件。

迪米特法則

也稱為最少知識原則,迪米特法則指出元件不應該瞭解其他元件的實作方式。這條規則推動了元件之間清晰、簡單的介面。

ShopSpinner團隊最初違反了這條規則,他們有一個定義應用伺服器叢集的堆積積疊,以及一個定義該叢集的負載平衡器和防火牆規則的分享網路堆積積疊。分享網路堆積積疊對應用伺服器堆積積疊瞭解太多細節。

提供者和消費者關係

在元件之間的依賴關係中,提供者元件建立或定義消費者元件使用的資源。

分享網路堆積積疊可能是提供者,建立網路地址塊,如子網路。應用基礎設施堆積積疊可能是分享網路堆積積疊的消費者,在提供者管理的子網路中設定伺服器和負載平衡器。

避免迴圈依賴

當追蹤從提供資源的元件到消費者的關係時,不應該找到迴圈(或週期)。換句話説,提供者元件不應該從其直接或間接消費者那裡消費資源。

ShopSpinner分享網路堆積積疊的例子存在迴圈依賴。應用伺服器堆積積疊將其叢集中的伺服器分配給分享網路堆積積疊中的網路結構。分享網路堆積積疊為應用伺服器堆積積疊中的特定伺服器叢集建立負載平衡器和防火牆規則。

ShopSpinner團隊可以透過將特定於應用伺服器堆積積疊的網路元素移入該堆積積疊來修復迴圈依賴,並減少網路堆積積疊對其他元件的瞭解。這也改善了內聚性和耦合度,因為網路堆積積疊不再包含與另一個堆積積疊元素最密切相關的元素。

使用測試驅動設計決策

持續測試基礎設施程式碼使得可測試性成為基礎設施元件的基本設計考量。

變更交付系統需要能夠在每個層級建立和測試基礎設施程式碼,從安裝監控代理的伺服器設定模組到構建容器叢集的堆積積疊程式碼。管道階段必須能夠快速獨立建立每個元件的例項。這種級別的測試對於依賴關係糾結的程式碼函式庫或需要半小時才能設定的大型元件是不可能的。

這些挑戰使許多引入基礎設施程式碼有效自動化測試機制的計劃脫軌。為設計不良的系統編寫和執行自動化測試很困難。

這就是自動化測試的秘密好處:它驅動更好的設計。持續測試和交付程式碼的唯一方法是實作和維護具有鬆散耦合和高內聚性的乾淨系統設計。

對於鬆散耦合的伺服器設定模組,實作自動化測試更容易。對於具有更乾淨、更簡單介面的模組,構建和使用模擬更容易。可以在管道中更快地設定和測試小型、定義良好的堆積積疊。

基礎設施模組化

基礎設施系統涉及不同型別的元件,每種元件可以由不同部分組成。伺服器例項可能由映像構建,使用參照一組伺服器設定模組的伺服器設定角色,這些模組又可能匯入程式碼函式庫。基礎設施堆積積疊可能由伺服器例項組成,並可能使用堆積積疊程式碼模組或函式庫。多個堆積積疊可能結合構成更大的環境或資產。

堆積積疊元件與堆積積疊作為元件

基礎設施堆積積疊是基礎設施的核心可佈署單元。堆積積疊是「架構量子」的一個例子,Ford、Parsons和Kua將其定義為「具有高功能內聚性的獨立可佈署元件,包括系統正確執行所需的所有結構元素。」換句話説,堆積積疊是可以自行推播到生產環境的元件。

堆積積疊可以由元件組成,堆積積疊本身也可以是元件。伺服器是堆積積疊的一個潛在元件,複雜到需要在本章後面深入討論。大多數堆積積疊管理工具也支援將堆積積疊程式碼放入模組中,或使用函式庫生成堆積積疊的元素。

# 分享網路模組定義
# modules/networking/main.tf
variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
}

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
  tags = {
    Name = "main-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = cidrsubnet(var.vpc_cidr, 8, 1)
  tags = {
    Name = "public-subnet"
  }
}

resource "aws_subnet" "private" {
  vpc_id     = aws_vpc.main.id
  cidr_block = cidrsubnet(var.vpc_cidr, 8, 2)
  tags = {
    Name = "private-subnet"
  }
}

output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_id" {
  value = aws_subnet.public.id
}

output "private_subnet_id" {
  value = aws_subnet.private.id
}

# 在堆積積疊A中使用網路模組
# stacks/stack_a/main.tf
module "network" {
  source   = "../../modules/networking"
  vpc_cidr = "10.0.0.0/16"
}

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  subnet_id     = module.network.public_subnet_id
  
  tags = {
    Name = "stack-a-web-server"
  }
}

# 在堆積積疊B中使用相同的網路模組
# stacks/stack_b/main.tf
module "network" {
  source   = "../../modules/networking"
  vpc_cidr = "10.1.0.0/16"
}

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.medium"
  subnet_id     = module.network.private_subnet_id
  
  tags = {
    Name = "stack-b-app-server"
  }
}

這個Terraform程式碼範例展示瞭如何建立和使用分享網路模組。模組定義了VPC和兩個子網路(公共和私有),並輸出它們的ID供其他資源使用。在兩個不同的堆積積疊中,我們使用相同的網路模組但提供不同的CIDR範圍,然後在這些網路中建立不同型別的EC2例項。這種方法實作了程式碼重用,同時保持了堆積積疊之間的獨立性。每個堆積積疊可以獨立佈署和管理,但它們分享相同的網路結構定義邏輯。

測試驅動模組化

自動化測試不僅是驗證程式碼正確性的工具,也是驅動更好系統設計的強大力量。對於基礎設施程式碼,這一點尤為重要。

# 模組測試範例 - 使用Terratest
package test

import (
  "testing"
  "github.com/gruntwork-io/terratest/modules/terraform"
  "github.com/stretchr/testify/assert"
)

func TestNetworkingModule(t *testing.T) {
  terraformOptions := &terraform.Options{
    TerraformDir: "../modules/networking",
    Vars: map[string]interface{}{
      "vpc_cidr": "10.10.0.0/16",
    },
  }
  
  defer terraform.Destroy(t, terraformOptions)
  terraform.InitAndApply(t, terraformOptions)
  
  vpcId := terraform.Output(t, terraformOptions, "vpc_id")
  publicSubnetId := terraform.Output(t, terraformOptions, "public_subnet_id")
  privateSubnetId := terraform.Output(t, terraformOptions, "private_subnet_id")
  
  assert.NotEmpty(t, vpcId)
  assert.NotEmpty(t, publicSubnetId)
  assert.NotEmpty(t, privateSubnetId)
}

這個Go測試程式碼使用Terratest框架測試我們的網路模組。測試初始化模組,應用設定,然後驗證輸出是否如預期。這種測試驅動的方法確保模組正確工作,同時也鼓勵更好的設計實踐。為了使模組可測試,我們需要定義清晰的輸入和輸出,這自然導致更低的耦合度和更高的內聚性。測試結束時的defer terraform.Destroy確保測試資源被清理,避免不必要的成本和資源洩漏。

設計模組化基礎設施的實用

根據上述原則,以下是設計模組化基礎設施的實用:

1. 識別自然邊界

尋找系統中的自然邊界,這些邊界通常是:

  • 不同團隊的責任區域
  • 不同變更頻率的元件
  • 不同安全要求的區域
# 團隊A負責的網路基礎設施
# team_a/networking/main.tf
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  
  name = "team-a-vpc"
  cidr = "10.0.0.0/16"
  
  azs             = ["us-west-2a", "us-west-2b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
  
  enable_nat_gateway = true
  single_nat_gateway = true
  
  tags = {
    Owner = "team-a"
    Environment = "production"
  }
}

# 團隊B負責的應用基礎設施
# team_b/application/main.tf
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "terraform-state"
    key    = "team-a/networking/terraform.tfstate"
    region = "us-west-2"
  }
}

resource "aws_security_group" "app" {
  name        = "application"
  description = "Allow application traffic"
  vpc_id      = data.terraform_remote_state.network.outputs.vpc_id
  
  # 安全規則...
  
  tags = {
    Owner = "team-b"
    Environment = "production"
  }
}

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.medium"
  subnet_id     = data.terraform_remote_state.network.outputs.private_subnets[0]
  security_groups = [aws_security_group.app.id]
  
  tags = {
    Name = "application-server"
    Owner = "team-b"
    Environment = "production"
  }
}

這個例子展示瞭如何根據團隊責任劃分基礎設施。團隊A負責網路基礎設施,使用標準VPC模組建立網路資源。團隊B負責應用基礎設施,透過遠端狀態資料源參照團隊A的網路資源,而不是直接依賴其程式碼。這種方法允許團隊獨立工作,同時保持必要的資源關係。每個團隊可以按照自己的節奏進行變更,只要他們維護穩定的介面(在這種情況下是遠端狀態輸出)。標籤清楚地標識了每個資源的所有者,便於管理和問題解決。

2. 設計清晰的介面

元件之間的介面應該:

  • 明確定義輸入和輸出
  • 隱藏實作細節
  • 保持穩定,即使內部實作變更
# 定義清晰介面的資料函式庫模組
# modules/database/main.tf
variable "identifier" {
  description = "Database identifier"
  type        = string
}

variable "allocated_storage" {
  description = "Allocated storage in GB"
  type        = number
  default     = 20
}

variable "engine" {
  description = "Database engine"
  type        = string
  default     = "postgres"
}

variable "engine_version" {
  description = "Database engine version"
  type        = string
  default     = "12.4"
}

variable "instance_class" {
  description = "Database instance class"
  type        = string
  default     = "db.t3.micro"
}

variable "username" {
  description = "Master username"
  type        = string
  sensitive   = true
}

variable "password" {
  description = "Master password"
  type        = string
  sensitive   = true
}

resource "aws_db_instance" "this" {
  identifier           = var.identifier
  allocated_storage    = var.allocated_storage
  engine               = var.engine
  engine_version       = var.engine_version
  instance_class       = var.instance_class
  username             = var.username
  password             = var.password
  skip_final_snapshot  = true
  
  # 許多其他設定選項在內部處理,不暴露給模組使用者
  backup_retention_period = 7
  backup_window           = "03:00-04:00"
  maintenance_window      = "mon:04:00-mon:05:00"
  multi_az                = true
  storage_encrypted       = true
  
  tags = {
    Name = var.identifier
  }
}

output "endpoint" {
  description = "Database connection endpoint"
  value       = aws_db_instance.this.endpoint
}

output "port" {
  description = "Database port"
  value       = aws_db_instance.this.port
}

output "name" {
  description = "Database name"
  value       = aws_db_instance.this.name
}

這個資料函式庫模組展示了良好的介面設計。它明確定義了使用者需要提供的輸入引數,許多引數有合理的預設值,減少了使用者的負擔。敏感資訊如使用者名和密碼被標記為sensitive,確保它們不會在日誌中明文顯示。模組在內部處理許多設定細節(如備份策略、維護視窗、加密等),這些是最佳實踐但不需要使用者直接指定。輸出僅包括使用者需要知道的連線資訊,隱藏了其他實作細節。這種設計使模組易於使用,同時允許在不破壞介面的情況下更改內部實作。

3. 實施變更管理策略

對於模組化基礎設施,需要明確的變更管理策略:

  • 版本控制所有元件
  • 定義明確的版本升級路徑
  • 自動化測試確保相容性
# 在專案中使用版本化模組
module "database" {
  source  = "terraform-aws-modules/rds/aws"
  version = "3.4.0"  # 明確指定版本
  
  identifier = "my-database"
  
  engine            = "mysql"
  engine_version    = "8.0.23"
  instance_class    = "db.t3.large"
  allocated_storage = 50
  
  name     = "mydb"
  username = var.db_username
  password = var.db_password
  port     = "3306"
  
  maintenance_window = "Mon:00:00-Mon:03:00"
  backup_window      = "03:00-06:00"
  
  tags = {
    Environment = "production"
    Project     = "my-project"
  }
}

這個例子展示瞭如何在專案中使用版本化的公共模組。透過明確指定version = “3.4.0”,我們確保即使模組發布新版本,我們的基礎設施也不會自動升級,從而避免意外變更。這種方法允許團隊控制何時升級依賴項,並在升級前進行適當的測試。使用公共登入檔中的模組(如terraform-aws-modules/rds/aws)也是一種良好實踐,因為這些模組通常經過廣泛測試並遵循最佳實踐。專案可以根據需要自定義模組引數,同時受益於模組的內建功能和安全設定。

小而簡單元件的實際效益

採用模組化方法構建基礎設施程式碼帶來許多實際效益:

  1. 加速變更:小型元件更容易理解和修改,減少了變更所需的時間
  2. 降低風險:隔離的元件限制了變更的影響範圍
  3. 提高可重用性:設計良好的元件可以在不同場景中重用
  4. 改善協作:不同團隊可以在各自的元件上獨立工作
  5. 簡化測試:小型元件更容易測試,提高了測試覆寫率
  6. 促進創新:模組化系統使實驗和改進變得更加容易

隨著系統的成長,這些效益變得越來越明顯。透過堅持小而簡單元件的核心實踐,組織可以維持高速變更的能力,同時持續提升系統品質。

基礎設施即程式碼的三個核心實踐——將一切定義為程式碼、持續測試和交付、以及構建小型元件——共同創造了一個正向迴圈:變更速度驅動更好的品質,更好的品質使更快的變更速度成為可能。這種迴圈是現代雲端基礎設施成功的關鍵。

在設計基礎設施元件時,記住Kent Beck的簡單設計規則:透過測試、揭示意圖、沒有重複、包含最少的元素。這些原則將指導你建立既強大又靈活的基礎設施系統,能夠隨著組織的需求而演化和成長。