《Terraform 101 從入門到實踐》這本小冊在南瓜慢說官方網站和GitHub兩個地方同步更新,書中的示例程式碼也是放在GitHub上,方便大家參考檢視。
軍書十二卷,卷卷有爺名。
為什麼需要狀態管理
Terraform的主要作用是管理雲平臺上的資源,透過宣告式的HCL配置來對映資源,如果雲平臺上沒有資源則需要建立,如果有則不用。那Terraform要實現這個功能有多種方式。
一種是每次執行apply命令時都呼叫API介面檢查一下遠端的雲資源是否與配置檔案一致,如果沒有則建立,如果有但不同則需要修改,如果有且相同則不用變更。這種機制能保證雲平臺的資源與HCL配置是一致的。缺點也是非常明顯的,每次都需要呼叫API去檢查遠端資源,效率很低,特別是當資源特別多的場景。
另一種方式是每次變更資源的時候,都會建立一個對映檔案,它儲存雲平臺資源的狀態。這樣每次執行apply
命令時,只需要檢查HCL配置與對映檔案的差異即可。
Terraform選擇的是第二種方式,透過對映檔案來儲存資源狀態,在Terraform的世界裡叫狀態檔案。Terraform這樣做是基於以下考慮:
- 雲平臺真實狀態的對映,解析狀態檔案即可以知道真實情況。
- 後設資料儲存,如資源之間的依賴關係,需要透過依賴關係來知道建立或銷燬順序。
- 提升效能,特別是在大規模雲平臺上,多次呼叫API去查詢資源狀態是很費時的。
- 同步狀態,透過遠端狀態檔案來同步狀態,這也是Terraform最佳的實踐。
講到這裡,已經回答了之前在第一章留下的思考題:
如果再次執行apply會不會再次建立一個檔案呢?還是建立失敗,因為檔案已存在?為什麼?
答案:不會建立,因為透過狀態檔案記錄了變更,Terraform判斷不再需要建立了。
狀態管理的示例
為了更多注意力放在狀態管理上,我們還是使用最簡單的例子local_file
,具體程式碼如下:
resource "local_file" "terraform-introduction" {
content = "https://www.pkslow.com"
filename = "${path.root}/terraform-guides-by-pkslow.txt"
}
我們以實際操作及現象來講解狀態檔案的作用和工作原理:
操作 | 現象及說明 |
---|---|
terraform apply | 生成資源:第一次生成 |
terraform apply | 沒有變化:狀態檔案生成,不需要再建立 |
terraform destroy | 刪除資源:根據狀態檔案的內容刪除 |
terraform apply | 生成資源:狀態顯示沒有資源,再次生成 |
刪除狀態檔案 | 沒有變化 |
terraform apply | 生成資源:沒有狀態檔案,直接生成資源和狀態檔案(外掛做了容錯處理,已存在也會新生成覆蓋) |
刪除狀態檔案 | 沒有變化 |
terraform destroy | 無法刪除資源,沒有資源存在的狀態 |
我們一直在講狀態檔案,我們先來看一下它的真面目。首先它的預設檔名是terraform.tfstate
,預設會放在當前目錄下。它是以json
格式儲存的資訊,示例中的內容如下:
{
"version": 4,
"terraform_version": "1.0.11",
"serial": 1,
"lineage": "acb408bb-2a95-65fd-02e6-c23487f7a3f6",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "local_file",
"name": "test-file",
"provider": "provider[\"registry.terraform.io/hashicorp/local\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"content": "https://www.pkslow.com",
"content_base64": null,
"directory_permission": "0777",
"file_permission": "0777",
"filename": "./terraform-guides-by-pkslow.txt",
"id": "6db7ad1bbf57df0c859cd5fc62ff5408515b5fc1",
"sensitive_content": null,
"source": null
},
"sensitive_attributes": [],
"private": "bnVsbA=="
}
]
}
]
}
可以看到它記錄了Terraform的版本資訊,還有資源的詳細資訊:包括型別、名字、外掛、屬性等。有這些資訊便可直接從狀態檔案裡解析出具體的資源。
狀態管理命令
可以透過terraform state
做一些狀態管理:
顯示狀態列表:
$ terraform state list
local_file.test-file
檢視具體資源的狀態資訊:
$ terraform state show local_file.test-file
# local_file.test-file:
resource "local_file" "test-file" {
content = "https://www.pkslow.com"
directory_permission = "0777"
file_permission = "0777"
filename = "./terraform-guides-by-pkslow.txt"
id = "6db7ad1bbf57df0c859cd5fc62ff5408515b5fc1"
}
顯示當前狀態資訊:
$ terraform state pull
重新命名:
$ terraform state mv local_file.test-file local_file.pkslow-file
Move "local_file.test-file" to "local_file.pkslow-file"
Successfully moved 1 object(s).
$ terraform state list
local_file.pkslow-file
要注意這裡只是修改狀態檔案的名字,程式碼裡的HCL並不會修改。
刪除狀態裡的資源:
$ terraform state rm local_file.pkslow-file
Removed local_file.pkslow-file
Successfully removed 1 resource instance(s).
遠端狀態
狀態檔案預設是在本地目錄上的terraform.tfstate
檔案,在團隊使用中,每個人的電腦環境獨立的,那麼需要保證每個人當前的狀態檔案都是最新且與現實資源真實對應,簡直是天方夜譚。而狀態不一致所帶的災難也是極其可怕的。所以,狀態檔案最好是要儲存在一個獨立的大家可共同訪問的位置。對於狀態的管理的配置,Terraform稱之為Backends
。
Backend
是兩種模式,分別是local
和remote
。local
模式很好理解,就是使用本地路徑來儲存狀態檔案。配置示例如下:
terraform {
backend "local" {
path = "pkslow.tfstate"
}
}
透過這樣配置後,不再使用預設的terraform.tfstate
檔案,而是使用自定義的檔名pkslow.tfstate
。
對於remote
模式,則有多種配置方式,Terraform支援的有:
- s3
- gcs
- oss
- etcd
- pg
- http
- kubernetes
等,能滿足主流雲平臺的需求。每一個配置可以參考官網,在本地我採用資料庫postgresql的方式,讓大家都能快速實驗。
我透過Docker的方式啟動PostgreSQL,命令如下:
$ docker run -itd \
--name terraform-postgres \
-e POSTGRES_DB=terraform \
-e POSTGRES_USER=pkslow \
-e POSTGRES_PASSWORD=pkslow \
-p 5432:5432 \
postgres:13
在terraform
塊中配置backend
,這裡指定資料庫連線資訊即可,更多引數請參考:https://www.terraform.io/lang...
terraform {
backend "pg" {
conn_str = "postgres://pkslow:pkslow@localhost:5432/terraform?sslmode=disable"
}
}
當然,把敏感資訊直接放在程式碼中並不合適,可以直接在命令列中傳入引數:
terraform init -backend-config="conn_str=postgres://pkslow:pkslow@localhost:5432/terraform?sslmode=disable"
執行init和apply之後,連線資料庫檢視,會建立一個叫terraform_remote_state
的Schema,在該Schema下有一張states表來儲存對應的狀態資訊,如下:
表中欄位name是namespace,而data是具體的狀態資訊,如下:
{
"version": 4,
"terraform_version": "1.0.11",
"serial": 0,
"lineage": "de390d13-d0e0-44dc-8738-d95b6d8f1868",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "local_file",
"name": "test-file",
"provider": "provider[\"registry.terraform.io/hashicorp/local\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"content": "https://www.pkslow.com",
"content_base64": null,
"directory_permission": "0777",
"file_permission": "0777",
"filename": "./terraform-guides-by-pkslow.txt",
"id": "6db7ad1bbf57df0c859cd5fc62ff5408515b5fc1",
"sensitive_content": null,
"source": null
},
"sensitive_attributes": [],
"private": "bnVsbA=="
}
]
}
]
}
Workspace 工作區
如果我們用Terraform程式碼生成了dev環境,但現在需要uat環境,該如何處理呢?
首先,不同環境的變數一般是不一樣的,我們需要定義各種的變數檔案如dev.tfvars
、uat.tfvars
和prod.tfvars
等。但只有各自變數是不夠的,因為還有狀態。狀態也必須要隔離,而Workspace
就是Terraform用來隔離狀態的方式。預設的工作區為default
,如果沒有指定,則表示工作於default
工作區中。而當指定了工作區,狀態檔案就會與工作區繫結。
建立一個工作區並切換:
$ terraform workspace new pkslow
切換到已存在的工作區:
$ terraform workspace select pkslow
而當我們處於某個工作區時,是可以獲取工作區的名字的,引用為:${terraform.workspace}
,示例如下:
resource "aws_instance" "example" {
count = "${terraform.workspace == "default" ? 5 : 1}"
# ... other arguments
}
之前講過預設的狀態檔名為terraform.tfstate
;而在多工作區的情況下(只要你建立了一個非預設工作區),狀態檔案就會存在terraform.tfstate.d
目錄下。而在遠端狀態的情況下,也會有一個對映,Key為工作區名,Value一般是狀態內容。
敏感資料
本地狀態檔案都是明文儲存狀態資訊的,所以要保護好自己的狀態檔案。對於遠端狀態檔案,有些儲存方案是支援加密的,會對敏感資料(sensitive
)進行加密。
狀態鎖
本地狀態檔案下不需要狀態鎖,因為只有一個人在變更。而遠端狀態的情況下,就可能出現競爭了。比如一個人在apply,而另一個人在destroy,那就亂了。而狀態鎖可以確保遠端狀態檔案只能被一個人使用。但不是所有遠端狀態的方式都支援鎖的,一般常用的都會支援,如GCS、S3等。
所以,每當我們在執行變更時,Terraform總會先嚐試去拿鎖,如果拿鎖失敗,就該命令失敗。可以強制解鎖,但要非常小心,一般只建議在自己明確知道安全的時候才使用,比如死鎖了。
共享狀態-資料來源
既然遠端狀態檔案是可以共享的,那狀態資訊也是可以共享的。這樣會帶來的一個好處是,即使兩個根模組,也是可以共享資訊的。比如我們在根模組A建立了一個資料庫,而根模組B需要用到資料庫的資訊如IP,這樣透過遠端狀態檔案就可以共享給根模組B了。
注意這裡我強調的是根模組,因為如果A和B在同一個根模組下,那就不需要透過遠端狀態的方式來共享狀態了。
遠端狀態的示例:
data "terraform_remote_state" "vpc" {
backend = "remote"
config = {
organization = "hashicorp"
workspaces = {
name = "vpc-prod"
}
}
}
resource "aws_instance" "foo" {
# ...
subnet_id = data.terraform_remote_state.vpc.outputs.subnet_id
}
本地狀態的示例:
data "terraform_remote_state" "vpc" {
backend = "local"
config = {
path = "..."
}
}
resource "aws_instance" "foo" {
# ...
subnet_id = data.terraform_remote_state.vpc.outputs.subnet_id
}
要注意的是,只有根模組的輸出變數才能被共享,子模組是不能被獲取的。