Zudoku
Features

Annotations

Annotations attach arbitrary key-value metadata to an instance. They use the same key syntax as Kubernetes annotations, so you can reuse your existing keys and tooling.

Unlike tags, which act as controller-level labels that only the platform sees, annotations also reach the guest. You can use them for three things:

  1. Custom metadata—the instance's startdata includes its annotations, so the guest can read them at runtime.
  2. Guest network configuration—the guest applies the CNI result in the unikraft.com/cni annotation to its network interfaces.
  3. Structured log output—you select which annotations the platform injects into the VM's console log output.

Dedicated unikraft CLI subcommands for annotations are coming soon. In the meantime, drive annotations through the API, which you can invoke with curl or the unikraft api command.

Annotation keys

An annotation is a set of key-value pairs. A value is any string without ASCII control characters (0x00–0x1f and 0x7f, which covers tab, newline and carriage return), and multi-byte UTF-8 goes through fine. The API rejects a value carrying an ASCII control character with 400 Bad Request. Keys follow the Kubernetes annotation key syntax:

  • A key can be a plain name (my-key) or carry an optional DNS prefix separated by a slash (example.com/annotation2).
  • Each key needs a name segment of at most 63 characters, containing alphanumerics, -, _, and ., and beginning and ending with an alphanumeric character.
  • The optional prefix is a DNS subdomain (a series of DNS labels joined by ., at most 253 characters) followed by a /. It can't be a wildcard domain.

An instance holds at most 256 annotations.

For example:

JSONCode
{ "test.unikraft.com/my-annotation": "value3", "my-key": "value1", "example.com/annotation2": "value2" }

Creating an instance with annotations

Add an annotations object to the instance body when you create it. Each entry is a key-value pair:

TerminalCode
unikraft api /v1/instances -X POST --metro fra \ '{ "name": "annotations-demo", "image": "<my-org>/annotations-demo:latest", "annotations": { "test.unikraft.com/my-annotation": "value3", "my-key": "value1", "example.com/annotation2": "value2" } }'

Annotation keys are Kubernetes-compatible, so they can be plain keys (my-key) or prefixed with a domain (example.com/annotation2).

Reading annotations from the guest

The platform includes the instance's annotations in its startdata, which the guest reads from /sys/class/uio/uio0/device/startdata:

TerminalCode
cat /sys/class/uio/uio0/device/startdata
JSONCode
{ "ip": "10.0.0.5/30", "uuid": "d0e22bb8-b4c1-44ea-bb58-197fedbeb2ab", "annotations": { "test.unikraft.com/my-annotation": "value3", "my-key": "value1", "example.com/annotation2": "value2" }, "mac": "12:b0:0a:00:00:05", "gw": "10.0.0.6", "hostname": "annotations-demo" }

Your app parses this JSON and reads the annotations object like any other startdata field.

Guest network configuration with CNI

unikraft.com/cni is a reserved annotation. Its value is a CNI Result document, and the guest applies the addresses it holds to its own network interfaces.

This hands the addressing of an instance to an external CNI plugin or IPAM system. The plugin decides the addresses, writes its result into the annotation, and the guest configures itself from it. Your image needs no agent for it and no boot script of its own.

The annotation pairs with an interface without an address, which reaches the guest as a bare link on a TAP device of your own. See Kubernetes for the CNI integration that Kraftlet drives on the cluster side.

The annotation value is the document as a JSON string, so the JSON inside it carries escapes:

TerminalCode
unikraft api /v1/instances -X POST --metro fra \ '{ "name": "cni-demo", "image": "<my-org>/cni-demo:latest", "network_interfaces": [ {}, { "name": "cni-demo-vpc", "tap_name": "vpc0", "mac": "0a:58:c0:a8:01:05", "autoconfig": false } ], "annotations": { "unikraft.com/cni": "{\"cniVersion\":\"1.0.0\",\"interfaces\":[{\"name\":\"eth0\",\"mac\":\"0a:58:c0:a8:01:05\"}],\"ips\":[{\"interface\":0,\"address\":\"192.168.1.5/24\"}]}" } }'

That instance keeps a pool-allocated primary interface and adds a second one on the vpc0 TAP device, with a MAC address you pick and no IP address. Unescaped, the annotation holds a standard CNI Result (types/100):

JSONCode
{ "cniVersion": "1.0.0", "interfaces": [ { "name": "eth0", "mac": "0a:58:c0:a8:01:05" } ], "ips": [ { "interface": 0, "address": "192.168.1.5/24" } ] }

The guest gives 192.168.1.5/24 to the link whose MAC address is 0a:58:c0:a8:01:05, and brings that link up.

Writing the document

