provider-ephemeral-resources

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Terraform Provider Ephemeral Resources

Terraform Provider 临时资源

Ephemeral resources (Terraform 1.10+) produce values that are never persisted to state or plan. They exist for exactly one job: handing secrets — tokens, generated passwords, short-lived certificates, decrypted values — to the parts of a configuration that need them, without writing them to disk. Any data source that returns a sensitive value is a candidate to be (or to also exist as) an ephemeral resource.
Official docs: Ephemeral Resources.
临时资源(Terraform 1.10+)生成的值绝不会持久化到状态或计划中。它们的唯一作用是将密钥——令牌、生成的密码、短期证书、解密后的值——传递给配置中需要这些内容的部分,同时不会将其写入磁盘。任何返回敏感值的数据源都可以作为(或同时作为)临时资源的候选。
官方文档:Ephemeral Resources

When to Use One

使用场景

SituationUse
Read-only lookup of non-sensitive dataData source
Value is sensitive and only needed at apply time (DB password for a provider block, token for a write-only attribute)Ephemeral resource
Sensitive value that downstream managed resources must store (e.g. as an attribute)Regular resource/data source — but pair with write-only attributes where possible
Credential that expires mid-operation (STS-style tokens, short-TTL leases)Ephemeral resource with
Renew
Ephemeral results can be used in provider configuration, write-only attributes, provisioner configuration, and other ephemeral contexts — but not in regular attributes, because those persist to state.
场景选择方案
非敏感数据的只读查询数据源
值为敏感数据且仅在应用阶段需要(Provider块的数据库密码、只写属性的令牌)临时资源
下游托管资源必须存储的敏感值(例如作为属性)常规资源/数据源——但尽可能搭配只写属性
操作中途过期的凭证(STS风格令牌、短TTL租期)
Renew
的临时资源
临时结果可用于Provider配置、只写属性、置备器配置和其他临时上下文——但不能用于常规属性,因为这些属性会持久化到状态中。

Lifecycle

生命周期

Terraform calls up to three methods per operation:
  • Open
    (required) — fetch or create the value; runs during plan and/or apply whenever the result is needed. There is no state to refresh and nothing to import.
  • Renew
    (optional) — called when the wall clock passes the
    RenewAt
    returned by
    Open
    /
    Renew
    , for values that expire while Terraform is still running. Renew cannot return a new result — it can only extend/refresh what
    Open
    produced (e.g. re-lease the same credential); if the value itself changes on renewal, the API is not renewable in this sense and
    Open
    must return a longer-lived value.
  • Close
    (optional) — called when Terraform is done with the value; revoke leases or delete temporary credentials here.
Open
can pass bytes forward via
resp.Private
;
Renew
and
Close
receive them — use this for lease IDs needed to renew/revoke.
Terraform在每次操作中最多调用三个方法:
  • Open
    (必填)——获取或创建值;在需要结果的计划和/或应用阶段运行。无需刷新状态,也没有可导入的内容。
  • Renew
    (可选)——当系统时间超过
    Open
    /
    Renew
    返回的
    RenewAt
    时调用,适用于Terraform运行期间过期的值。Renew不能返回新结果——只能扩展/刷新
    Open
    生成的内容(例如续约相同凭证);如果续约时值本身发生变化,则该API不支持此类续约,
    Open
    必须返回更长期限的值。
  • Close
    (可选)——当Terraform不再需要该值时调用;在此处撤销租期或删除临时凭证。
Open
可通过
resp.Private
传递字节数据;
Renew
Close
会接收这些数据——可将续约/撤销所需的租期ID存放在这里。

Implementation

实现代码

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
follows the same ProviderData-cast pattern as resources (the
provider-resources
skill, if available, shows it); the client comes from
resp.EphemeralResourceData
set in the provider's
Configure
.
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

Registration

注册方式

The provider opts in via
provider.ProviderWithEphemeralResources
:
go
var _ provider.ProviderWithEphemeralResources = &examplecloudProvider{}

func (p *examplecloudProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
    return []func() ephemeral.EphemeralResource{
        NewTokenEphemeralResource(),
    }
}
Set
resp.EphemeralResourceData = client
in the provider's
Configure
alongside
ResourceData
/
DataSourceData
.
Provider通过
provider.ProviderWithEphemeralResources
启用该功能:
go
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

