This RFD proposes developing and maintaining an official [packer] plugin for Oxide. The plugin will enable users to build customized Oxide images that meet their specific application, operating system, and security requirements.
Background
HashiCorp (IBM) Packer
Packer is a tool used to build identical images for multiple platforms from a single configuration. It is widely adopted in the infrastructure-as-code ecosystem with plugins available for popular platforms such as AWS, GCP, Azure, VMware, and Docker. Examples of images built by Packer include AMIs for AWS EC2, VMDK/VMX files for VMware, and container images for Docker.
In addition to building images for individual use, Packer is also used to build public images such as [github-actions-runner-images] and [almalinux-cloud-images]. Packer’s plugin architecture allows companies to develop a plugin to support their platform (e.g., a Packer plugin to build [oxide-images]).
Oxide Image Management
Oxide supports image management through its UI, API, and CLI, but lacks integration with industry image building tools such as Packer. This creates friction for users that have invested in building images with Packer and want to migrate to or adopt Oxide.
Oxide users must upload an image to Oxide before they can boot an instance. Many Linux distributions publish cloud images (e.g., [ubuntu-cloud-images]) that can be uploaded to Oxide and used to boot an instance. However, these cloud images are generic and often require customization to meet customer needs (e.g., install packages, configure the system). There are two common approaches to image customization.
Run time customization. This is often done using user data (e.g., [cloud-init]) or a configuration management tool (e.g., Ansible, Chef).
Build time customization. This is often done by editing the image directly (e.g., using
libguestfs
tools) or by booting an instance from the image, customizing it, and creating a new image.
Each approach has its own pros and cons. Run time customization is trivial to do but can lead to configuration drift since there’s no concept of versioned images. Build time customization is more tedious to do but eliminates configuration drift using versioned images.
Packer automates build time customization of images by abstracting platform APIs and providing functionality to boot an instance from an image, customize it, and create a new image. A Packer plugin for Oxide will ease the burden of creating images on Oxide and allow users to focus on building customized images for their needs.
Plugin Considerations
Programming Language
The plugin will be implemented in Go to comply with the requirements listed in [developing-plugins]. The plugin will use the official Oxide Go SDK to communicate with Oxide.
Release Versioning
Plugin releases will use semantic versioning to comply with the requirements listed in [developing-plugins]. Specifically, the version constraint requirements listed in [packer-settings].
Acceptance Testing
There will be acceptance tests for the plugin to test plugin functionality
against Oxide. The acceptance tests will use the [packer-plugin-sdk-acctest]
package, which provides exported testing functions such as acctest.TestPlugin
.
The example below shows how acctest.TestPlugin
will be called.
acctest.TestPlugin(t, &acctest.PluginTestCase{
Name: "TestPlugin",
Type: "oxide-instance",
Setup: func() error {
// Set up testing resources.
},
Check: func(*exec.Cmd, string) error {
// Assert Packer plugin functionality.
},
Teardown: func() error {,
// Clean up testing resources.
}
Template: renderPackerTemplate("template.pkr.hcl.tmpl"),
})
Plugin Components
A Packer plugin must contain one or more components. Packer plugins support the following components.
[packer-component-builder] - Builds an image for a platform.
[packer-component-data-source] - Retrieves information for use in a build.
[packer-component-post-processor] - Operates on the image artifact once built.
[packer-component-provisioner] - Provisions an image before it’s finished building.
Most plugins implement the builder component since building images is the primary function of Packer.
Builder
The plugin will have a builder component named oxide-instance
to create
custom images on Oxide. The builder will launch a temporary instance, connect
to it using its external IP, provision it, and then create an image from the
instance’s boot disk. The resulting image can be used to launch new instances.
The builder configuration will contain attributes as described below. These may change during implementation.
type Config struct {
// Common Packer configuration that applies to all builders.
common.PackerConfig `mapstructure:",squash"`
// Packer communicator configuration to configure how Packer connects to the
// instance for provisioning.
Comm communicator.Config `mapstructure:",squash"`
// Oxide API URL (e.g., `https://oxide.sys.example.com`). If not specified, this
// defaults to the value of the `OXIDE_HOST` environment variable.
Host string `mapstructure:"host" required:"true"`
// Oxide API token. If not specified, this defaults to the value of the
// `OXIDE_TOKEN` environment variable.
Token string `mapstructure:"token" required:"true"`
// Image ID to use for the instance's boot disk. This can be obtained from the
// `oxide-image` data source.
BootDiskImageID string `mapstructure:"boot_disk_image_id" required:"true"`
// Name or ID of the project where the temporary instance and resulting image
// will be created.
Project string `mapstructure:"project" required:"true"`
}
The builder component will implement the packer.Builder
interface which is
required to register the component as a builder component.
var _ packer.Builder = (*Builder)(nil)
type Builder struct{}
func (b *Builder) ConfigSpec() hcldec.ObjectSpec {
// Generate the builder configuration specification.
}
func (b *Builder) Prepare(...interface{}) ([]string, []string, error) {
// Prepare the builder configuration.
}
func (b *Builder) Run(context.Context, packer.Ui, packer.Hook) (packer.Artifact, error) {
// Run the steps necessary to create an Oxide image.
}
The builder component will be registered with the plugin.
pluginSet.RegisterBuilder("instance", new(instance.Builder))
Data Source
The plugin will have a data source component named oxide-image
to fetch the
image ID for an Oxide image using its name. The image can be a project image or
silo image.
The data source configuration will contain attributes as described below. These may change during implementation.
type Config struct {
// Oxide API URL (e.g., `https://oxide.sys.example.com`). If not specified, this
// defaults to the value of the `OXIDE_HOST` environment variable.
Host string `mapstructure:"host" required:"true"`
// Oxide API token. If not specified, this defaults to the value of the
// `OXIDE_TOKEN` environment variable.
Token string `mapstructure:"token" required:"true"`
// Name of the image to fetch.
Name string `mapstructure:"name" required:"true"`
// Name or ID of the project containing the image to fetch. Leave blank to fetch
// a silo image instead of a project image.
Project string `mapstructure:"project"`
}
The data source component will implement the packer.Datasource
interface which
is required to register the component as a data source component.
var _ packer.Datasource = (*Datasource)(nil)
type Datasource struct{}
func (d *DatasourceV2) ConfigSpec() hcldec.ObjectSpec {
// Generate the data source configuration specification.
}
func (d *DatasourceV2) Configure(...interface{}) error {
// Configure the data source configuration.
}
func (d *DatasourceV2) Execute() (cty.Value, error) {
// Run the steps necessary to retrieve Oxide image information.
}
func (d *DatasourceV2) OutputSpec() hcldec.ObjectSpec {
// Generate the data source output specification.
}
The data source component will be registered with the plugin.
pluginSet.RegisterDatasource("image", new(image.Datasource))
Post-Processor
The plugin will not have a post-processor component. Perhaps a future iteration of the plugin will introduce a post-processor component.
Provisioner
The plugin will not have a provisioner component. Perhaps a future iteration of the plugin will introduce a provisioner component.
Even though the plugin won’t have a provisioner component, the plugin will be
compatible with Packer’s built-in provisioners (e.g., shell
, file
). Refer to
[_using_provisioners] for more information.
User Experience
New Configuration
Users new to Packer will create a Packer [hcl] configuration that uses the Packer plugin for Oxide.
packer {
required_plugins {
oxide = {
version = ">= 0.1.0"
source = "github.com/oxidecomputer/oxide"
}
}
}
# Data source component.
data "oxide-image" "ubuntu" {
name = "noble"
}
# Builder component.
source "oxide-instance" "example" {
project = "packer-acc-test"
boot_disk_image_id = data.oxide-image.ubuntu.image_id
# SSH communicator configuration.
ssh_username = "ubuntu"
}
# Build pipeline definition.
build {
sources = [
"source.oxide-instance.example",
]
}
Next they will export the OXIDE_HOST
and OXIDE_TOKEN
environment variables
to configure how Packer will authenticate to Oxide.
# Bash
export OXIDE_HOST='https://oxide.sys.example.com'
export OXIDE_TOKEN='oxide-token-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
# Fish
set --export OXIDE_HOST 'https://oxide.sys.example.com'
set --export OXIDE_TOKEN 'oxide-token-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
Then they will initialize the Packer configuration to download the Packer plugin for Oxide.
> packer init template.pkr.hcl
Installed plugin github.com/oxidecomputer/oxide v0.1.0 in "/home/oxide/.config/packer/plugins/github.com/oxidecomputer/oxide/packer-plugin-oxide_v0.1.0_x5.0_linux_amd64"
Finally, they will build the images specified by the Packer configuration.
> packer build template.pkr.hcl
oxide-instance.example: output will be in this color.
# ...
Build 'oxide-instance.example' finished after 38 seconds 976 milliseconds.
==> Wait completed after 38 seconds 976 milliseconds
==> Builds finished. The artifacts of successful builds are:
--> oxide-instance.example: packer-1751062438
Existing Configuration
Users with an existing Packer configuration will add the Oxide plugin to their configuration next to other plugins, easing the migration path to Oxide.
source "oxide-instance" "example" {}
source "vmware-iso" "example" {}
source "amazon-ebs" "example" {}
build {
sources = [
"source.oxide-instance.example",
"source.vmware-iso.example",
"source.amazon-ebs.example",
]
}
Then they will initialize Packer and run a build as they did before.
Using Provisioners
The plugin will be compatible with Packer’s built-in provisioners (e.g.,
shell
, file
). This allows users to customize images using the same
provisioners they already use with other plugins.
Provisioners use a communicator to connect to the instance and provision it. There is an SSH communicator for use with Linux and a WinRM communicator for use with Windows.
The following example uses provisioners to customize the image by installing packages and uploading files.
data "oxide-image" "ubuntu" {
name = "noble"
}
source "oxide-instance" "example" {
project = "packer-acc-test"
boot_disk_image_id = data.oxide-image.ubuntu.image_id
# SSH communicator configuration.
ssh_username = "ubuntu"
}
build {
sources = [
"source.oxide-instance.example",
]
# Install packages.
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y ca-certificates",
]
}
# Packer recommends using a 2-step workflow for uploading files.
# 1) Upload the file to a location the provisioning user has access to.
provisioner "file" {
content = "Hello from Packer!"
destination = "/tmp/hello.txt"
}
# 2) Use the shell provisioner to move the files and set permissions.
provisioner "shell" {
inline = [
"sudo cp /tmp/hello.txt /opt/hello.txt",
"sudo chmod 0644 /opt/hello.txt",
]
}
}
Determinations
Oxide will develop and maintain an official [packer] plugin. The plugin source code will be located on GitHub at [oxidecomputer-packer-plugin-oxide] and Oxide will request that the plugin be listed on [packer-integrations].
Open Questions
Will this plugin support [hcp-packer]?
Not initially. According to [hcp-packer-support], HCP Packer is under active development. This will be monitored and adjusted in the future accordingly.
Will this plugin support Windows images?
Yes! Windows images are supported. Packer uses the [packer-winrm-communicator] to connect to Windows instances and provision them.
Will this plugin be listed as a Packer integration?
Yes! This plugin will be listed on [packer-integrations] where it will show up as a community integration.
Security Considerations
Will this plugin redact sensitive information in logs?
Yes. Sensitive attributes will be marked as secret using
packer.LogSecretFilter.Set
so they are redacted when logging. The Oxide API token is considered a sensitive attribute.
External References
[AlmaLinux cloud images] https://github.com/search?q=repo%3AAlmaLinux%2Fcloud-images%20language%3AHCL%20pkr.hcl&type=code
[cloud-init] https://cloud-init.io/
[Developing Plugins] https://developer.hashicorp.com/packer/docs/plugins/creation
[GitHub Actions runner images] https://github.com/search?q=repo%3Aactions%2Frunner-images+language%3AHCL+pkr.hcl&type=code
[HCP Packer] https://developer.hashicorp.com/hcp/docs/packer
[HCP Packer Support] https://developer.hashicorp.com/packer/docs/plugins/creation/hcp-support
[oxidecomputer/packer-plugin-oxide] https://github.com/oxidecomputer/packer-plugin-oxide
[Oxide images] https://docs.oxide.computer/guides/creating-and-sharing-images
[Builder] https://developer.hashicorp.com/packer/docs/plugins/creation/custom-builders
[Data Source] https://developer.hashicorp.com/packer/docs/plugins/creation/custom-datasources
[Post-Processor] https://developer.hashicorp.com/packer/docs/plugins/creation/custom-post-processors
[Provisioner] https://developer.hashicorp.com/packer/docs/plugins/creation/custom-provisioners
[HashiCorp (IBM) Packer] https://developer.hashicorp.com/packer
[Packer Integrations] https://developer.hashicorp.com/packer/integrations
[hashicorp/packer-plugin-sdk/acctest] https://pkg.go.dev/github.com/hashicorp/packer-plugin-sdk/acctest
[Packer Settings] https://developer.hashicorp.com/packer/docs/templates/hcl_templates/blocks/packer
[WinRM Communicator] https://developer.hashicorp.com/packer/docs/communicators/winrm
[Ubuntu Cloud Images] https://cloud-images.ubuntu.com/