《Terraform 101 從入門到實踐》 第五章 HCL語法

南瓜慢說發表於2023-02-18
《Terraform 101 從入門到實踐》這本小冊在南瓜慢說官方網站GitHub兩個地方同步更新,書中的示例程式碼也是放在GitHub上,方便大家參考檢視。

介紹了Terraform一些比較基礎的概念後,我們可以先了解一下Terraform的語法,也就是HCL的語法。

變數Variables

變數是實現程式碼複用的一種方式,同樣的程式碼不同的變數往往會有不同的效果。而在Terraform裡,有一個概念非常重要,就是變數都是從屬於模組的。變數無法跨模組引用。即在模組A定義的變數X,無法在模組B中直接引用。但父模組的變數,可以作為子模組的入參;而子模組的輸出變數可以被父模組獲取。

變數型別

從語言角度

跟任何程式語言一樣,變數都是有型別的,Terraform的變數型別從語言的角度可分為兩大類:基本型別和組合型別,具體如下:

基本型別:

  • 字串string,如"pkslow.com"
  • 數字number,如3195.11
  • 布林值bool,如true

組合型別:

  • 列表list(<T>),如["dev", "uat", "prod"]
  • 集合set(<T>),如set(...)
  • 對映map(<T>),如{name="Larry", age="18"}
  • 物件object({name1=T1, name2=T2})
  • 元組tuple([T1,T2,T3...])

如果不想指定某個型別,可以用any來表示任意型別;或者不指定,預設為任意型別。

從功能角度

從功能角度來看,變數可以分為輸入變數、輸出變數和本地變數。

輸入變數是模組接收外部變數的方式,它定義在variable塊中,如下:

variable "image_id" {
  type = string
}

variable "availability_zone_names" {
  type    = list(string)
  default = ["us-west-1a"]
}

variable "docker_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 8300
      external = 8300
      protocol = "tcp"
    }
  ]
}

輸出變數定義了一個模組對外返回的變數,透過output塊來定義,如下:

output "instance_ip_addr" {
  value = aws_instance.server.private_ip
}

本地變數是模組內定義且可引用的臨時變數,在locals塊中定義,如下:

locals {
  service_name = "forum"
  owner        = "Community Team"
}

輸入變數Input Variable

輸入變數是定義在variable塊中的,它就像是函式的入參。

定義輸入變數

定義variable有很多可選屬性:

  • 型別type:指定變數是什麼型別;如果沒有指定,則可以是任意型別;
  • 預設值default:變數的預設值,定義後可以不用提供變數的值,注意它的值的型別要與type對應上;
  • 說明description:說明這個變數的作用和用途;
  • 校驗validation:提供校驗邏輯來判斷輸入的變數是否合法;
  • 敏感性sensitive:定義變數是否敏感,如果是則不會顯示;預設為false
  • 可空nullable:如果為true則可以為空,否則不能。預設為true

所有屬性都顯性指定如下面例子所示:

variable "env" {
  type        = string
  default     = "dev"
  description = "environment name"
  sensitive   = false
  nullable    = false
  validation {
    condition     = contains(["dev", "uat", "prod"], var.env)
    error_message = "The env must be one of dev/uat/prod."
  }
}

這個變數名為env,表示環境名,預設值為dev,這個值必須為devuatprod中的其中一個。如果輸出一個非法的值,會報錯:

$ terraform plan -var="env=sit"
╷
│ Error: Invalid value for variable
│ 
│   on input.tf line 1:
│    1: variable "env" {
│ 
│ The env must be one of dev/uat/prod.

使用輸入變數

只有定義了變數才可以使用,使用的方式是var.name。比如這裡定義了兩個變數envrandom_string_length

variable "env" {
  type        = string
  default     = "dev"
}

variable "random_string_length" {
  type    = number
  default = 10
}

則使用如下:

resource "random_string" "random" {
  length  = var.random_string_length
  lower   = true
  special = false
}

locals {
  instance_name = "${var.env}-${random_string.random.result}"
}

output "instance_name" {
  value = local.instance_name
}

傳入變數到根模組

要從外部傳入變數到根模組,有多種方式,常見的有以下幾種,按優先順序從低到高:

  • 環境變數export TF_VAR_image_id=ami-abc123
  • terraform.tfvars檔案;
  • terraform.tfvars.json檔案;
  • *.auto.tfvars*.auto.tfvars.json檔案;
  • 命令列引數-var傳入一個變數;命令列引數-var-file傳入一個變數的集合檔案;

在實踐中,最常用的還是透過命令列來傳入引數,因為一般需要指定不同環境的特定變數,所以會把變數放到檔案中,然後透過命令列指定特定環境的主檔案:

$ terraform apply -var="env=uat"
$ terraform apply -var-file="prod.tfvars"

prod.tfvars的內容如下:

env                  = "prod"
random_string_length = 12

我們可以定義dev.tfvarsuat.tfvarsprod.tfvars等,要使用不同環境的變數就直接改變檔名即可。

輸出變數Output Variable

有輸入就有輸出,輸出變數就像是模組的返回值,比如我們呼叫一個模組去建立一臺服務,那就要獲取服務的IP,這個IP事先是不知道,它是伺服器建立完後的結果之一。輸出變數有以下作用:

  • 子模組的輸出變數可以暴露一些資源的屬性;
  • 根模組的輸出變數可以在apply後輸出到控制檯;
  • 根模組的輸出變數可以透過remote state的方式共享給其它Terraform配置,作為資料來源。

定義輸出變數

輸出變數需要定義在output塊中,如下:

output "instance_ip_addr" {
  value = aws_instance.server.private_ip
}

這個value可以是reource的屬性,也可以是各種變數計算後的結果。只要在執行apply的時候才會去計算輸出變數,像plan是不會執行計算的。

還可以定義輸出變數的一些屬性:

  • description:輸出變數的描述,說明清楚這個變數是幹嘛的;
  • sensitive:如果是true,就不會在控制檯列印出來;
  • depends_on:顯性地定義依賴關係。

完整的定義如下:

output "instance_ip_addr" {
  value       = aws_instance.server.private_ip
  description = "The private IP address of the main server instance."
  sensitive   = false
  depends_on = [
    # Security group rule must be created before this IP address could
    # actually be used, otherwise the services will be unreachable.
    aws_security_group_rule.local_access,
  ]
}

引用輸出變數

引用輸出變數很容易,表示式為module.<module name>.<output name>,如果前面的輸出變數定義在模組pkslow_server中,則引用為:module.pkslow_server.instance_ip_addr

本地變數Local Variable

本地變數有點類似於其它語言程式碼中的區域性變數,在Terraform模組中,它的一個重要作用是避免重複計算一個值。

locals {
  instance_name = "${var.env}-${random_string.random.result}-${var.suffix}"
}

這裡定義了一個本地變數instance_name,它的值是一個複雜的表示式。這時我們可以透過local.xxx的形式引用,而不用再寫複雜的表示式了。如下:

output "instance_name" {
  value = local.instance_name
}

這裡要特別注意:定義本地變數的關鍵字是locals塊,裡面可以有多個變數;而引用的關鍵字是local,並沒有s

一般我們是建議需要重複引用的複雜的表示式才使用本地變數,不然太多本地變數就會影響可讀性。

對變數的引用

定義了變數就需要對其進行引用,前面的講解其實已經講過了部分變數的引用,這些把所有列出來。

型別引用方式
資源Resources<Resource Type>.<Name>
輸入變數Input Variablesvar.<NAME>
本地變數Local Valueslocal.<NAME>
子模組的輸出module.<Module Name>.<output Name>
資料來源Data Sourcesdata.<Data Type>.<Name>
路徑和Terraform相關path.module:模組所在路徑
path.root:根模組的路徑
path.cwd:一般與根模組相同,其它高階用法除外
terraform.workspace:工作區名字
塊中的本地變數count.index:count迴圈的下標;
each.key/each.value:for each迴圈的鍵值;
self:在provisioner的引用;

上面都是單值的引用,如果是List或Map這種複雜型別,就要使用中括號[]來引用。

aws_instance.example[0].id:引用其中一個元素;

aws_instance.example[*].id:引用列表的所有id值;

aws_instance.example["a"].id:引用key為a的元素;

[for value in aws_instance.example: value.id]:返回所有id為列表;

運算子

與其它語言一樣,Terraform也有運算子可以用,主要是用於數值計算和邏輯計算。以下運算子按優先順序從高到低如下:

  1. !取反,-取負
  2. *乘號,/除號,%取餘
  3. +加號,-減號
  4. >>=<<=:比較符號
  5. ==等於,!=不等於
  6. &&與門
  7. ||或門

當然,用小括號可以改變這些優秀級,如(1 + 2) * 3

注意:對於結構化的資料比較需要注意型別是否一致。比如var.list == []按理說應該返回true,而list為空時。當[]實際表示是元組tuple([]),所以它們不匹配。可以使用length(var.list) == 0的方式。

條件表示式

條件表示式的作用是在兩個值之間選一個,條件為真則選第一個,條件為假則選第二個。形式如下:

condition ? true_value : false_value

示例如下:

env = var.env !="" ? var.env : "dev"

意思是給env賦值,如果var.env不為空就把輸入變數var.env的值賦給它,如果為空則賦預設值dev

for表示式

使用for表示式可以建立一些複雜的值,而且可以使用一些轉換和計算對值計算再返回。如將字串列表轉化成大寫:

> [for s in ["larry", "Nanhua", "Deng"] : upper(s)]
[
  "LARRY",
  "NANHUA",
  "DENG",
]

可以獲取下標和值:

> [for i,v in ["larry", "Nanhua", "Deng"] : "${i}.${v}"]
[
  "0.larry",
  "1.Nanhua",
  "2.Deng",
]

對於Map的for表示式:

> [for k,v in {name: "Larry Deng", age: 18, webSite: "www.pkslow.com"} : "${k}: ${v}"]
[
  "age: 18",
  "name: Larry Deng",
  "webSite: www.pkslow.com",
]

透過條件過濾資料:

> [for i in range(1, 10) : i*3 if i%2==0]
[
  6,
  12,
  18,
  24,
]

動態塊Dynamic Block

動態塊的作用是根據變數重複某一塊配置。這在Terraform是會遇見的。

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"]
    }
  }
}

比如這裡的例子,就會重複setting塊。重複的次數取決於for_each後面跟的變數。

相關文章