《Terraform 101 從入門到實踐》這本小冊在南瓜慢說官方網站和GitHub兩個地方同步更新,書中的示例程式碼也是放在GitHub上,方便大家參考檢視。
介紹了Terraform一些比較基礎的概念後,我們可以先了解一下Terraform的語法,也就是HCL的語法。
變數Variables
變數是實現程式碼複用的一種方式,同樣的程式碼不同的變數往往會有不同的效果。而在Terraform裡,有一個概念非常重要,就是變數都是從屬於模組的。變數無法跨模組引用。即在模組A定義的變數X,無法在模組B中直接引用。但父模組的變數,可以作為子模組的入參;而子模組的輸出變數可以被父模組獲取。
變數型別
從語言角度
跟任何程式語言一樣,變數都是有型別的,Terraform的變數型別從語言的角度可分為兩大類:基本型別和組合型別,具體如下:
基本型別:
- 字串string,如
"pkslow.com"
- 數字number,如
319
或5.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
,這個值必須為dev
、uat
和prod
中的其中一個。如果輸出一個非法的值,會報錯:
$ 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
。比如這裡定義了兩個變數env
和random_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.tfvars
、uat.tfvars
和prod.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 Variables | var.<NAME> |
本地變數Local Values | local.<NAME> |
子模組的輸出 | module.<Module Name>.<output Name> |
資料來源Data Sources | data.<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
。
注意:對於結構化的資料比較需要注意型別是否一致。比如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
後面跟的變數。