provider-ephemeral-resources について
Terraform 1.10 以降の Provider で一時リソースを実装するスキルです。短命な機密値を state や plan に保存せず渡す方法を、ライフサイクル、登録、テスト、ドキュメントとともに説明します。
主な機能
- 01Open、Renew、Close のライフサイクル
- 02Private によるリース情報の受け渡し
- 03一時リソースの登録
- 04エコープロバイダーによるテスト
- 05ライフサイクルの文書化
活用シーン
- 01短命なアクセストークンの受け渡し
- 02state に保存しないパスワードの生成
- 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 の受け入れテスト
- ドキュメントページで有効期間、更新、失効の動作を説明