Contents

dev-tf

Contents

Terraform Provider 开发规范

1. 目的

本文定义基于 Terraform Plugin Framework(以下简称 TPF) 开发 Terraform Provider 的统一规范。

目标:

  • 保证 Provider 行为符合 Terraform 生命周期语义。
  • 保证 Resource / Data Source 代码结构统一。
  • 降低状态漂移、永久 Diff、状态不一致等问题。
  • 提高 Provider 的可维护性、可测试性和扩展性。
  • 统一 API Client、Schema、Diagnostics、日志、测试及发布方式。

本文适用于使用 Go 开发的 Terraform Provider。


2. 技术栈

推荐技术栈:

Go
Terraform Plugin Framework
Terraform Plugin Testing
Terraform Plugin Log
Terraform Plugin Framework Validators
Terraform Plugin Framework Plan Modifiers

核心依赖:

github.com/hashicorp/terraform-plugin-framework
github.com/hashicorp/terraform-plugin-framework-validators
github.com/hashicorp/terraform-plugin-log
github.com/hashicorp/terraform-plugin-testing

禁止新项目使用:

terraform-plugin-sdk/v2
schema.Resource
schema.ResourceData
d.Set(...)
d.Get(...)

除非:

  1. 正在迁移历史 SDKv2 Provider;
  2. 必须通过 terraform-plugin-mux 保持兼容;
  3. 存在 Plugin Framework 暂时无法满足的历史兼容需求。

3. Terraform 版本

新 Provider SHOULD 使用:

providerserver.Serve(
    ctx,
    provider.New,
    providerserver.ServeOpts{
        Address: providerAddress,
    },
)

推荐使用 Plugin Protocol v6。

Provider Terraform 版本约束建议:

terraform {
  required_version = ">= 1.3.0"

  required_providers {
    example = {
      source  = "company/example"
      version = "~> 1.0"
    }
  }
}

除非业务确实需要,否则不要为了兼容非常老的 Terraform 版本而降低 Provider 架构质量。


4. 项目目录规范

推荐目录:

terraform-provider-example/
├── .github/
│   └── workflows/
├── docs/
├── examples/
│   ├── data-sources/
│   ├── provider/
│   └── resources/
├── internal/
│   ├── client/
│   │   ├── client.go
│   │   ├── request.go
│   │   ├── response.go
│   │   └── errors.go
│   │
│   └── provider/
│       ├── provider.go
│       ├── provider_model.go
│       │
│       ├── resource_instance.go
│       ├── resource_instance_model.go
│       ├── resource_instance_test.go
│       │
│       ├── data_source_instance.go
│       ├── data_source_instance_model.go
│       └── data_source_instance_test.go
│
├── main.go
├── go.mod
├── go.sum
├── Makefile
├── README.md
└── CHANGELOG.md

大型 Provider MAY 进一步拆分:

internal/provider/
├── provider.go
├── common/
├── resources/
│   ├── instance/
│   ├── network/
│   └── volume/
└── datasources/
    ├── instance/
    ├── network/
    └── volume/

原则:

Terraform Layer
       │
       ▼
Provider / Resource / DataSource
       │
       ▼
API Client
       │
       ▼
Remote API

Terraform Resource MUST NOT 直接实现复杂 HTTP 请求逻辑。


5. 命名规范

5.1 Provider

Provider 名称:

example

Provider Address:

registry.terraform.io/company/example

5.2 Resource

格式:

<provider>_<resource>

例如:

example_instance
example_network
example_volume
example_security_group

禁止:

example_create_instance
example_instance_resource
example_vm_create

Resource 表示一个资源对象,而不是操作。


5.3 Data Source

格式同样采用:

<provider>_<object>

例如:

data "example_instance" "example" {}

当 Resource 与 Data Source 同名时是正常的。


5.4 Go 类型

推荐:

type instanceResource struct {}

type instanceResourceModel struct {}

type instanceDataSource struct {}

type instanceDataSourceModel struct {}

Constructor:

func NewInstanceResource() resource.Resource {
	return &instanceResource{}
}

6. Provider 规范

Provider MUST 实现:

provider.Provider

推荐同时进行编译期接口检查:

var _ provider.Provider = &exampleProvider{}
var _ provider.ProviderWithConfigure = &exampleProvider{}

例如:

type exampleProvider struct {
	version string
}

Metadata:

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

7. Provider 配置模型

Provider Model SHOULD 单独定义:

type providerModel struct {
	Endpoint types.String `tfsdk:"endpoint"`
	Token    types.String `tfsdk:"token"`
}

禁止直接使用:

string
int
bool

承载 Terraform Schema 数据。

应该使用:

types.String
types.Int64
types.Bool
types.Float64
types.List
types.Set
types.Map
types.Object

因为 Terraform 值除了具体值外,还可能存在:

