iT邦幫忙

2023 iThome 鐵人賽

DAY 7
0

原簡體中文教程連結: Introduction.《Terraform入門教程》


我們將在本章講解 Terraform 配置文件的編寫。

Terraform 早期僅支持使用 HCL(Hashicorp Configuration Language) 語法的 .tf 文件,近些年來也開始支持JSON。HashiCorp 甚至修改了他們的 json 解析器,使得他們的 json 可以支持註解,但 HCL 相比起 JSON 來說有著更好的可讀性,所以我們還是會以 HCL 來講解。其實我個人是不太喜歡用 JSON 編寫 Terraform 程式碼的,有些團隊使用 JSON 是因為他們是用其他程式碼來生成相應的 JSON 格式的 Terraform 程式碼(比如自行研發的 GUI 工具,通過拖曳的方式定義基礎設施,繼而生成相關程式碼)。我個人不太喜歡這種方式,因為它鼓勵用戶從零開始拖曳出所需的所有基礎設施,而不是通過組裝成熟的可複用的模組化程式碼。我個人認為應該像對待業務邏輯程式碼一樣對待基礎設施程式碼。

這裡特別之處一點,我們將在這一章節提到模組 (Module) 的概念,但我們會在後續單獨的章節專門講解模塊。在本章內,讀者可以簡單地將一個模組理解成一個含有多個 Terraform 程式碼文件的目錄,不包含其子目錄。

本章內容基本是對官方文件的翻譯,英語閱讀能力好的讀者應該直接閱讀官方文件獲取最權威的信息。


1.4.1.1. 類型

Terraform 的某些類型之間存在隱式類型轉換規則,如果無法隱式轉換類型,那麼不同類型資料間的賦值將會報錯。

Terraform 類型分為原始類型與複雜類型兩大類。

1.4.1.1.1. 原始型

原型分為三類:stringnumberbool

  • string 代表一組 Unicode 字串,例如:"hello"

  • number 代表數字,可以是整數,也可以是小數。

  • bool 代表布林值,要麼為 true,要麼為 falsebool 值可以用做邏輯判斷。
    numberbool 都可以和 string 進行隱式轉換,當我們把 numberbool 類型的值賦給 string 類型的值,或是反過來時,Terraform 會自動替我們轉換類型,其中:

  • true 值會被轉換為 "true",反之亦然

  • false 值會被轉換為 "false",反之亦然

  • 15 會被轉換為 "15"3.1415 會被轉換為 "3.1415",反之亦然

1.4.1.1.2. 複雜類型

複雜型別是一組值所組成的符合型別,有兩類複雜型別。

一種是集合類型。一個集合包含了一組同一類型的值。集合內元素的類型成為元素類型。一個集合變數在構造時必須確定集合類型。集合內所有元素的類型必須相同。

Terraform 支援三種集合:

  • list(...):列表是一組值的連續集合,可以用下標存取內部元素,下標從 0 開始。例如名為 llistl[0] 就是第一個元素。list 類型的宣告可以是 list(number)list(string)list(bool) 等,括號中的類型即為元素類型。
  • map(...):字典類型(或稱為映射類型),代表一組鍵唯一的鍵值對,鍵類型必須是 string,值類型任意。map(number) 代表鍵為 string 類型而值為 number 類型,其餘類推。map 值有兩種宣告方式,一種是類似 {"foo": "bar", "bar": "baz"},另一種是 {foo="bar", bar="baz"}。鍵可以不用雙引號,但如果鍵是以數字開頭則例外。多對鍵值對之間要用逗號分隔,也可以用換行符號分隔。建議使用 = 號(Terraform 代碼規格中規定按等號對齊,使用等號會使得代碼在格式化後更加美觀)
  • set(...):集合類型,代表一組不重複的值。

以上集合類型都支援通配類型縮寫,例如 list 等價於 list(any)map 等價於 map(any)set 等價於 set(any)any 代表支援任意的元素類型,前提是所有元素都是一個類型。例如,將 list(number) 賦給 list(any) 是合法的,list(string) 賦給 list(any) 也是合法的,但是 list 內部所有的元素必須是同一種類型的。

第二種複雜類型是結構化類型。一個結構化類型允許多個不同類型的值組成一個類型。結構化類型需要提供一個 schema 結構資訊作為參數來指明元素的結構。

Terraform 支援兩種結構化類型:

  • object(...):物件是指一組由具有名稱和類型的屬性所構成的符合類型,它的 schema 資訊由 { \<KEY\>=\<TYPE\>, \<KEY\>=\<TYPE\>,...} 的形式描述,例如 object({age=number, name=string}),代表由名為 "age" 類型為 number ,以及名為 "name" 類型為 "string" 兩個屬性組成的物件。賦給 object 類型的合法值必須含有所有屬性值,但是可以擁有多餘的屬性(多餘的屬性在賦值時會被拋棄)。例如對 object({age=number,name=string}) 來說,{ age=18 } 是一個非法值,而 { age=18, name="john", gender="male" } 是一個合法值,但賦值時 gender 會被拋棄
  • tuple(...):元組類似 list,也是一組值的連續集合,但每個元素都有獨立的類型。元組同 list 一樣,也可以用下標存取內部元素,下標從 0 開始。元組 schema 用 [\<TYPE\>, \<TYPE\>, ...] 的形式描述。元組的元素數量必須與 schema 宣告的型別數量相等,且每個元素的型別必須與元組 schema 對應位置的型別相等。例如 tuple([string, number, bool]),類型的一個合法值可以是 ["a", 15, true]
    複雜型別也支援隱式型別轉換。

