mcpField
P
provider-ephemeral-resources

provider-ephemeral-resources

github.com/hashicorp開発909951 回インストール今日更新
リポジトリ

provider-ephemeral-resources について

Terraform 1.10 以降の Provider で一時リソースを実装するスキルです。短命な機密値を state や plan に保存せず渡す方法を、ライフサイクル、登録、テスト、ドキュメントとともに説明します。

主な機能

  1. 01Open、Renew、Close のライフサイクル
  2. 02Private によるリース情報の受け渡し
  3. 03一時リソースの登録
  4. 04エコープロバイダーによるテスト
  5. 05ライフサイクルの文書化

活用シーン

  1. 01短命なアクセストークンの受け渡し
  2. 02state に保存しないパスワードの生成
  3. 03更新可能な認証情報リースの管理

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/provider-ephemeral-resourcesを入力して読み込まれたか確認します。

詳細

Terraform プロバイダーのエフェメラルリソース

エフェメラルリソース(Terraform 1.10 以降)は、ステートにもプランにも決して保存されない値を生成します。役割はただ一つ、必要とする構成の各部分に、シークレット(トークン、生成されたパスワード、有効期間の短い証明書、復号された値など)をディスクに書き込まずに渡すことです。機密値を返すデータソースは、エフェメラルリソースにする、またはエフェメラルリソースとしても提供する候補になります。

公式ドキュメント:Ephemeral Resources。

使用するタイミング

状況使用するもの
機密ではないデータの読み取り専用の検索データソース
値が機密で、適用時にのみ必要(プロバイダーブロックの DB パスワード、書き込み専用属性に渡すトークンなど)エフェメラルリソース
下流の管理対象リソースが保存する必要のある機密値(属性として保存する場合など)通常のリソース/データソース。ただし、可能な限り書き込み専用属性と組み合わせてください
操作の途中で期限切れになる認証情報(STS 形式のトークン、有効期間の短いリースなど)Renew を備えたエフェメラルリソース

エフェメラルな結果は、プロバイダー構成、書き込み専用属性、プロビジョナー構成、その他のエフェメラルコンテキストで使用できます。ただし、ステートに保存される通常の属性には使用できません。

ライフサイクル

Terraform は、各操作で最大 3 つのメソッドを呼び出します。

  • Open(必須)— 値を取得または作成します。結果が必要になるたびに、プラン時および/または適用時に実行されます。更新するステートも、インポートするものもありません。
  • Renew(任意)— Terraform の実行中に値が期限切れになる場合、Open/Renew が返した RenewAt を現在時刻が過ぎると呼び出されます。Renew は新しい結果を返せません。Open が生成した値を延長/更新することしかできません(例:同じ認証情報を再リースする)。更新時に値自体が変わる場合、この意味で更新可能な API ではないため、Open はより有効期間の長い値を返す必要があります。
  • Close(任意)— Terraform が値を使い終えたときに呼び出されます。リースの取り消しや一時的な認証情報の削除は、ここで行います。

Open は resp.Private を介してバイト列を渡せます。Renew と Close はそのバイト列を受け取るため、更新/取り消しに必要なリース ID などに使用します。

実装

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 はリソースと同じ ProviderData のキャストパターンに従います(利用可能な場合は provider-resources スキルで確認できます)。クライアントは、プロバイダーの Configure で設定された resp.EphemeralResourceData から取得します。

登録

プロバイダーは provider.ProviderWithEphemeralResources を介してオプトインします。

var _ provider.ProviderWithEphemeralResources = &examplecloudProvider{}

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

ResourceData/DataSourceData とともに、プロバイダーの Configure で resp.EphemeralResourceData = client を設定します。

設計ルール

  • 値を決してログに記録せず、診断情報にも含めないでください。 永続化しないことが目的です。トークンを含むエラーメッセージは、その目的を台無しにします。
  • それでもシークレット属性には Sensitive: true を設定してください。エフェメラル値自体のライフサイクル出力での表示を防ぎます。
  • プランモディファイアもインポートも id の規約もありません。適用対象となるステートがないためです。
  • スキーマ入力には、データソースの引数と同じルールを適用します。独自に考案した識別子ではなく、API の識別子(role_name など)を公開してください。
  • 実際の有効期限より前に余裕を持たせて RenewAt を設定してください。Terraform の更新は遅延実行され、正確なタイマーに基づいて行われるわけではありません。
  • 上流の値を取り消せない場合は、取り消しが行われるかのような誤解を招く、何もしない Close を実装せず、省略してください。

テスト

エフェメラルな結果はステートに到達しないため、テストでは間接的に検証します。標準的な方法は、echoprovider を介してエフェメラル値を通常のリソースに渡し、テストで検証できるようにすることです。最低限、基本的なオープンと使用のテスト、および必須フィールドと併せた属性ごとのテストをカバーします。利用可能な場合は provider-test-patterns スキルを使用してください。エフェメラルリソースのテストに関するリファレンスでは、echoprovider のセットアップ、バージョンの条件指定(tfversion.SkipBelow(tfversion.Version1_10_0))、複数ステップのパターンを説明しています。

ドキュメント

レジストリのドキュメントは docs/ephemeral-resources/<name>.md にあり、他のページ種別と同様に tfplugindocs によって生成されます。ワークフローには、利用可能であれば provider-docs スキルを使用してください。更新と失効の動作を明記してください。 Terraform の実行を終了したときに認証情報が失効するかどうかを、ユーザーが把握できるようにする必要があります。

チェックリスト

  • 値を本当に永続化してはならない(そうでなければ、データソースのほうがシンプルです)
  • Open を実装済み。Renew/Close は API がサポートする場合にのみ実装
  • シークレット属性に Sensitive: true を設定済み。値がログや診断情報に出力されない
  • リース/ハンドルを結果ではなく Private 経由で渡す
  • 期限切れになる認証情報には余裕を持たせて RenewAt を設定
  • EphemeralResources() に登録済み。プロバイダーの Configure で EphemeralResourceData を設定
  • Terraform >= 1.10 にバージョンを限定した、echo-provider の受け入れテスト
  • ドキュメントページで有効期間、更新、失効の動作を説明

コメント