Null
Unknown
Known

普通 Go 类型无法正确表达 Terraform 的 Unknown / Null 语义。


8. Provider Schema 规范

示例:

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.",
			},

			"token": schema.StringAttribute{
				Optional:  true,
				Sensitive: true,
				Description: "API access token.",
			},
		},
	}
}

所有 Schema 字段 SHOULD 添加:

Description

敏感字段 MUST:

Sensitive: true

例如:

password
secret
token
access_key
private_key
client_secret

9. 环境变量

认证配置 SHOULD 支持环境变量。

例如:

EXAMPLE_ENDPOINT
EXAMPLE_TOKEN

优先级:

Terraform Provider Configuration
        >
Environment Variable
        >
Default Value

例如:

endpoint := data.Endpoint.ValueString()

if endpoint == "" {
	endpoint = os.Getenv("EXAMPLE_ENDPOINT")
}

禁止要求用户将 Secret 强制写入 Terraform 文件:

provider "example" {
  token = "123456"
}

应该同时允许:

export EXAMPLE_TOKEN=xxxx

10. Configure 规范

Provider Configure SHOULD 只负责:

  1. 读取 Provider 配置;
  2. 参数校验;
  3. 创建 API Client;
  4. 将 Client 传递给 Resource / Data Source。

例如:

func (p *exampleProvider) Configure(
	ctx context.Context,
	req provider.ConfigureRequest,
	resp *provider.ConfigureResponse,
) {
	var data providerModel

	resp.Diagnostics.Append(
		req.Config.Get(ctx, &data)...,
	)

	if resp.Diagnostics.HasError() {
		return
	}

	client, err := client.New(client.Config{
		Endpoint: data.Endpoint.ValueString(),
		Token:    data.Token.ValueString(),
	})
	if err != nil {
		resp.Diagnostics.AddError(
			"Unable to Create API Client",
			err.Error(),
		)
		return
	}

	resp.ResourceData = client
	resp.DataSourceData = client
}

禁止:

在 Configure 中创建基础设施
在 Configure 中查询业务资源
在 Configure 中执行资源初始化

11. API Client 规范

API Client MUST 与 Terraform Framework 解耦。

正确:

package client

type Client struct {
	httpClient *http.Client
	endpoint   string
	token      string
}

func (c *Client) GetInstance(
	ctx context.Context,
	id string,
) (*Instance, error)

错误:

func (c *Client) GetInstance(
	ctx context.Context,
	id types.String,
	resp *resource.ReadResponse,
)

client package MUST NOT 依赖:

terraform-plugin-framework
terraform-plugin-log

API Client SHOULD 可以单独作为普通 Go SDK 使用。


12. HTTP Client 规范

所有 API 请求 MUST 接受:

context.Context

例如:

func (c *Client) do(
	ctx context.Context,
	method string,
	path string,
	body any,
	result any,
) error

请求必须:

http.NewRequestWithContext(...)

禁止:

http.NewRequest(...)

忽略 Terraform Cancel。


13. Resource 接口

标准 Resource SHOULD 实现:

resource.Resource
resource.ResourceWithConfigure
resource.ResourceWithImportState

例如:

var _ resource.Resource = &instanceResource{}
var _ resource.ResourceWithConfigure = &instanceResource{}
var _ resource.ResourceWithImportState = &instanceResource{}

根据需要 MAY 实现:

resource.ResourceWithModifyPlan
resource.ResourceWithValidateConfig
resource.ResourceWithUpgradeState

14. Resource 生命周期

一个标准 Resource:

Metadata
   ↓
Schema
   ↓
Configure
   ↓
Create
Read
Update
Delete
   ↓
ImportState

必须理解 Terraform 的核心语义:

Configuration
      ↓
Plan
      ↓
Apply
      ↓
Remote Resource
      ↓
State

Provider 的职责不是简单地“调用 CRUD API”,而是:

让 Configuration、Plan、Remote Object 和 Terraform State 最终保持一致。


15. Resource Model

每个 Resource SHOULD 定义独立 Model。

例如:

type instanceResourceModel struct {
	ID        types.String `tfsdk:"id"`
	Name      types.String `tfsdk:"name"`
	Region    types.String `tfsdk:"region"`
	CPU       types.Int64  `tfsdk:"cpu"`
	Status    types.String `tfsdk:"status"`
	CreatedAt types.String `tfsdk:"created_at"`
}

字段排列建议:

ID
用户配置字段
嵌套配置字段
Computed 字段
时间字段

16. Schema 属性规则

Schema 属性主要分成:

Required

用户必须配置:

schema.StringAttribute{
	Required: true,
}

例如:

name
region
network_id

Optional

用户可以配置:

schema.StringAttribute{
	Optional: true,
}

Computed

由远程 API 返回:

schema.StringAttribute{
	Computed: true,
}