Give every address a MAC address to land on:

  • Each ips[] entry points through its interface index at an entry of the document's own interfaces[] array, not at the instance's network_interfaces. An entry with no interface field uses index 0.
  • That interfaces[] entry supplies the MAC address of the interface to configure, so interfaces[].name can say anything. Without a MAC address to match, the index counts the instance's non-loopback interfaces from 0 instead. The entry carries no mac, the document holds no interfaces[] array, or the index sits past the end of that array.
  • Set the MAC address yourself when you create the instance, or read it from the instance status.
  • Addresses take CIDR notation, IPv4 or IPv6, and the prefix length is mandatory.
  • The document needs a cniVersion of 0.3.1, 0.4.0, 1.0.0, or 1.1.0, and has to be a result rather than a network configuration.
  • Keep the document on one line, because annotation values reject control characters.

Only interfaces[].mac, ips[].interface, and ips[].address have any effect. routes, gateway, dns, mtu, and sandbox do nothing, so you can pass a plugin's full result through untouched. Set the default gateway and the resolver with the instance's own gateway and nameserver fields.

Changing addresses

Running instances pick up updates to their annotations, and reconcile them against what's currently configured, so an address change doesn't require a restart.

When nothing happens

A single MAC address that matches no interface stops the whole document from applying, leaving every interface untouched. Check that each MAC address in the document matches one on the instance, then read the instance logs, which name any document that fails to parse or apply.

Patching annotations

Update the annotations on an existing instance with a PATCH /instances request. The body is an array of patch operations, each naming the target instance, the annotations property, and one of three operations: set, add, or del.

Annotation changes apply only while the instance is in the stopped state. If you patch a running instance, the platform accepts the request and queues the change, then applies it the next time the instance stops.

Replace all annotations (set)

set replaces all existing annotations with the object you provide:

TerminalCode
unikraft api /v1/instances -X PATCH --metro fra \ '[{ "name": "annotations-demo", "prop": "annotations", "op": "set", "value": { "env": "production", "team": "platform" } }]'

Add or update annotations (add)

add merges the given annotations into the existing set. It overwrites keys that already exist and leaves the other keys untouched:

TerminalCode
unikraft api /v1/instances -X PATCH --metro fra \ '[{ "name": "annotations-demo", "prop": "annotations", "op": "add", "value": { "added-key": "added-value" } }]'

Delete annotations (del)

del removes specific keys. The value is an array of key names (or a single key as a string):

TerminalCode
unikraft api /v1/instances -X PATCH --metro fra \ '[{ "name": "annotations-demo", "prop": "annotations", "op": "del", "value": ["my-key", "example.com/annotation2"] }]'

Annotations in log output

Beyond the guest, the platform can inject selected annotations into a VM's console log output. This helps with structured logging to an external collector.

This forwarding happens at the node level rather than through the instances API, so coordinate with Unikraft to enable it for your deployment. The platform exposes two options:

TerminalCode
--vmm-console-ports "app:socket/json:/tmp/vector.sock,app2:socket/json:/tmp/vector.sock" --vmm-console-annotations "app:my-key+test.unikraft.com/my-annotation,app2:*"
  • --vmm-console-ports defines named console ports as <name>:<type>[/<format>][:<path>]. Here app and app2 are socket ports using the json format, each writing to a Vector socket.
  • --vmm-console-annotations selects which annotations appear in each port's output, as <port>:<key1>+<key2>,... or <port>:*:
    • app:my-key+test.unikraft.com/my-annotation—forward only the listed annotation keys (joined with +) to port app.
    • app2:*—the * wildcard forwards all annotations to port app2.

When the guest writes to the app console (for example, echo "blub" > /dev/vport2p1), the collector receives the message together with the selected annotations:

JSONCode
{ "annotations": { "my-key": "value1", "test.unikraft.com/my-annotation": "value3" }, "host": "(unnamed)", "message": "blub", "name": "app", "source_type": "socket", "timestamp": "2026-05-08T08:35:26.489812959Z", "uuid": "9d262ffd-878f-47f3-98c3-bb017c11d690" }

Annotation events

The platform's event log carries a vm.annotate event, which it emits whenever an instance's annotations change:

JSONCode
{ "type": "vm.annotate", "timestamp": "2026-08-19T10:04:11Z", "data": { "vm": "b495f451-0370-4d8e-8b94-a5b22d2212f9", "annotations": { "env": "prod", "team": "search" } } }

An external system consumes the event to keep track of the instance's annotation values, without polling the instance API. The event also gives log processing a way to map an instance UUID back to its annotations.

Limitations

  • An instance holds at most 256 annotations.
  • Annotation keys must follow the Kubernetes key syntax, and values are strings with no ASCII control characters in them.
  • A patched annotation change applies once the instance reaches the stopped state.
  • A unikraft.com/cni document holds at most eight interfaces and 16 addresses, and an ips[].interface index stays below eight.
  • The guest applies only interfaces[].mac, ips[].interface and ips[].address from that document, and needs a recent base-compat guest kernel to apply it at all.
  • Dedicated unikraft CLI subcommands aren't available yet, so drive annotations through the API.
  • The platform sets up log-output forwarding at the node level, not through the instances API.
  • The vm.annotate event needs switching on in the node's event configuration.

Learn more

Last modified on