Terraform 會嘗試轉換類似的類型,轉換規則有:

  • objectmap:如果一個 map 的鍵集合含有 object 規定的所有屬性,那麼 map 可以被轉換為 objectmap 裡多餘的鍵值對會被拋棄。由 map -> object -> map 的轉換可能會遺失資料。
  • tuplelist:當一個 list 元素的數量剛好等於一個 tuple 宣告的長度時, list 可以轉換為 tuple。例如:值為 ["18", "true", "john"]list 轉換為 tuple([number,bool, string]) 的結果為 [18, true, "john"]
  • settuple:當一個 listtuple 被轉換為一個 set,那麼重複的值將被丟棄,並且值原有的順序也會遺失。如果一個 set 被轉換到 list 或是 tuple,那麼元素將按照以下順序排列:如果 set 的元素是 string,那麼將按照字段順序排列;其他類型的元素不承諾任何特定的排列順序。
    複雜型別轉換時,元素類型將在可能的情況下發生隱式轉換,類似上述 listtuple 轉換舉的例子。

如果類型不匹配,Terraform 會報錯,例如我們試圖把 object({name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12}) 轉換到 map(string) 類型,這是不合法的,因為 name 的值為 list,無法轉換為 string

1.4.1.1.3. any

any 是Terraform 中非常特殊的一種類型約束,它本身並非一個類型,而只是一個佔位符。每當一個值被賦予一個由 any 約束的複雜類型時,Terraform 會嘗試計算出一個最精確的類型來取代 any

例如我們把 ["a", "b", "c"] 賦給 list(any),它在 Terraform 中實際的物理類型首先被編譯成 tuple([string, string, string]) ,然後 Terraform 認為 tuplelist 相似,所以會嘗試將它轉換為 list(string)。然後 Terraform 發現 list(string) 符合 list(any) 的約束,所以會用 string 取代 any,於是賦值後最終的型別是 list(string)

由於即使是 list(any),所有元素的類型也必須是一樣的,所以某些類型轉換到 list(any) 時會對元素進行隱式類型轉換。例如將 ["a", 1, "b"] 賦給 list(any),Terraform 發現 1 可以轉換到 "1",所以最終的值是 ["a", "1", "b"],最終的型別會是 list(string)。再例如我們想把 ["a", \[\], "b"] 轉換成 list(any),由於 Terraform 無法找到一個合適的目標類型使得所有元素都能成功隱式轉換過去,所以 Terraform 會報錯,要求所有元素都必須是同一個類型的。

宣告類型時如果不想有任何的約束,那麼可以用 any

variable "no_type_constraint" {
  type = any
}

這樣的話,Terraform 可以將任何類型的資料賦予它。

1.4.1.1.4. null

存在一種特殊值是無類型的,那就是 nullnull 代表資料缺失。如果我們把一個參數設為 null,Terraform 會認為你忘記為它賦值。如果該參數有預設值,那麼 Terraform 會使用預設值;如果沒有又剛好該參數是必填字短,Terraform 會報錯。null 在條件式中非常有用,你可以在某項條件不滿足時跳過對某參數的賦值。

1.4.1.1.5. object 的optional 成員

自 Terraform 1.3 開始,我們可以在 object 類型定義中使用 optional 修飾屬性。

在 1.3 之前,如果一個 variable 的類型為 object,那麼使用時必須傳入一個結構完全相符的物件。例如:

variable "an_object" {
  type = object({
    a = string
    b = string
    c = number
  })
}

如果我們想要傳入一個物件給 var.an_object,但不準備給 bc 賦值,我們必須這樣:

{
  a = "a"
  b = null
  c = null
}

傳入的物件必須完全符合類型定義的結構,即使我們不想對某些屬性賦值。這使得我們如果想要定義一些比較複雜,屬性比較多的 object 類型時會給使用者在使用上造成一些麻煩。

Terraform 1.3 允許我們為一個屬性加入 optional 宣告,還是用上面的範例:

variable "with_optional_attribute" {
  type = object({
    a = string                # a required attribute
    b = optional(string)      # an optional attribute
    c = optional(number, 127) # an optional attribute with default value
  })
}

這裡我們將 b 宣告為 optional,如果傳入的物件沒有 b,則會使用 null 作為值;c 不但宣告為 optional 的,還添加了 127 作為預設值,傳入的物件如果沒有 c,那麼會使用 127 作為它的值。

