原簡體中文教程連結: Introduction.《Terraform入門教程》
在 Terraform 程式碼中引用一個模組,使用的是 module
區塊。
每當在程式碼中新增、刪除或修改一個 module
區塊之後,都要執行 terraform init
或是 terraform get
指令來取得模組程式碼並安裝到本機磁碟上。
module
區塊定義了一個 source
參數,指定了模組的來源;Terraform 目前支援如下模組來源:
我們後面會一一講解這些模組源的使用。source
使用的是 URL 風格的參數,但某些來源支援在 source
參數中透過額外參數指定模組版本。
出於消除重複程式碼的目的我們可以重構我們的根模組程式碼,將一些擁有重複模式的程式碼重構為可重複呼叫的嵌入模組,透過本地路徑來引用。
許多的模組來源類型都支援從目前系統環境中讀取認證訊息,例如環境變數或系統設定檔。我們在介紹模組來源的時候會介紹到這方面的資訊。
我們建議每個模組把期待被重複使用的基礎設施聲明在各自的根模組位置上,但是直接引用其他模組的嵌入模組也是可行的。
使用本地路徑可以使我們引用同一專案內定義的子模組:
module "consul" {
source = "./consul"
}
一個本機路徑必須以 ./
或 ../
為前綴來標示要使用的本機路徑,以區別於使用 Terraform Registry 路徑。
本機路徑引用模組和其他來源類型有一個區別,本地路徑引用的模組不需要下載相關原始碼,程式碼已經存在於本地相關路徑的磁碟上了。
Registry 目前是 Terraform 官方力推的模組倉庫方案,採用了 Terraform 客製化的協議,支援版本化管理和使用模組。
官方提供的公共倉庫保存和索引了大量公共模組,在這裡可以輕鬆搜尋到各種官方和社區提供的高品質模組。
讀者也可以透過 Terraform Cloud 服務維護一個私有模組倉庫,或是透過實作 Terraform 模組註冊協定來實現一個私有倉庫。
公共倉庫的的模組可以用 <NAMESPACE>/<NAME>/<PROVIDER>
形式的來源位址來引用,在公共倉庫上的模組介紹頁面上都包含了確切的來源位址,例如:
module "consul" {
source = "hashicorp/consul/aws"
version = "0.1.0"
}
對於那些託管在其他倉庫的模組,在來源位址頭部新增 <HOSTNAME>/
部分,指定私有倉庫的主機名稱:
module "consul" {
source = "app.terraform.io/example-corp/k8s-cluster/azurerm"
version = "1.1.0"
}
如果你使用的是 SaaS 版本的 Terraform Cloud,那麼託管在上面的私有倉庫的主機名稱是 app.terraform.io
。如果使用的是私有部署的 Terraform 企業版,那麼託管在上面的私有倉庫的主機名稱就是 Terraform 企業版服務的主機名稱。
模組倉庫支援版本化。你可以在 module
區塊中指定模組的版本約束。
如果要引用私有倉庫的模組,你需要先透過設定命令列工具設定檔來設定存取憑證。
Terraform 發現 source
當參數的值如果是以 github.com
為前綴時,會將其自動辨識為一個 GitHub 來源:
module "consul" {
source = "github.com/hashicorp/example"
}
上面的例子會自動使用 HTTPS 協定來複製倉庫。如果要使用 SSH 協議,那麼請使用如下的位址:
module "consul" {
source = "git@github.com:hashicorp/example.git"
}
GitHub 來源的處理與後面要介紹的通用 Git 倉庫是一樣的,所以他們取得 git 憑證和透過 ref
參數引用特定版本的方式都是一樣的。如果要存取私有倉庫,你需要額外設定 git 憑證。
Terraform 發現 source
當參數的值如果是以 bitbucket.org
為前綴時,會將其自動辨識為一個 Bitbucket 來源:
module "consul" {
source = "bitbucket.org/hashicorp/terraform-consul-aws"
}
這種捷徑方法只針對公共倉庫有效,因為 Terraform 必須存取 ButBucket API 來了解倉庫使用的是 Git 還是 Mercurial 協定。
Terraform 根據倉庫的類型來決定將它作為一個 Git 倉庫還是 Mercurial 倉庫來處理。後面的章節會介紹如何為存取倉庫設定存取憑證以及指定要使用的版本號。
可以透過在位址開頭加上特殊的 git::
前綴來指定使用任意的 Git 倉庫。在前綴後面跟隨的是一個合法的 Git URL。
使用 HTTPS 和SSH 協定的範例:
module "vpc" {
source = "git::https://example.com/vpc.git"
}
module "storage" {
source = "git::ssh://username@example.com/storage.git"
}
Terraform 使用 git clone
指令安裝模組程式碼,所以 Terraform 會使用本機 Git 系統配置,包括存取憑證。若要存取私有 Git 倉庫,必須先配置對應的憑證。
如果使用了 SSH 協議,那麼會自動使用系統設定的 SSH 證書。通常情況下我們透過這種方法存取私有倉庫,因為這樣可以不需要互動式提示就可以存取私有倉庫。
如果使用 HTTP/HTTPS 協議,或其他需要使用者名稱、密碼作為憑證,你需要設定 Git 憑證儲存來選擇一個合適的憑證來源。
預設情況下,Terraform 會克隆預設分支。可以透過 ref
參數來指定版本:
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2.0"
}
ref 參數會被用作 git checkout 指令的參數,可以是分支名或是 tag 名。
使用 SSH 協定時,我們更推薦 ssh://
的位址。你也可以選擇 scp 風格的語法,故意忽略 ssh://
的部分,只留 git::
,例如:
module "storage" {
source = "git::username@example.com:storage.git"
}
可以透過在位址開頭加上特殊的 hg::
前綴來指定使用任意的 Mercurial 倉庫。在前綴後面跟隨的是一個合法的 Mercurial URL:
module "vpc" {
source = "hg::http://example.com/vpc.hg"
}
Terraform 會透過執行 hg clone
指令從 Mercurial 倉庫安裝模組程式碼,所以 Terraform 會使用本地 Mercurial 系統配置,包括存取憑證。若要存取私有 Mercurial 倉庫,必須先配置對應的憑證。
如果使用了 SSH 協議,那麼會自動使用系統設定的 SSH 證書。通常情況下我們透過這種方法存取私有倉庫,因為這樣可以不需要互動式提示就可以存取私有倉庫。
類似 Git 來源,我們可以透過 ref
參數指定非預設的分支或標籤來選擇特定版本:
module "vpc" {
source = "hg::http://example.com/vpc.hg?ref=v1.2.0"
}
當我們使用 HTTP 或 HTTPS 位址時,Terraform 會向指定 URL 發送 GET 請求,期待返回另一個來源位址。這種間接的方法使得 HTTP 可以成為一個更複雜的模組來源位址的指標。
然後 Terraform 會再發送 GET 請求到先前回應的位址上,並附加一個查詢參數 terraform-get=1
,這樣伺服器可以選擇當 Terraform 來查詢時可以傳回一個不一樣的位址。
如果對應的狀態碼是成功的(200 範圍的成功狀態碼),Terraform 就會透過以下位置來取得下一個存取位址:
X-Terraform-Get
值terraform-get
的 html meta 元素:<meta name="terraform-get"
content="github.com/hashicorp/example" />
不管用哪一種方式回傳的位址,Terraform 都會像本章提到的其他的來源位址一樣處理它。
如果 HTTP/HTTPS 位址需要認證憑證,可以在 HOME 資料夾下設定一個 .netrc
文件,詳見相關文件
也有一種特殊情況,如果 Terraform 發現位址有著常見的存檔檔案的後綴名,那麼 Terraform 會跳過 terraform-get=1
重定向的步驟,直接將回應內容當作模組程式碼使用。
module "vpc" {
source = "https://example.com/vpc-module.zip"
}
目前支援的後綴名有:
zip
tar.bz2
和 tbz2
tar.gz
和 tgz
tar.xz
和 txz
如果 HTTP 位址不以這些文件名結尾,但又的確指向模組存檔文件,那麼可以使用 archive
參數來強制依照這種行為處理位址:
module "vpc" {
source = "https://example.com/vpc-module?archive=zip"
}
你可以把模組存檔保存在 AWS S3 桶裡,使用 s3::
為地址前綴,後面跟著一個 S3 物件存取地址
module "consul" {
source = "s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"
}
Terraform 辨識到 s3::
前綴後會使用 AWS 風格的認證機制存取給定位址。這使得這種來源位址也可以搭配其他提供了 S3 協定相容的物件儲存服務使用,只要他們的認證方式與 AWS 相同即可。
儲存在 S3 桶內的模組存檔檔案格式必須與上面 HTTP 來源提到的支援的格式相同,Terraform 會下載並解壓縮模組程式碼。
模組安裝器會從下列位置尋找 AWS 憑證,並依照優先順序排列:
AWS_ACCESS_KEY_ID
和 AWS_SECRET_ACCESS_KEY
環境變數.aws/credentials
檔案內的預設 profile你可以把模組存檔保存在 Google 雲端 GCS 儲存桶裡,使用 gcs::
作為位址前綴,後面跟著一個 GCS 物件存取位址:
module "consul" {
source = "gcs::https://www.googleapis.com/storage/v1/modules/foomodule.zip"
}
模組安裝器會使用 Google 雲端 SDK 的憑證來存取 GCS。要設定憑證,你可以:
GOOGLE_APPLICATION_CREDENTIALS
環境變數配置服務帳號的金鑰文件gcloud auth application-default login
設定引用版本控制系統或是物件儲存服務中的模組時,模組本身可能存在於存檔檔案的子資料夾內。我們可以使用特殊的 //
語法來指定 Terraform 使用檔案內特定路徑作為模組程式碼所在位置,例如:
hashicorp/consul/aws//modules/consul-cluster
git::https://example.com/network.git//modules/vpc
https://example.com/network-module.zip//modules/vpc
s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/network.zip//modules/vpc
如果來源位址中包含又參數,例如指定特定版本號的 ref
參數,那麼把子資料夾路徑放在參數之前:
git::https://example.com/network.git//modules/vpc?ref=v1.2.0
Terraform 會解壓縮整個存檔檔案後,讀取特定子資料夾。所以,對於一個存在於子資料夾中的模組來說,透過本地路徑引用同一個存檔內的另一個模組是安全的。
我們剛剛介紹如何用 source
指定模組來源,以下我們繼續講解如何在程式碼中使用一個模組。
我們可以把模組理解成類似函數,如同函數有輸入參數表和輸出值一樣,我們之前介紹過 Terraform 程式碼有輸入變數和輸出值。我們在 module
區塊的區塊體內除了 source
參數,還可以對該模組的輸入變數賦值:
module "servers" {
source = "./app-cluster"
servers = 5
}
在這個例子裡,我們將會建立 ./app-cluster
資料夾下 Terraform 宣告的一系列資源,該模組的 servers
輸入變數的值被我們設定成了 5。
在程式碼中新增、刪除或是修改一個某塊的 source
,都需要重新執行 terraform init
指令。預設情況下,該指令不會升級已安裝的模組(例如 source
未指定版本,過去安裝了舊版本模組程式碼,那麼執行 terraform init
不會自動更新到新版本);可以執行 terraform init -upgrade
來強制更新到最新版本模組。
在模組中定義的資源和資料來源都是封裝的,所以模組呼叫者無法直接存取它們的輸出屬性。然而,模組可以聲明一系列輸出值,來選擇性地輸出特定的資料供模組呼叫者使用。
舉例來說,如果 ./app-cluster
模組定義了名為 instance_ids
的輸出值,那麼模組的呼叫者可以像這樣引用它:
resource "aws_elb" "example" {
# ...
instances = module.servers.instance_ids
}
除了 source
以外,目前 Terraform 還支援在 module
區塊上聲明其他一些可選元參數:
version
:指定引用的模組版本,在後面的部分會詳細介紹count
和 for_each
:這是 Terraform 0.13 開始支援的特性,類似 resource
與 data
,我們可以建立多個 module
實例providers
:透過傳入一個 map
我們可以指定模組中的 Provider 配置,我們將在後面詳細介紹depends_on
:建立整個模組和其他資源之間的明確依賴。直到依賴項創建完畢,否則聲明了依賴的模組內部所有的資源及內嵌的模組資源都會被延遲處理。模組的依賴行為與資源的依賴行為相同除了上述元參數以外,lifecycle
參數目前還不能用於模組,但關鍵字被保留以便將來實現。
使用 registry 作為模組來源時,可以使用 version
元參數約束使用的模組版本:
module "consul" {
source = "hashicorp/consul/aws"
version = "0.0.5"
servers = 3
}
version
元參數的格式與 Provider 版本約束的格式一致。在滿足版本限制的前提下,Terraform 會使用目前已安裝的最新版本的模組實例。如果目前沒有滿足約束的版本被安裝過,那麼會下載符合約束的最新的版本。
version
元參數只能配合 registry 使用,公共的或私有的模組倉庫都可以。其他類型的模組來源可能支援版本化,也可能不支援。本地路徑模組不支援版本化。
可以透過在 module
區塊上聲明 for_each
或 count
來創造多實例模組。在使用上 module
上的 for_each
和 count
與資源、資料來源區塊上的使用是一樣的。
# my_buckets.tf
module "bucket" {
for_each = toset(["assets", "media"])
source = "./publish_bucket"
name = "${each.key}_bucket"
}
# publish_bucket/bucket-and-cloudfront.tf
variable "name" {} # this is the input parameter of the module
resource "aws_s3_bucket" "example" {
# Because var.name includes each.key in the calling
# module block, its value will be different for
# each instance of this module.
bucket = var.name
# ...
}
resource "aws_iam_user" "deploy_user" {
# ...
}
這個範例定義了一個位於 ./publish_bucket
目錄下的本機子模組,模組建立了一個 S3 儲存桶,封裝了桶的資訊以及其他實作細節。
我們透過 for_each
參數聲明了模組的多個實例,傳入一個 map
或是 set
作為參數值。另外,因為我們使用了 for_each
,所以在 module
區塊裡可以使用 each
對象,例子裡我們使用了 each.key
。如果我們使用的是 count
參數,那麼我們可以使用 count.index
。
子模組裡所建立的資源在執行計畫或 UI 中的名稱會以 作為 module.module_name[module index]
前綴。如果一個模組沒有宣告 count
或者 for_each
,那麼資源位址將不包含 module index。
在上面的例子裡,./publish_bucket
模組包含了 aws_s3_bucket.example
資源,所以兩個 S3 桶實例的名字分別是 module.bucket["assets"].aws_s3_bucket.example
以及 module.bucket["media"].aws_s3_bucket.example
。
當程式碼中聲明了多個模組時,資源如何與 Provider 實例關聯就需要特殊考慮。
每一個資源都必須關聯一個 Provider 配置。不像 Terraform 其他的概念,Provider 配置在 Terraform 專案中是全域的,可以跨模組共用。Provider 配置聲明只能放在根模組中。
Provider 有兩種方式傳遞給子模組:隱式繼承,或是明確透過 module
區塊的 providers
參數傳遞。
一個旨在被重複使用的模組不允許宣告任何 provider
區塊,只有使用"代理 Provider"模式的情況除外,我們後面會介紹這種模式。
出於向前相容 Terraform 0.10 及更早版本的考慮,Terraform 目前在模組程式碼中只用到了 Terraform 0.10 及更早版本的功能時,不會針對模組程式碼中聲明 provider
區塊報錯,但這是一個不被推薦的遺留模式。一個含有自己的 provider
塊定義的遺留模組與 for_each
、count
和 depends_on
等 0.13 引入的新特性是不相容的。
Provider 配置被用於相關資源的所有操作,包括銷毀遠端資源物件以及更新狀態資訊等。Terraform 會在狀態檔案中儲存最近用來執行所有資源變更的 Provider 配置的參考。當一個 resource
區塊被刪除時,狀態檔案中的相關記錄會被用來定位到對應的配置,因為原來包含 provider
參數(如果宣告了的話)的 resource
區塊已經不存在了。
這導致了,你必須確保刪除所有相關的資源配置定義以後才能刪除一個 Provider 配置。如果 Terraform 發現狀態檔案中記錄的某個資源對應的 Provider 設定已經不存在了會報錯,要求你重新給予相關的 Provider 設定。
雖然 Provider 設定資訊在模組間共享,每個模組還是得聲明各自的模組需求,這樣 Terraform 才能決定一個適用於所有模組配置的 Provider 版本。
為了定義這樣的版本約束要求,可以在 terraform
區塊中使用 required_providers
區塊:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 2.7.0"
}
}
}
有關 Provider 的 source
和版本約束的資訊我們已經在前文中有所記述,在此不再贅述。
為了方便,在一些簡單的程式碼中,一個子模組會從呼叫者自動繼承預設的 Provider 配置。這表示明確 provider
區塊聲明僅位於根模組中,且下游子模組可以簡單地聲明使用該類型 Provider 的資源,這些資源會自動關聯到根模組的 Provider 配置上。
例如,根模組可能只含有一個 provider
塊和一個 module
塊:
provider "aws" {
region = "us-west-1"
}
module "child" {
source = "./child"
}
子模組可以聲明任意關聯 aws
類型 Provider 的資源而無需額外聲明 Provider 配置:
resource "aws_s3_bucket" "example" {
bucket = "provider-inherit-example"
}
當每種類型的 Provider 都只有一個實例時我們建議使用這種方式。
要注意的是,只有 Provider 配置會被子模組繼承,Provider 的 source
或是版本約束條件則不會被繼承。每一個模組都必須聲明各自的 Provider 需求條件,這在使用非 HashiCorp 的 Provider 時尤其重要。
當不同的子模組需要不同的 Provider 實例,或子模組需要的 Provider 實例與呼叫者自己使用的不同時,我們需要在 module
區塊上宣告 providers
參數來傳遞子模組要使用的 Provider 實例。例如:
# The default "aws" configuration is used for AWS resources in the root
# module where no explicit provider instance is selected.
provider "aws" {
region = "us-west-1"
}
# An alternate configuration is also defined for a different
# region, using the alias "usw2".
provider "aws" {
alias = "usw2"
region = "us-west-2"
}
# An example child module is instantiated with the alternate configuration,
# so any AWS resources it defines will use the us-west-2 region.
module "example" {
source = "./example"
providers = {
aws = aws.usw2
}
}
module
區塊裡的 providers
參數類似 resource
區塊裡的 provider
參數,差異是前者接收的是一個 map
而不是單個 string
,因為一個模組可能含有多個不同的 Provider。
providers
的 map
的鍵就是子模組中宣告的 Provider 需求中的名字,值就是目前模組中對應的 Provider 配置的名字。
如果 module
區塊內宣告了 providers
參數,那麼它將重載所有預設的繼承行為,所以你需要確保給定的 map
覆寫了子模組所需的所有 Provider。這避免了顯式賦值與隱式繼承混用時所帶來的混亂與意外。
額外的 Provider 配置(使用 alias
參數的)將永遠不會被子模組隱式繼承,所以必須明確通過 providers
傳遞。例如,一個模組配置了兩個 AWS 區域之間的網路打通,所以需要配置一個來源區域 Provider 和目標區域Provider。在這種情況下,根模組程式碼看起來是這樣的:
provider "aws" {
alias = "usw1"
region = "us-west-1"
}
provider "aws" {
alias = "usw2"
region = "us-west-2"
}
module "tunnel" {
source = "./tunnel"
providers = {
aws.src = aws.usw1
aws.dst = aws.usw2
}
}
子目錄 ./tunnel
必須包含像下面的範例那樣聲明"Provider 代理",聲明模組呼叫者必須用這些名字傳遞的Provider 配置:
provider "aws" {
alias = "src"
}
provider "aws" {
alias = "dst"
}
./tunnel
模組中的每一種資源都應該透過 provider
參數聲明它使用的是 aws.src
還是 aws.dst
。
一個 Provider 代理配置只包含 alias
參數,它就是一個模組間傳遞 Provider 配置的佔位符,聲明了模組期待明確傳遞的額外(帶有 alias
的)Provider 配置。
要注意的是,一個完全為空的 Provider 配置區塊也是合法的,但沒有必要。只有在模組內需要帶 alias
的Provider 時才需要代理程式配置區塊。如果模組中只是用預設 Provider 時請不要宣告代理程式配置區塊,也不要僅為了宣告 Provider 版本約束而使用代理程式配置區塊。
原簡體中文教程連結: Introduction.《Terraform入門教程》