用途
临时资源(Ephemeral Resources,Terraform 1.10+)产生的值永远不会持久化到 state 或 plan。它们只做一件事:把机密——令牌、生成的密码、短期证书、解密后的值——交给配置中需要它们的部分,而不写入磁盘。任何返回敏感值的数据源,都可以考虑(或同时)作为临时资源存在。
何时使用
- 只读查询非敏感数据:使用数据源。
- 值敏感且仅在 apply 时需要(例如 provider 块使用的数据库密码、只写属性的令牌):使用临时资源。
- 敏感值需要被下游托管资源存储(例如作为属性):使用常规资源/数据源,但尽可能配合只写属性。
- 凭据在操作中途过期(STS 风格令牌、短 TTL 租约):使用带 Renew 的临时资源。
临时结果可用于 provider 配置、只写属性、provisioner 配置以及其他临时上下文,但不能用于常规属性,因为那些会持久化到 state。
生命周期
Terraform 每次操作最多调用三个方法:
- Open(必需):获取或创建值;在 plan 和/或 apply 期间需要结果时运行。没有 state 可刷新,也没有可导入的内容。
- Renew(可选):当墙上时钟超过 Open/Renew 返回的 RenewAt 时调用,用于在 Terraform 仍在运行时过期的值。Renew 不能返回新结果——它只能延长/刷新 Open 产生的值(例如重新租用同一凭据);如果值本身在续期时改变,则该 API 在此意义上不可续期,Open 必须返回更长期的值。
- Close(可选):Terraform 用完该值时调用;在此撤销租约或删除临时凭据。
Open 可以通过 resp.Private 向前传递字节;Renew 和 Close 接收它们——用于续期/撤销所需的租约 ID。
实现
代码示例(Go):
var (
_ ephemeral.EphemeralResource = &tokenEphemeralResource{}
_ ephemeral.EphemeralResourceWithConfigure = &tokenEphemeralResource{}
_ ephemeral.EphemeralResourceWithRenew = &tokenEphemeralResource{}
_ ephemeral.EphemeralResourceWithClose = &tokenEphemeralResource{}
)
func NewTokenEphemeralResource() ephemeral.EphemeralResource {
return &tokenEphemeralResource{}
}
type tokenEphemeralResource struct {
client *examplecloud.Client
}
type tokenEphemeralResourceModel struct {
RoleName types.String `tfsdk:"role_name"`
Token types.String `tfsdk:"token"`
LeaseID types.String `tfsdk:"lease_id"`
}
func (r *tokenEphemeralResource) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_token"
}
func (r *tokenEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"role_name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Role to obtain a token for.",
},
"token": schema.StringAttribute{
Computed: true,
Sensitive: true,
MarkdownDescription: "The issued token. Never persisted to state.",
},
"lease_id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Identifier of the token lease.",
},
},
}
}
func (r *tokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
var data tokenEphemeralResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
lease, err := r.client.IssueToken(ctx, data.RoleName.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error opening Token",
fmt.Sprintf("issuing token for role (%s): %s", data.RoleName.ValueString(), err),
)
return
}
data.Token = types.StringValue(lease.Token)
data.LeaseID = types.StringValue(lease.ID)
resp.RenewAt = lease.ExpiresAt.Add(-2 * time.Minute) // renew with margin
resp.Private.SetKey(ctx, "lease_id", []byte(lease.ID))
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
}
func (r *tokenEphemeralResource) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {
leaseID, diags := req.Private.GetKey(ctx, "lease_id")
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
lease, err := r.client.RenewLease(ctx, string(leaseID))
if err != nil {
resp.Diagnostics.AddError("Error renewing Token", err.Error())
return
}
resp.RenewAt = lease.ExpiresAt.Add(-2 * time.Minute)
}
func (r *tokenEphemeralResource) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {
leaseID, diags := req.Private.GetKey(ctx, "lease_id")
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.RevokeLease(ctx, string(leaseID)); err != nil {
resp.Diagnostics.AddError("Error closing Token", err.Error())
}
}
Configure 遵循与资源相同的 ProviderData 类型转换模式(provider-resources 技能,如可用,展示了该模式);客户端来自 provider 的 Configure 中设置的 resp.EphemeralResourceData。
注册
Provider 通过 provider.ProviderWithEphemeralResources 选择加入:
var _ provider.ProviderWithEphemeralResources = &examplecloudProvider{}
func (p *examplecloudProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
NewTokenEphemeralResource(),
}
}
在 provider 的 Configure 中,与 ResourceData/DataSourceData 一起设置 resp.EphemeralResourceData = client。
设计规则
- 永远不要记录该值,永远不要将其放入诊断信息。整个要点就是非持久化;包含令牌的错误消息会破坏这一点。
- 仍然将机密属性标记为 Sensitive: true——它保护临时值自身生命周期输出中的渲染。
- 没有 plan 修饰符、没有导入、没有 id 约定——没有 state 可供它们作用。
- Schema 输入遵循与数据源参数相同的规则;暴露 API 的标识符(role_name),而不是编造的。
- 在真实过期前设置带安全余量的 RenewAt;Terraform 是惰性续期,而非精确计时器。
- 如果上游值无法撤销,跳过 Close,而不是实现一个暗示会撤销的空操作。
测试
临时结果永远不会到达 state,因此测试间接断言它们——标准模式通过 echoprovider 将临时值回显到测试可检查的常规资源中。最低覆盖:一个基本的打开并使用测试,以及针对必需字段的逐属性测试。使用 provider-test-patterns 技能(如可用)——其临时测试参考涵盖 echoprovider 设置、版本门控(tfversion.SkipBelow(tfversion.Version1_10_0))和多步骤模式。
文档
Registry 文档位于 docs/ephemeral-resources/<name>.md,由 tfplugindocs 像其他页面类型一样生成。使用 provider-docs 技能(如可用)了解工作流;明确记录续期/撤销行为——用户需要知道关闭 Terraform 运行是否会撤销凭据。
检查清单
- 值确实必须不持久化(否则数据源更简单)
- 已实现 Open;仅在 API 支持处实现 Renew/Close
- 机密属性 Sensitive: true;值从不记录或出现在诊断中
- 租约/句柄通过 Private 传递,而非通过结果
- 对会过期的凭据设置带余量的 RenewAt
- 在 EphemeralResources() 中注册;在 provider Configure 中设置 EphemeralResourceData
- Echo-provider 验收测试,版本门控到 Terraform >= 1.10
- 文档页面解释生命周期、续期和撤销行为