Comprehensive documentation and best practices for building Terraform providers with terraform-plugin-framework (v1.17.0). Covers providers, resources, schemas, types, validators, testing, and common pitfalls.
Overall
score
97%
Explore advanced framework capabilities including actions and ephemeral resources.
Actions are provider-defined operations that can be triggered independently of resource lifecycle. They provide a way to execute operations without managing state.
Note: Actions are a newer feature in terraform-plugin-framework. Check version compatibility.
type Action interface {
// Metadata returns action name
Metadata(context.Context, ActionMetadataRequest, *ActionMetadataResponse)
// Schema defines action parameters
Schema(context.Context, ActionSchemaRequest, *ActionSchemaResponse)
// Run executes the action
Run(context.Context, ActionRunRequest, *ActionRunResponse)
}package action
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/action"
"github.com/hashicorp/terraform-plugin-framework/action/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ action.Action = (*RestartServiceAction)(nil)
type RestartServiceAction struct {
client *APIClient
}
func NewRestartServiceAction() action.Action {
return &RestartServiceAction{}
}
func (a *RestartServiceAction) Metadata(ctx context.Context, req action.MetadataRequest, resp *action.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_restart_service"
}
func (a *RestartServiceAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Restart a service",
Attributes: map[string]schema.Attribute{
"service_id": schema.StringAttribute{
Description: "ID of service to restart",
Required: true,
},
"force": schema.BoolAttribute{
Description: "Force restart even if service is healthy",
Optional: true,
},
"status": schema.StringAttribute{
Description: "Status after restart",
Computed: true,
},
},
}
}
func (a *RestartServiceAction) Run(ctx context.Context, req action.RunRequest, resp *action.RunResponse) {
var config struct {
ServiceID types.String `tfsdk:"service_id"`
Force types.Bool `tfsdk:"force"`
}
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Execute action
force := false
if !config.Force.IsNull() {
force = config.Force.ValueBool()
}
result, err := a.client.RestartService(ctx, config.ServiceID.ValueString(), force)
if err != nil {
resp.Diagnostics.AddError("Restart Failed", err.Error())
return
}
// Set result
var resultData struct {
ServiceID types.String `tfsdk:"service_id"`
Force types.Bool `tfsdk:"force"`
Status types.String `tfsdk:"status"`
}
resultData.ServiceID = config.ServiceID
resultData.Force = config.Force
resultData.Status = types.StringValue(result.Status)
resp.Diagnostics.Append(resp.Result.Set(ctx, &resultData)...)
}func (p *ExampleProvider) Actions(ctx context.Context) []func() action.Action {
return []func() action.Action{
NewRestartServiceAction,
NewBackupDatabaseAction,
}
}Actions are invoked via Terraform CLI (exact syntax depends on Terraform version):
terraform action example_restart_service \
-var service_id="svc-123" \
-var force=trueUse actions for:
Don't use actions for:
Ephemeral resources are session-scoped resources that exist only for the duration of a Terraform operation. They:
type EphemeralResource interface {
// Metadata returns resource type name
Metadata(context.Context, EphemeralResourceMetadataRequest, *EphemeralResourceMetadataResponse)
// Schema defines resource schema
Schema(context.Context, EphemeralResourceSchemaRequest, *EphemeralResourceSchemaResponse)
// Open creates the ephemeral resource
Open(context.Context, EphemeralResourceOpenRequest, *EphemeralResourceOpenResponse)
// Renew refreshes the ephemeral resource (optional)
Renew(context.Context, EphemeralResourceRenewRequest, *EphemeralResourceRenewResponse)
// Close destroys the ephemeral resource
Close(context.Context, EphemeralResourceCloseRequest, *EphemeralResourceCloseResponse)
}package ephemeral
import (
"context"
"time"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ ephemeral.EphemeralResource = (*TemporaryCredentialResource)(nil)
type TemporaryCredentialResource struct {
client *APIClient
}
func NewTemporaryCredentialResource() ephemeral.EphemeralResource {
return &TemporaryCredentialResource{}
}
func (r *TemporaryCredentialResource) Metadata(ctx context.Context, req ephemeral.EphemeralResourceMetadataRequest, resp *ephemeral.EphemeralResourceMetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_temporary_credential"
}
func (r *TemporaryCredentialResource) Schema(ctx context.Context, req ephemeral.EphemeralResourceSchemaRequest, resp *ephemeral.EphemeralResourceSchemaResponse) {
resp.Schema = schema.Schema{
Description: "Temporary credential for accessing resources",
Attributes: map[string]schema.Attribute{
"role": schema.StringAttribute{
Description: "Role to assume",
Required: true,
},
"duration": schema.Int64Attribute{
Description: "Duration in seconds (default 3600)",
Optional: true,
},
"access_key": schema.StringAttribute{
Description: "Temporary access key",
Computed: true,
Sensitive: true,
},
"secret_key": schema.StringAttribute{
Description: "Temporary secret key",
Computed: true,
Sensitive: true,
},
"expires_at": schema.StringAttribute{
Description: "Expiration time",
Computed: true,
},
},
}
}Create the ephemeral resource:
func (r *TemporaryCredentialResource) Open(ctx context.Context, req ephemeral.EphemeralResourceOpenRequest, resp *ephemeral.EphemeralResourceOpenResponse) {
var config struct {
Role types.String `tfsdk:"role"`
Duration types.Int64 `tfsdk:"duration"`
}
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Default duration
duration := 3600
if !config.Duration.IsNull() {
duration = int(config.Duration.ValueInt64())
}
// Create temporary credential
cred, err := r.client.CreateTemporaryCredential(ctx, config.Role.ValueString(), duration)
if err != nil {
resp.Diagnostics.AddError("Failed to create credential", err.Error())
return
}
// Set result
var result struct {
Role types.String `tfsdk:"role"`
Duration types.Int64 `tfsdk:"duration"`
AccessKey types.String `tfsdk:"access_key"`
SecretKey types.String `tfsdk:"secret_key"`
ExpiresAt types.String `tfsdk:"expires_at"`
}
result.Role = config.Role
result.Duration = types.Int64Value(int64(duration))
result.AccessKey = types.StringValue(cred.AccessKey)
result.SecretKey = types.StringValue(cred.SecretKey)
result.ExpiresAt = types.StringValue(cred.ExpiresAt.Format(time.RFC3339))
// Set renew time (refresh before expiration)
renewAt := cred.ExpiresAt.Add(-5 * time.Minute)
resp.RenewAt = renewAt
resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...)
}Refresh the ephemeral resource before expiration:
func (r *TemporaryCredentialResource) Renew(ctx context.Context, req ephemeral.EphemeralResourceRenewRequest, resp *ephemeral.EphemeralResourceRenewResponse) {
var data struct {
Role types.String `tfsdk:"role"`
Duration types.Int64 `tfsdk:"duration"`
AccessKey types.String `tfsdk:"access_key"`
SecretKey types.String `tfsdk:"secret_key"`
ExpiresAt types.String `tfsdk:"expires_at"`
}
resp.Diagnostics.Append(req.Private.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Renew credential
cred, err := r.client.RenewCredential(ctx, data.AccessKey.ValueString())
if err != nil {
resp.Diagnostics.AddError("Failed to renew credential", err.Error())
return
}
// Update result
data.ExpiresAt = types.StringValue(cred.ExpiresAt.Format(time.RFC3339))
renewAt := cred.ExpiresAt.Add(-5 * time.Minute)
resp.RenewAt = renewAt
resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
}Clean up the ephemeral resource:
func (r *TemporaryCredentialResource) Close(ctx context.Context, req ephemeral.EphemeralResourceCloseRequest, resp *ephemeral.EphemeralResourceCloseResponse) {
var data struct {
AccessKey types.String `tfsdk:"access_key"`
}
req.Private.Get(ctx, &data)
// Revoke credential
if !data.AccessKey.IsNull() {
err := r.client.RevokeCredential(ctx, data.AccessKey.ValueString())
if err != nil {
resp.Diagnostics.AddWarning("Failed to revoke credential", err.Error())
// Don't return error - resource is being cleaned up anyway
}
}
}func (p *ExampleProvider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
NewTemporaryCredentialResource,
}
}ephemeral "example_temporary_credential" "admin" {
role = "admin"
duration = 7200
}
resource "example_server" "web" {
name = "web-server"
# Use ephemeral credential
access_key = ephemeral.example_temporary_credential.admin.access_key
secret_key = ephemeral.example_temporary_credential.admin.secret_key
}Use ephemeral resources for:
Don't use for:
Both actions and ephemeral resources support private state for internal data not exposed to users:
func (r *Resource) Open(ctx context.Context, req ephemeral.EphemeralResourceOpenRequest, resp *ephemeral.EphemeralResourceOpenResponse) {
// Store private data
privateData := struct {
InternalID string `json:"internal_id"`
Token string `json:"token"`
}{
InternalID: "internal-123",
Token: "secret-token",
}
resp.Diagnostics.Append(resp.Private.Set(ctx, privateData)...)
// Set public result
resp.Diagnostics.Append(resp.Result.Set(ctx, &publicResult)...)
}
func (r *Resource) Renew(ctx context.Context, req ephemeral.EphemeralResourceRenewRequest, resp *ephemeral.EphemeralResourceRenewResponse) {
// Read private data
var privateData struct {
InternalID string `json:"internal_id"`
Token string `json:"token"`
}
resp.Diagnostics.Append(req.Private.Get(ctx, &privateData)...)
if resp.Diagnostics.HasError() {
return
}
// Use private data
// ...
}func TestRestartServiceAction(t *testing.T) {
mockClient := &MockAPIClient{}
mockClient.On("RestartService", mock.Anything, "svc-123", true).Return(&RestartResult{
Status: "running",
}, nil)
action := &RestartServiceAction{client: mockClient}
req := action.RunRequest{
Config: /* config with service_id and force */,
}
resp := &action.RunResponse{}
action.Run(context.Background(), req, resp)
require.False(t, resp.Diagnostics.HasError())
mockClient.AssertExpectations(t)
}func TestTemporaryCredentialResource(t *testing.T) {
mockClient := &MockAPIClient{}
mockClient.On("CreateTemporaryCredential", mock.Anything, "admin", 3600).Return(&Credential{
AccessKey: "AKIATEST",
SecretKey: "secretTest",
ExpiresAt: time.Now().Add(1 * time.Hour),
}, nil)
resource := &TemporaryCredentialResource{client: mockClient}
openReq := ephemeral.EphemeralResourceOpenRequest{
Config: /* config with role */,
}
openResp := &ephemeral.EphemeralResourceOpenResponse{}
resource.Open(context.Background(), openReq, openResp)
require.False(t, openResp.Diagnostics.HasError())
require.NotNil(t, openResp.RenewAt)
// Test Close
closeReq := ephemeral.EphemeralResourceCloseRequest{
Private: openResp.Private,
}
closeResp := &ephemeral.EphemeralResourceCloseResponse{}
resource.Close(context.Background(), closeReq, closeResp)
mockClient.AssertExpectations(t)
}// Mark sensitive attributes
"secret_key": schema.StringAttribute{
Computed: true,
Sensitive: true, // Masked in logs
},
// Use private state for internal tokens
resp.Private.Set(ctx, privateData) // Not visible to usersContinue to Testing to learn about unit and acceptance testing patterns.