Skip to content
Go back

Writing a Terraform Action

Disclaimer: I am working at HashiCorp (now IBM) as part of the Terraform Core team. The postings on this site are my own and don’t necessarily represent IBM’s positions, strategies or opinions.
Since I am involved in Terraform my opinions can sometimes be (unconsciously) biased. I hope you enjoy the post anyway.

Content

What are Terraform Actions?

Terraform Actions are a new block in the Terraform language that allows you to express non-CRUD operations in your configuration. Please see Introduction to Terraform Actions for a detailed introduction.

Prerequisites

If you want to add an Action to your provider you need to do this through the terraform-plugin-framework package. If your provider still uses SDK/v2 you can take a look at this post on adding muxing to add the terraform-plugin-framework to your provider.

Once you have the plugin framework, we need to update the go.mod to the following versions:

require (
    // These are already final versions
	github.com/hashicorp/terraform-plugin-framework v1.16.0
	github.com/hashicorp/terraform-plugin-go v0.29.0
	github.com/hashicorp/terraform-plugin-mux v0.21.0

	// These will be updated to final versions soon
	github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1
	github.com/hashicorp/terraform-plugin-docs v0.22.1-0.20250916190709-65411a4e6cbd
)

Then run go mod tidy to update the dependencies.

To run actions you need the current Terraform version as well: v1.14.0-beta2 is the first version of Terraform to support Actions.

How to add an Action

Let’s take a look at how to add an Action to your provider. As a very simple example, I will use the terraform-provider-bufo that exposes an action that prints an image of Bufo the frog on your terminal. It’s very minimal, that’s why it makes a good example to get started.

In your provider.go file, you need to / should add an assertion to indicate that your provider has actions: var _ provider.ProviderWithActions = &BufoProvider{}

This should give you a compiler error that the Actions method is missing, so let’s add it:

// Source: https://github.com/austinvalle/terraform-provider-bufo/blob/62736f71a1db9d3bf167c4bbda16bbb9044d41dc/internal/provider/provider.go#L33-L37
func (p *BufoProvider) Actions(ctx context.Context) []func() action.Action {
	return []func() action.Action{
		NewPrintBufo,
	}
}

Now let’s take a look at the NewPrintBufo implementation:

// Source: https://github.com/austinvalle/terraform-provider-bufo/blob/9c39751bc738eb2072f2fbb6a22d11edef3cc1e0/internal/provider/bufo_print.go#L1-L127
package provider

import (
	"context"
	"fmt"
	"image"
	"math/rand"

	"github.com/hashicorp/terraform-plugin-framework/action"
	"github.com/hashicorp/terraform-plugin-framework/action/schema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
	"github.com/qeesung/image2ascii/convert"
)

var (
	_ action.Action = (*printBufo)(nil)
)

func NewPrintBufo() action.Action {
	return &printBufo{}
}

type printBufo struct{}

func (a *printBufo) Metadata(ctx context.Context, req action.MetadataRequest, resp *action.MetadataResponse) {
	resp.TypeName = req.ProviderTypeName + "_print"
}

func (a *printBufo) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
	resp.Schema = schema.Schema{
		Description: "Prints a bufo image as ASCII art. List of available bufos found at: https://github.com/austinvalle/terraform-provider-bufo/tree/main/internal/provider/bufos",
		Attributes: map[string]schema.Attribute{
			"name": schema.StringAttribute{
				Description: "Name of the bufo to print (for example: `bufo-the-builder`), [see the github repo](https://github.com/austinvalle/terraform-provider-bufo/tree/main/internal/provider/bufos) for a list of bufos. " +
					"If no name is provided, a random bufo will be selected.",
				Optional: true,
				Validators: []validator.String{
					ValidBufoName(),
				},
			},
			"ratio": schema.Float64Attribute{
				Description: "The ratio to scale the width/height of the bufo from the original, defaults to 0.5.",
				Optional:    true,
			},
			"color": schema.BoolAttribute{
				Description: "Color the printed bufo, defaults to false.",
				Optional:    true,
			},
		},
	}
}

