地形陷阱

地形陷阱
讓我們重點介紹一些陷阱,包括與循環、if 語句和部署技術相關的陷阱,以及影響 Terraform 的更常見問題:

  • count 和 for_each 參數有限制;
  • 限制零停機部署;
  • 再好的計劃也可能失敗;
  • 重構也有其陷阱;
  • 延遲一致性與延遲是一致的。

count 和 for_each 參數有限制

本章中的範例在迴圈和條件邏輯中廣泛使用了 count 參數和 for_each 表達式。 它們表現良好,但有兩個您需要注意的重要限制。

  • Count 和 for_each 不能引用任何資源輸出變數。
  • count 和 for_each 不能在模組配置中使用。

count 和 for_each 不能引用任何資源輸出變數

假設您需要部署多個 EC2 伺服器,並且由於某種原因您不想使用 ASG。 你的程式碼可能是這樣的:

resource "aws_instance" "example_1" {
   count             = 3
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

讓我們一一看看。

由於 count 參數設定為靜態值,因此此程式碼將正常運作:當您執行 apply 命令時,它將建立三個 EC2 伺服器。 但是,如果您想在目前 AWS 區域內的每個可用區 (AZ) 中部署伺服器該怎麼辦? 您可以讓程式碼從 aws_availability_zones 資料來源載入區域列表,然後循環遍歷每個區域並使用 count 參數和陣列索引存取在其中建立一個 EC2 伺服器:

resource "aws_instance" "example_2" {
   count                   = length(data.aws_availability_zones.all.names)
   availability_zone   = data.aws_availability_zones.all.names[count.index]
   ami                     = "ami-0c55b159cbfafe1f0"
   instance_type       = "t2.micro"
}

data "aws_availability_zones" "all" {}

該程式碼也可以正常工作,因為 count 參數可以毫無問題地引用資料來源。 但是,如果您需要建立的伺服器數量取決於某些資源的輸出,會發生什麼情況? 為了證明這一點,最簡單的方法是使用 random_integer 資源,顧名思義,它會傳回一個隨機整數:

resource "random_integer" "num_instances" {
  min = 1
  max = 3
}

此程式碼產生 1 到 3 之間的隨機數。讓我們看看如果我們嘗試在 aws_instance 資源的 count 參數中使用此資源的輸出會發生什麼:

resource "aws_instance" "example_3" {
   count             = random_integer.num_instances.result
   ami                = "ami-0c55b159cbfafe1f0"
   instance_type = "t2.micro"
}

如果您在此程式碼上執行 terraform plan,您將收到以下錯誤:

Error: Invalid count argument

   on main.tf line 30, in resource "aws_instance" "example_3":
   30: count = random_integer.num_instances.result

The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.

Terraform 要求在建立或修改任何資源之前的規劃階段計算 count 和 for_each。 這意味著 count 和 for_each 可以引用文字、變數、資料來源,甚至是資源清單(只要它們的長度可以在調度時確定),但不能引用計算的資源輸出變數。

count 和 for_each 不能在模組配置中使用

有一天,您可能會想在模組配置中新增計數參數:

module "count_example" {
     source = "../../../../modules/services/webserver-cluster"

     count = 3

     cluster_name = "terraform-up-and-running-example"
     server_port = 8080
     instance_type = "t2.micro"
}

此程式碼嘗試在模組內使用 count 來建立 webserver-cluster 資源的三個副本。 或者您可能希望透過將其 count 參數設為 0,基於某些布林條件來使連接模組成為可選。這可能看起來像是合理的程式碼,但在執行 terraform plan 時您會收到此錯誤:

Error: Reserved argument name in module block

   on main.tf line 13, in module "count_example":
   13: count = 3

The name "count" is reserved for use in a future version of Terraform.

不幸的是,從 Terraform 0.12.6 開始,不支援在模組資源中使用 count 或 for_each 。 根據 Terraform 0.12 發行說明 (http://bit.ly/3257bv4),HashiCorp 計劃在未來添加此功能,因此根據您閱讀本書的時間,它可能已經可用。 為了確定, 請在此處閱讀 Terraform 變更日誌.

零停機部署的局限性

將 create_before_destroy 區塊與 ASG 結合使用是建立零停機部署的絕佳解決方案,但有一個警告除外:不支援自動縮放規則。 或者更準確地說,這會在每次部署時將 ASG 大小重置回 min_size,如果您使用自動縮放規則來增加運行的伺服器數量,這可能會出現問題。

例如,webserver-cluster 模組包含一對 aws_autoscaling_schedule 資源,這會在上午 9 點將叢集中的伺服器數量從 11 台增加到 9 台。 如果您在上午 XNUMX 點進行部署,新的 ASG 將僅使用兩台伺服器而不是十台伺服器啟動,並保持這種狀態直到第二天上午 XNUMX 點。

可以透過多種方式規避此限制。

  • 將 aws_autoscaling_schedule 中的重複參數從 0 9 * * * (「上午 9 點運行」)更改為 0-59 9-17 * * * (「上午 9 點到下午 5 點每分鐘運行一次」)。 如果 ASG 已經有十台伺服器,再次執行此自動縮放規則不會改變任何內容,這正是我們想要的。 但如果ASG是最近才部署的,這項規則將保證最多一分鐘內其伺服器數量達到XNUMX台。 這並不是一個完全優雅的方法,從十台伺服器到兩台伺服器的大幅跳躍也會為用戶帶來問題。
  • 建立一個自訂腳本,該腳本使用 AWS API 來確定 ASG 中活動伺服器的數量,使用外部資料來源呼叫該腳本(請參閱第 249 頁的「外部資料來源」),並將 ASG 的desired_capacity 參數設為由劇本。 這樣,每個新的 ASG 實例將始終以與現有 Terraform 程式碼相同的容量運行,並使其更難維護。

當然,Terraform 理想情況下應該內建對零停機部署的支持,但截至 2019 年 XNUMX 月,HashiCorp 團隊還沒有計劃添加此功能(詳細資訊 - 這裡).

正確的計劃可能無法成功實施

有時 plan 指令會產生完全正確的部署計劃,但 apply 指令會傳回錯誤。 例如,嘗試新增 aws_iam_user 資源,其名稱與您先前在第 2 章中建立的 IAM 使用者所使用的名稱相同:

resource "aws_iam_user" "existing_user" {
   # Подставьте сюда имя уже существующего пользователя IAM,
   # чтобы попрактиковаться в использовании команды terraform import
   name = "yevgeniy.brikman"
}

現在,如果您執行 plan 指令,Terraform 將輸出一個看似合理的部署方案:

Terraform will perform the following actions:

   # aws_iam_user.existing_user will be created
   + resource "aws_iam_user" "existing_user" {
         + arn                  = (known after apply)
         + force_destroy   = false
         + id                    = (known after apply)
         + name               = "yevgeniy.brikman"
         + path                 = "/"
         + unique_id         = (known after apply)
      }

Plan: 1 to add, 0 to change, 0 to destroy.

如果執行 apply 命令,您將收到以下錯誤:

Error: Error creating IAM User yevgeniy.brikman: EntityAlreadyExists:
User with name yevgeniy.brikman already exists.

   on main.tf line 10, in resource "aws_iam_user" "existing_user":
   10: resource "aws_iam_user" "existing_user" {

當然,問題是具有該名稱的 IAM 用戶已經存在。 這種情況不僅會發生在 IAM 使用者身上,而且會發生在幾乎所有資源上。 有人可能手動或使用命令列建立了此資源,但無論哪種方式,匹配 ID 都會導致衝突。 此錯誤有多種變體,常常讓 Terraform 新手感到驚訝。

關鍵點是 terraform plan 指令只考慮 Terraform 狀態檔中指定的資源。 如果以其他方式建立資源(例如,透過點擊 AWS 控制台手動建立),它們最終不會出現在狀態檔案中,因此 Terraform 在執行排程命令時不會考慮它們。 結果,乍一看似乎正確的計劃最終會失敗。

我們可以從中吸取兩個教訓。

  • 如果您已經開始使用 Terraform,請不要使用其他任何東西。 如果您的基礎架構的一部分是使用 Terraform 進行管理的,您將無法再手動修改它。 否則,您不僅會面臨奇怪的 Terraform 錯誤的風險,而且還會抵消 IaC 的許多好處,因為程式碼將不再是您的基礎設施的準確表示。
  • 如果您已經擁有一些基礎設施,請使用導入指令。 如果您開始將 Terraform 與現有基礎架構結合使用,則可以使用 terraform import 指令將其新增至狀態檔案。 這樣 Terraform 將知道需要管理哪些基礎架構。 導入指令有兩個參數。 第一個是設定檔中的資源位址。 這裡的語法與資源連結相同:_。 (如 aws_iam_user.existing_user)。 第二個參數是要導入的資源的ID。 假設資源 ID aws_iam_user 是使用者名稱(例如 yevgeniy.brikman),資源 ID aws_instance 是 EC2 伺服器 ID(例如 i-190e22e5)。 如何導入資源通常在其頁面底部的文件中指出。

    以下是一個導入命令,用於同步您添加到 Terraform 配置中的 aws_iam_user 資源以及第 2 章中的 IAM 用戶(當然,用您的名字替換 yevgeniy.brikman):

    $ terraform import aws_iam_user.existing_user yevgeniy.brikman

    Terraform 將呼叫 AWS API 來尋找您的 IAM 使用者並在其與 Terraform 配置中的 aws_iam_user.existing_user 資源之間建立狀態檔案關聯。 從現在開始,當您執行 plan 命令時,Terraform 將知道 IAM 使用者已經存在,並且不會嘗試再次建立它。

    值得注意的是,如果您已經有很多資源想要匯入 Terraform,手動編寫程式碼並一次匯入每個資源可能會很麻煩。 因此,值得研究像 Terraforming (http://terraforming.dtan4.net/) 這樣的工具,它可以自動從您的 AWS 帳戶匯入程式碼和狀態。

    重構也有其陷阱

    重構 是程式設計中的常見做法,您可以更改程式碼的內部結構,同時保持外部行為不變。 這是為了讓程式碼更清晰、更整潔、更易於維護。 重構是一項不可或缺的技術,應該經常使用。 但當涉及到 Terraform 或任何其他 IaC 工具時,您必須非常小心一段程式碼的「外部行為」的含義,否則會出現意想不到的問題。

    例如,一種常見的重構類型是將變數或函數的名稱替換為更容易理解的名稱。 許多 IDE 內建了對重構的支持,可以自動重命名整個專案中的變數和函數。 在通用程式語言中,這是一個您可能不會想到的微不足道的過程,但在 Terraform 中您必須非常小心,否則您可能會遇到中斷。

    例如,webserver-cluster 模組有一個輸入變數 cluster_name:

    variable "cluster_name" {
       description = "The name to use for all the cluster resources"
       type          = string
    }

    想像一下,您開始使用此模組來部署一個名為 foo 的微服務。 稍後,您要將服務重新命名為 bar。 此更改可能看起來微不足道,但實際上可能會導致服務中斷。

    事實上,webserver-cluster 模組在許多資源中使用了 cluster_name 變量,包括兩個安全性群組和 ALB 的 name 參數:

    resource "aws_lb" "example" {
       name                    = var.cluster_name
       load_balancer_type = "application"
       subnets = data.aws_subnet_ids.default.ids
       security_groups      = [aws_security_group.alb.id]
    }

    如果您變更資源的名稱參數,Terraform 將刪除資源的舊版本並在其位置建立新版本。 但是,如果該資源是 ALB,則在刪除它和下載新版本之間,您將沒有將流量重定向到 Web 伺服器的機制。 同樣,如果刪除安全群組,您的伺服器將開始拒絕任何網路流量,直到建立新群組。

    您可能感興趣的另一種重構類型是更改 Terraform ID。 我們以 webserver-cluster 模組中的 aws_security_group 資源為例:

    resource "aws_security_group" "instance" {
      # (...)
    }

    此資源的識別符稱為實例。 想像一下,在重構過程中,您決定將其更改為更容易理解的(在您看來)名稱 cluster_instance:

    resource "aws_security_group" "cluster_instance" {
       # (...)
    }

    最終會發生什麼事? 沒錯:中斷。

    Terraform 將每個資源 ID 與雲端提供者 ID 相關聯。 例如,iam_user 與 AWS IAM 使用者 ID 關聯,aws_instance 與 AWS EC2 伺服器 ID 關聯。 如果您將資源 ID 變更為 Terraform(例如從實例變更為 cluster_instance,例如 aws_security_group 的情況),則它將顯示為好像您刪除了舊資源並新增了新資源。 如果您套用這些更改,Terraform 將刪除舊的安全群組並建立新的安全群組,同時您的伺服器開始拒絕任何網路流量。

    您應該從這次討論中吸取以下四個重要教訓。

    • 始終使用計劃命令。 它可以揭示所有這些障礙。 仔細檢查其輸出,並注意 Terraform 計劃刪除很可能不應刪除的資源的情況。
    • 先創建再刪除。 如果要替換資源,請在刪除原始資源之前仔細考慮是否需要建立替換資源。 如果答案是肯定的,create_before_destroy 可以提供協助。 透過執行兩個步驟可以手動實現相同的結果:首先將新資源新增至設定並執行 apply 命令,然後從設定中刪除舊資源並再次使用 apply 命令。
    • 更改標識符需要更改狀態。 如果您想要變更與資源關聯的 ID(例如,將 aws_security_group 從實例重新命名為 cluster_instance)而不刪除資源並建立其新版本,則必須相應地更新 Terraform 狀態檔案。 切勿手動執行此操作 - 請改用 terraform state 命令。 重命名識別碼時,您應該執行 terraform state mv 命令,其語法如下:
      terraform state mv <ORIGINAL_REFERENCE> <NEW_REFERENCE>

      ORIGINAL_REFERENCE 是引用目前形式的資源的表達式,NEW_REFERENCE 是要將其移動到的位置。 例如,將 aws_security_group 群組從 instance 重新命名為 cluster_instance 時,需要執行以下命令:

      $ terraform state mv 
         aws_security_group.instance 
         aws_security_group.cluster_instance

      這告訴 Terraform 之前與 aws_security_group.instance 關聯的狀態現在應與 aws_security_group.cluster_instance 關聯。 如果重命名並運行此命令 terraform plan 後沒有顯示任何更改,那麼您所做的一切都是正確的。

    • 某些設定無法變更。 許多資源的參數是不可更改的。 如果您嘗試變更它們,Terraform 將刪除舊資源並在其位置建立新資源。 每個資源頁面通常會指示更改特定設定時會發生什麼,因此請務必檢查文件。 請務必使用 plan 指令並考慮使用 create_before_destroy 策略。

    延遲一致性與延遲一致...

    一些雲端提供者的 API(例如 AWS)是非同步的並且具有延遲一致性。 非同步表示介面可以立即回傳回應,而無需等待請求的操作完成。 延遲一致性意味著更改可能需要時間才能傳播到整個系統; 發生這種情況時,您的回應可能不一致,並且取決於哪個資料來源副本回應您的 API 呼叫。

    例如,想像一下,您對 AWS 進行 API 調用,要求它建立 EC2 伺服器。 API 將幾乎立即傳回「成功」回應(201 Created),而無需等待伺服器本身建立。 如果您嘗試立即連接到它,它幾乎肯定會失敗,因為此時 AWS 仍在初始化資源,或者伺服器尚未啟動。 此外,如果您再次呼叫以獲取有關此伺服器的信息,您可能會收到錯誤(404 Not Found)。 問題是,有關此 EC2 伺服器的資訊在變得隨處可用之前仍可能在整個 AWS 中傳播,您必須等待幾秒鐘。

    每當您使用具有惰性一致性的非同步 API 時,您必須定期重試請求,直到操作完成並透過系統傳播。 不幸的是,AWS SDK 並沒有為此提供任何好的工具,而 Terraform 專案曾經遭受許多錯誤,例如 6813 (https://github.com/hashicorp/terraform/issues/6813):

    $ terraform apply
    aws_subnet.private-persistence.2: InvalidSubnetID.NotFound:
    The subnet ID 'subnet-xxxxxxx' does not exist

    換句話說,您建立一個資源(如子網路),然後嘗試獲取有關它的一些資訊(如新建的子網路的 ID),而 Terraform 找不到它。 大多數這些錯誤(包括 6813)已修復,但它們仍然不時出現,特別是當 Terraform 新增對新資源類型的支援時。 這很煩人,但在大多數情況下不會造成任何傷害。 當您再次執行 terraform apply 時,一切都應該正常,因為此時訊息已經傳播到整個系統。

    此摘錄摘自葉夫根尼·布里克曼 (Evgeniy Brikman) 的書 “Terraform:程式碼層級的基礎架構”.

來源: www.habr.com

添加評論