mcpField
P
provider-ephemeral-resources

provider-ephemeral-resources

github.com/hashicorp开发909951 次安装今天更新
代码仓库

关于 provider-ephemeral-resources

面向 Terraform 1.10+ Provider 的临时资源实现技能,讲解如何传递短期敏感值而不将其写入状态或计划,并涵盖生命周期、注册、测试和文档。

主要功能

  1. 01Open、Renew、Close 生命周期
  2. 02通过 Private 传递租约信息
  3. 03注册临时资源
  4. 04回显式测试方法
  5. 05生命周期文档指南

适用场景

  1. 01传递短期访问令牌
  2. 02生成不写入状态的密码
  3. 03管理可续期凭据租约

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/hashicorp/agent-skills --skill provider-ephemeral-resources

安装后在 Claude Code 中输入/provider-ephemeral-resources确认已加载。

详细介绍

Terraform Provider 短暂资源

短暂资源(Terraform 1.10+)生成的值绝不会持久化到状态或计划中。它们只用于一件事:将密钥——令牌、生成的密码、短期证书、解密后的值——传递给配置中需要它们的部分,而不将其写入磁盘。任何返回敏感值的数据源都可以考虑改为短暂资源,或同时提供短暂资源。

官方文档:短暂资源。

何时使用

情况用途
只读查询非敏感数据数据源
值是敏感的,且只在 apply 时需要(例如 provider 配置块中的数据库密码、只写属性所需的令牌)短暂资源
下游托管资源必须存储的敏感值(例如作为某个属性)常规资源/数据源——但尽可能搭配只写属性使用
操作过程中会过期的凭据(STS 风格的令牌、短 TTL 租约)带有 Renew 的短暂资源

短暂资源的结果可用于 provider 配置、只写属性、置备程序配置及其他短暂上下文,但不能用于常规属性,因为这些属性会持久化到状态中。

生命周期

Terraform 在每次操作中最多会调用三个方法:

  • Open(必需)——获取或创建值;在需要结果的计划和/或应用期间运行。没有状态可供刷新,也无需导入。
  • Renew(可选)——当系统时钟超过 Open/Renew 返回的 RenewAt 时调用,适用于 Terraform 仍在运行时会过期的值。Renew 不能返回新结果,只能延长/刷新 Open 生成的值(例如重新租赁相同的凭据);如果值本身在续期时会改变,那么从这个意义上说,该 API 不支持续期,Open 必须返回有效期更长的值。
  • Close(可选)——Terraform 不再需要该值时调用;可在此撤销租约或删除临时凭据。

Open 可以通过 resp.Private 传递字节;Renew 和 Close 会接收这些字节——可用它传递续期/撤销所需的租约 ID。

实现

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 中设置 resp.EphemeralResourceData = client,并与 ResourceData/DataSourceData 一并设置。

设计规则

  • 绝不记录该值,也绝不将其写入诊断信息。 不持久化正是短暂资源的意义所在;错误消息中如果包含令牌,就违背了这一点。
  • 仍应将密钥属性标记为 Sensitive: true——这可以避免在短暂值自身的生命周期输出中渲染该值。
  • 不使用计划修饰器,不支持导入,也没有 id 惯例——这些机制都没有状态可供操作。
  • 架构输入遵循与数据源参数相同的规则;公开 API 的标识符(role_name),而非自行发明的标识符。
  • 设置 RenewAt 时,应在实际过期时间之前留出安全余量;Terraform 采用延迟续期,而不是按精确计时器续期。
  • 如果上游值无法撤销,请省略 Close,而不要实现一个暗示会执行撤销的空操作。

测试

短暂资源的结果不会进入状态,因此测试需间接验证这些结果——标准做法是通过 echoprovider 将短暂值回显到测试可以检查的常规资源中。最低限度的覆盖范围:基本的打开并使用测试,以及与必需字段一同进行的逐属性测试。请使用 provider-test-patterns 技能(如果可用)——其中的短暂资源测试参考涵盖了 echoprovider 设置、版本门控(tfversion.SkipBelow(tfversion.Version1_10_0))和多步骤模式。

文档

注册表文档位于 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 进行版本门控
  • 文档页面说明有效期、续期和撤销行为

评论

支持 Markdown