← All posts

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:

MethodWhen calledWhat it does
openStart of applyCreate the credential/session, return result to Terraform
renewBefore expiry (optional)Rotate the credential, extend the lease
closeEnd of applyRevoke 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:

  • PrivateState is frozen=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 in open.
  • s_resource is the schema helper for ephemeral resources (there’s no separate s_ephemeral_resource).
  • sensitive=True on 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

MethodRequiredPurpose
get_schemayesDeclares config inputs and result outputs
openyesCreate the credential, return (result, private_state, renew_at)
renewyesRotate before expiry, return (new_private_state, new_renew_at)
closeyesRevoke and clean up
validatenoReturn 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: