Terraform Provider Actions Implementation Guide
Overview
Terraform Actions enable imperative operations during the Terraform lifecycle. Actions are experimental features that allow performing provider operations at specific lifecycle events (before/after create, update, destroy).
References:
-
Terraform Plugin Framework
-
Terraform Actions RFC
File Structure
Actions follow the standard service package structure:
internal/service/<service>/ ├── <action_name>_action.go # Action implementation ├── <action_name>_action_test.go # Action tests └── service_package_gen.go # Auto-generated service registration
Documentation structure:
website/docs/actions/ └── <service>_<action_name>.html.markdown # User-facing documentation
Changelog entry:
.changelog/ └── <pr_number_or_description>.txt # Release note entry
Action Schema Definition
Actions use the Terraform Plugin Framework with a standard schema pattern:
func (a *actionType) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ // Required configuration parameters "resource_id": schema.StringAttribute{ Required: true, Description: "ID of the resource to operate on", }, // Optional parameters with defaults "timeout": schema.Int64Attribute{ Optional: true, Description: "Operation timeout in seconds", Default: int64default.StaticInt64(1800), Computed: true, }, }, } }
Common Schema Issues
Pay special attention to the schema definition - common issues after a first draft:
Type Mismatches
-
Using types.String instead of fwtypes.String in model structs
-
Using types.StringType instead of fwtypes.StringType in schema
-
Mixing framework types with plugin-framework types
List/Map Element Types
// WRONG - missing ElementType "items": schema.ListAttribute{ Optional: true, }
// CORRECT "items": schema.ListAttribute{ Optional: true, ElementType: fwtypes.StringType, }
Computed vs Optional
-
Attributes with defaults must be both Optional: true and Computed: true
-
Don't mark action inputs as Computed unless they have defaults
Validator Imports
// Ensure proper imports "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
Region/Provider Attribute
-
Use framework-provided region handling when available
-
Don't manually define provider-specific config in schema if framework handles it
Nested Attributes
-
Use appropriate nested object types for complex structures
-
Ensure nested types are properly defined
Schema Validation Checklist
Before submitting, verify:
-
All attributes have descriptions
-
List/Map attributes have ElementType defined
-
Validators are imported and applied correctly
-
Model struct uses correct framework types
-
Optional attributes with defaults are marked Computed
-
Code compiles without type errors
-
Run go build to catch type mismatches
Action Invoke Method
The Invoke method contains the action logic:
func (a *actionType) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { var data actionModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
// Create provider client
conn := a.Meta().Client(ctx)
// Progress updates for long-running operations
resp.Progress.Set(ctx, "Starting operation...")
// Implement action logic with error handling
// Use context for timeout management
// Poll for completion if async operation
resp.Progress.Set(ctx, "Operation completed")
}
Key Implementation Requirements
- Progress Reporting
-
Use resp.SendProgress(action.InvokeProgressEvent{...}) for real-time updates
-
Provide meaningful progress messages during long operations
-
Update progress at key milestones
-
Include elapsed time for long operations
- Timeout Management
-
Always include configurable timeout parameter (default: 1800s)
-
Use context.WithTimeout() for API calls
-
Handle timeout errors gracefully
-
Validate timeout ranges (typically 60-7200 seconds)
- Error Handling
-
Add diagnostics with resp.Diagnostics.AddError()
-
Provide clear error messages with context
-
Include API error details when relevant
-
Map provider error types to user-friendly messages
-
Document all possible error cases
Example error handling:
// Handle specific errors var notFound *types.ResourceNotFoundException if errors.As(err, ¬Found) { resp.Diagnostics.AddError( "Resource Not Found", fmt.Sprintf("Resource %s was not found", resourceID), ) return }
// Generic error handling resp.Diagnostics.AddError( "Operation Failed", fmt.Sprintf("Could not complete operation for %s: %s", resourceID, err), )
- Provider SDK Integration
-
Use provider SDK clients from a.Meta().<Service>Client(ctx)
-
Handle pagination for list operations
-
Implement retry logic for transient failures
-
Use appropriate error types
- Parameter Validation
-
Use framework validators for input validation
-
Validate resource existence before operations
-
Check for conflicting parameters
-
Validate against provider naming requirements
- Polling and Waiting
For operations that require waiting for completion:
result, err := wait.WaitForStatus(ctx, func(ctx context.Context) (wait.FetchResult[*ResourceType], error) { // Fetch current status resource, err := findResource(ctx, conn, id) if err != nil { return wait.FetchResult[*ResourceType]{}, err } return wait.FetchResult[*ResourceType]{ Status: wait.Status(resource.Status), Value: resource, }, nil }, wait.Options[*ResourceType]{ Timeout: timeout, Interval: wait.FixedInterval(5 * time.Second), SuccessStates: []wait.Status{"AVAILABLE", "COMPLETED"}, TransitionalStates: []wait.Status{"CREATING", "PENDING"}, ProgressInterval: 30 * time.Second, ProgressSink: func(fr wait.FetchResult[any], meta wait.ProgressMeta) { resp.SendProgress(action.InvokeProgressEvent{ Message: fmt.Sprintf("Status: %s, Elapsed: %v", fr.Status, meta.Elapsed.Round(time.Second)), }) }, }, )
Common Action Patterns
Batch Operations
-
Process items in configurable batches
-
Report progress per batch
-
Handle partial failures gracefully
-
Support prefix/filter parameters
Command Execution
-
Submit command and get operation ID
-
Poll for completion status
-
Retrieve and report output
-
Handle timeout during polling
-
Validate resources exist before execution
Service Invocation
-
Invoke service with parameters
-
Wait for completion (if synchronous)
-
Return output/results
-
Handle service-specific errors
Resource State Changes
-
Validate current state
-
Apply state change
-
Poll for target state
-
Handle transitional states
Async Job Submission
-
Submit job with configuration
-
Get job ID
-
Optionally wait for completion
-
Report job status
Action Triggers
Actions are invoked via action_trigger lifecycle blocks in Terraform configurations:
action "provider_service_action" "name" { config { parameter = value } }
resource "terraform_data" "trigger" { lifecycle { action_trigger { events = [after_create] actions = [action.provider_service_action.name] } } }
Available Trigger Events
Terraform 1.14.0 Supported Events:
-
before_create
-
Before resource creation
-
after_create
-
After resource creation
-
before_update
-
Before resource update
-
after_update
-
After resource update
Not Supported in Terraform 1.14.0:
-
before_destroy
-
Not available (will cause validation error)
-
after_destroy
-
Not available (will cause validation error)
Testing Actions
Acceptance Tests
-
Test action invocation with valid parameters
-
Test timeout scenarios
-
Test error conditions
-
Verify provider state changes
-
Test progress reporting
-
Test with custom parameters
-
Test trigger-based invocation
Test Pattern
func TestAccServiceAction_basic(t *testing.T) { ctx := acctest.Context(t)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_14_0),
},
Steps: []resource.TestStep{
{
Config: testAccActionConfig_basic(),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourceExists(ctx, "provider_resource.test"),
),
},
},
})
}
Test Cleanup with Sweep Functions
Add sweep functions to clean up test resources:
func sweepResources(region string) error { ctx := context.Background() client := /* get client for region */
input := &service.ListInput{
// Filter for test resources
}
var sweeperErrs *multierror.Error
pages := service.NewListPaginator(client, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)
if err != nil {
sweeperErrs = multierror.Append(sweeperErrs, err)
continue
}
for _, item := range page.Items {
id := item.Id
// Skip non-test resources
if !strings.HasPrefix(id, "tf-acc-test") {
continue
}
_, err := client.Delete(ctx, &service.DeleteInput{
Id: id,
})
if err != nil {
sweeperErrs = multierror.Append(sweeperErrs, err)
}
}
}
return sweeperErrs.ErrorOrNil()
}
Testing Best Practices
Service-Specific Prerequisites
-
Always check for service-specific prerequisites that must be met before actions can succeed
-
Document prerequisites in action documentation and test configurations
Error Pattern Matching
-
Terraform wraps action errors with additional context
-
Use flexible regex patterns: regexache.MustCompile(
(?s)Error Title.*key phrase)
Test Patterns Not Applicable to Actions
-
Actions trigger on lifecycle events, not config reapplication
-
Before/After Destroy Tests: Not supported in Terraform 1.14.0
Running Tests
Compile test to check for errors:
go test -c -o /dev/null ./internal/service/<service>
Run specific action tests:
TF_ACC=1 go test ./internal/service/<service> -run TestAccServiceAction_ -v
Run sweep to clean up test resources:
TF_ACC=1 go test ./internal/service/<service> -sweep=<region> -v
Documentation Standards
Each action documentation file must include:
Front Matter
subcategory: "Service Name" layout: "provider" page_title: "Provider: provider_service_action" description: |- Brief description of what the action does.
Header with Warnings
-
Beta/Alpha notice about experimental status
-
Warning about potential unintended consequences
-
Link to provider documentation
Example Usage
-
Basic usage example
-
Advanced usage with all options
-
Trigger-based example with terraform_data
-
Real-world use case examples
Argument Reference
-
List all required and optional arguments
-
Include descriptions and defaults
-
Note any validation rules
Documentation Linting
-
Run terrafmt fmt before submission
-
Verify with terrafmt diff
Changelog Entry Format
Create a changelog entry in .changelog/ directory:
.changelog/<pr_number_or_description>.txt
Content format:
action/provider_service_action: Brief description of the action
Pre-Submission Checklist
Before submitting your action implementation:
-
Code compiles: go build -o /dev/null .
-
Tests compile: go test -c -o /dev/null ./internal/service/<service>
-
Code formatted: make fmt
-
Documentation formatted: terrafmt fmt website/docs/actions/<action>.html.markdown
-
Changelog entry created
-
Schema uses correct types
-
All List/Map attributes have ElementType
-
Progress updates implemented for long operations
-
Error messages include context and resource identifiers
-
Documentation includes multiple examples
-
Documentation includes prerequisites and warnings
References
-
Terraform Plugin Framework Documentation
-
Terraform Provider Development
-
terraform-plugin-framework GitHub
-
terraform-plugin-testing