Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ agent-skills/
│ ├── new-terraform-provider/
│ ├── run-acceptance-tests/
│ ├── provider-actions/
│ ├── provider-ephemeral-resources/
│ └── provider-resources/
├── packer/
│ ├── builders/
Expand Down Expand Up @@ -112,6 +113,7 @@ npx skills add hashicorp/agent-skills/terraform/module-generation/skills/terrafo
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/new-terraform-provider
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/run-acceptance-tests
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-actions
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-ephemeral-resources
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-resources

# Packer builder skills
Expand Down Expand Up @@ -171,6 +173,7 @@ Skills for developing Terraform providers:
| `new-terraform-provider` | Scaffold a new Terraform provider |
| `run-acceptance-tests` | Run and debug provider acceptance tests |
| `provider-actions` | Implement provider actions (lifecycle operations) |
| `provider-ephemeral-resources` | Implement ephemeral resources for non-persisted secrets |
| `provider-resources` | Implement resources and data sources |

### packer-builders
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to the HashiCorp Agent Skills.
## Unreleased

### Added
- `provider-ephemeral-resources` skill for implementing ephemeral resources
- `terraform-search-import` skill for discovering existing resources with Terraform Search and bulk import

## 0.1.0
Expand Down
3 changes: 3 additions & 0 deletions terraform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Skills for developing Terraform providers.
| new-terraform-provider | Scaffold a new Terraform provider |
| run-acceptance-tests | Run and debug provider acceptance tests |
| provider-actions | Implement provider actions (lifecycle operations) |
| provider-ephemeral-resources | Implement ephemeral resources for non-persisted secrets |
| provider-resources | Implement resources and data sources |
| provider-test-patterns | Acceptance test patterns for terraform-plugin-testing |

Expand Down Expand Up @@ -65,6 +66,7 @@ npx skills add hashicorp/agent-skills/terraform/module-generation/skills/terrafo
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/new-terraform-provider
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/run-acceptance-tests
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-actions
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-ephemeral-resources
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-resources
npx skills add hashicorp/agent-skills/terraform/provider-development/skills/provider-test-patterns
```
Expand Down Expand Up @@ -99,6 +101,7 @@ terraform/
└── skills/
├── new-terraform-provider/
├── provider-actions/
├── provider-ephemeral-resources/
├── provider-resources/
├── run-acceptance-tests/
└── provider-test-patterns/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
---
name: provider-ephemeral-resources
description: >-
Implement Terraform provider ephemeral resources with the Plugin
Framework: the Open/Renew/Close lifecycle, ephemeral schema design,
registration via EphemeralResources, renewal for expiring credentials,
and how ephemeral values flow into write-only attributes and provider
configuration. Use when adding an ephemeral resource, exposing
secrets/tokens/certificates that must never persist in state or plan,
deciding between an ephemeral resource and a data source, or wiring
short-lived credentials from one provider into another.
license: MPL-2.0
metadata:
copyright: Copyright IBM Corp. 2026
version: "0.0.1"
---

# 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](https://developer.hashicorp.com/terraform/plugin/framework/ephemeral-resources).

## When to Use One

| Situation | Use |
|---|---|
| Read-only lookup of non-sensitive data | Data 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

```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`.

## 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`.

## 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
Loading