provider-ephemeral-resources

Implement Terraform provider ephemeral resources with the Plugin Framework: the Open/Renew/Close lifecycle, ephemeral schema design, registration via…

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

Mehr Skills von hashicorp

provider-actions
hashicorp
Implement Terraform Provider actions using the Plugin Framework. Use when developing imperative operations that execute at lifecycle events (before/after…
official
new-terraform-provider
hashicorp
Use this when scaffolding a new Terraform provider with the Plugin Framework: workspace layout, go module setup, provider server main.go, and a provider.go…
official
terraform-test
hashicorp
Comprehensive guide for writing and running Terraform tests. Use when creating test files (.tftest.hcl), writing test scenarios with run blocks, validating…
official
terraform-test
hashicorp
Umfassender Leitfaden zum Schreiben und Ausführen von Terraform-Tests mit Assertions, Mocking und Modulvalidierung. Schreiben Sie Testdateien mit der .tftest.hcl-Syntax und Run-Blöcken, die im Plan- oder Apply-Modus ausgeführt werden, und unterstützen sequenzielle und parallele Ausführung mit optionaler Zustandsisolation. Bedingungen für Ressourcenattribute, Ausgaben und Datenquellen prüfen; verwenden Sie expect_failures, um zu validieren, dass ungültige Eingaben ordnungsgemäß abgewiesen werden. Mock-Provider (Terraform 1.7.0+) simulieren Infrastrukturverhalten ohne...
official
provider-actions
hashicorp
Implementieren von imperativen Terraform-Provider-Aktionen bei Ressourcen-Lebenszyklus-Ereignissen unter Verwendung des Plugin-Frameworks. Unterstützt Vorher/Nachher-Erstellungs- und Vorher/Nachher-Aktualisierungs-Lebenszyklus-Trigger (Lösch-Ereignisse sind in Terraform 1.14.0 nicht verfügbar). Erfordert eine korrekte Schema-Definition mit den richtigen Framework-Typen, ElementType für Sammlungen und Validatoren für die Eingabevalidierung. Beinhaltet Fortschrittsberichterstattung, Timeout-Verwaltung und umfassende Fehlerbehandlung für langlaufende Operationen. Implementiert Polling und...
official
aws-ami-builder
hashicorp
Erstellt benutzerdefinierte Amazon Machine Images mit Packer's amazon-ebs Builder. Automatisiert die AMI-Erstellung aus Quell-AMIs mittels HCL-Vorlagen mit Provisionern zur Anpassung (Shell-Skripte, Datei-Uploads, Konfigurationsmanagement). Unterstützt Multi-Region-AMI-Verteilung über ami_regions und flexible Quell-AMI-Filterung nach Name, Besitzer und Virtualisierungstyp. Authentifiziert über Umgebungsvariablen, AWS-Anmeldedatei oder IAM-Instanzprofile; enthält Validierungs- und Build-Befehle für Vorlagen...
official
new-terraform-provider
hashicorp
Erstellt ein neues Terraform-Provider-Gerüst mit dem Plugin-Framework. Generiert einen neuen Go-Modul-Workspace mit der standardmäßigen "terraform-provider-"-Namenskonvention und initialisiert erforderliche Abhängigkeiten. Stellt eine Vorlage für die main.go-Datei bereit, die den HashiCorp-Plugin-Framework-Mustern folgt, mit TODO-Markierungen zur Anpassung. Validiert das Setup durch Ausführen von Build- und Testbefehlen, um sicherzustellen, dass der Provider kompiliert und erste Prüfungen besteht. Verwaltet den Workspace, indem die Absicht vor der Erstellung eines neuen... bestätigt wird.
official
azure-verified-modules
hashicorp
Zertifizierungsanforderungen und Best Practices für Azure Terraform-Module, die AVM-Konformität anstreben. Erzwingt Provider-Versionseinschränkungen (azurerm >= 4.0, < 5.0; azapi >= 2.0, < 3.0) und verbietet git-basierte Modulreferenzen zugunsten von festgelegten Terraform-Registry-Quellen. Schreibt Klein-Schlangenschreibweise für alle Bezeichner, präzise Variablentypen, diskrete Ausgabeattribute mittels Anti-Korruptionsschicht-Muster und alphabetisch geordnete Lokale vor. Erfordert Feature-Toggle-Variablen für neu hinzugefügte Ressourcen...
official