Terraform Provider 版本管理策略

Terraform Provider Version Management Strategy

在現代基礎設施即程式碼(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"
    }
  }
}

2. 依賴鎖定檔案(.terraform.lock.hcl)

.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

Terraform Cloud 私有 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

Terraform Cloud/Enterprise 整合

 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 版本管理是維護穩定、可重現基礎設施的關鍵。透過本文介紹的策略,您可以:

  1. 確保一致性:使用版本約束和鎖定檔案確保團隊使用相同版本
  2. 降低風險:透過漸進式升級和測試驗證降低版本升級風險
  3. 提高效率:利用 CI/CD 自動化版本檢查和部署流程
  4. 增強安全性:使用私有 Registry 控制 Provider 來源
  5. 促進協作:標準化版本管理流程,提升團隊協作效率

建議團隊制定明確的版本管理政策,定期審查和更新 Provider 版本,並在變更前充分測試,以確保基礎設施的穩定運作。

參考資源

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy