原簡體中文教程連結: Introduction.《Terraform入門教程》
表達式用來在設定檔中進行一些計算。最簡單的表達式就是字面量,例如 "hello",或 5。Terraform 也支援一些更複雜的表達式,例如引用其他 resource 的輸出值、數學計算、布林條件計算,以及一些內建的函數。
Terraform 配置中許多地方都可以使用表達式,但某些特定的場景下限制了可以使用的表達式的類型,例如只準使用特定資料類型的字面量,或是禁止使用 resource 的輸出值。
我們在類型章節中已經基本介紹了類型以及類型相關的字面量,下面我們來介紹一些其他的表達式。
list 和 tuple 可以透過下標存取成員,例如 local.list[3]
、var.tuple[2]
。map 和 object 可以透過屬性存取成員,例如 local.object.attrname
、local.map.keyname
。由於 map 的 key 是使用者定義的,可能無法成為合法的 Terraform 標識符,所以訪問 map 成員時我們建議使用方括號:local.map["keyname"]
。
Terraform 中定義了多種命名值,表達式中的每一個命名值都關聯到一個具體的值,我們可以用單一命名值作為一個表達式,或是組合多個命名值來計算出一個新值。
命名值有以下種類:
<RESOURCE TYPE>.<NAME>
:表示一個資源物件。凡是不符合後面列出的命名值模式的表達式都會被 Terraform 解釋為一個託管資源。如果資源聲明了 count
元參數,那麼該表達式表示的是一個物件實例的 list。如果資源聲明了 for_each
元參數,那麼該表達式表示的是一個物件實例的 map。var.<NAME>
:表示一個輸入變數local.<NAME>
:表示一個局部值module.<MODULE_NAME>.<OUTPUT_NAME>
:表示一個模組的一個輸出值data.<DATA_TYPE>.<NAME>
:表示一個資料來源實例。如果資料來源宣告了 count 元參數,那麼該表達式表示的是一個資料來源實例 list。如果資料來源宣告了 for_each 元參數,那麼該表達式表示的是一個資料來源實例 map。path.module
:表示目前模組在檔案系統中的路徑path.root
:表示根模組(呼叫 Terraform 命令列執行的程式碼檔案所在的模組)在檔案系統中的路徑path.cwd
:表示目前工作目錄的路徑。一般來說該路徑等同於 path.root,但在呼叫 Terraform 命令列時如果指定了程式碼路徑,那麼二者將會不同。terraform.workspace
:目前使用的 Workspace(我們在狀態管理的"狀態的隔離儲存"中介紹過)雖然這些命名表達式可以使用 .<NAME>
號碼來存取物件的各種屬性,但實際上他們實際類型並不是我們在類型章節裡提到的 object。兩者的差別在於,object 同時支援 .<NAME>
使用或 ["<NAME>"]
兩種方式存取物件成員屬性,而上述命名表達式僅支援 .<NAME>
。
在某些特定表達式或上下文當中,有一些特殊的命名值可以被使用,他們是局部命名值。幾種比較常見的局部命名值有:
count.index
:表達目前 count 下標序號each.key
:表達目前 for_each 迭代器實例self
:在預置器中指稱聲明預置器的資源建構資源或是模組時常會使用含有命名值的表達式賦值,Terraform 會分析這些表達式並自動計算出物件之間的依賴關係。
最常見的引用類型就是引用一個 resource 或 data 塊定義的物件的輸出屬性。由於這些資源與資料來源物件結構可能非常複雜,因此對它們的輸出屬性的引用表達式也可能非常複雜。
比如下面這個例子:
resource "aws_instance" "example" {
ami = "ami-abc123"
instance_type = "t2.micro"
ebs_block_device {
device_name = "sda2"
volume_size = 16
}
ebs_block_device {
device_name = "sda3"
volume_size = 20
}
}
aws_instance
文件列出了該類型所支援的所有輸入參數和內嵌區塊,以及對外輸出的屬性清單。所有這些不同的資源類型 Schema 都可以在引用中使用,如下所示:
aws_instance.example.ami
表達式來引用aws_instance.example.id
的表達式來引用ebs_block_device
參數可以透過後面會介紹的展開表達式(splat expression)來訪問,例如我們獲取所有的 ebs_block_device
列表 device_name:aws_instance.example.ebs_block_device[*].device_name
aws_instance
類型裡的內嵌區塊並沒有任何輸出屬性,但如果 ebs_block_device
新增了一個名為 "id" 的輸出屬性,那麼可以用 aws_instance.example.ebs_block_device[*].id
表達式來存取含有所有 id 的列表aws_instance
類型有一個假想的內嵌區塊類型 device
並規定 device 可以賦予這樣的一個邏輯鍵,那麼程式碼看起來就會是這樣的:device "foo" {
size = 2
}
device "bar" {
size = 4
}
我們可以使用鍵來存取特定區塊的數據,例如:aws_instance.example.device["foo"].size
要取得一個 device 名稱到 device 大小的映射,可以使用 for 表達式:
{for k, device in aws_instance.example.device : k => device.size}
當一個資源宣告了 count 參數,那麼資源本身就變成了一個資源物件清單而非單一資源。這種情況下要存取資源輸出屬性,要麼使用展開表達式,要麼使用下標索引:
aws_instance.example[*].id
:傳回所有 instance 的 id 列表aws_instance.example[0].id
:返回第一個 instance 的 id當一個資源宣告了 for_each
參數,那麼資源本身就變成了一個資源物件字典而非單一資源。這種情況下要存取資源的輸出屬性,要麼使用特定鍵,要麼使用 for
表達式:
aws_instance.example["a"].id
:傳回 "a" 對應的實例的 id[for value in aws_instance.example: value.id]
:傳回所有 instance 的 id注意不像使用 count,使用 for_each 的資源集合不能直接使用展開表達式,展開表達式只能適用於列表。你可以把字典轉換成列表後再使用展開表達式:
values(aws_instance.example)[*].id
當 Terraform 在計算變更計畫時,有些資源輸出屬性無法立即求值,因為他們的值取決於遠端 API 的回傳值。比如說,有一個遠端物件可以在創建時回傳一個生成的唯一 id,Terraform 無法在創建它之前就預知這個值。
為了允許在計算變更階段就能計算含有這種值的表達式,Terraform 使用了一個特殊的"尚不知曉(unknown value)"佔位符來代替這些結果。大部分時候你不需要特意理會它們,因為 Terraform 語言會自動處理這些尚不知曉的值,比如說使兩個尚不知曉的值相加得到的會是一個尚不知曉的值。
然而,有些情況下表達式中含有尚不知曉的值會有明顯的影響:
count
元參數不可以為尚不知曉,因為變更計畫必須明確知道到底要維護多少個目標實例尚不知曉值在執行 terraform plan
時會被輸出為"(not yet known)"。
一個操作符是一種用以轉換或合併一個或多個表達式的表達式。操作子要嘛是把兩個值計算為第三個值,也就是二元運算子;要嘛是把一個值轉換成另一個值,也就是一元運算子。
二元運算子位於兩個表達式的中間,類似 1+2。一元操作符位於一個表達式的前面,類似 !true
。
Terraform 語言支援一組算數和邏輯操作符,它們的功能類似 JavaScript 或 Ruby 裡的操作符功能。
當一個表達式中含有多個操作符時,它們的優先順序時:
!
,-
(負號)\*
,/
,%
+
,-
(減號)\>
,>=
,<
,<=
==
,!=
&&
||
可以使用小括號覆蓋預設優先權。如果沒有小括號,高優先權運算子會被先計算,例如 1+2*3 會被解釋成 1+(2*3) 而不是 (1+2)*3。
不同的運算子可以按它們之間相似的行為被歸納為幾組,每一組操作符都期待被給予特定類型的值。Terraform會在類型不符時嘗試進行隱式類型轉換,如果失敗則會拋錯。
a + b
:返回 a
與 b
的和a - b
:返回 a
與 b
的差a * b
:返回 a
與 b
的積a / b
:返回 a
與 b
的商a % b
:返回 a
與 b
的模。此運算符一般僅在 a
與 b
是整數時有效-a
:返回 a
與 -1
的商a == b
:如果 a
與 b
類型與值都相等返回 true
,否則返回 false
a != b
:與 ==
相反a
< b
:如果 a
比 b
小則為 true
,否則為 false
a
> b
:如果 a
比 b
大則為 true
,否則為 false
a
<= b
:若 a
比 b
小或相等則為 true
,否則為 false
a
= b
:若 a
比 b
大或相等則為 true
,否則為 false
a || b
:a
或 b
中有至少一個為 true
則為 true
,否則為 false
a && b
:a
與比都為 true
則為 true
,否則為false
!a
:如果 a
為 true
則為 false
,如果 a
為 false
則為 true
條件式是判斷布林表達式的結果以便於在後續兩個值當中選擇一個:
condition ? true_val : false_val
如果 condition 表達式為 true,那麼結果是 true_value,反之則為 false_value。
一個常見的條件式用法是使用預設值來取代非法值:
var.a != "" ? var.a : "default-a"
如果輸入變數 a
的值是空字串,那麼結果會是 default-a
,否則傳回輸入變數 a
的值。
條件式的判斷條件可以使用上述的任意運算子。供選擇的兩個值也可以是任意類型,但它們的類型必須相同,這樣 Terraform 才能判斷條件表達式的輸出類型。
Terraform 支援在計算表達式時使用一些內建函數,函數呼叫表達式類似操作符,通用語法是:
<FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)
函數名標明了要呼叫的函數。每一個函數都定義了數量不等、類型不一的入參以及不同類型的回傳值。
有些函數定義了不定長的入參表,例如,min
函數可以接收任意多個數值類型入參,傳回其中最小的數值:
min(55, 3453, 2)
如果想要把列表或元組的元素當作參數傳遞給函數,那麼我們可以使用展開符:
min([55, 2453, 2]...)
展開符號使用的是三個獨立的 .
號組成的 ...
,不是 Unicode 的省略號 …
。展開符是一種只能用在函數呼叫場景下的特殊語法。
有關完整的內建函數我們可能會在今後撰寫對應的章節介紹。
for 表達式是將一種複雜類型對應成另一種複雜類型的表達式。輸入類型值中的每一個元素都會被映射為一個或零個結果。
舉例來說,如果 var.list
是字串列表,那麼下面的表達式將會把列表元素全部轉為大寫:
[for s in var.list : upper(s)]
這裡 for
表達式迭代了 var.list
中每一個元素(就是 s
),然後計算了 upper(s)
,最後建構了一個包含了所有 upper(s)
結果的新元組,元組內元素順序與來源列表相同。
for
表達式周圍的括號類型決定了輸出值的類型。上面的例子我們使用了方括號,所以輸出型別是元組。如果使用的是花括號,那麼輸出類型是對象,for
表達式內部冒號後面應該使用以 =>
符號分隔的表達式:
{for s in var.list : s => upper(s)}
這個表達式傳回一個對象,而對象的成員屬性名稱就是來源列表中的元素,值就是對應的大寫值。
一個 for
表達式還可以包含一個可選的 if
子句用以過濾結果,這可能會減少傳回的元素數量:
[for s in var.list : upper(s) if s != ""]
被 for
迭代的也可以是物件或字典,這樣的話迭代器就會被表示為兩個臨時變數:
[for k, v in var.map : length(k) + length(v)]
最後,如果傳回類型是物件(使用花括號)那麼表達式中可以使用 ...
符號實作 group by:
{for s in var.list : substr(s, 0, 1) => s... if s != ""}
展開表達式提供了一種類似 for
表達式的簡潔表達方式。比如說 var.list
包含一組對象,每個物件都有一個屬性 id,那麼讀取所有 id 的 for 表達式會是這樣:
[for o in var.list : o.id]
與之等價的展開表達式是這樣的:
var.list[*].id
這個特殊的 [*]
符號迭代了列表中每一個元素,然後傳回了它們在 .
號碼右邊的屬性值。
展開表達式只能被用於列表(所以使用 for_each
參數的資源不能使用展開表達式,因為它的型別是字典)。然而,如果一個展開表達式被用於一個既不是列表又不是元組的值,那麼這個值會被自動包裝成一個單元素的列表然後被處理。
比如說, var.single_object[*].id
等價於 [var.single_object][*].id
。大部分場景下這種行為沒有什麼意義,但在存取不確定是否會定義 count
參數的資源時,這種行為很有幫助,例如:
aws_instance.example[*].id
上面的表達式不論 aws_instance.example
定義了 count
與否都會傳回實例的 id 列表,這樣如果我們以後為 aws_instance.example
添加了 count
參數我們也不需要修改這個表達式。
曾經存在另一種舊的展開表達式語法,它是一種比較弱化的展開表達式,現在應該盡量避免使用。
這種舊的展開表達式使用 .*
而不是 [*]
:
var.list.*.interfaces[0].name
要特別注意該表達式與現有的展開表達式結果不同,它的行為等價於:
[for o in var.list : o.interfaces][0].name
而現有 [*]
展開表達式的行為等價於:
[for o in var.list : o.interfaces[0].name]
注意兩者右邊括號的位置。
在頂級區塊,例如 resource 區塊當中,一般只能以類似 name = expression
的形式進行一對一的賦值。大部分情況下這已經夠用了,但某些資源類型包含了可重複的內嵌區塊,無法使用表達式循環賦值:
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name" # can use expressions here
setting {
# but the "setting" block is always a literal block
}
}
你可以用 dynamic
區塊來動態建立重複的 setting
這樣的內嵌區塊:
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name"
application = "${aws_elastic_beanstalk_application.tftest.name}"
solution_stack_name = "64bit Amazon Linux 2018.03 v2.11.4 running Go 1.12.6"
dynamic "setting" {
for_each = var.settings
content {
namespace = setting.value["namespace"]
name = setting.value["name"]
value = setting.value["value"]
}
}
}
dynamic
可以在 resource
、data
、provider
和 provisioner
區塊內使用。一個 dynamic
區塊類似於 for
表達式,只不過它產生的是內嵌塊。它可以迭代一個複雜類型資料然後為每一個元素產生對應的內嵌區塊。在上面的例子裡:
dynamic
的標籤(也就是 "setting")確定了我們要產生的內嵌塊種類for_each
參數提供了需要迭代的複雜型別值dynamic
區塊的標籤(也就是 setting
)labels
參數(可選)是一個表示區塊標籤的有序列表,用以順序產生一組內嵌區塊。有 labels
參數的表達式裡可以使用暫時的 iterator
變數content
區塊定義了要產生的內嵌區塊的區塊體。你可以在 content
區塊內部使用臨時的 iterator
變數for_each
參數可以是集合或結構化類型,所以你可以使用 for
表達式或是展開表達式來轉換一個現有集合的類型。iterator 變數(上面的範例裡就是 setting
)有兩個屬性:
key
:迭代容器如果是 map,那就是當前元素的鍵;迭代容器如果是 list,那麼就是當前元素在 list 中的下標序號;如果是 for_each 表達式產出的 set,那麼 key 和 value 是一樣的,這時我們不應該使用 key。value
:當前元素的值dynamic
區塊只能產生屬於目前區塊定義過的內嵌區塊參數。無法產生諸如 lifecycle
、provisioner
這樣的元參數,因為 Terraform 必須在確保這些元參數求值的計算是成功的。for_each
的值必須是不為空的 map 或 set。如果你需要根據內嵌資料結構或多個資料結構的元素組合來宣告資源實例集合,你可以使用 Terraform 表達式和函數來產生適當的值。
過度使用 dynamic
區塊會導致程式碼難以閱讀以及維護,所以我們建議只在需要建構可重複使用的模組程式碼時才使用 dynamic
區塊。盡可能手寫內嵌塊。
Terraform 有兩種不同的字串字面量。最通用的就是用一對雙引號包裹的字符,例如 "hello"
。在雙引號之間,反斜線 \
被用來進行轉義。Terraform 支援的轉義符有:
Sequence | Replacement |
---|---|
\n | 換行 |
\r | 回車 |
\t | 製表符 |
\" | 雙引號(不會截斷字串) |
\|反斜線 | |
\uNNNN | 普通字元對映平面的Unicode字元(NNNN代表四位元16進位數) |
\UNNNNNNNN | 補充字元映射平面的Unicode字元(NNNNNNNN代表八位元16進位數) |
另一種字串表達式稱為 "heredoc" 風格,是受 Unix Shell 語言啟發。它可以使用自訂的分隔符號更清晰地表達多行字串: |
<<EOT
hello
world
EOT
<< 標記後面直到行尾組成的識別碼開啟了字串,然後 Terraform 會把剩下的行都加進字串,直到遇到與識別符完全相等的字串為止。在上面的例子裡,EOT 就是識別符。任何字元都可以用作標識符,但傳統上標識符一般以 EO 起頭。上面例子裡的 EOT 代表"文本的結尾(end of text)"。
上面例子裡的 heredoc 風格字串要求內容必須對齊行頭,這在區塊內宣告時看起來會比較奇怪:
block {
value = <<EOT
hello
world
EOT
}
為了改善可讀性,Terraform 也支援縮排的 heredoc,只要把 << 改成 <<-:
block {
value = <<-EOT
hello
world
EOT
}
上面的例子裡,Terraform 會以最靠近行頭的行作為基準來調整行頭縮進,得到的字串是這樣的:
hello
world
heredoc 中的反斜杠不會被解釋成轉義,而只會是簡單的反斜杠。
雙引號和 heredoc 兩種字串都支援字串模版,模版的形式是 ${...}
以及 %{...}
。如果想要表達 ${
或 %{
的字面量,那麼可以重複第一個字元:$${
和 %%{
。
字串模版允許我們在字串中嵌入表達式,或是透過其他值動態建構字串。
一個 ${...}
序列稱為插值,插值計算花括號之間的表達式的值,有必要的話將之轉換為字串,然後插入字串模版,形成最終的字串:
"Hello, ${var.name}!"
上面的例子裡,輸入變數 var.name
的值被存取後插入了字串模版,產生了最終的結果,例如:"Hello, Juan!"
一個 %{...}
序列被稱為命令,命令可以是一個布林表達式或是對集合的迭代,類似條件表達式以及 for
表達式。有兩種指令:
if \<BOOL\>
/ else
/ endif
指令根據布林表達式的結果在兩個模版中選擇一個:"Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"
else
部分可以省略,這樣如果布林表達結果為 false 那麼就會插入空字串。
for \<NAME\> in \<COLLECTION\>
/ endfor
指令迭代一個結構化物件或集合,用每一個元素渲染模版,然後把它們拼接起來:<<EOT
%{ for ip in aws_instance.example.*.private_ip }
server ${ip}
%{ endfor }
EOT
for
關鍵字後緊跟的名字被用作代表迭代器元素的臨時變量,可以用來在內嵌模版中使用。
為了在不添加額外空格和換行的前提下提升可讀性,所有的模版序列都可以在首尾添加 ~
符號。如果有 ~
符號,那麼模版序列會去除字串左右的空白(空格以及換行)。如果 ~
出現在頭部,那麼會去除字串左側的空白;如果出現在尾部,那麼就會去除字串右邊的空白:
<<EOT
%{ for ip in aws_instance.example.*.private_ip ~}
server ${ip}
%{ endfor ~}
EOT
在上面的例子裡,命令符後面的換行符號被忽略了,但是 server ${ip}
後面的換行符號被保留了,這確保了每一個元素產生一行輸出:
server 10.1.16.154
server 10.1.16.1
server 10.1.16.34
當使用模版指令時,我們推薦使用 heredoc 風格字串,用多行模版提升可讀性。雙引號字串內最好只使用插值。
Terraform 曾經只支援在表達式中使用插值,例如
resource "aws_instance" "example" {
ami = var.image_id
# ...
}
這種語法是在 Terraform 0.12 後才支援的。在 Terrafor 0.11 及更早的版本中,這段程式碼只能被寫成這樣:
resource "aws_instance" "example" {
ami = "${var.image_id}"
# ...
}
Terraform 0.12 保持了向前相容,所以現在這樣的程式碼也仍然是合法的。讀者也許會在一些 Terraform 程式碼和文件中繼續看到這樣的寫法,但請盡量避免繼續這樣書寫純插值字串,而是直接使用表達式。
原簡體中文教程連結: Introduction.《Terraform入門教程》