Packer’s plugin system lets you extend its core functionality, and building a custom builder is how you make Packer provision infrastructure on a platform it doesn’t natively support.

Let’s see a builder in action, specifically one that simulates creating a VM on a fictional "AwesomeCloud" provider.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/hashicorp/packer-plugin-sdk/builder"
	"github.com/hashicorp/packer-plugin-sdk/common"
	"github.com/hashicorp/packer-plugin-sdk/communicator"
	"github.com/hashicorp/packer-plugin-sdk/packer"
)

// Config is the configuration for our AwesomeCloud builder.
type Config struct {
	VMName     string `mapstructure:"vm_name"`
	CPUCores int    `mapstructure:"cpu_cores"`
	MemoryMB   int    `mapstructure:"memory_mb"`
	ImageRef   string `mapstructure:"image_ref"`
	common.PackerConfig `mapstructure:",squash"` // Embed Packer's common config
}

// AwesomeCloudBuilder is our custom builder.
type AwesomeCloudBuilder struct {
	ctx    context.Context
	config *Config
	packer packer.Packer
}

// Prepare gets the configuration and validates it.
func (b *AwesomeCloudBuilder) Prepare(rawConfig map[string]interface{}) ([]string, error) {
	err := b.config.Decode(rawConfig)
	if err != nil {
		return nil, fmt.Errorf("error decoding builder config: %w", err)
	}

	// Basic validation
	if b.config.VMName == "" {
		return nil, fmt.Errorf("vm_name is required")
	}
	if b.config.ImageRef == "" {
		return nil, fmt.Errorf("image_ref is required")
	}

	var warnings []string
	// Add more validation as needed...
	return warnings, nil
}

// Provision is where the actual infrastructure creation happens.
func (b *AwesomeCloudBuilder) Provision(ui packer.Ui, _ map[string]interface{}, artifact builder.Artifact) error {
	ui.Say("Creating AwesomeCloud VM...")

	// Simulate VM creation
	vmID := fmt.Sprintf("vm-%s-%d", b.config.VMName, time.Now().UnixNano())
	ui.Say(fmt.Sprintf("Provisioning VM with ID: %s", vmID))

	// Simulate some work
	time.Sleep(2 * time.Second)

	ui.Say("VM created successfully.")

	// In a real scenario, you'd upload files, run commands, etc.
	// This example focuses on the builder itself.
	return nil
}

// Cancel stops the build.
func (b *AwesomeCloudBuilder) Cancel() {
	// Implement cancellation logic if your builder supports it
	log.Println("AwesomeCloud builder cancelled.")
}

// BuilderId returns the unique ID of this builder.
func (b *AwesomeCloudBuilder) BuilderId() string {
	return "awesomecloud.vm"
}

// Packer is the interface for Packer.
func (b *AwesomeCloudBuilder) Packer(p packer.Packer) {
	b.packer = p
}

// NewAwesomeCloudBuilder creates a new instance of our builder.
func NewAwesomeCloudBuilder() builder.Builder {
	return &AwesomeCloudBuilder{
		ctx: context.Background(),
	}
}

// This is the main function that would be part of a plugin
// For demonstration, we'll simulate its usage.
func main() {
	// In a real plugin, Packer would call NewAwesomeCloudBuilder
	// and then use the methods on the returned interface.
	// For this example, we'll manually instantiate and call.

	builderInstance := NewAwesomeCloudBuilder()
	builderInstance.Packer(nil) // Simulate Packer being passed

	config := &Config{
		VMName:   "my-awesome-vm",
		CPUCores: 2,
		MemoryMB: 2048,
		ImageRef: "awesomecloud-os-v1.2.3",
	}
	builderInstance.(*AwesomeCloudBuilder).config = config // Manually set config for demo

	ui := &mockUI{} // Mock UI for demonstration

	// Simulate Prepare
	_, err := builderInstance.Prepare(map[string]interface{}{
		"vm_name":  "my-awesome-vm",
		"cpu_cores": 2,
		"memory_mb": 2048,
		"image_ref": "awesomecloud-os-v1.2.3",
	})
	if err != nil {
		log.Fatalf("Prepare failed: %v", err)
	}

	// Simulate Provision
	artifact := &mockArtifact{} // Mock artifact
	err = builderInstance.Provision(ui, nil, artifact)
	if err != nil {
		log.Fatalf("Provision failed: %v", err)
	}

	fmt.Println("\nBuilder executed successfully (simulated).")
}

// --- Mock Implementations for Demonstration ---

type mockUI struct{}

func (m *mockUI) Say(msg string) {
	fmt.Printf("UI SAY: %s\n", msg)
}

func (m *mockUI) Error(msg string) {
	fmt.Printf("UI ERROR: %s\n", msg)
}

func (m *mockUI) Warn(msg string) {
	fmt.Printf("UI WARN: %s\n", msg)
}

func (m *mockUI) Message(msg string) {
	fmt.Printf("UI MESSAGE: %s\n", msg)
}

func (m *mockUI) Ask(prompt string) (string, error) {
	return "", nil // Not needed for this demo
}

func (m *mockUI) AskSecret(prompt string) (string, error) {
	return "", nil // Not needed for this demo
}

type mockArtifact struct{}

func (m *mockArtifact) Id() (string, error) {
	return "mock-artifact-id", nil
}

func (m *mockArtifact) State(key string) (string, error) {
	return "", nil // Not needed for this demo
}

func (m *mockArtifact) Files() (map[string]string, error) {
	return nil, nil // Not needed for this demo
}

func (m *mockArtifact) BuilderId() string {
	return "mock.builder"
}

To make Packer aware of your custom builder, you’d typically create a Go plugin. The main function in the example above is simplified to show the core logic of a builder. In a real plugin, Packer discovers and loads these builders dynamically.

The mental model for a custom builder revolves around three key phases:

  1. Configuration (Config struct and Prepare method): This is where you define the parameters your builder accepts. This could be anything from VM names, instance types, image IDs, to cloud provider credentials. The Prepare method is crucial for validating these inputs, ensuring all required fields are present and that values are within acceptable ranges. It also handles decoding the HCL (HashiCorp Configuration Language) into your Go struct.

  2. Provisioning (Provision method): This is the heart of your builder. Here, you’ll interact with the target infrastructure’s API. For a cloud provider, this means making API calls to create a virtual machine, attach disks, configure networking, etc. You’ll use the packer.Ui interface to send messages back to the user (e.g., "Creating VM…", "VM created successfully."). The builder.Artifact interface is what you return from the build process, representing the created infrastructure and any associated data (like connection strings or IP addresses).

  3. Lifecycle Management (Cancel method): This allows Packer to gracefully stop an ongoing build if requested. Your Cancel implementation should attempt to clean up any partially created resources on the target platform.

The BuilderId() method is essential for Packer to uniquely identify your builder type across all installed plugins. When you define a builder in your Packer template, you’ll use this ID.

The core of how Packer manages this is through interfaces. Your custom builder needs to implement the builder.Builder interface, which defines methods like BuilderId(), Prepare(), and Provision(). Packer’s plugin SDK provides helper types and interfaces to streamline this process.

A common pitfall for new plugin developers is not properly handling context cancellation. If your Provision method makes long-running API calls, you should check b.ctx.Done() periodically to ensure your builder can respond to Cancel() requests promptly. Ignoring this can lead to builds that are stuck and cannot be terminated without manual intervention on the target infrastructure.

The next step after creating a custom builder is often developing custom provisioners, which allow you to define reusable steps for configuring the infrastructure created by your builder.

Want structured learning?

Take the full Packer course →