Terraform Provider 開發與擴展

Terraform Provider Development and Extension

Provider 開發概述

Terraform Provider 是 Terraform 與各種雲端服務、SaaS 平台或 API 之間的橋樑。透過開發自定義 Provider,我們可以讓 Terraform 管理任何具有 API 的服務。本文將帶你從零開始,了解如何開發一個完整的 Terraform Provider。

Provider 的核心職責包括:

  • 處理與目標 API 的認證
  • 定義可管理的 Resource 和 Data Source
  • 實作 CRUD(Create、Read、Update、Delete)操作
  • 處理狀態管理與差異計算

開發環境準備

Go 語言環境

Terraform Provider 使用 Go 語言開發,首先需要安裝 Go 環境:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 下載並安裝 Go(建議使用 1.21 以上版本)
wget https://go.dev/dl/go1.21.6.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.6.linux-amd64.tar.gz

# 設定環境變數
export PATH=$PATH:/usr/local/go/bin
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin

# 驗證安裝
go version

Terraform 安裝

1
2
3
4
5
6
7
# 使用 tfenv 管理 Terraform 版本
git clone https://github.com/tfutils/tfenv.git ~/.tfenv
export PATH="$HOME/.tfenv/bin:$PATH"

# 安裝最新版本
tfenv install latest
tfenv use latest

Provider 架構說明

一個標準的 Terraform Provider 專案結構如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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

Terraform Plugin SDK

Terraform 提供了 Plugin Framework 來簡化 Provider 開發。以下是初始化專案的步驟:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 建立專案目錄
mkdir terraform-provider-example
cd terraform-provider-example

# 初始化 Go module
go mod init github.com/yourname/terraform-provider-example

# 安裝 Plugin Framework
go get github.com/hashicorp/terraform-plugin-framework
go get github.com/hashicorp/terraform-plugin-go

建立基本 Provider 結構

main.go

 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
package main

import (
    "context"
    "flag"
    "log"

    "github.com/hashicorp/terraform-plugin-framework/providerserver"
    "github.com/yourname/terraform-provider-example/internal/provider"
)

var version string = "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/yourname/example",
        Debug:   debug,
    }

    err := providerserver.Serve(context.Background(), provider.New(version), opts)
    if err != nil {
        log.Fatal(err.Error())
    }
}

internal/provider/provider.go

 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
package provider

import (
    "context"

    "github.com/hashicorp/terraform-plugin-framework/datasource"
    "github.com/hashicorp/terraform-plugin-framework/provider"
    "github.com/hashicorp/terraform-plugin-framework/provider/schema"
    "github.com/hashicorp/terraform-plugin-framework/resource"
    "github.com/hashicorp/terraform-plugin-framework/types"
)

type ExampleProvider struct {
    version string
}

type ExampleProviderModel struct {
    Endpoint types.String `tfsdk:"endpoint"`
    ApiKey   types.String `tfsdk:"api_key"`
}

func New(version string) func() provider.Provider {
    return func() provider.Provider {
        return &ExampleProvider{
            version: version,
        }
    }
}

func (p *ExampleProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
    resp.TypeName = "example"
    resp.Version = p.version
}

func (p *ExampleProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "endpoint": schema.StringAttribute{
                Optional:    true,
                Description: "API endpoint URL",
            },
            "api_key": schema.StringAttribute{
                Optional:    true,
                Sensitive:   true,
                Description: "API key for authentication",
            },
        },
    }
}

func (p *ExampleProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config ExampleProviderModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    if resp.Diagnostics.HasError() {
        return
    }
    // 初始化 API client 並傳遞給 resources 和 data sources
}

func (p *ExampleProvider) Resources(ctx context.Context) []func() resource.Resource {
    return []func() resource.Resource{
        NewExampleResource,
    }
}

func (p *ExampleProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
    return []func() datasource.DataSource{
        NewExampleDataSource,
    }
}

實作 Resource

Resource 是 Terraform 中可以被建立、讀取、更新和刪除的物件。以下是一個完整的 Resource 實作範例:

 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
package provider

import (
    "context"

    "github.com/hashicorp/terraform-plugin-framework/resource"
    "github.com/hashicorp/terraform-plugin-framework/resource/schema"
    "github.com/hashicorp/terraform-plugin-framework/types"
)

type ExampleResource struct{}

type ExampleResourceModel struct {
    ID   types.String `tfsdk:"id"`
    Name types.String `tfsdk:"name"`
    Tags types.Map    `tfsdk:"tags"`
}

func NewExampleResource() resource.Resource {
    return &ExampleResource{}
}

func (r *ExampleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
    resp.TypeName = req.ProviderTypeName + "_resource"
}

func (r *ExampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Description: "Manages an example resource.",
        Attributes: map[string]schema.Attribute{
            "id": schema.StringAttribute{
                Computed:    true,
                Description: "Resource identifier",
            },
            "name": schema.StringAttribute{
                Required:    true,
                Description: "Resource name",
            },
            "tags": schema.MapAttribute{
                Optional:    true,
                ElementType: types.StringType,
                Description: "Resource tags",
            },
        },
    }
}

func (r *ExampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    var plan ExampleResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
    if resp.Diagnostics.HasError() {
        return
    }
    // 呼叫 API 建立資源
    plan.ID = types.StringValue("generated-id")
    resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}

func (r *ExampleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var state ExampleResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    if resp.Diagnostics.HasError() {
        return
    }
    // 呼叫 API 讀取資源狀態
    resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}

func (r *ExampleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    var plan ExampleResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
    if resp.Diagnostics.HasError() {
        return
    }
    // 呼叫 API 更新資源
    resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}

func (r *ExampleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
    var state ExampleResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    if resp.Diagnostics.HasError() {
        return
    }
    // 呼叫 API 刪除資源
}

實作 Data Source

Data Source 用於從外部服務讀取資料,供 Terraform 配置中使用:

 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
package provider

import (
    "context"

    "github.com/hashicorp/terraform-plugin-framework/datasource"
    "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
    "github.com/hashicorp/terraform-plugin-framework/types"
)

type ExampleDataSource struct{}

type ExampleDataSourceModel struct {
    ID     types.String `tfsdk:"id"`
    Name   types.String `tfsdk:"name"`
    Status types.String `tfsdk:"status"`
}

func NewExampleDataSource() datasource.DataSource {
    return &ExampleDataSource{}
}

func (d *ExampleDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
    resp.TypeName = req.ProviderTypeName + "_data"
}

func (d *ExampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
    resp.Schema = schema.Schema{
        Description: "Fetches example data.",
        Attributes: map[string]schema.Attribute{
            "id": schema.StringAttribute{
                Required:    true,
                Description: "Data source identifier",
            },
            "name": schema.StringAttribute{
                Computed:    true,
                Description: "Data source name",
            },
            "status": schema.StringAttribute{
                Computed:    true,
                Description: "Current status",
            },
        },
    }
}

func (d *ExampleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    var config ExampleDataSourceModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    if resp.Diagnostics.HasError() {
        return
    }
    // 呼叫 API 讀取資料
    config.Name = types.StringValue("fetched-name")
    config.Status = types.StringValue("active")
    resp.Diagnostics.Append(resp.State.Set(ctx, config)...)
}

本地測試與除錯

編譯與安裝本地 Provider

1
2
3
4
5
6
7
8
# 編譯 Provider
go build -o terraform-provider-example

# 建立本地開發目錄
mkdir -p ~/.terraform.d/plugins/local/yourname/example/1.0.0/linux_amd64

# 複製編譯後的 binary
cp terraform-provider-example ~/.terraform.d/plugins/local/yourname/example/1.0.0/linux_amd64/

測試配置

建立 examples/main.tf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
terraform {
  required_providers {
    example = {
      source  = "local/yourname/example"
      version = "1.0.0"
    }
  }
}

provider "example" {
  endpoint = "https://api.example.com"
  api_key  = var.api_key
}

resource "example_resource" "test" {
  name = "my-resource"
  tags = {
    environment = "development"
  }
}

data "example_data" "info" {
  id = example_resource.test.id
}

執行測試

1
2
3
4
cd examples
terraform init
terraform plan
terraform apply

發布到 Terraform Registry

  1. 準備 GitHub Repository:確保程式碼託管在 GitHub 上,且 repository 名稱符合 terraform-provider-<NAME> 格式。

  2. 設定 GPG 簽名:Terraform Registry 要求所有發布的版本都必須經過 GPG 簽名。

  3. 使用 GoReleaser:建立 .goreleaser.yml 設定檔來自動化發布流程。

  4. 註冊 Provider:前往 Terraform Registry 並連結你的 GitHub repository。

  5. 建立 Release:透過 GitHub Actions 自動觸發發布流程。

參考資料

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