iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0

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


1.4.8.1. 表達式

表達式用來在設定檔中進行一些計算。最簡單的表達式就是字面量,例如 "hello",或 5。Terraform 也支援一些更複雜的表達式,例如引用其他 resource 的輸出值、數學計算、布林條件計算,以及一些內建的函數。

Terraform 配置中許多地方都可以使用表達式,但某些特定的場景下限制了可以使用的表達式的類型,例如只準使用特定資料類型的字面量,或是禁止使用 resource 的輸出值。

我們在類型章節中已經基本介紹了類型以及類型相關的字面量,下面我們來介紹一些其他的表達式。

1.4.8.1.1. 下標與屬性

list 和 tuple 可以透過下標存取成員,例如 local.list[3]var.tuple[2]。map 和 object 可以透過屬性存取成員,例如 local.object.attrnamelocal.map.keyname。由於 map 的 key 是使用者定義的,可能無法成為合法的 Terraform 標識符,所以訪問 map 成員時我們建議使用方括號:local.map["keyname"]

1.4.8.1.2. 引用命名值

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>

1.4.8.1.3. 局部命名值

在某些特定表達式或上下文當中,有一些特殊的命名值可以被使用,他們是局部命名值。幾種比較常見的局部命名值有:

  • count.index:表達目前 count 下標序號
  • each.key:表達目前 for_each 迭代器實例
  • self:在預置器中指稱聲明預置器的資源

1.4.8.1.4. 命名值的依賴關係

建構資源或是模組時常會使用含有命名值的表達式賦值,Terraform 會分析這些表達式並自動計算出物件之間的依賴關係。

1.4.8.1.5. 引用資源輸出屬性

最常見的引用類型就是引用一個 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 都可以在引用中使用,如下所示:

  • ami 參數可以在其他地方用 aws_instance.example.ami 表達式來引用
  • id 屬性可以用 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

1.4.8.1.6. 尚不知曉的數值

當 Terraform 在計算變更計畫時,有些資源輸出屬性無法立即求值,因為他們的值取決於遠端 API 的回傳值。比如說,有一個遠端物件可以在創建時回傳一個生成的唯一 id,Terraform 無法在創建它之前就預知這個值。

為了允許在計算變更階段就能計算含有這種值的表達式,Terraform 使用了一個特殊的"尚不知曉(unknown value)"佔位符來代替這些結果。大部分時候你不需要特意理會它們,因為 Terraform 語言會自動處理這些尚不知曉的值,比如說使兩個尚不知曉的值相加得到的會是一個尚不知曉的值。

然而,有些情況下表達式中含有尚不知曉的值會有明顯的影響:

  • count 元參數不可以為尚不知曉,因為變更計畫必須明確知道到底要維護多少個目標實例
  • 如果尚未知道的值被用於資料來源,那麼資料來源在計算變更計劃階段就無法讀取,它會被推遲到執行階段讀取。在這種情況下,在計劃階段該資料來源的一切輸出均為尚不知曉
  • 如果聲明 module 區塊時傳遞給模組輸入變數的表達式使用了尚不知曉值,那麼在模組程式碼中任何使用了該輸入變數值的表達式的值都將是尚不知曉
  • 如果模組輸出值表達式中含有尚不知曉值,任何使用該模組輸出值的表達式都將尚不知曉
  • Terraform 會嘗試驗證尚未知道值的資料類型是否合法,但仍有可能無法正確檢查資料類型,導致執行階段發生錯誤

尚不知曉值在執行 terraform plan 時會被輸出為"(not yet known)"。

1.4.8.1.7. 算數與邏輯運算符

一個操作符是一種用以轉換或合併一個或多個表達式的表達式。操作子要嘛是把兩個值計算為第三個值,也就是二元運算子;要嘛是把一個值轉換成另一個值,也就是一元運算子。

二元運算子位於兩個表達式的中間,類似 1+2。一元操作符位於一個表達式的前面,類似 !true

Terraform 語言支援一組算數和邏輯操作符,它們的功能類似 JavaScript 或 Ruby 裡的操作符功能。

當一個表達式中含有多個操作符時,它們的優先順序時:

  1. !- (負號)
  2. \*/%
  3. +- (減號)
  4. \>>=<<=
  5. ==!=
  6. &&
  7. ||

可以使用小括號覆蓋預設優先權。如果沒有小括號,高優先權運算子會被先計算,例如 1+2*3 會被解釋成 1+(2*3) 而不是 (1+2)*3。

不同的運算子可以按它們之間相似的行為被歸納為幾組,每一組操作符都期待被給予特定類型的值。Terraform會在類型不符時嘗試進行隱式類型轉換,如果失敗則會拋錯。

1.4.8.1.7.1. 算數運算符

  • a + b:返回 ab 的和
  • a - b:返回 ab 的差
  • a * b:返回 ab 的積
  • a / b:返回 ab 的商
  • a % b:返回 ab 的模。此運算符一般僅在 ab 是整數時有效
  • -a:返回 a-1 的商

1.4.8.1.7.2. 相等性操作符

  • a == b:如果 ab 類型與值都相等返回 true,否則返回 false
  • a != b:與 == 相反

1.4.8.1.7.3. 比較操作符

  • a < b:如果 ab 小則為 true,否則為 false
  • a > b:如果 ab 大則為 true,否則為 false
  • a <= b:若 ab 小或相等則為 true,否則為 false
  • a = b:若 ab 大或相等則為 true,否則為 false

1.4.8.1.7.4. 邏輯運算符

  • a || bab 中有至少一個為 true 則為 true,否則為 false
  • a && ba 與比都為 true 則為 true ,否則為false
  • !a:如果 atrue 則為 false,如果 afalse 則為 true

1.4.8.1.8. 條件式

條件式是判斷布林表達式的結果以便於在後續兩個值當中選擇一個:

condition ? true_val : false_val

如果 condition 表達式為 true,那麼結果是 true_value,反之則為 false_value。

一個常見的條件式用法是使用預設值來取代非法值:

var.a != "" ? var.a : "default-a"

如果輸入變數 a 的值是空字串,那麼結果會是 default-a,否則傳回輸入變數 a 的值。

條件式的判斷條件可以使用上述的任意運算子。供選擇的兩個值也可以是任意類型,但它們的類型必須相同,這樣 Terraform 才能判斷條件表達式的輸出類型。

1.4.8.1.9. 函數調用

Terraform 支援在計算表達式時使用一些內建函數,函數呼叫表達式類似操作符,通用語法是:

<FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)

函數名標明了要呼叫的函數。每一個函數都定義了數量不等、類型不一的入參以及不同類型的回傳值。

有些函數定義了不定長的入參表,例如,min 函數可以接收任意多個數值類型入參,傳回其中最小的數值:

min(55, 3453, 2)

1.4.8.1.9.1. 展開函數入參

如果想要把列表或元組的元素當作參數傳遞給函數,那麼我們可以使用展開符:

min([55, 2453, 2]...)

展開符號使用的是三個獨立的 . 號組成的 ... ,不是 Unicode 的省略號 。展開符是一種只能用在函數呼叫場景下的特殊語法。

有關完整的內建函數我們可能會在今後撰寫對應的章節介紹。

1.4.8.1.10. for表達式

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 != ""}

1.4.8.1.11. 展開表達式(Splat Expression)

展開表達式提供了一種類似 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 參數我們也不需要修改這個表達式。

1.4.8.1.11.1. 遺留的舊有展開表達式

曾經存在另一種舊的展開表達式語法,它是一種比較弱化的展開表達式,現在應該盡量避免使用。

這種舊的展開表達式使用 .* 而不是 [*]

var.list.*.interfaces[0].name

要特別注意該表達式與現有的展開表達式結果不同,它的行為等價於:

[for o in var.list : o.interfaces][0].name

而現有 [*] 展開表達式的行為等價於:

[for o in var.list : o.interfaces[0].name]

注意兩者右邊括號的位置。

1.4.8.1.12. dynamic塊

在頂級區塊,例如 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 可以在 resourcedataproviderprovisioner 區塊內使用。一個 dynamic 區塊類似於 for 表達式,只不過它產生的是內嵌塊。它可以迭代一個複雜類型資料然後為每一個元素產生對應的內嵌區塊。在上面的例子裡:

  • dynamic 的標籤(也就是 "setting")確定了我們要產生的內嵌塊種類
  • for_each 參數提供了需要迭代的複雜型別值
  • iterator 參數(可選)設定了用以表示目前迭代元素的臨時變數名稱。如果沒有設定 iterator,那麼臨時變數名稱預設就是 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 區塊只能產生屬於目前區塊定義過的內嵌區塊參數。無法產生諸如 lifecycleprovisioner 這樣的元參數,因為 Terraform 必須在確保這些元參數求值的計算是成功的。

for_each 的值必須是不為空的 map 或 set。如果你需要根據內嵌資料結構或多個資料結構的元素組合來宣告資源實例集合,你可以使用 Terraform 表達式和函數來產生適當的值。

1.4.8.1.12.1. dynamic區塊的最佳實踐

過度使用 dynamic 區塊會導致程式碼難以閱讀以及維護,所以我們建議只在需要建構可重複使用的模組程式碼時才使用 dynamic 區塊。盡可能手寫內嵌塊。

1.4.8.1.13. 字串字面量

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 兩種字串都支援字串模版,模版的形式是 ${...} 以及 %{...}。如果想要表達 ${%{ 的字面量,那麼可以重複第一個字元:$${%%{

1.4.8.1.14. 字串模版

字串模版允許我們在字串中嵌入表達式,或是透過其他值動態建構字串。

1.4.8.1.14.1. 插值(Interpolation)

一個 ${...} 序列稱為插值,插值計算花括號之間的表達式的值,有必要的話將之轉換為字串,然後插入字串模版,形成最終的字串:

"Hello, ${var.name}!"

上面的例子裡,輸入變數 var.name 的值被存取後插入了字串模版,產生了最終的結果,例如:"Hello, Juan!"

1.4.8.1.14.2. 命令(Directive)

一個 %{...} 序列被稱為命令,命令可以是一個布林表達式或是對集合的迭代,類似條件表達式以及 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 風格字串,用多行模版提升可讀性。雙引號字串內最好只使用插值。

1.4.8.1.15. Terraform內插

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入門教程》


上一篇
Day11-【入門教程】資料來源
下一篇
Day13-【入門教程】重載文件, 程式碼風格規範, Checks
系列文
Terraform 繁體中文25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言