例如:

id
status
created_at
public_ip

Optional + Computed

API 有默认值,同时用户允许覆盖:

schema.StringAttribute{
	Optional: true,
	Computed: true,
}

典型:

hostname
description
disk_type

这种属性必须特别关注 Plan / Read 行为,避免产生永久 Diff。


17. ID 规范

Resource MUST 有稳定的唯一 ID。

推荐:

"id": schema.StringAttribute{
	Computed: true,
}

ID MUST:

唯一
稳定
不可随名称变化
能够查询远程资源

优先:

UUID
云平台 Resource ID
API Object ID

避免使用:

name
display_name
description

作为唯一 State ID。


18. Schema Validator

字段存在约束时 MUST 优先使用 Validator。

例如:

Validators: []validator.String{
	stringvalidator.OneOf(
		"running",
		"stopped",
	),
}

长度:

stringvalidator.LengthBetween(1, 64)

数值:

int64validator.Between(1, 128)

禁止将可以在 Terraform validate/plan 阶段发现的错误拖到 API 请求阶段。


19. Plan Modifier

Plan Modifier 用于控制 Terraform Plan 行为。

常用:

RequiresReplace
UseStateForUnknown

19.1 RequiresReplace

远程 API 不支持修改的属性:

PlanModifiers: []planmodifier.String{
	stringplanmodifier.RequiresReplace(),
},

例如:

region
availability_zone
image_id
resource_type

修改这些属性时:

destroy old
create new

而不是调用 Update。


20. UseStateForUnknown

稳定的 Computed 属性 MAY:

PlanModifiers: []planmodifier.String{
	stringplanmodifier.UseStateForUnknown(),
},

例如:

id
created_at

但必须谨慎。

禁止为了消除 Diff 对所有 Computed 字段添加:

UseStateForUnknown()

例如以下字段通常不适合:

status
ip_address
updated_at
runtime_state

因为远程值可能真正发生变化。

原则:

Plan Modifier 用于表达资源语义,而不是隐藏 Provider Bug。


21. Create 规范

Create 标准流程:

读取 Plan
    ↓
转换 API Request
    ↓
Create API
    ↓
查询实际资源
    ↓
转换 Terraform Model
    ↓
写 State

例如:

func (r *instanceResource) Create(
	ctx context.Context,
	req resource.CreateRequest,
	resp *resource.CreateResponse,
) {
	var plan instanceResourceModel

	resp.Diagnostics.Append(
		req.Plan.Get(ctx, &plan)...,
	)

	if resp.Diagnostics.HasError() {
		return
	}

	instance, err := r.client.CreateInstance(
		ctx,
		buildCreateRequest(plan),
	)
	if err != nil {
		resp.Diagnostics.AddError(
			"Unable to Create Instance",
			err.Error(),
		)
		return
	}

	state := flattenInstance(instance)

	resp.Diagnostics.Append(
		resp.State.Set(ctx, &state)...,
	)
}

Create 后 SHOULD 使用 API 实际返回值写 State。

不要单纯:

resp.State.Set(ctx, &plan)

因为:

API Default
服务端转换
Computed 字段
Normalized Value

可能与 Plan 不一致。


22. Read 规范

Read 是 Terraform Provider 最重要的方法。

Read SHOULD:

读取 State ID
   ↓
请求 Remote API
   ↓
资源存在?
  /     \
是      否
│       │
写State RemoveResource

例如:

func (r *instanceResource) Read(
	ctx context.Context,
	req resource.ReadRequest,
	resp *resource.ReadResponse,
) {
	var state instanceResourceModel

	resp.Diagnostics.Append(
		req.State.Get(ctx, &state)...,
	)

	if resp.Diagnostics.HasError() {
		return
	}

	instance, err := r.client.GetInstance(
		ctx,
		state.ID.ValueString(),
	)

	if errors.Is(err, client.ErrNotFound) {
		resp.State.RemoveResource(ctx)
		return
	}

	if err != nil {
		resp.Diagnostics.AddError(
			"Unable to Read Instance",
			err.Error(),
		)
		return
	}

	state = flattenInstance(instance)

	resp.Diagnostics.Append(
		resp.State.Set(ctx, &state)...,
	)
}

23. 404 处理

这是强制规范。

当 Terraform State 中存在:

example_instance.foo

但 Remote API 返回:

404 Not Found

MUST:

resp.State.RemoveResource(ctx)

禁止:

resp.Diagnostics.AddError(
	"Resource Not Found",
	"...",
)

否则用户永远无法通过:

terraform apply

重新创建资源。


24. Update 规范

Update:

读取 Plan
    ↓
读取 State
    ↓
比较需要修改的字段
    ↓
Update API
    ↓
重新 Read
    ↓
写入最终 State

推荐:

var plan instanceResourceModel
var state instanceResourceModel

req.Plan.Get(ctx, &plan)
req.State.Get(ctx, &state)

只提交变化字段:

if !plan.Name.Equal(state.Name) {
	request.Name = plan.Name.ValueString()
}

避免每次 Update 将整个对象发送给 API。


25. Update 后重新读取

远程 API Update 后 SHOULD:

Update()
   ↓
Get()
   ↓
State.Set()

而不是假设:

API result == Terraform Plan

因为 API 可能:

格式标准化
生成默认字段
修改状态
异步处理
重写名称

26. Delete 规范

Delete:

读取 State
   ↓
调用 Delete API
   ↓
等待删除完成(如果需要)

如果 API 返回:

404

Delete SHOULD 视为成功。

原因:

Terraform 的目标是:

resource does not exist

远端已经不存在,目标已经实现。

Delete MUST 具备幂等性。


27. ImportState

所有可被 API 查询的 Resource SHOULD 支持:

terraform import

实现:

func (r *instanceResource) ImportState(
	ctx context.Context,
	req resource.ImportStateRequest,
	resp *resource.ImportStateResponse,
) {
	resource.ImportStatePassthroughID(
		ctx,
		path.Root("id"),
		req,
		resp,
	)
}

用户:

terraform import example_instance.foo 123456

Import 后:

terraform plan

SHOULD 尽可能产生:

No changes

28. Import ID 格式

复杂 Resource MAY 使用复合 ID:

project_id/resource_id

例如:

terraform import example_network.foo project-001/network-001

必须:

明确文档化
可稳定解析
向后兼容

一旦公开 Import ID 格式,不应随意修改。


29. Data Source 规范

Data Source 原则:

只读取,不修改 Remote Resource。

通常实现:

datasource.DataSource
datasource.DataSourceWithConfigure

Data Source 不允许:

Create
Update
Delete

典型流程:

读取 Config
   ↓
调用 Query API
   ↓
转换 Model
   ↓
State.Set

30. Data Source 查询条件

查询字段:

"name": schema.StringAttribute{
	Required: true,
}

返回字段:

"id": schema.StringAttribute{
	Computed: true,
}

如果允许:

id
name

任选其一查询,MUST 使用 Validator 保证:

ExactlyOneOf
ConflictsWith

或使用 Data Source 配置校验实现等价逻辑。


31. Flatten / Expand 规范

Terraform Model 和 API Model MUST 分离。

例如:

Terraform Model
      ↕
Mapper
      ↕
API Model

推荐:

func buildCreateRequest(
	model instanceResourceModel,
) client.CreateInstanceRequest

以及:

func flattenInstance(
	instance *client.Instance,
) instanceResourceModel

避免在 CRUD 方法中出现几十行:

types.StringValue(...)
types.Int64Value(...)

转换逻辑。


32. Null / Unknown

TPF 开发必须正确理解:

Null
Unknown
Known

读取字段前:

if data.Name.IsNull() {
	...
}

if data.Name.IsUnknown() {
	...
}

禁止无条件:

data.Name.ValueString()

尤其是在:

ValidateConfig
ModifyPlan
Optional 属性
Computed 属性

中。


33. Set 与 List

集合无顺序语义时 SHOULD 使用:

Set

例如:

security_group_ids
tags
allowed_ips

有顺序语义时使用:

List

例如:

startup_steps
priority_rules
ordered_routes

禁止因为 API 返回数组就机械使用 List。

否则 API 顺序变化可能导致:

permanent diff

34. Map

Key-Value 数据 SHOULD 使用:

schema.MapAttribute

典型:

tags = {
  env  = "prod"
  team = "backend"
}

而不是:

tag {
  key   = "env"
  value = "prod"
}

除非 API 本身需要复杂 Tag 对象。


35. 嵌套对象

复杂结构推荐:

SingleNestedAttribute
ListNestedAttribute
SetNestedAttribute

应根据业务语义选择。

不要为了“以后可能扩展”而过度设计深层嵌套 Schema。

建议嵌套层级:

<= 3

超过三层 SHOULD 评估是否应该拆分 Resource。


36. Diagnostics

Framework 方法禁止直接:

return err

应该:

resp.Diagnostics.AddError(
	"Unable to Create Instance",
	fmt.Sprintf(
		"Unable to create instance: %s",
		err,
	),
)

处理 Framework 调用:

resp.Diagnostics.Append(
	req.Plan.Get(ctx, &data)...,
)

if resp.Diagnostics.HasError() {
	return
}

这是强制模式。


37. Error Summary

错误 Summary MUST:

短
明确
可搜索
稳定

推荐:

Unable to Create Instance
Unable to Read Network
Unable to Configure API Client
Invalid Provider Configuration

不要:

Error
Failed
Something went wrong
API error

38. API Error

API Client SHOULD 定义结构化 Error:

type APIError struct {
	StatusCode int
	Code       string
	Message    string
}

并提供:

var ErrNotFound = errors.New("resource not found")

或者:

func IsNotFound(err error) bool

Resource 不应该解析字符串:

if strings.Contains(err.Error(), "404")

39. 日志

Terraform Layer SHOULD 使用:

tflog.Debug()
tflog.Info()
tflog.Warn()
tflog.Error()

例如:

tflog.Debug(
	ctx,
	"Creating instance",
	map[string]any{
		"name": plan.Name.ValueString(),
	},
)

禁止:

fmt.Println()
log.Println()

40. 敏感信息日志

以下内容 MUST NOT 输出:

password
token
secret
private_key
Authorization
Cookie
AccessKeySecret
完整 HTTP Header

错误:

tflog.Debug(ctx, "request", map[string]any{
	"token": token,
})

任何 HTTP Debug 日志都必须执行 Secret 脱敏。


41. Context

所有可能阻塞的操作 MUST 使用:

ctx context.Context

包括:

HTTP
Retry
Polling
Wait
Database
RPC

Retry:

select {
case <-ctx.Done():
	return ctx.Err()

case <-time.After(delay):
}

禁止:

time.Sleep(30 * time.Second)

长时间无条件阻塞。


42. Retry

以下错误 MAY Retry:

429 Too Many Requests
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
部分网络瞬时错误

以下错误通常 MUST NOT Retry:

400
401
403
404
业务校验错误

推荐:

Exponential Backoff
+
Jitter

Retry MUST:

有最大次数/时间
支持 context cancel
记录必要日志

43. Eventually Consistent API

对于异步 API:

Create
  ↓
Pending
  ↓
Creating
  ↓
Running

Provider SHOULD Poll:

for {
	instance, err := client.GetInstance(...)

	switch instance.Status {
	case "running":
		return instance, nil

	case "failed":
		return nil, errors.New(...)
	}

	// wait with ctx
}

等待逻辑 SHOULD 抽象:

waitInstanceReady()
waitInstanceDeleted()

禁止 CRUD 中复制 Polling 代码。


44. Terraform State 原则

State MUST 表示:

Remote Resource 当前实际状态。

禁止:

为了防止 Diff 保留旧值
为了通过测试伪造状态
为了隐藏 API 差异不刷新字段

Read 必须尽可能刷新 Remote 状态。


45. Permanent Diff

以下情况必须重点排查:

terraform apply
terraform plan

如果第二次 plan 仍然存在变化:

Provider 存在潜在问题

常见原因:

API 默认值没有写回
List 顺序不稳定
Optional/Computed 使用错误
大小写标准化
时间格式差异
Null 与空字符串混用
Null 与空 List 混用
Set Hash 不一致
API 返回字段和 Plan 不一致

Resource SHOULD 满足:

Apply
 ↓
Plan
 ↓
No changes

46. Null 与空值

必须统一:

null
""
[]
{}

的语义。

禁止 API:

null

有时转换为:

types.StringNull()

有时转换成:

types.StringValue("")

同一个字段必须保持转换策略稳定。


47. 时间格式

Terraform 中时间 SHOULD 使用:

RFC3339

例如:

2026-09-07T23:30:00+08:00

避免:

2026/09/07
09-07-2026
2026-09-07 23:30:00

除非 Remote API 的业务格式必须原样暴露。


48. Resource State Upgrade

已经发布的 Resource Schema 不允许随意进行 Breaking Change。

例如:

V1:

instance_name

V2:

name

不能简单删除旧字段。

涉及 State Schema 变化 SHOULD 使用:

resource.ResourceWithUpgradeState

维护 State Migration。

必须考虑:

旧 terraform.tfstate
      ↓
升级 Provider
      ↓
新 Provider 是否还能读取

49. Breaking Change

以下操作属于潜在 Breaking Change:

删除 Resource
删除 Attribute
Attribute Rename
改变 Attribute Type
Optional → Required
Computed → Required
修改 Import ID
修改 ID 生成逻辑
修改 List/Set 类型
修改默认行为

必须通过 Major Version 或 State Upgrade 处理。


50. API Version 与 Provider Version

不要把:

Provider Version

和:

Remote API Version

绑定。

例如:

Provider v2.3.0

不意味着:

API v2

Remote API Compatibility SHOULD 在 Client 层处理。


51. 测试规范

测试分:

Unit Test
Acceptance Test

推荐:

                   Acceptance
                      ▲
                      │
             Mapper / Client Test
                      ▲
                      │
                   Unit

52. Unit Test

Unit Test SHOULD 覆盖:

flatten
expand
validator
ID parser
API error parser
response normalization
request building

例如:

func TestFlattenInstance(t *testing.T)
func TestParseImportID(t *testing.T)
func TestBuildCreateInstanceRequest(t *testing.T)

推荐 Table Driven Test:

func TestParseImportID(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		wantErr bool
	}{
		{
			name:    "valid",
			input:   "project-1/instance-1",
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// ...
		})
	}
}

53. Acceptance Test

Resource SHOULD 至少存在一个:

TestAccXXXResource

例如:

func TestAccInstanceResource_basic(t *testing.T)

Acceptance Test 应覆盖:

Create
Read
Refresh
Update
Import
Destroy

关键 Resource SHOULD 额外覆盖:

RequiresReplace
External deletion
Invalid configuration
Optional fields
Default values

Acceptance Test 使用真实 Terraform 生命周期,而不是只调用 Go 方法。


54. Acceptance Test Provider

使用:

ProtoV6ProviderFactories

例如:

ProtoV6ProviderFactories: map[string]func() (
	protfw.ProviderServer,
	error,
){
	"example": providerserver.NewProtocol6WithError(
		New("test")(),
	),
},

统一通过测试 Provider Factory 启动。


55. TF_ACC

Acceptance Test MUST 通过:

TF_ACC=1 go test ./... -v

普通:

go test ./...

不应该意外创建真实资源。

CI 中 Acceptance Test 应明确配置:

TF_ACC=1
Credentials
Test Region
Timeout

56. 测试资源命名

Acceptance Test 创建的资源 MUST 能区分测试资源。

推荐:

tf-acc-xxxxx

例如:

tf-acc-instance-a8f7c

不得:

test
demo
instance1

避免误删人工资源。


57. Acceptance Test 清理

Acceptance Test MUST 考虑异常退出后的资源清理。

应:

随机名称
独立项目/账号
测试标签
资源 TTL
CheckDestroy

测试账号不得使用生产账号。


58. examples

每个 Resource SHOULD 提供:

examples/resources/<resource>/resource.tf

例如:

examples/resources/example_instance/resource.tf

内容:

resource "example_instance" "example" {
  name   = "example"
  region = "sg"
  cpu    = 2
}

每个 Data Source:

examples/data-sources/<data-source>/data-source.tf

59. 文档

每个 Resource 文档 MUST 描述:

用途
Example
Arguments
Attributes
Import
注意事项

尤其必须明确:

Force Replacement / RequiresReplace 字段
Default 值
Import ID
枚举值
敏感字段

60. gofmt / lint

提交代码前 MUST:

go fmt ./...
go vet ./...
go test ./...

推荐:

golangci-lint run

CI MUST 检查:

fmt
vet
lint
unit test
build

61. Makefile

推荐:

.PHONY: fmt
fmt:
	go fmt ./...

.PHONY: vet
vet:
	go vet ./...

.PHONY: test
test:
	go test ./...

.PHONY: testacc
testacc:
	TF_ACC=1 go test ./... -v

.PHONY: lint
lint:
	golangci-lint run

.PHONY: build
build:
	go build ./...

62. main.go

main.go MUST 保持简单:

package main

import (
	"context"
	"log"

	"github.com/hashicorp/terraform-plugin-framework/providerserver"

	"example.com/terraform-provider-example/internal/provider"
)

var version = "dev"

func main() {
	ctx := context.Background()

	opts := providerserver.ServeOpts{
		Address: "registry.terraform.io/company/example",
		Debug:   false,
	}

	err := providerserver.Serve(
		ctx,
		provider.New(version),
		opts,
	)

	if err != nil {
		log.Fatal(err)
	}
}

禁止把业务逻辑放入:

main.go

63. Provider 注册

Provider SHOULD 集中注册 Resource:

func (p *exampleProvider) Resources(
	ctx context.Context,
) []func() resource.Resource {
	return []func() resource.Resource{
		NewInstanceResource,
		NewNetworkResource,
		NewVolumeResource,
	}
}

Data Source:

func (p *exampleProvider) DataSources(
	ctx context.Context,
) []func() datasource.DataSource {
	return []func() datasource.DataSource{
		NewInstanceDataSource,
		NewNetworkDataSource,
	}
}

不要使用复杂的动态注册机制。


64. Resource 文件模板

一个标准 Resource 推荐结构:

package provider

type instanceResource struct {
	client *client.Client
}

type instanceResourceModel struct {
	ID   types.String `tfsdk:"id"`
	Name types.String `tfsdk:"name"`
}

func NewInstanceResource() resource.Resource {
	return &instanceResource{}
}

func (r *instanceResource) Metadata(...) {}

func (r *instanceResource) Schema(...) {}

func (r *instanceResource) Configure(...) {}

func (r *instanceResource) Create(...) {}

func (r *instanceResource) Read(...) {}

func (r *instanceResource) Update(...) {}

func (r *instanceResource) Delete(...) {}

