Building Your First Ephemeral Resource
This is part 4 of a four-part series. The earlier parts cover a resource, a data source, and a function.
The provider we’ve built so far manages durable state: servers exist between Terraform runs, and their IDs are stored in .tfstate. Some infrastructure is intentionally short-lived — access tokens, database sessions, TLS certificates. That’s what ephemeral resources are for.
Ephemeral resources are never written to state. Terraform creates them fresh each run, keeps them alive during the apply, and destroys them when it’s done.
The Lifecycle
Regular resources have create → read → update → delete. Ephemeral resources have:
| Method | When called | What it does |
|---|---|---|
open | Start of apply | Create the credential/session, return result to Terraform |
renew | Before expiry (optional) | Rotate the credential, extend the lease |
close | End of apply | Revoke the credential, clean up |
Add the Ephemeral Resource
Create my_provider/session_token.py:
import secrets
from datetime import datetime, timedelta, timezone
import attrs
from pyvider.ephemerals import BaseEphemeralResource, EphemeralResourceContext, register_ephemeral_resource
from pyvider.resources.private_state import PrivateState
from pyvider.schema import PvsSchema, a_num, a_str, s_resource
from my_provider.server import Server
@attrs.define(frozen=True)
class SessionTokenConfig:
server_id: str
ttl_seconds: int = 3600
@attrs.define(frozen=True)
class SessionTokenResult:
token: str # sensitive — Terraform won't log this
token_id: str
expires_at: str
@attrs.define(frozen=True)
class SessionTokenPrivateState(PrivateState):
"""Stored encrypted in Terraform's working memory (never in .tfstate)."""
token: str
token_id: str
ttl_seconds: int
@register_ephemeral_resource("session_token")
class SessionToken(BaseEphemeralResource):
"""Temporary access token for a mycloud server."""
config_class = SessionTokenConfig
result_class = SessionTokenResult
private_state_class = SessionTokenPrivateState
@classmethod
def get_schema(cls) -> PvsSchema:
return s_resource({
# Inputs
"server_id": a_str(required=True, description="Server to grant access to"),
"ttl_seconds": a_num(optional=True, description="Token lifetime in seconds"),
# Outputs (computed, never stored in .tfstate)
"token": a_str(computed=True, sensitive=True, description="Access token"),
"token_id": a_str(computed=True, description="Token identifier"),
"expires_at": a_str(computed=True, description="ISO-8601 expiry timestamp"),
})
async def validate(self, config: SessionTokenConfig) -> list[str]:
errors = []
if config.server_id not in Server._servers:
errors.append(f"server {config.server_id!r} does not exist")
if config.ttl_seconds < 60:
errors.append("ttl_seconds must be at least 60")
return errors
async def open(
self, ctx: EphemeralResourceContext[SessionTokenConfig, None]
) -> tuple[SessionTokenResult, SessionTokenPrivateState, datetime]:
token_id = f"tok-{secrets.token_hex(6)}"
token = secrets.token_urlsafe(32)
expires_at = datetime.now(timezone.utc) + timedelta(seconds=ctx.config.ttl_seconds)
result = SessionTokenResult(token=token, token_id=token_id, expires_at=expires_at.isoformat())
private = SessionTokenPrivateState(token=token, token_id=token_id, ttl_seconds=ctx.config.ttl_seconds)
return result, private, expires_at
async def renew(
self, ctx: EphemeralResourceContext[None, SessionTokenPrivateState]
) -> tuple[SessionTokenPrivateState, datetime]:
new_token = secrets.token_urlsafe(32)
expires_at = datetime.now(timezone.utc) + timedelta(seconds=ctx.private_state.ttl_seconds)
new_private = SessionTokenPrivateState(
token=new_token,
token_id=ctx.private_state.token_id,
ttl_seconds=ctx.private_state.ttl_seconds,
)
return new_private, expires_at
async def close(
self, ctx: EphemeralResourceContext[None, SessionTokenPrivateState]
) -> None:
pass # real provider: revoke ctx.private_state.token via API
A few things worth noting:
PrivateStateisfrozen=True— each renewal produces a new object rather than mutating in place.validate()(not_validate_config()) is the hook for ephemeral resources. It has a default that returns[], so it’s optional — but validating early beats failing inopen.s_resourceis the schema helper for ephemeral resources (there’s no separates_ephemeral_resource).sensitive=Trueon the token means Terraform redacts it from plan output and logs.
Register It
Add the import to my_provider/__init__.py:
from pyvider.providers import BaseProvider, ProviderMetadata, register_provider
from pyvider.schema import PvsSchema, s_provider
import my_provider.server
import my_provider.server_info
import my_provider.names
import my_provider.session_token # ← new
@register_provider("mycloud")
class MyCloudProvider(BaseProvider):
def __init__(self) -> None:
super().__init__(
metadata=ProviderMetadata(name="mycloud", version="0.1.0", protocol_version="6")
)
@classmethod
def get_schema(cls) -> PvsSchema:
return s_provider({})
The Terraform Configuration
Ephemeral resources use the ephemeral block (available in OpenTofu 1.10+ / Terraform 1.10+):
terraform {
required_providers {
mycloud = {
source = "example.com/tutorial/mycloud"
version = "0.1.0"
}
}
}
provider "mycloud" {}
locals {
server_name = provider::mycloud::generate_name("web", "prod")
}
resource "mycloud_server" "web" {
name = local.server_name
}
# Ephemeral — created each run, never in .tfstate
ephemeral "mycloud_session_token" "web_access" {
server_id = mycloud_server.web.id
ttl_seconds = 900
}
data "mycloud_server_info" "web" {
server_id = mycloud_server.web.id
}
output "token_id" { value = ephemeral.mycloud_session_token.web_access.token_id }
output "server_status" { value = data.mycloud_server_info.web.status }
# Note: the token itself is sensitive and won't appear in outputs
Terraform calls open at the start of apply (after the server exists), makes the result available during the run, then calls close at the end. The token never touches .tfstate.
Required Methods
| Method | Required | Purpose |
|---|---|---|
get_schema | yes | Declares config inputs and result outputs |
open | yes | Create the credential, return (result, private_state, renew_at) |
renew | yes | Rotate before expiry, return (new_private_state, new_renew_at) |
close | yes | Revoke and clean up |
validate | no | Return errors early; default returns [] |
All four abstract methods must be implemented even if renew and close are no-ops for your use case.
What’s in the Complete Provider
my_provider/
├── __init__.py # provider class + component imports
├── server.py # mycloud_server (resource)
├── server_info.py # mycloud_server_info (data source)
├── names.py # provider::mycloud::generate_name (function)
└── session_token.py # mycloud_session_token (ephemeral resource)
Where to go next:
- Full documentation — complete API reference
- Demo provider — working example with all four component types
- pyvider-components — production-ready components to study or reuse