provider-ephemeral-resources

Par hashicorp · agent-skills

Implémenter des ressources éphémères pour un provider Terraform avec le Plugin Framework : le cycle de vie Open/Renew/Close, la conception du schéma éphémère, l'enregistrement via `EphemeralResources`, le renouvellement pour les identifiants à expiration, et la façon dont les valeurs éphémères circulent vers les attributs en écriture seule et la configuration du provider. À utiliser lors de l'ajout d'une ressource éphémère, de l'exposition de secrets/tokens/certificats qui ne doivent jamais persister dans l'état ou le plan, du choix entre une ressource éphémère et une data source, ou du câblage d'identifiants de courte durée d'un provider vers un autre.

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

Ressources Éphémères du Fournisseur Terraform

Les ressources éphémères (Terraform 1.10+) produisent des valeurs qui ne sont jamais persistées dans l'état ou le plan. Elles existent pour une seule raison : transmettre des secrets — tokens, mots de passe générés, certificats de courte durée, valeurs déchiffrées — aux parties de la configuration qui en ont besoin, sans les écrire sur le disque. Toute source de données qui retourne une valeur sensible est candidate pour être (ou aussi exister comme) une ressource éphémère.

Documentation officielle : Ephemeral Resources.

Quand en utiliser une

Situation Utiliser
Recherche en lecture seule de données non sensibles Source de données
Valeur sensible et nécessaire uniquement au moment de l'apply (mot de passe DB pour un bloc provider, token pour un attribut en écriture seule) Ressource éphémère
Valeur sensible que les ressources gérées en aval doivent stocker (par ex. comme attribut) Ressource/source de données classique — mais utiliser des attributs en écriture seule si possible
Credential qui expire en cours d'opération (tokens style STS, leases courte durée) Ressource éphémère avec Renew

Les résultats éphémères peuvent être utilisés dans la configuration provider, les attributs en écriture seule, la configuration du provisioner et autres contextes éphémères — mais pas dans les attributs classiques, car ceux-ci persistent dans l'état.

Cycle de vie

Terraform appelle jusqu'à trois méthodes par opération :

  • Open (obligatoire) — récupérer ou créer la valeur ; s'exécute pendant le plan et/ou l'apply chaque fois que le résultat est nécessaire. Il n'y a pas d'état à rafraîchir et rien à importer.
  • Renew (optionnel) — appelé quand l'horloge système dépasse le RenewAt retourné par Open/Renew, pour les valeurs qui expirent pendant que Terraform s'exécute. Renew ne peut pas retourner un nouveau résultat — il peut seulement étendre/rafraîchir ce que Open a produit (par ex. relancer le même credential) ; si la valeur elle-même change au renouvellement, l'API n'est pas renouvelable dans ce sens et Open doit retourner une valeur plus durable.
  • Close (optionnel) — appelé quand Terraform a fini avec la valeur ; révoquer les leases ou supprimer les credentials temporaires ici.

Open peut transmettre des octets via resp.Private ; Renew et Close les reçoivent — utiliser cela pour les IDs de lease nécessaires au renouvellement/révocation.

Implémentation

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 suit le même pattern de transtypage ProviderData que les ressources (la skill provider-resources, si disponible, le montre) ; le client provient de resp.EphemeralResourceData défini dans Configure du provider.

Enregistrement

Le provider se déclare via provider.ProviderWithEphemeralResources :

var _ provider.ProviderWithEphemeralResources = &examplecloudProvider{}

func (p *examplecloudProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource {
    return []func() ephemeral.EphemeralResource{
        NewTokenEphemeralResource(),
    }
}

Définir resp.EphemeralResourceData = client dans Configure du provider à côté de ResourceData/DataSourceData.

Règles de conception

  • Ne jamais logger la valeur, ne jamais la mettre dans un diagnostic. L'idée est la non-persistance ; un message d'erreur contenant le token le défait.
  • Marquer quand même l'attribut secret avec Sensitive: true — cela protège le rendu dans la sortie du cycle de vie de la valeur éphémère elle-même.
  • Pas de plan modifiers, pas d'import, pas de convention id — il n'y a pas d'état sur lequel ils agiraient.
  • Les entrées du schéma suivent les mêmes règles que les arguments de source de données ; exposer les identifiants de l'API (role_name), pas d'identifiants inventés.
  • Définir RenewAt avec une marge de sécurité avant l'expiration réelle ; Terraform renouvelle paresseusement, pas sur une minuterie précise.
  • Si la valeur en amont ne peut pas être révoquée, ignorer Close plutôt que d'implémenter un no-op qui suggère que la révocation se produit.

Tests

Les résultats éphémères ne rejoignent jamais l'état, donc les tests les affirment indirectement — le pattern standard fait échouer la valeur éphémère à travers le echoprovider dans une ressource classique que le test peut inspecter. Couverture minimale : un test basique open-and-use et des tests par attribut à côté des champs obligatoires. Utiliser la skill provider-test-patterns (si disponible) — sa référence de test éphémère couvre la configuration echoprovider, le version gating (tfversion.SkipBelow(tfversion.Version1_10_0)), et les patterns multi-étapes.

Documentation

La documentation du registre se trouve dans docs/ephemeral-resources/<name>.md, générée par tfplugindocs comme tout autre type de page. Utiliser la skill provider-docs (si disponible) pour le workflow ; documenter explicitement le comportement de renouvellement/révocation — les utilisateurs doivent savoir si fermer leur exécution Terraform révoque le credential.

Checklist

  • [ ] Valeur qui ne doit vraiment pas persister (sinon une source de données est plus simple)
  • [ ] Open implémenté ; Renew/Close seulement où l'API les supporte
  • [ ] Attributs secret avec Sensitive: true ; valeur jamais loggée ou dans les diagnostics
  • [ ] Lease/handle transmis via Private, pas via le résultat
  • [ ] RenewAt défini avec marge pour les credentials expirant
  • [ ] Enregistré dans EphemeralResources() ; EphemeralResourceData défini dans provider Configure
  • [ ] Tests d'acceptance echo-provider, version-gatés à Terraform >= 1.10
  • [ ] Page de docs expliquant la durée de vie, le renouvellement et le comportement de révocation

Skills similaires