func (r *instanceResource) ImportState(...) {}

统一函数排列顺序:

New
Metadata
Schema
Configure
ValidateConfig
ModifyPlan
Create
Read
Update
Delete
ImportState
UpgradeState
Helper

65. Resource Configure

标准写法:

func (r *instanceResource) Configure(
	ctx context.Context,
	req resource.ConfigureRequest,
	resp *resource.ConfigureResponse,
) {
	if req.ProviderData == nil {
		return
	}

	client, ok := req.ProviderData.(*client.Client)
	if !ok {
		resp.Diagnostics.AddError(
			"Unexpected Resource Configure Type",
			fmt.Sprintf(
				"Expected *client.Client, got: %T",
				req.ProviderData,
			),
		)
		return
	}

	r.client = client
}

Resource MUST 检查类型断言。

禁止:

r.client = req.ProviderData.(*client.Client)

避免 panic。


66. Panic

Provider MUST NOT 因普通用户配置导致 Panic。

禁止:

value := ptr.Value
client := data.(*Client)
items[0]

没有边界或 nil 检查。

Terraform Provider 是长期运行的外部 Plugin Process。

一次 Panic 可能导致:

Plugin crashed
Request cancelled
Terraform apply failed

67. 并发

不要假设 Resource 方法串行执行。

Terraform 可以同时执行:

resource A Create
resource B Create
data source C Read

因此:

Client
Cache
Token
Global State

必须线程安全。

禁止:

var currentProjectID string

这种全局业务状态。


68. Mutex

只有 Remote API 本身要求串行操作时才 SHOULD 使用锁。

并且优先:

resource scoped lock

而不是:

global mutex

禁止因为 API Client 非线程安全就把整个 Provider 串行化。

应该修复 Client。


69. API Client Timeout

HTTP Client MUST 设置 Timeout:

&http.Client{
	Timeout: 30 * time.Second,
}

但对于:

Create VM
Delete VM
Long Poll

应该区分:

HTTP Request Timeout
Resource Operation Timeout

不能简单把 HTTP Timeout 配置成:

30 minutes

70. Rate Limit

如果 Remote API 存在限流:

QPS
Burst
429

SHOULD 在 Client 层统一处理:

rate limiter
retry
backoff

不要每个 Resource 自己实现一套限流。


71. API 分页

Data Source/List API 必须正确处理分页。

禁止只请求:

page=1
size=100

然后假设所有数据都返回。

统一 Client:

func (c *Client) ListAllInstances(
	ctx context.Context,
) ([]Instance, error)

或者明确暴露分页语义。


72. Remote Resource Drift

必须支持用户在 Terraform 外部修改资源。

例如:

Terraform State:
cpu = 2

Remote:
cpu = 4

执行:

terraform plan

Read 后 Terraform SHOULD 能检测:

4 -> 2

除非字段设计为仅 Computed。

这就是 Terraform Drift Detection。


73. External Delete

如果用户手动删除:

example_instance.foo

Terraform:

terraform plan

应该显示:

+ create

实现关键就是 Read 中:

resp.State.RemoveResource(ctx)

74. API 默认值

API 默认值有两种处理方式。

方案 A:

Terraform Schema 明确默认逻辑。

方案 B:

Optional: true,
Computed: true,

由 API 决定,然后 Read 写入。

禁止 Terraform 一套默认值、API 又一套不同默认值。

否则极易产生:

inconsistent result after apply

75. Inconsistent Result After Apply

遇到:

Provider produced inconsistent result after apply

重点检查:

Create 写 State 是否与 Plan 一致
Optional/Computed 是否合理
API 是否修改输入
Null/Unknown 是否转换错误
List/Set 顺序
Plan Modifier
API 默认值

禁止通过:

UseStateForUnknown
忽略字段
强行写 Plan

简单掩盖问题。


76. Description

每个字段必须提供有意义的 Description:

正确:

Description: "ID of the VPC where the instance is created."

错误:

Description: "VPC ID."

尤其应说明:

是否 Required
业务含义
取值范围
修改是否重建
默认行为

但不要在 Description 中重复 Terraform 自动生成的信息。


77. Deprecated 字段

删除字段前 SHOULD 先 Deprecated。

迁移过程:

v1.4
field_a

v1.5
field_a Deprecated
field_b Added

v2.0
field_a Removed

必须给予用户至少一个合理迁移周期。


78. Commit 规范

建议 Conventional Commits:

feat:
fix:
docs:
test:
refactor:
chore:
ci:

例如:

feat(instance): support instance import

fix(network): remove state when remote network is missing

docs(volume): document disk_type replacement behavior

test(instance): add update acceptance test

79. Pull Request 检查

