Build and Test Environments
Build and test workloads often need dynamic code execution in a controlled runtime. You want to swap test logic, keep startup latency low, and avoid rebuilding full images for every test change.
With Unikraft Cloud, you can run a reusable Go runtime image and inject test logic via ROMs. At startup, the instance compiles ROM code into a Go plugin and executes it, giving you fast iteration with strong isolation.
Why run build and test environments on Unikraft Cloud
Fast test iteration
Build and test logic changes frequently. With ROM-based deployment, you package and push a small ROM instead of rebuilding the full runtime image:
- Keep the runtime stable.
- Update only test payloads.
- Roll forward and back by switching ROM image tags.
Runtime compilation in isolated microVMs
Each environment runs in its own microVM.
The runtime compiles ROM code inside the instance (go build -buildmode=plugin) and loads it as a plugin.
This isolates compilation and execution while keeping the runtime reusable.
Template-based cold start optimization
The base image can create an instance template. New environments launch from that template, reducing startup time for bursty CI-style workloads.
Scale-to-zero economics
Test environments are often idle between runs. With scale-to-zero enabled, idle instances consume no active resources while still waking when needed.
Getting started
This guide uses the build-environments example.
The base image contains a Go server (server.go) that:
- Writes to
/uk/libukp/template_instanceto create a template. - Reads ROM source from
/rom/rom.go. - Compiles it to
/run/rom.sousinggo build -buildmode=plugin. - Loads the
Handler()function from the plugin and serves its output over HTTP.
Prerequisites
-
Install the CLI: Use the unikraft CLI or the legacy kraft CLI. You need a BuildKit builder. The easiest way is via Docker.
-
Clone the examples repository and enter the example directory:
Code
Make sure to log into Unikraft Cloud and pick a metro close to you.
This guide uses fra (Frankfurt, 🇩🇪):
Step 1: Package and push the base runtime image
Package and push the base Go runtime image (see server.go for the runtime implementation):
The server in server.go loads /rom/rom.go, compiles it to /run/rom.so using go build -buildmode=plugin, and invokes Handler() from the plugin.
Step 2: Create a template from the base image
Create a short-lived instance from the base image (without ROM attached).
The server writes to /uk/libukp/template_instance and turns the instance into a template before serving requests:
The output shows the instance address and other details:
This instance is short-lived, since right before the server starts, it triggers a conversion into a template. To check that the template is ready, run:
Step 3: Package ROM payloads
Each ROM contains a Go function implementation.
Step 4: Launch test environments from template
Create an instance with the first ROM:
The instance will compile the ROM into a plugin on first start, which may take a few seconds. To check the progress, you can view the instance logs:
Create another instance with the second ROM:
The instance will compile the ROM into a plugin on first start, which may take a few seconds. To check the progress, you can view the instance logs:
Both instances run the same base Go runtime, but execute different ROM payloads, demonstrating fast iteration with reusable layers.
List the instances and note their FQDN values:
Test both instances using the FQDNs from the listing:
Code
Code
Why this pattern works
This build/test environment uses three reusable layers:
-
Base runtime image: A Go server plus toolchain. Build once, reuse many times.
-
ROM payload: A source-only image (
rom.go). Update per test case. -
Template snapshot: A pre-initialized runtime used to speed up new instance startup.
Cleanup
Learn more
Or visit the CLI Reference or the legacy CLI reference.