provider-ephemeral-resources

작성자: hashicorp

Terraform 공급자 임시 리소스를 Plugin Framework로 구현: Open/Renew/Close 수명주기, 임시 스키마 설계, 등록 방법…

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

Terraform Provider Ephemeral Resources

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.

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.

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.

Implementation

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.

Registration

The provider opts in via provider.ProviderWithEphemeralResources:

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.

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.

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.

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.

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

hashicorp의 다른 스킬

provider-framework-migration
hashicorp
Terraform 제공자 리소스와 데이터 소스를 Plugin SDKv2에서 Plugin Framework로 마이그레이션: 하나의 제공자에서 두 플러그인을 멀티플렉싱(terraform-plugin-mux, …)
provider-configuration
hashicorp
Terraform provider 구성 및 인증을 Plugin Framework로 구현: 자격 증명을 위한 provider 스키마(Optional + Sensitive 속성),…
terraform-test
hashicorp
Terraform 테스트 작성 및 실행을 위한 종합 가이드로, 어설션, 모킹, 모듈 검증을 포함합니다. .tftest.hcl 구문을 사용하여 테스트 파일을 작성하며, plan 또는 apply 모드로 실행되는 run 블록을 지원하고, 선택적 상태 격리와 함께 순차 및 병렬 실행을 지원합니다. 리소스 속성, 출력, 데이터 소스에 대한 조건을 어설션하고, expect_failures를 사용하여 잘못된 입력이 적절히 거부되는지 검증합니다. Mock 제공자(Terraform 1.7.0+)는 인프라 동작을 시뮬레이션합니다...
terraform-policy
hashicorp
Terraform Policy 파일(.policy.hcl, .policytest.hcl, Sentinel→tfpolicy)을 작성, 테스트 또는 변환합니다. 트리거: policy.hcl, policytest, convert sentinel, tfpolicy,…
terraform-search-import
hashicorp
Terraform Search 쿼리를 사용하여 기존 클라우드 리소스를 검색하고 이를 Terraform 관리로 대량 가져옵니다. 관리되지 않는 인프라를 도입할 때 사용합니다…
aws-ami-builder
hashicorp
Packer의 amazon-ebs 빌더로 사용자 지정 Amazon 머신 이미지를 구축합니다. HCL 템플릿과 프로비저너(셸 스크립트, 파일 업로드, 구성 관리)를 사용해 소스 AMI에서 AMI 생성을 자동화합니다. ami_regions를 통한 다중 리전 AMI 배포와 이름, 소유자, 가상화 유형별 유연한 소스 AMI 필터링을 지원합니다. 환경 변수, AWS 자격 증명 파일 또는 IAM 인스턴스 프로파일을 통해 인증하며 템플릿 검증 및 빌드 명령을 포함합니다...
tfctl
hashicorp
tfctl CLI를 사용하여 HCP Terraform / Terraform Cloud / Terraform Enterprise와 상호작용합니다. 전체 API를 지원합니다. 모든 HCP Terraform 또는 Terraform Cloud 또는…에 사용하세요.
provider-actions
hashicorp
Plugin Framework를 사용하여 리소스 수명 주기 이벤트에서 명령형 Terraform Provider 작업을 구현합니다. 생성 전/후 및 업데이트 전/후 수명 주기 트리거를 지원합니다(소멸 이벤트는 Terraform 1.14.0에서 사용 불가). 올바른 프레임워크 유형, 컬렉션용 ElementType, 입력 검증용 유효성 검사기를 포함한 적절한 스키마 정의가 필요합니다. 장기 실행 작업을 위한 진행 보고, 타임아웃 관리, 포괄적인 오류 처리를 포함합니다. 폴링 및...