Resource PR 必须确认:

  • Metadata 正确
  • Schema Description 完整
  • Sensitive 字段正确标记
  • Required / Optional / Computed 合理
  • Validator 完整
  • RequiresReplace 正确
  • Null / Unknown 正确处理
  • Create 使用真实 API 状态
  • Read 正确刷新状态
  • Read 404 RemoveResource
  • Update 后重新读取状态
  • Delete 404 视为成功
  • ImportState 已实现
  • API Client 支持 Context
  • 无 Secret 日志
  • Unit Test 完整
  • Acceptance Test 完整
  • examples 已添加
  • 文档已更新
  • terraform apply 后再次 plan 无 Diff

80. Resource Definition of Done

一个 Resource 只有同时满足以下条件才算完成:

Create      ✓
Read        ✓
Update      ✓ / RequiresReplace
Delete      ✓
Import      ✓
Drift       ✓
404         ✓
Validation  ✓
Unit Test   ✓
Acc Test    ✓
Docs        ✓
Examples    ✓
No Diff     ✓

其中最后一项尤其重要:

terraform apply
terraform plan

期望:

No changes. Your infrastructure matches the configuration.

81. 核心设计原则

Provider 开发必须遵循以下原则:

1. State 是事实,不是缓存

State = Remote Resource 当前 Terraform 可表达的真实状态

2. Read 是 Resource 的核心

Create / Update / Import 最终都应该能够通过 Read 得到一致状态。

理想状态:

Create ─┐
Update ─┼─> Read ─> State
Import ─┘

3. API Model 与 Terraform Model 分离

Terraform Schema
      ↕
Terraform Model
      ↕
Mapper
      ↕
API Model

禁止 Terraform 类型污染 API Client。


4. Schema 表达资源语义

不要把 Schema 当 DTO。

Schema 需要表达:

Required
Optional
Computed
Sensitive
Validation
Replacement
Unknown
State

5. Plan Modifier 不用于隐藏 Bug

不要为了得到:

No changes

而滥用:

UseStateForUnknown
RequiresReplace
Computed

必须解决真正的数据一致性问题。


6. Provider 必须幂等

应该满足:

Apply #1
    ↓
Resource Created

Apply #2
    ↓
No Changes

而不是:

Apply #2
    ↓
Update Again

82. 推荐开发流程

新增 Resource:

1. 分析 Remote API
       ↓
2. 确定 Terraform Resource 边界
       ↓
3. 设计 Schema
       ↓
4. 定义 Terraform Model
       ↓
5. 编写 API Client
       ↓
6. 编写 Mapper
       ↓
7. 实现 Create
       ↓
8. 实现 Read
       ↓
9. 实现 Update
       ↓
10. 实现 Delete
       ↓
11. 实现 Import
       ↓
12. Unit Test
       ↓
13. Acceptance Test
       ↓
14. terraform apply
       ↓
15. terraform plan
       ↓
16. Drift Test
       ↓
17. External Delete Test
       ↓
18. Documentation

不建议:

先写 CRUD
     ↓
再随便补 Schema
     ↓
最后处理 Terraform Diff

Schema 应该在编码前完成基本设计。


83. 推荐 Resource 架构

最终推荐统一采用:

                 Terraform Core
                       │
                       ▼
            Terraform Plugin Framework
                       │
              ┌────────┴────────┐
              │                 │
           Resource         DataSource
              │                 │
              └────────┬────────┘
                       │
                       ▼
                    Mapper
                       │
                       ▼
                  API Client
                       │
                       ▼
                Remote Service

依赖方向必须保持:

provider
   ↓
client

而不能:

client
   ↓
provider

84. 最终强制规则摘要

以下规则定义为 MUST:

  1. 新 Provider 使用 Terraform Plugin Framework。
  2. Terraform Model 使用 types.* 表达 Terraform 数据。
  3. API Client 与 Terraform Framework 解耦。
  4. 所有远程请求接受 context.Context。
  5. Secret Schema 必须设置 Sensitive: true。
  6. Schema 必须正确区分 Required / Optional / Computed。
  7. 不可变字段使用 RequiresReplace。
  8. 不得滥用 UseStateForUnknown。
  9. Create / Update 后必须以 Remote API 实际状态为准。
  10. Read 遇到 Remote 404 必须 RemoveResource。
  11. Delete 遇到 Remote 404 必须视为成功。
  12. 可查询 Resource 必须支持 Import。
  13. Provider 不得因用户输入 Panic。
  14. 不得记录 Token / Password / Secret。
  15. Remote API 与 Terraform Model 必须通过 Mapper 隔离。
  16. List / Set 必须按照业务是否存在顺序语义选择。
  17. 已发布 Schema 的 Breaking Change 必须考虑 State Upgrade。
  18. Resource 必须具备 Acceptance Test。
  19. Acceptance Test 必须验证 Import。
  20. Resource 必须通过 Apply 后再次 Plan 无永久 Diff 的测试。