《Terraform 101 從入門到實踐》這本小冊在南瓜慢說官方網站和GitHub兩個地方同步更新,書中的示例代碼也是放在GitHub上,方便大家參考查看。 軍書十二捲,捲捲有爺名。 為什麼需要狀態管理 Terraform的主要作用是管理雲平臺上的資源,通過聲明式的HCL配置來映射資源,如果雲平臺 ...
《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/language/settings/backends/pg
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
}
要註意的是,只有根模塊的輸出變數才能被共用,子模塊是不能被獲取的。