optional 修飾符有這樣兩個參數:

  • 類型:(必填)第一個參數標明了屬性的類型
  • 預設值:(選填)第二個參數定義了 Terraform 在物件中沒有定義該屬性值時所使用的預設值。預設值必須與類型參數相容。如果沒有指定預設值,Terraform 會使用 null 作為預設值。
    一個包含非 null 預設值的 optional 屬性在模組內使用時可以確保不會讀到 null 值。當使用者沒有設定該屬性,或是明確設定為 null 時,Terraform 會使用預設值,所以模組內無需再次判斷該屬性是否為 null

Terraform 採用自上而下的順序來設定物件的預設值,也就是說,Terraform 會先套用 optional 修飾符中的指定的預設值,然後再為其中可能存在的內嵌物件設定預設值。

1.4.1.1.5.1. 範例:帶有optional 屬性和預設值的內嵌結構

下面的範例示範了一個輸入變數,用來描述一個儲存了靜態網站內容的儲存桶。此變數的型別包含了一系列的 optional 屬性,包括 website,不但其本身是 optional 的,其內部包含了數個 optional 的屬性以及預設值。

variable "buckets" {
  type = list(object({
    name    = string
    enabled = optional(bool, true)
    website = optional(object({
      index_document = optional(string, "index.html")
      error_document = optional(string, "error.html")
      routing_rules  = optional(string)
    }), {})
  }))
}

以下給出一個範例 terraform.tfvars 文件,為 var.buckets 定義了三個儲存桶:

  • production 配置了一條重定向的路由規則
  • archived 使用了預設配置,但被關閉了
  • docs 使用文字檔案取代了索引頁和錯誤頁
    production 桶子沒有指定索引頁和錯誤頁,archived 桶子完全忽略了網站配置。Terraform 會使用 bucket 類型約束中指定的預設值。
buckets = [
  {
    name = "production"
    website = {
      routing_rules = <<-EOT
      [
        {
          "Condition" = { "KeyPrefixEquals": "img/" },
          "Redirect"  = { "ReplaceKeyPrefixWith": "images/" }
        }
      ]
      EOT
    }
  },
  {
    name = "archived"
    enabled = false
  },
  {
    name = "docs"
    website = {
      index_document = "index.txt"
      error_document = "error.txt"
    }
  },
]

此配置會產生如下的 variable 值:

  • productiondocs 桶,Terraform 會將 enabled 設為 true。Terraform 會同時使用預設值配置 website,然後使用 docs 中指定的值來覆寫預設值。
  • archiveddocs 桶,Terraform 會將 routing_rules 設為 null。當 Terraform 沒有讀取到 optional 的屬性,且屬性上沒有設定預設值時,Terraform 會將這些屬性設為 null
  • 對於 archived 桶,Terraform 會將 website 屬性設為 buckets 類型約束中定義的預設值。
tolist([
  {
    "enabled" = true
    "name" = "production"
    "website" = {
      "error_document" = "error.html"
      "index_document" = "index.html"
      "routing_rules" = <<-EOT
      [
        {
          "Condition" = { "KeyPrefixEquals": "img/" },
          "Redirect"  = { "ReplaceKeyPrefixWith": "images/" }
        }
      ]

      EOT
    }
  },
  {
    "enabled" = false
    "name" = "archived"
    "website" = {
      "error_document" = "error.html"
      "index_document" = "index.html"
      "routing_rules" = tostring(null)
    }
  },
  {
    "enabled" = true
    "name" = "docs"
    "website" = {
      "error_document" = "error.txt"
      "index_document" = "index.txt"
      "routing_rules" = tostring(null)
    }
  },
])

1.4.1.1.5.2. 範例:有條件地設定一個預設屬性

有時我們需要根據其他資料的值來動態決定是否要為一個 optional 參數設定值。在這種場景下,發起呼叫的 module 區塊可以使用條件表達式搭配 null 來動態地決定是否設定該參數。

還是上一個例子中的 variable "buckets" 的例子,使用下面演示的例子可以根據新輸入參數 var.legacy_filenames 的值來有條件地覆蓋 website 對象中 index_document 以及 error_document 的設定:

variable "legacy_filenames" {
  type     = bool
  default  = false
  nullable = false
}

module "buckets" {
  source = "./modules/buckets"

  buckets = [
    {
      name = "maybe_legacy"
      website = {
        error_document = var.legacy_filenames ? "ERROR.HTM" : null
        index_document = var.legacy_filenames ? "INDEX.HTM" : null
      }
    },
  ]
}

var.legacy_filenames 設定為 true 時,呼叫會覆蓋 document 的檔名。當它的值為 false 時,呼叫不會指定這兩個檔名,這使得模組使用定義的預設值。


原簡體中文教程連結: Introduction.《Terraform入門教程》


上一篇
Day6-【入門教程】Terraform基礎概念—狀態管理
下一篇
Day8-【入門教程】Terraform代碼的書寫—配置語法及輸入變數
系列文
Terraform 繁體中文25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言