dev-tf
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(...)
除非:
- 正在迁移历史 SDKv2 Provider;
- 必须通过 terraform-plugin-mux 保持兼容;
- 存在 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 只负责:
- 读取 Provider 配置;
- 参数校验;
- 创建 API Client;
- 将 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:
- 新 Provider 使用 Terraform Plugin Framework。
- Terraform Model 使用
types.*表达 Terraform 数据。 - API Client 与 Terraform Framework 解耦。
- 所有远程请求接受
context.Context。 - Secret Schema 必须设置
Sensitive: true。 - Schema 必须正确区分 Required / Optional / Computed。
- 不可变字段使用
RequiresReplace。 - 不得滥用
UseStateForUnknown。 - Create / Update 后必须以 Remote API 实际状态为准。
- Read 遇到 Remote 404 必须
RemoveResource。 - Delete 遇到 Remote 404 必须视为成功。
- 可查询 Resource 必须支持 Import。
- Provider 不得因用户输入 Panic。
- 不得记录 Token / Password / Secret。
- Remote API 与 Terraform Model 必须通过 Mapper 隔离。
- List / Set 必须按照业务是否存在顺序语义选择。
- 已发布 Schema 的 Breaking Change 必须考虑 State Upgrade。
- Resource 必须具备 Acceptance Test。
- Acceptance Test 必须验证 Import。
- Resource 必须通过 Apply 后再次 Plan 无永久 Diff 的测试。