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 的测试。