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:
-
Configuration (
Configstruct andPreparemethod): 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. ThePreparemethod 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. -
Provisioning (
Provisionmethod): 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 thepacker.Uiinterface to send messages back to the user (e.g., "Creating VM…", "VM created successfully."). Thebuilder.Artifactinterface is what you return from the build process, representing the created infrastructure and any associated data (like connection strings or IP addresses). -
Lifecycle Management (
Cancelmethod): This allows Packer to gracefully stop an ongoing build if requested. YourCancelimplementation 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.