在現代基礎設施即程式碼(Infrastructure as Code, IaC)的實踐中,Terraform 已成為業界標準工具。然而,隨著專案規模擴大和團隊協作需求增加,Provider 版本管理成為確保基礎設施穩定性和可重現性的關鍵因素。本文將深入探討 Terraform Provider 版本管理的各種策略與最佳實務。
1. Provider 版本約束語法
Terraform 提供靈活的版本約束語法,讓開發者能精確控制 Provider 版本。以下是常見的版本約束運算子:
基本版本約束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.31.0" # 精確版本
}
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.0.0" # 最低版本
}
google = {
source = "hashicorp/google"
version = "~> 5.0" # 允許 5.x.x,但不允許 6.0.0
}
}
}
|
版本約束運算子說明
| 運算子 | 說明 | 範例 |
|---|
= 或無運算子 | 精確版本匹配 | = 5.31.0 |
!= | 排除特定版本 | != 5.30.0 |
>, >=, <, <= | 版本比較 | >= 5.0.0, < 6.0.0 |
~> | 允許最右邊版本號增加 | ~> 5.31.0 允許 5.31.x |
複合版本約束
1
2
3
4
5
6
7
8
| terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0.0, < 6.0.0, != 5.25.0"
}
}
}
|
.terraform.lock.hcl 是 Terraform 0.14 版本引入的依賴鎖定機制,用於確保跨環境的一致性。
鎖定檔案結構
1
2
3
4
5
6
7
8
9
10
11
12
| # This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "5.31.0"
constraints = "~> 5.31.0"
hashes = [
"h1:ltYPMnpPL/JXGQA04q4UrSQ8v9C9QFGPM2y+VoW4n9Q=",
"zh:0cdc3841f4b6e62c62a2b56d7f1d15ddf4e75a0f3b68a9b1a41ebeb85a6e1a71",
"zh:1f2a7dbecd06f5fcce1c2cd2b16c6c4f78ffe8e6f1c9e0c9f4e8f7a9c3b5e7d1",
]
}
|
鎖定檔案管理命令
1
2
3
4
5
6
7
8
9
10
11
12
| # 初始化並建立/更新鎖定檔案
terraform init
# 僅更新鎖定檔案,不下載 Provider
terraform init -upgrade
# 針對多平台環境更新鎖定檔案
terraform providers lock \
-platform=linux_amd64 \
-platform=darwin_amd64 \
-platform=darwin_arm64 \
-platform=windows_amd64
|
版本控制最佳實務
1
2
3
| # 將鎖定檔案納入版本控制
git add .terraform.lock.hcl
git commit -m "chore: update terraform provider lock file"
|
重要提醒:
- 務必將
.terraform.lock.hcl 納入版本控制 - 不要將
.terraform 目錄納入版本控制 - 團隊成員應使用相同的鎖定檔案以確保一致性
3. 版本升級策略
漸進式升級流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # 步驟 1:檢視目前使用的 Provider 版本
terraform providers
# 步驟 2:檢查可用的更新版本
terraform init -upgrade
# 步驟 3:執行計畫以檢視變更影響
terraform plan
# 步驟 4:在測試環境驗證
terraform apply -target=module.test_resources
# 步驟 5:完整部署
terraform apply
|
版本升級檢查清單
1
2
3
4
5
6
7
8
9
10
11
12
| # 建議的版本升級配置
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
# 使用悲觀版本約束,限制主版本升級
version = "~> 5.0"
}
}
}
|
自動化版本檢查腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
| #!/bin/bash
# check-provider-updates.sh
echo "檢查 Terraform Provider 更新..."
# 取得目前版本資訊
terraform version -json | jq '.provider_selections'
# 檢查是否有可用更新
terraform init -upgrade -backend=false 2>&1 | grep -i "upgrading"
# 產生版本報告
terraform providers lock -platform=linux_amd64 2>&1
|
4. 多 Provider 版本管理
多雲環境配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.31"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.85"
}
google = {
source = "hashicorp/google"
version = "~> 5.10"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.24"
}
}
}
|
Provider 別名配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # 同一 Provider 的多區域配置
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
provider "aws" {
alias = "ap_northeast_1"
region = "ap-northeast-1"
}
# 使用別名部署資源
resource "aws_s3_bucket" "us_bucket" {
provider = aws.us_east_1
bucket = "my-us-bucket"
}
resource "aws_s3_bucket" "ap_bucket" {
provider = aws.ap_northeast_1
bucket = "my-ap-bucket"
}
|
模組中的 Provider 版本要求
1
2
3
4
5
6
7
8
9
10
11
12
| # modules/networking/versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0.0"
}
}
}
# 模組不應硬編碼 Provider 配置
# 而是從根模組繼承
|
5. 私有 Provider Registry
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| terraform {
required_providers {
internal = {
source = "app.terraform.io/my-organization/internal"
version = "~> 1.0"
}
}
cloud {
organization = "my-organization"
workspaces {
name = "production"
}
}
}
|
自建 Provider Registry
使用 Artifactory 或 Nexus 作為私有 Registry:
1
2
3
4
5
6
7
8
| terraform {
required_providers {
custom = {
source = "registry.internal.company.com/myorg/custom"
version = "~> 2.0"
}
}
}
|
Provider 映射配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # ~/.terraformrc 或 terraform.rc
provider_installation {
network_mirror {
url = "https://registry.internal.company.com/v1/providers/"
include = ["registry.terraform.io/hashicorp/*"]
}
filesystem_mirror {
path = "/usr/share/terraform/providers"
include = ["registry.terraform.io/hashicorp/*"]
}
direct {
exclude = ["registry.terraform.io/hashicorp/*"]
}
}
|
6. Provider 開發與發佈
Provider 專案結構
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| terraform-provider-example/
├── main.go
├── go.mod
├── go.sum
├── internal/
│ └── provider/
│ ├── provider.go
│ ├── resource_example.go
│ └── data_source_example.go
├── examples/
│ └── main.tf
├── docs/
│ ├── index.md
│ └── resources/
│ └── example.md
└── .goreleaser.yml
|
Provider 主程式範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // main.go
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/myorg/terraform-provider-example/internal/provider"
)
var version = "dev"
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "registry.terraform.io/myorg/example",
Debug: debug,
}
err := providerserver.Serve(context.Background(), provider.New(version), opts)
if err != nil {
log.Fatal(err.Error())
}
}
|
GoReleaser 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| # .goreleaser.yml
version: 2
builds:
- env:
- CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}'
flags:
- -trimpath
ldflags:
- '-s -w -X main.version={{.Version}}'
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
binary: '{{ .ProjectName }}_v{{ .Version }}'
archives:
- format: zip
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
checksum:
name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
algorithm: sha256
signs:
- artifacts: checksum
args:
- "--batch"
- "--local-user"
- "{{ .Env.GPG_FINGERPRINT }}"
- "--output"
- "${signature}"
- "--detach-sign"
- "${artifact}"
release:
draft: true
|
發佈流程
1
2
3
4
5
6
7
8
9
10
11
| # 建立版本標籤
git tag v1.0.0
git push origin v1.0.0
# 使用 GoReleaser 發佈
goreleaser release --clean
# 發佈至 Terraform Registry
# 1. 確保 GitHub Repository 已連結至 Terraform Registry
# 2. 建立符合語意化版本的 Git 標籤
# 3. Terraform Registry 會自動偵測並發佈
|
7. 測試與相容性驗證
驗收測試框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // internal/provider/resource_example_test.go
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestAccResourceExample_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccResourceExampleConfig("test-value"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(
"example_resource.test", "name", "test-value"),
),
},
},
})
}
func testAccResourceExampleConfig(name string) string {
return fmt.Sprintf(`
resource "example_resource" "test" {
name = %[1]q
}
`, name)
}
|
版本相容性測試矩陣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| # .github/workflows/test.yml
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
terraform-version:
- '1.5.*'
- '1.6.*'
- '1.7.*'
go-version:
- '1.21'
- '1.22'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ matrix.terraform-version }}
terraform_wrapper: false
- name: Run Tests
run: go test -v ./...
env:
TF_ACC: "1"
|
整合測試範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # test/integration/main.tf
terraform {
required_providers {
example = {
source = "myorg/example"
version = ">= 1.0.0"
}
}
}
provider "example" {
api_endpoint = var.api_endpoint
api_key = var.api_key
}
resource "example_resource" "test" {
name = "integration-test-${random_id.suffix.hex}"
description = "Integration test resource"
}
output "resource_id" {
value = example_resource.test.id
}
|
8. CI/CD 整合最佳實務
GitHub Actions 工作流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
| # .github/workflows/terraform.yml
name: Terraform CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TF_VERSION: "1.7.0"
TF_WORKING_DIR: "./infrastructure"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
working-directory: ${{ env.TF_WORKING_DIR }}
- name: Terraform Init
run: terraform init -backend=false
working-directory: ${{ env.TF_WORKING_DIR }}
- name: Terraform Validate
run: terraform validate
working-directory: ${{ env.TF_WORKING_DIR }}
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
working_directory: ${{ env.TF_WORKING_DIR }}
- name: Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: ${{ env.TF_WORKING_DIR }}
framework: terraform
plan:
runs-on: ubuntu-latest
needs: [validate, security-scan]
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Terraform Init
run: terraform init
working-directory: ${{ env.TF_WORKING_DIR }}
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
working-directory: ${{ env.TF_WORKING_DIR }}
continue-on-error: true
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
const output = `#### Terraform Plan 📖
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\`
*Pushed by: @${{ github.actor }}*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
apply:
runs-on: ubuntu-latest
needs: [validate, security-scan]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Terraform Init
run: terraform init
working-directory: ${{ env.TF_WORKING_DIR }}
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: ${{ env.TF_WORKING_DIR }}
|
Provider 版本更新自動化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| # .github/workflows/provider-updates.yml
name: Check Provider Updates
on:
schedule:
- cron: '0 9 * * 1' # 每週一早上 9 點執行
workflow_dispatch:
jobs:
check-updates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Check for Provider Updates
id: check
run: |
terraform init -upgrade -backend=false 2>&1 | tee update-log.txt
if grep -q "Upgrading" update-log.txt; then
echo "updates_available=true" >> $GITHUB_OUTPUT
else
echo "updates_available=false" >> $GITHUB_OUTPUT
fi
- name: Create Pull Request
if: steps.check.outputs.updates_available == 'true'
uses: peter-evans/create-pull-request@v6
with:
commit-message: "chore: update terraform provider versions"
title: "[Automated] Terraform Provider Updates"
body: |
This PR contains automated provider version updates.
Please review the changes and run `terraform plan` before merging.
branch: automated/provider-updates
labels: dependencies, terraform
|
1
2
3
4
5
6
7
8
9
10
| # backend.tf
terraform {
cloud {
organization = "my-organization"
workspaces {
tags = ["app:my-application", "env:production"]
}
}
}
|
版本鎖定驗證腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| #!/bin/bash
# scripts/validate-lock-file.sh
set -e
echo "驗證 Terraform 鎖定檔案..."
# 檢查鎖定檔案是否存在
if [ ! -f ".terraform.lock.hcl" ]; then
echo "錯誤:找不到 .terraform.lock.hcl 檔案"
exit 1
fi
# 檢查鎖定檔案是否包含所有必要平台的雜湊值
PLATFORMS=("linux_amd64" "darwin_amd64" "darwin_arm64")
for platform in "${PLATFORMS[@]}"; do
if ! grep -q "$platform" .terraform.lock.hcl; then
echo "警告:鎖定檔案可能缺少 $platform 平台的雜湊值"
fi
done
# 驗證鎖定檔案與配置一致性
terraform init -backend=false
terraform providers lock -platform=linux_amd64 -platform=darwin_amd64 -platform=darwin_arm64
# 檢查是否有變更
if git diff --exit-code .terraform.lock.hcl > /dev/null; then
echo "鎖定檔案驗證通過"
else
echo "錯誤:鎖定檔案需要更新"
git diff .terraform.lock.hcl
exit 1
fi
|
總結
有效的 Terraform Provider 版本管理是維護穩定、可重現基礎設施的關鍵。透過本文介紹的策略,您可以:
- 確保一致性:使用版本約束和鎖定檔案確保團隊使用相同版本
- 降低風險:透過漸進式升級和測試驗證降低版本升級風險
- 提高效率:利用 CI/CD 自動化版本檢查和部署流程
- 增強安全性:使用私有 Registry 控制 Provider 來源
- 促進協作:標準化版本管理流程,提升團隊協作效率
建議團隊制定明確的版本管理政策,定期審查和更新 Provider 版本,並在變更前充分測試,以確保基礎設施的穩定運作。
參考資源