func (a *printBufo) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
	var config printBufoModel

	resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
	if resp.Diagnostics.HasError() {
		return
	}

	var bufoFileName string
	if config.Name.IsNull() {
		// Choose a random bufo
		bufoEntries, err := bufos.ReadDir("bufos")
		if err != nil {
			resp.Diagnostics.AddError(
				"Failed to select a random bufo",
				fmt.Sprintf("There was an issue finding a random bufo: %s", err),
			)
			return
		}

		randomBufoIdx := rand.Intn(len(bufoEntries))
		bufoFileName = bufoEntries[randomBufoIdx].Name()
	} else {
		bufoFileName = fmt.Sprintf("%s.png", config.Name.ValueString())
	}

	bufoLocation := fmt.Sprintf("bufos/%s", bufoFileName)

	bufoFile, err := bufos.Open(bufoLocation)
	if err != nil {
		resp.Diagnostics.AddError(
			"Failed to retrieve bufo",
			fmt.Sprintf("There was an issue finding bufo at %q: %s", bufoFileName, err),
		)
		return
	}

	converter := convert.NewImageConverter()
	img, _, err := image.Decode(bufoFile)
	if err != nil {
		resp.Diagnostics.AddError(
			"Failed to decode bufo",
			fmt.Sprintf("There was an issue decoding bufo at %q: %s", bufoFileName, err),
		)
		return
	}

	ratio := 0.5
	if !config.Ratio.IsNull() {
		ratio = config.Ratio.ValueFloat64()
	}

	colored := false
	if !config.Color.IsNull() {
		colored = config.Color.ValueBool()
	}

	bufoAscii := converter.Image2ASCIIString(img, &convert.Options{
		Ratio:       ratio,
		FixedWidth:  -1,
		FixedHeight: -1,
		Colored:     colored,
	})

	resp.SendProgress(action.InvokeProgressEvent{
		Message: fmt.Sprintf("\n\nBufo: %q\n\n%s", bufoFileName, bufoAscii),
	})
}

type printBufoModel struct {
	Name  types.String  `tfsdk:"name"`
	Ratio types.Float64 `tfsdk:"ratio"`
	Color types.Bool    `tfsdk:"color"`
}

As you can see we start out by having a type assertion that the printBufo pointer adheres to the action.Action interface:

var (
	_ action.Action = (*printBufo)(nil)
)

This makes it easier to spot potential issues with the implementation.

Then we have the Metadata method, which reports the type name of the action back (in this case bufo_print).

Next, we have a Schema method which returns the schema for the action. This is fairly similar to how it is done for resources and data sources, but there is one difference. If you take a look at the imports, the schema we define is coming from "github.com/hashicorp/terraform-plugin-framework/action/schema", so it’s a specialized subset of the normal schema. This will allow Terraform to evolve this schema independently of the normal provider schema, e.g. limiting or extending its capabilities.

Now let’s take a look at the core of the implementation, the Invoke method.

The first thing we do is to extract the configuration:

var config printBufoModel

resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
	return
}

Then we have the core logic of the provider, in this case we load the embedded bufo image and convert it to ASCII art. Finally, we print the ASCII art to the console by using the resp.SendProgress method:

resp.SendProgress(action.InvokeProgressEvent{
	Message: fmt.Sprintf("\n\nBufo: %q\n\n%s", bufoFileName, bufoAscii),
})

And that wraps up the implementation of the printBufo action.

Users can now use it like this:

terraform {
  required_providers {
    bufo = {
      source = "austinvalle/bufo"
    }
  }
}

resource "terraform_data" "test" {
  lifecycle {
    action_trigger {
      events  = [after_create]
      actions = [action.bufo_print.success]
    }
  }
}

action "bufo_print" "success" {
  config {
    # random colorized bufo
    color = true
  }
}

Next Steps

Please leave a comment

Do you have feedback around actions or do you want to hear about a specific topic around Terraform / Software Development / Language Design / Infrastructure as Code? Please let me know in the comments below.


Share this post on:




Previous Post
Terraform Action Patterns and Guidelines
Next Post
Introduction to Terraform Actions