實現多人協作的 Terraform 組態管理:以 GCP Cloud Storage 與 AWS S3 為例
Terraform 是當前主流的基礎設施即代碼(IaC)工具,能夠快速構建基礎設施。然而,在多人協作的環境中,必須確保不同人員之間不會發生 race condition 的情況,以避免潛在的衝突和問題。
為了解決這個問題,Terraform 提供了遠端狀態(remote state)和鎖定(lock)機制。遠端狀態(remote state)指的是將 apply 後的結果存儲在遠端儲存庫中,使得多人可以訪問同一份狀態,從而確保一致性。而鎖定(lock)機制則是避免多人同時修改狀態文件而引發的問題。
Terraform Lock 機制
產生 Lock ID :
當使用者開始對 Terraform 的狀態文件(.state)進行修改時,Terraform 會產生 Lock 相關資訊如下:
json{ "ID":"be65a702-cd4b-543c-3e5c-9af9bc7b3d18", "Operation":"OperationTypeApply", "Info":"", "Who":"andy51002000@cs-682367130783-default", "Version":"1.7.5","Created":"2024-04-14T07:19:41.825202066Z", "Path":"terraform.tfstate" }
建立 .tflock 文件:
同時,Terraform 會在與狀態文件(.state)相同的目錄下創建一個名為 .tflock 的文件。這個文件用於表示當前狀態文件(.state)的鎖定狀態。
鎖定持續時間:
當使用者開始修改狀態文件(.state)時,該 .tflock 文件會一直存在,直到該使用者完成操作。這樣可以確保其他使用者在該時間段內無法修改同一個狀態文件(.state),從而避免衝突。
鎖定釋放:
當使用者完成對狀態文件(.state)的修改操作後,Terraform 會自動釋放 .tflock。這時候,.tflock 文件將被刪除。
使用 GCP Cloud Storage
在 GCP Cloud Storage 中實現多人協作模式非常簡單。只需在 Terraform 的後端(backend)中指定 Cloud Storage bucket 即可。當執行 apply 操作時,Terraform 會在該 bucket 中建立 lock 檔案(.tflock),等待流程完成後再刪除。
terraform { backend "gcs" { bucket = "your-bucket-name" prefix = "terraform/state" } }
以下是 Remote backend 使用 GCS 實現鎖定(lock)機制的做法
go// Lock writes to a lock file, ensuring file creation. Returns the generation // number, which must be passed to Unlock(). func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { // update the path we're using // we can't set the ID until the info is written info.Path = c.lockFileURL() infoJson, err := json.Marshal(info) if err != nil { return "", err } lockFile := c.lockFile() w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(c.storageContext) err = func() error { if _, err := w.Write(infoJson); err != nil { return err } return w.Close() }() if err != nil { return "", c.lockError(fmt.Errorf("writing %q failed: %v", c.lockFileURL(), err)) } info.ID = strconv.FormatInt(w.Attrs().Generation, 10) return info.ID, nil }
通過條件限制 storage.Conditions{DoesNotExist: true},只有當 lock 文件(.tflock)不存在時才允許寫入。這樣確保了在 GCS 中只會有一個 lock 文件(.tflock)存在。
在寫入過程中,如果出現任何錯誤,例如寫入失敗或其他錯誤,則無法完成鎖定(lock)操作。此時,程式會返回一個錯誤,表示無法進行鎖定(lock),並且不會繼續處理狀態文件(.state)的更新。這樣可以確保在出現任何問題時,不會對狀態文件(.state)進行不正確的修改。
使用 AWS S3
相比之下,在 AWS S3 中實現多人協作稍微複雜一些。除了指定 S3 的 bucket 外,還需要透過 DynamoDB 來實現鎖定(lock)機制。
terraform { backend "s3" { bucket = "your-bucket-name" key = "terraform/state" region = "your-region" dynamodb_table = "terraform_locks" } }
以下是 Remote backend 使用 AWS S3 以 Dynamo 實現鎖定(lock)機制的做法
gofunc (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { ctx := context.TODO() log := c.logger(operationLockerLock) if c.ddbTable == "" { return "", nil } info.Path = c.lockPath() if info.ID == "" { lockID, err := uuid.GenerateUUID() if err != nil { return "", err } info.ID = lockID } log = logWithLockInfo(log, info) ctx, baselog := baselogging.NewHcLogger(ctx, log) ctx = baselogging.RegisterLogger(ctx, baselog) log.Info("Locking remote state") putParams := &dynamodb.PutItemInput{ Item: map[string]dynamodbtypes.AttributeValue{ "LockID": &dynamodbtypes.AttributeValueMemberS{ Value: c.lockPath(), }, "Info": &dynamodbtypes.AttributeValueMemberS{ Value: string(info.Marshal()), }, }, TableName: aws.String(c.ddbTable), ConditionExpression: aws.String("attribute_not_exists(LockID)"), } _, err := c.dynClient.PutItem(ctx, putParams) if err != nil { lockInfo, infoErr := c.getLockInfo(ctx) if infoErr != nil { err = errors.Join(err, infoErr) } lockErr := &statemgr.LockError{ Err: err, Info: lockInfo, } return "", lockErr } return info.ID, nil }
只有當 lock 資訊不存在時才允許寫入。
留言
張貼留言