原簡體中文教程連結: Introduction.《Terraform入門教程》
我們將在本章講解 Terraform 配置文件的編寫。
Terraform 早期僅支持使用 HCL(Hashicorp Configuration Language) 語法的 .tf 文件,近些年來也開始支持JSON。HashiCorp 甚至修改了他們的 json 解析器,使得他們的 json 可以支持註解,但 HCL 相比起 JSON 來說有著更好的可讀性,所以我們還是會以 HCL 來講解。其實我個人是不太喜歡用 JSON 編寫 Terraform 程式碼的,有些團隊使用 JSON 是因為他們是用其他程式碼來生成相應的 JSON 格式的 Terraform 程式碼(比如自行研發的 GUI 工具,通過拖曳的方式定義基礎設施,繼而生成相關程式碼)。我個人不太喜歡這種方式,因為它鼓勵用戶從零開始拖曳出所需的所有基礎設施,而不是通過組裝成熟的可複用的模組化程式碼。我個人認為應該像對待業務邏輯程式碼一樣對待基礎設施程式碼。
這裡特別之處一點,我們將在這一章節提到模組 (Module) 的概念,但我們會在後續單獨的章節專門講解模塊。在本章內,讀者可以簡單地將一個模組理解成一個含有多個 Terraform 程式碼文件的目錄,不包含其子目錄。
本章內容基本是對官方文件的翻譯,英語閱讀能力好的讀者應該直接閱讀官方文件獲取最權威的信息。
Terraform 的某些類型之間存在隱式類型轉換規則,如果無法隱式轉換類型,那麼不同類型資料間的賦值將會報錯。
Terraform 類型分為原始類型與複雜類型兩大類。
原型分為三類:string
、number
、bool
。
string
代表一組 Unicode 字串,例如:"hello"
。
number
代表數字,可以是整數,也可以是小數。
bool
代表布林值,要麼為 true
,要麼為 false
。bool
值可以用做邏輯判斷。number
和 bool
都可以和 string
進行隱式轉換,當我們把 number
或 bool
類型的值賦給 string
類型的值,或是反過來時,Terraform 會自動替我們轉換類型,其中:
true
值會被轉換為 "true"
,反之亦然
false
值會被轉換為 "false"
,反之亦然
15
會被轉換為 "15"
,3.1415
會被轉換為 "3.1415"
,反之亦然
複雜型別是一組值所組成的符合型別,有兩類複雜型別。
一種是集合類型。一個集合包含了一組同一類型的值。集合內元素的類型成為元素類型。一個集合變數在構造時必須確定集合類型。集合內所有元素的類型必須相同。
Terraform 支援三種集合:
list(...)
:列表是一組值的連續集合,可以用下標存取內部元素,下標從 0
開始。例如名為 l
的 list
,l[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 會嘗試轉換類似的類型,轉換規則有:
object
和 map
:如果一個 map
的鍵集合含有 object
規定的所有屬性,那麼 map
可以被轉換為 object
,map
裡多餘的鍵值對會被拋棄。由 map
-> object
-> map
的轉換可能會遺失資料。tuple
和 list
:當一個 list
元素的數量剛好等於一個 tuple
宣告的長度時, list
可以轉換為 tuple
。例如:值為 ["18", "true", "john"]
的 list
轉換為 tuple([number,bool, string])
的結果為 [18, true, "john"]
set
和 tuple
:當一個 list
或 tuple
被轉換為一個 set
,那麼重複的值將被丟棄,並且值原有的順序也會遺失。如果一個 set
被轉換到 list
或是 tuple
,那麼元素將按照以下順序排列:如果 set
的元素是 string
,那麼將按照字段順序排列;其他類型的元素不承諾任何特定的排列順序。list
到 tuple
轉換舉的例子。如果類型不匹配,Terraform 會報錯,例如我們試圖把 object({name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12})
轉換到 map(string)
類型,這是不合法的,因為 name
的值為 list
,無法轉換為 string
。
any
是Terraform 中非常特殊的一種類型約束,它本身並非一個類型,而只是一個佔位符。每當一個值被賦予一個由 any
約束的複雜類型時,Terraform 會嘗試計算出一個最精確的類型來取代 any
。
例如我們把 ["a", "b", "c"]
賦給 list(any)
,它在 Terraform 中實際的物理類型首先被編譯成 tuple([string, string, string])
,然後 Terraform 認為 tuple
和 list
相似,所以會嘗試將它轉換為 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 可以將任何類型的資料賦予它。
存在一種特殊值是無類型的,那就是 null
。null
代表資料缺失。如果我們把一個參數設為 null
,Terraform 會認為你忘記為它賦值。如果該參數有預設值,那麼 Terraform 會使用預設值;如果沒有又剛好該參數是必填字短,Terraform 會報錯。null
在條件式中非常有用,你可以在某項條件不滿足時跳過對某參數的賦值。
自 Terraform 1.3 開始,我們可以在 object
類型定義中使用 optional
修飾屬性。
在 1.3 之前,如果一個 variable
的類型為 object
,那麼使用時必須傳入一個結構完全相符的物件。例如:
variable "an_object" {
type = object({
a = string
b = string
c = number
})
}
如果我們想要傳入一個物件給 var.an_object
,但不準備給 b
和 c
賦值,我們必須這樣:
{
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
修飾符有這樣兩個參數:
null
作為預設值。null
預設值的 optional
屬性在模組內使用時可以確保不會讀到 null
值。當使用者沒有設定該屬性,或是明確設定為 null
時,Terraform 會使用預設值,所以模組內無需再次判斷該屬性是否為 null
。Terraform 採用自上而下的順序來設定物件的預設值,也就是說,Terraform 會先套用 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
值:
production
和 docs
桶,Terraform 會將 enabled
設為 true
。Terraform 會同時使用預設值配置 website
,然後使用 docs
中指定的值來覆寫預設值。archived
和 docs
桶,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)
}
},
])
有時我們需要根據其他資料的值來動態決定是否要為一個 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入門教程》