关于 provider-actions
使用 Plugin Framework 实现 Terraform Provider Actions 的开发指南,涵盖 Provider 配置、Schema、调用、进度、错误处理、测试及文档,适用于生命周期触发的操作。
主要功能
- 01配置 ProviderWithActions
- 02定义并校验 Action Schema
- 03实现调用与进度报告
- 04处理超时和错误
- 05测试生命周期触发的 Action
- 06编写 Action 文档
适用场景
- 01为 Provider 添加首个 Action
- 02实现由 Terraform 生命周期事件触发的 API 操作
- 03测试 Action 结果及失败场景
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ hashicorp/ agent-skills --skill provider-actions安装后在 Claude Code 中输入/provider-actions确认已加载。
详细介绍
Terraform 提供程序操作实现指南
概述
Terraform 操作可在 Terraform 生命周期中执行命令式操作。操作是实验性功能,允许在特定生命周期事件(创建、更新、销毁前后)执行提供程序操作。
参考资料:
首个操作的设置
首次向尚未包含任何操作的提供程序添加操作时,需要执行若干一次性脚手架设置步骤:
- 实现
ProviderWithActions— 向提供程序添加Actions()方法,使其返回[]func() action.Action。 - 在
Configure中设置ActionData— 提供程序的Configure方法必须设置resp.ActionData = v,与现有的ResourceData、DataSourceData和EphemeralResourceData赋值并列。 - 创建
ActionWithConfigure基础类型 — 如果提供程序使用嵌入式基础类型(例如ResourceWithConfigure),则创建等效的ActionWithConfigure类型,实现action.ConfigureRequest/action.ConfigureResponse。 - 操作架构辅助函数变体 — 如果提供程序通过辅助函数注入常见架构属性(例如
namespace),则需要操作架构变体,因为action/schema类型与resource/schema类型不同。
文件结构
大多数提供程序将操作与资源放在提供程序包中:
internal/provider/
├── <action_name>_action.go # Action implementation
└── <action_name>_action_test.go # Action tests
(大型多服务提供程序会改用 internal/service/<service>/ 包,
请遵循目标仓库的布局。)
文档与其他生成的文档放在一起:
docs/actions/
└── <action_name>.md # User-facing documentation
(某些较旧的大型提供程序会手动编写
website/docs/actions/<name>.html.markdown,请遵循仓库的做法。)
操作架构定义
操作使用 Terraform Plugin Framework,并遵循标准架构模式:
func (a *actionType) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
// Required configuration parameters
"resource_id": schema.StringAttribute{
Required: true,
Description: "ID of the resource to operate on",
},
// Optional parameters with defaults
"timeout": schema.Int64Attribute{
Optional: true,
Description: "Operation timeout in seconds",
Default: int64default.StaticInt64(1800),
Computed: true,
},
},
}
}
常见架构问题
请特别留意架构定义 —— 初稿完成后常见的问题包括:
-
类型不匹配
- 模型结构体使用
types.String/types.Int64,架构使用来自github.com/hashicorp/terraform-plugin-framework/types的types.StringType—— 不要混用其他包中的类型 - 一些大型提供程序会在其基础上封装自定义类型包(例如
terraform-provider-aws 的内部
fwtypes);在此类仓库中, 应始终遵循其约定,而不是使用普通类型
- 模型结构体使用
-
列表/映射元素类型
// WRONG - missing ElementType "items": schema.ListAttribute{ Optional: true, } // CORRECT "items": schema.ListAttribute{ Optional: true, ElementType: types.StringType, } -
计算属性与可选属性
- 带默认值的属性必须同时设置
Optional: true和Computed: true - 除非操作输入有默认值,否则不要将其标记为
Computed
- 带默认值的属性必须同时设置
-
验证器导入
// Ensure proper imports "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" -
区域/提供程序属性(多区域提供程序,例如 AWS)
- 如果提供程序有共享的区域处理逻辑,请使用该逻辑
- 不要在操作架构中手动重新定义提供程序级配置
-
嵌套属性
- 对复杂结构使用合适的嵌套对象类型
- 确保正确定义嵌套类型
架构验证清单
提交前,请确认:
- 所有属性均有描述
- 列表/映射属性已定义 ElementType
- 已正确导入并应用验证器
- 模型结构体使用了正确的框架类型
- 带默认值的可选属性已标记为 Computed
- 代码编译时没有类型错误
- 运行
go build以发现类型不匹配
操作 Invoke 方法
Invoke 方法包含操作逻辑:
func (a *actionType) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
var data actionModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// a.client was stored by Configure (from req.ProviderData), the same
// pattern resources use.
resp.SendProgress(action.InvokeProgressEvent{Message: "Starting operation..."})
// Implement action logic with error handling
// Use context for timeout management
// Poll for completion if async operation
resp.SendProgress(action.InvokeProgressEvent{Message: "Operation completed"})
}
关键实现要求
1. 进度报告
- 使用
resp.SendProgress(action.InvokeProgressEvent{...})实时更新进度 - 在耗时操作期间提供有意义的进度消息
- 在关键节点更新进度
- 对耗时操作注明已用时间
2. 超时管理
- 始终包含可配置的超时参数(默认值:1800s)
- 对 API 调用使用
context.WithTimeout() - 妥善处理超时错误
- 验证超时范围(通常为 60-7200 秒)
3. 错误处理
- 使用
resp.Diagnostics.AddError()添加诊断信息 - 提供清晰且包含上下文的错误消息
- 在相关情况下包含 API 错误详情
- 将提供商错误类型映射为用户友好的消息
- 记录所有可能的错误情况
错误处理示例:
// Handle specific errors
var notFound *types.ResourceNotFoundException
if errors.As(err, ¬Found) {
resp.Diagnostics.AddError(
"Resource Not Found",
fmt.Sprintf("Resource %s was not found", resourceID),
)
return
}
// Generic error handling
resp.Diagnostics.AddError(
"Operation Failed",
fmt.Sprintf("Could not complete operation for %s: %s", resourceID, err),
)
4. 提供商 SDK 集成
- 使用 Configure 时保存的 API 客户端(
a.client),并与资源和数据源共享 - 为列表操作处理分页
- 为瞬时故障实现重试逻辑
- 使用适当的错误类型
5. 参数验证
- 使用框架验证器验证输入
- 在操作前验证资源是否存在
- 检查是否存在冲突参数
- 根据提供商的命名要求进行验证
6. 轮询与等待
对于需要等待完成的操作,应在上下文截止时间内使用定时器进行轮询,并在过程中报告进度。(或者使用 github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry 中的 retry.StateChangeConf,这是资源使用的相同等待器原语。)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// Poll fast, report slow: progress events cross the plugin protocol, so
// throttle them instead of emitting one per poll.
start := time.Now()
var lastProgress time.Time
for {
res, err := findResource(ctx, a.client, id)
if err != nil {
resp.Diagnostics.AddError("Error polling operation", fmt.Sprintf("checking status of %s: %s", id, err))
return
}
switch res.Status {
case "AVAILABLE", "COMPLETED":
resp.SendProgress(action.InvokeProgressEvent{Message: "Operation completed"})
return
case "CREATING", "PENDING":
if time.Since(lastProgress) >= 30*time.Second {
lastProgress = time.Now()
resp.SendProgress(action.InvokeProgressEvent{
Message: fmt.Sprintf("Status: %s, Elapsed: %v", res.Status, time.Since(start).Round(time.Second)),
})
}
default:
resp.Diagnostics.AddError("Operation Failed", fmt.Sprintf("%s entered unexpected status %q", id, res.Status))
return
}
select {
case <-ctx.Done():
resp.Diagnostics.AddError("Operation Timed Out", fmt.Sprintf("%s did not complete within %v", id, timeout))
return
case <-ticker.C:
}
}
常见操作模式
批量操作
- 按可配置的批次处理项目
- 按批次报告进度
- 妥善处理部分失败
- 支持前缀/筛选参数
命令执行
- 提交命令并获取操作 ID
- 轮询完成状态
- 获取并报告输出
- 处理轮询期间的超时
- 执行前验证资源是否存在
服务调用
- 使用参数调用服务
- 等待完成(如果为同步调用)
- 返回输出/结果
- 处理服务特定错误
资源状态变更
- 验证当前状态
- 应用状态变更
- 轮询目标状态
- 处理过渡状态
异步作业提交
- 使用配置提交作业
- 获取作业 ID
- 可选择等待完成
- 报告作业状态
操作触发器
在 Terraform 配置中,通过 action_trigger 生命周期块调用操作。没有对应触发器的独立 action 块只会声明操作,但不会执行。
HCL 语法
操作参数必须放在 config {} 块中。触发器引用使用 action. 前缀,actions 是一个列表。事件使用不带引号的标识符。
action "provider_service_action" "name" {
config {
parameter = value
}
}
resource "terraform_data" "trigger" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.provider_service_action.name]
}
}
}
可用的触发器事件
支持的事件(截至 Terraform 1.14):
before_create- 创建资源之前after_create- 创建资源之后before_update- 更新资源之前after_update- 更新资源之后
不支持的事件(截至 Terraform 1.14;请查看最新发行说明):
before_destroy- 不可用(将导致验证错误)after_destroy- 不可用(将导致验证错误)
测试操作
验收测试
- 使用有效参数测试操作调用
- 测试超时场景
- 测试错误情况
- 验证提供商状态变更
- 测试进度报告
- 使用自定义参数进行测试
- 测试基于触发器的调用
测试模式
func TestAccExampleAction_basic(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_14_0),
},
Steps: []resource.TestStep{
{
Config: testAccActionConfig_basic(),
ConfigStateChecks: []statecheck.StateCheck{
// assert the observable effect of the action on the
// triggering resource
},
},
},
})
}
使用清理函数清理测试
测试中调用的操作可能会遗留真实资源;应注册清理器(列出 → 筛选名称带有测试前缀的资源 → 删除),以便清理泄漏的资源。清理器并非操作专用——请使用 provider-test-patterns 技能(如果可用),了解清理函数模式、注册方式、TestMain 和依赖顺序。
使用 terraform_data 作为无操作触发器
对于不需要真实基础设施的操作测试,terraform_data 可用作无操作触发器资源。这对错误情况和验证测试很有帮助:
resource "terraform_data" "trigger" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.provider_service_action.test]
}
}
}
action "provider_service_action" "test" {
config {
param = "invalid-value"
}
}
使用 PostApplyFunc 验证副作用
操作不会生成可通过 resource.TestCheckResourceAttr 检查的状态。使用 resource.TestStep 上的 PostApplyFunc 在应用后查询 API,并确认操作产生了预期的副作用:
Steps: []resource.TestStep{
{
Config: testConfig,
PostApplyFunc: func() {
// query the API to verify the action's side effect occurred
},
},
},
测试最佳实践
特定服务的前置条件
- 始终检查操作成功执行前必须满足的特定服务前置条件
- 在操作文档和测试配置中记录前置条件
错误模式匹配
- Terraform 会为操作错误添加额外上下文
- 使用灵活的正则表达式模式:
regexp.MustCompile(\(?s)Error Title.*key phrase`)`
不适用于操作的测试模式
- 操作由生命周期事件触发,而非重新应用配置
- 销毁前/后测试:截至 Terraform 1.14 尚不支持
运行测试
先进行编译检查,然后运行针对性的验收测试:
go test -c -o /dev/null ./internal/provider
TF_ACC=1 go test ./internal/provider -run TestAccExampleAction_ -timeout 60m
如可用,请使用 run-acceptance-tests 技能来设置环境变量、
调试失败的测试以及运行清理器。
文档规范
如果提供程序使用 tfplugindocs,请用它生成操作文档(如可用,请使用
provider-docs 技能完成此工作流)。每个操作文档页面必须包含:
-
前置元数据(仅限手写的旧版布局)
--- subcategory: "Service Name" layout: "provider" page_title: "Provider: provider_service_action" description: |- Brief description of what the action does. --- -
带警告的标题
- 关于实验状态的 Beta/Alpha 通知
- 关于可能产生意外后果的警告
- 提供程序文档链接
-
使用示例
- 基本用法示例
- 包含所有选项的高级用法示例
- 使用
terraform_data的基于触发器的示例 - 真实用例示例
-
参数参考
- 列出所有必需和可选参数
- 包含说明和默认值
- 注明所有验证规则
-
文档检查(可选工具)
- 如果仓库使用
terrafmt,请在提交前运行terrafmt fmt, 并使用terrafmt diff进行验证
- 如果仓库使用
变更日志条目格式(提供程序特定约定)
一些提供程序(例如 terraform-provider-aws)使用
go-changelog 跟踪发行说明:每个 PR
在 .changelog/ 目录中对应一个文件。请查看目标仓库的 CONTRIBUTING 指南;
如果仓库不使用此工具,则跳过此项。
.changelog/<pr_number>.txt
内容格式:
action/provider_service_action: Brief description of the action
提交前检查清单
提交操作实现前:
- 代码可编译:
go build -o /dev/null . - 测试可编译:
go test -c -o /dev/null ./internal/provider - 代码已格式化:
gofmt(或仓库的make fmt) - 已按仓库约定生成或格式化文档
- 已创建变更日志条目(如果仓库使用)
- 架构使用正确的类型
- 所有 List/Map 属性均有 ElementType
- 已为长时间运行的操作实现进度更新
- 错误消息包含上下文和资源标识符
- 文档包含多个示例
- 文档包含前置条件和警告
参考资料
- Terraform 插件框架文档
- Terraform 提供程序开发
- terraform-plugin-framework GitHub
- terraform-plugin-testing
- 编写 Terraform 操作(博客)
- 参考实现:
terraform-provider-tfe(action_query_run.go、action_query_run_test.go)、terraform-provider-vault(action_rotate_root.go)