Design Rules

设计规则

  • Never log the value, never put it in a diagnostic. The whole point is non-persistence; an error message containing the token defeats it.
  • Mark the secret attribute
    Sensitive: true
    anyway — it guards rendering in the ephemeral value's own lifecycle output.
  • No plan modifiers, no import, no
    id
    convention — there is no state for any of them to act on.
  • Schema inputs follow the same rules as data source arguments; expose the API's identifiers (
    role_name
    ), not invented ones.
  • Set
    RenewAt
    with a safety margin before the real expiry; Terraform renews lazily, not on a precise timer.
  • If the upstream value cannot be revoked, skip
    Close
    rather than implementing a no-op that suggests revocation happens.
  • 绝不要记录该值,也不要将其放入诊断信息中。临时资源的核心就是不持久化;包含令牌的错误消息会完全违背这一目的。
  • 仍需将密钥属性标记为
    Sensitive: true
    ——这可以防止在临时值自身的生命周期输出中显示该值。
  • 无需计划修饰符、导入功能或
    id
    约定——因为没有可供这些功能作用的状态。
  • Schema输入遵循与数据源参数相同的规则;暴露API的标识符(如
    role_name
    ),而非自定义的标识符。
  • 设置
    RenewAt
    时要预留安全余量,早于实际过期时间;Terraform会延迟续约,而非精确按时续约。
  • 如果上游值无法撤销,则跳过
    Close
    ,而非实现一个空操作,避免让用户误以为会执行撤销操作。

Testing

测试方法

Ephemeral results never reach state, so tests assert them indirectly — the standard pattern echoes the ephemeral value through the
echoprovider
into a regular resource the test can inspect. Minimum coverage: a basic open-and-use test and per-attribute tests alongside required fields. Use the
provider-test-patterns
skill (if available) — its ephemeral testing reference covers the echoprovider setup, version gating (
tfversion.SkipBelow(tfversion.Version1_10_0)
), and multi-step patterns.
临时结果永远不会进入状态,因此测试需间接断言——标准模式是通过
echoprovider
将临时值传递到常规资源中,再由测试检查该资源。最低测试覆盖范围:基础的打开与使用测试,以及必填字段的逐属性测试。使用
provider-test-patterns
技能(若可用)——其临时测试参考内容涵盖了echoprovider设置、版本控制(
tfversion.SkipBelow(tfversion.Version1_10_0)
)和多步骤模式。

Documentation

文档编写

Registry docs live at
docs/ephemeral-resources/<name>.md
, generated by
tfplugindocs
like every other page type. Use the
provider-docs
skill (if available) for the workflow; document the renewal/revocation behavior explicitly — users need to know whether closing their Terraform run revokes the credential.
Registry文档存放在
docs/ephemeral-resources/<name>.md
,由
tfplugindocs
生成,与其他类型页面的生成方式一致。使用
provider-docs
技能(若可用)完成工作流;需明确记录续约/撤销行为——用户需要知道关闭Terraform运行时是否会撤销凭证。

Checklist

检查清单

  • Value genuinely must not persist (otherwise a data source is simpler)
  • Open
    implemented;
    Renew
    /
    Close
    only where the API supports them
  • Secret attributes
    Sensitive: true
    ; value never logged or in diagnostics
  • Lease/handle passed via
    Private
    , not via the result
  • RenewAt
    set with margin for expiring credentials
  • Registered in
    EphemeralResources()
    ;
    EphemeralResourceData
    set in provider Configure
  • Echo-provider acceptance tests, version-gated to Terraform >= 1.10
  • Docs page explains lifetime, renewal, and revocation behavior
  • 值确实必须不持久化(否则使用数据源更简单)
  • 已实现
    Open
    ;仅在API支持时实现
    Renew
    /
    Close
  • 密钥属性已标记
    Sensitive: true
    ;值从未被记录或放入诊断信息
  • 租期/句柄通过
    Private
    传递,而非通过结果传递
  • 针对过期凭证设置了带余量的
    RenewAt
  • 已在
    EphemeralResources()
    中注册;已在Provider的Configure中设置
    EphemeralResourceData
  • 回声Provider验收测试,版本限制为Terraform >= 1.10
  • 文档页面说明了生命周期、续约和撤销行为