Kubernetes
Unikraft Cloud integrates seamlessly with any Kubernetes cluster through a virtual kubelet known as Kraftlet. This is a lightweight Kubernetes node implementation which connects your cluster to Unikraft Cloud's high-performance compute instead of running real pods locally. This enables developers to deploy and manage Unikraft microVMs as if they were native Kubernetes pods.
This integration extends Kubernetes' scheduling and orchestration capabilities to the Unikraft Cloud platform. This allows workloads to take advantage of microVM-level I/O performance, security, cold start and transparent scale-to-zero efficiency while retaining full compatibility with existing Kubernetes tooling.
Upon startup, Kraftlet will register itself as a worker node with the Kubernetes API. Once Kraftlet registers itself as a node, Kubernetes can schedule Pods onto it.
Any Pod scheduled to the Kraftlet node won't run as a container within the cluster. Instead, it will run a highly optimized microVM on Unikraft Cloud. Kraftlet will manage the Pod lifecycle to make sure the apps are up and running.
Getting started
Make sure to log into Unikraft Cloud and pick a metro close to you. Grab the metro name and your API token from the dashboard and set them as environment variables:
Code
You can install Kraftlet into a Kubernetes cluster using its Helm chart:
Code
The chart deploys Kraftlet as a StatefulSet. You can check if Kraftlet is running by checking its pods:
Code
Which should return a single pod running:
Code
You can also check if the kraftlet successfully registered as a node:
Code
Which should, among other nodes, return Kraftlet:
Code
Node taint and labels
Kraftlet taints its node so that nothing lands on Unikraft Cloud by accident.
The default taint is virtual-kubelet.io/provider=ukc with the NoSchedule effect, so every Pod you want on Unikraft Cloud needs a matching toleration and a node selector.
Turn the taint off with --set node.taint.enable=false, or change it through the other node.taint.* values.
Kraftlet advertises the following labels on its node, which you can select on:
| Label | Value |
|---|---|
kubernetes.io/hostname | The node name, kraftlet by default. |
kubernetes.io/role | agent |
kubernetes.io/os | linux |
kubernetes.io/arch | The value of node.architecture, amd64 by default. |
type | kubelet |
unikraft.com/virtual-kubelet | true |
Anything you add under node.labels joins this set.
With kraftlet.replication.enabled=true, each replica registers a node named after its own Pod, such as kraftlet-0 and kraftlet-1.
Select on unikraft.com/virtual-kubelet rather than on kubernetes.io/hostname to spread Pods across every replica.
Autoscaling the cluster
The chart above registers one node per Kraftlet, against capacity that already exists. To let the cluster create that capacity on demand, add the Karpenter provider for Unikraft. It launches a machine when a Pod has nowhere to run, installs Kraftlet against it, joins it as a node, and removes both again once the capacity goes unused.
Examples
Below are examples of Kubernetes configurations that define Unikraft Cloud apps through Kubernetes concepts.
You will notice that each workload object defines tolerations and nodeSelector so Pods get scheduled on the Kraftlet node.
Make sure Kraftlet is up and running before trying out examples below.
Simple app
The configuration below defines an app with three replicas running the NGINX image and a single Kubernetes service that exposes port 443.
For each service backed by a Pod scheduled to the Kraftlet node, Kraftlet will create a corresponding service.
In this case, Kraftlet will create three NGINX instances and a single service that exposes port 443.
Code
You can apply the configuration with:
Code
Once applied, you can check the status of your newly created pods in the Kubernetes cluster:
Code
Code
Your app is now managed from the Kubernetes cluster, but is actually running on Unikraft Cloud.
To check the instances, run:
Which will return a list of instances created from pods above:
Kraftlet derives every Unikraft Cloud resource name from a hash, so the names carry no trace of the Pod they belong to. Use the tags Kraftlet attaches instead to find the instance behind a Pod:
Code
As you can see, all instances have the same FQDN. This is because Kraftlet created a corresponding Unikraft Cloud service for the Kubernetes service defined in YAML above. You can check the created service with the following command:
The generated FQDN follows the hashed service name.
Annotate the Kubernetes Service with cloud.unikraft.v1.services/domain to pick a readable hostname instead, as described under Service annotations.
You can now manage your app running in Unikraft Cloud via Kubernetes resources!
Stateful apps
The example below deploys a stateful app on Unikraft Cloud that has access to a volume.
To support provisioning Unikraft Cloud volumes through Kubernetes, Kraftlet watches PersistentVolumeClaim (PVC) objects with storage class ukc-volume.
Creating a new PVC object with that storage class triggers Kraftlet to create a Unikraft Cloud volume and a PersistentVolume object that marks the PVC as Bound.
This watcher is off by default.
Install the chart with --set kraftlet.enablePvcWatcher=true to let Kraftlet manage ukc-volume claims.
Below is an example PVC with the Unikraft Cloud storage class you can apply to your cluster.
Code
Once applied, you can check the created PVC status:
Code
Code
You can also check the volume on Unikraft Cloud:
At this point, the volume exists but no instance mounts it, so it reports the available state.
To create an instance that uses the volume, create a Kubernetes Pod that references the PVC:
Code
Once the Pod runs, the same volume reports the mounted state and names the instance that attached it.
Every Pod that references the same ukc-volume claim mounts the same Unikraft Cloud volume.
Kraftlet internals
This section describes how Kraftlet translates Kubernetes objects into Unikraft Cloud resources.
Resource names
Kraftlet names every Unikraft Cloud resource after a base62-encoded hash of the Kubernetes object that owns it. Hashing makes the names opaque, so map a resource back to its Kubernetes owner through the tags below rather than through the name.
Tags
Kraftlet tags every instance and volume it creates with the Kubernetes object it belongs to:
| Tag | Instances | Volumes |
|---|---|---|
kraftlet:node=<node> | Yes | Yes |
k8s.io:namespace=<namespace> | Yes | Yes |
k8s.io:pod=<pod> | Yes | No |
k8s.io:container=<container> | Yes | No |
k8s.io:pvc=<claim> | No | Yes |
Kraftlet replaces characters outside A-Za-z0-9-+_.:= with _ and truncates any tag longer than 256 bytes.
Filter on these tags to find the resources behind a Kubernetes object:
Code
Ports and handlers
When Kraftlet maps a Kubernetes Service port to a Unikraft Cloud service, it derives the handler from the port number automatically:
| Port | Handler applied |
|---|---|
80 | http |
443 | tls + http |
| Any other port | tls |
This is why the example above produces 443:8080/tls+http in the service list.
Kraftlet infers tls+http from port 443.
A Service port only maps to a container when the port's targetPort matches a port the container declares, either by number or by name.
Kraftlet skips container ports without such a match, so declare every port you expose in the container specification.
A Pod that backs no Service, or whose Service ports match no container port, still reaches the network as long as it declares exactly one container port.
In that case Kraftlet creates a service on port 443 with the tls and http handlers, and the platform assigns a generated FQDN.
A Pod that backs no Service and declares more than one container port fails, and so does a Pod whose labels match more than one Kubernetes Service.
Multi-container pods
Kraftlet maps each container in a pod to a separate Unikraft Cloud instance. When a Pod has a single container, the Unikraft Cloud service covers the whole Kubernetes Service. When a Pod has more than one container, each container gets its own Unikraft Cloud service derived from the Service name and the container name.
Kraftlet supports init containers. Kraftlet schedules both regular containers and init containers as Unikraft Cloud instances, and deletes them together when you delete the Pod.
Some containers only make sense inside a cluster, such as a log shipper or a service mesh sidecar that a Unikraft Cloud instance never needs.
List those in the cloud.unikraft.v1.instances/ignore annotation and Kraftlet skips them.
An ignored container gets no instance and no service, and Kraftlet reports it as running so the Pod still becomes ready.
Compute resources
Kraftlet sizes each instance from the container resource block, preferring limits over requests:
| Instance property | Source | Default |
|---|---|---|
| Memory | limits.memory, otherwise requests.memory, rounded up to whole MiB | 128 MiB |
| vCPUs | limits.cpu, otherwise requests.cpu, rounded up to whole CPUs | 1 |
A request such as cpu: 500m yields a single vCPU, and cpu: 2 yields two.
Kraftlet passes the rest of the container specification through as well:
| Pod or container field | Unikraft Cloud instance property |
|---|---|
image | Image, prefixed with oci:// when Kraftlet resolves pull credentials |
imagePullPolicy | Pull policy |
command and args | Instance arguments, concatenated in that order |
env | Instance environment |
spec.restartPolicy | Restart policy: Always, OnFailure or Never |
The cloud.unikraft.v1.instances/template annotation changes this mapping.
Kraftlet then creates the instance from the named instance template.
The template supplies the image, resources, arguments, environment and volumes, and Kraftlet adds only the service, ROMs, plugins, scale-to-zero settings and tags.
Environment variables
Kraftlet resolves the container environment in the cluster before it creates the instance, the same way a kubelet does.
It supports envFrom with a ConfigMap or Secret reference, valueFrom with configMapKeyRef, secretKeyRef or fieldRef, $(VAR) expansion between variables, and the service link variables Kubernetes injects for Services in the same namespace.
It marks optional references that go missing with a Pod event instead of failing.
It doesn't support resourceFieldRef.
Files from ConfigMaps, Secrets and images
Kraftlet turns file-shaped volume mounts into ROMs, one ROM per container, and mount. The mount path becomes the ROM mountpoint, and every key becomes a file inside it:
| Pod volume source | ROM content |
|---|---|
configMap | One file per key, or one file per entry under items |
secret | One file per key, or one file per entry under items |
downwardAPI | One file per entry, holding the referenced Pod field |
projected | The merged content of its ConfigMap, Secret, downward API and service account token sources |
image | The referenced image, attached directly as a ROM image |
Kraftlet requests service account tokens from the API server through the TokenRequest API, so projected tokens carry the expiry the Pod asks for.
It honors optional: true on ConfigMap and Secret sources and skips whatever it can't find.
Kraftlet resolves ROM content when it creates the instance. Later edits to a ConfigMap or Secret don't reach an instance that already runs, so restart the Pod to pick them up.
Volumes
Kraftlet maps the remaining volume types onto Unikraft Cloud storage:
| Pod volume | Unikraft Cloud resource |
|---|---|
Claim with the ukc-volume storage class | One volume per claim, shared by every Pod that mounts it |
| Claim with any other CSI storage class | One volume per Pod, staged through the CSI driver |
emptyDir | One volume per Pod and mount, sized from sizeLimit and defaulting to 100 MiB |
emptyDir with medium: Memory | Nothing, Kraftlet skips the mount |
hostPath | One managed volume per Pod and mount |
Kraftlet rounds an emptyDir size limit up to whole MiB.
It deletes the volumes it created for emptyDir and hostPath mounts together with the Pod, while a ukc-volume claim keeps its volume until you delete the claim.
hostPath support stays off unless you set KRAFTLET_ENABLE_HOST_PATH_VOLUMES=true through kraftlet.env.
Third-party CSI drivers
Kraftlet can serve claims that belong to another storage system, such as a cloud block store, by driving that system through its CSI driver.
Register each driver under csi.plugins as a driverName: host:port pair, or as a path to its socket.
Code
For a Pod that mounts such a claim, Kraftlet waits for the volume attachment when the driver needs one, then calls the driver to stage the volume under csi.stagingBasePath.
It then creates a managed volume that points at the staging path.
It health-checks every registered driver on the csi.healthCheckInterval and refuses to stage through a driver that reports unhealthy.
Setting any csi value also makes the chart advertise the volumes.kubernetes.io/controller-managed-attach-detach node annotation.
The platform resolves a managed volume path on the machine that runs the instance. This flow expects Kraftlet to stage volumes on that same machine, which holds for on-prem and bring-your-own-cloud installations.
Init containers
Kraftlet runs init containers as ordinary instances, one after another, before it creates the instances for the regular containers. Each init instance starts with autostart off, restarts off and scale-to-zero off. Kraftlet starts it, waits for it to stop, and treats a non-zero exit code as a failure.
A Pod with the Always or OnFailure restart policy makes Kraftlet retry a failed init instance with an exponential backoff that grows from one second to five minutes.
With Never, the first failure stops the Pod from starting.
Private registries
Kraftlet reads the Secrets listed under spec.imagePullSecrets and passes the matching credentials to the platform with the image.
It accepts both the kubernetes.io/dockerconfigjson and the kubernetes.io/dockercfg Secret types, and matches an entry to the image by registry host.
An image without a registry host, such as nginx:latest, and an image on docker.io both match the index.docker.io entry.
Pod status
Kraftlet refreshes the status of every Pod it manages from the platform on the kraftlet.podStatusUpdateInterval, which defaults to 15 seconds.
It derives the Pod phase from the state of the backing instances:
| Instance state | Pod phase |
|---|---|
starting | Pending |
running, draining, stopping | Running |
standby | Running, or Pending while the platform reports a failure |
stopped after a clean shutdown | Succeeded |
stopped after a fault or a failed image pull | Failed |
| Any state, once the platform stopped the instance for insufficient quota | Failed |
The container status carries the detail behind a failure.
An instance the platform stopped for running out of memory surfaces as OOMKilled with exit code 137.
A failed image pull surfaces as ErrImagePull, a stop for insufficient quota surfaces as QuotaExceeded with exit code 1, and any other platform-side stop surfaces as PlatformError.
Note that the running quota is dynamic, so on the next request, an instance which failed to start with QuotaExceeded might succeed.
An instance with exactly one network interface also contributes its private IP as the Pod IP.
Kraftlet records what it does on the Pod as events:
Code
CreateInstanceFailed and PodCreateServiceFailed carry the platform error that blocked the Pod.
kubectl logs works against a Pod on the Kraftlet node.
Kraftlet serves it from the instance console and returns the last 4096 bytes.
The --tail and --limit-bytes flags move that window, and Kraftlet counts both in bytes rather than in lines.
Node capacity and conditions
Kraftlet reports the node capacity from your Unikraft Cloud quotas, so the Kubernetes scheduler stops placing Pods once you run out of headroom:
| Node resource | Quota |
|---|---|
cpu | Live vCPU quota |
memory | Live memory quota |
pods | Instance quota |
Kraftlet reports allocatable capacity equal to capacity, and refreshes both on every node status interval. It also maps quota exhaustion and platform health onto node conditions:
| Condition | Kraftlet sets it when |
|---|---|
Ready | The platform answers its health check |
MemoryPressure | The workloads use up the live memory quota |
DiskPressure | The volumes use up the storage quota |
PIDPressure | The instances use up the live instance quota |
UnikraftPlatformHealthy | The platform health endpoint reports a healthy state |
An unreachable platform turns Ready to false, so the scheduler stops placing new Pods on the node until the platform answers again.
Pod rescheduling
Idle instances go to standby and hold no live memory, which lets a node carry far more Pods than its live memory quota can run at once.
Resuming one can fail once the quota is full, and the platform then leaves the instance in a stopped or scaled-to-zero state rather than starting it.
Kraftlet reports such a Pod as Failed with the reason QuotaExceeded, and then deletes the Pod so the controller that owns it schedules a replacement.
The node reports MemoryPressure as soon as its next quota refresh finds the live memory quota exhausted.
That condition becomes a node.kubernetes.io/memory-pressure:NoSchedule taint, which your Pods don't tolerate, so the replacement stays off that node until live memory frees up.
On a cluster that autoscales with Karpenter, the replacement lands on another node, or on a node the autoscaler creates for it.
Keep these in mind when running your workloads:
- Run Pods under a Deployment, ReplicaSet, StatefulSet or Job, since Kraftlet reclaims only a Pod that has a controller behind it.
It leaves a bare Pod
Failedin place, because deleting that Pod would leave nothing behind to replace it. - Reclaiming a Pod runs the same teardown as a delete, so its instances and Pod-scoped volumes (including
emptyDirandhostPathvolumes) go with it. Volumes behind PersistentVolumeClaims remain until you delete their claims.
Kraftlet reclaims failed Pods by default.
Set kraftlet.enableFailedPodReclaim=false to leave them in place instead.
Resource lifecycle
Kraftlet adds the cloud.unikraft.v1/resources finalizer to every Pod it accepts, so a delete only completes once Kraftlet removes the Unikraft Cloud resources.
When you delete a Kubernetes object, Kraftlet deletes the corresponding Unikraft Cloud resources:
| Kubernetes object deleted | Unikraft Cloud resources deleted |
|---|---|
| Pod or Deployment replica | Instances for its containers and init containers, the service once no instance uses it, the volumes for its emptyDir and hostPath mounts, and any certificate the service held |
PersistentVolumeClaim with the ukc-volume class | The volume behind the claim |
Kraftlet also cleans up a Pod that finishes on its own.
Once every container reaches a successful stop and the Pod phase becomes Succeeded, Kraftlet deletes its instances and volumes.
Turn off finalizers by setting KRAFTLET_POD_FINALIZER="", but be careful-this risks leaking resources on the Unikraft platform when the Kraftlet node is draining.
Platform features
Kraftlet supports the following Unikraft Cloud platform features on Kraftlet-managed resources:
-
Frequently deployed workloads can go into instance templates. Templates pre-warm the snapshot, reducing cold-start latency for every new instance created from the template. Point a Pod at one with the
cloud.unikraft.v1.instances/templateannotation. -
Instances that back a service or carry plugins suspend automatically when idle. Scale-to-zero runs by default for those instances and you can configure it through Pod annotations. Kraftlet turns it off for an instance with neither a service nor a plugin, since nothing would wake it again.
-
Kraftlet ships ConfigMaps, Secrets, downward API fields and image volumes to instances as ROMs.
-
A Pod can attach plugins to its instances through a ConfigMap, and Kraftlet annotates the Pod with the endpoint of each running plugin.
-
hostPathmounts and volumes staged through third-party CSI drivers become managed volumes. -
Every instance and volume Kraftlet creates carries the identity of its Kubernetes owner.
-
Instances can carry addresses and TAP devices that you choose instead of ones from the platform pool. Kraftlet drives this through CNI plugins.
-
Instance metadata that also reaches the guest. Kraftlet writes the CNI result that a guest configures its own interfaces from.
Custom networking with CNI
Limited Access
CNI-based networking for Kraftlet is available as part of enterprise plans. To enable it for your account, reach out to the Unikraft Cloud Discord or send an email to support@unikraft.com.
The Container Network Interface (CNI) configures the networking stack of a workload through plugins. This split lets vendors such as AWS, Tigera and Isovalent each ship their own plugin for configuring networking. Kubernetes builds on the same interface, and the kubelet invokes the installed CNI plugins on every node of the cluster.
Kraftlet takes that role for Pods that land on the Kraftlet node. It invokes the CNI plugins of your cluster and passes the resulting interfaces and addresses to Unikraft Cloud. The platform then creates the instance with custom network interfaces rather than with interfaces from its own address pool. Instances join the same networks as native Pods and follow the same address management as the rest of the cluster.
A companion component, remote-cni, exposes the plugins over gRPC for clusters where Kraftlet runs apart from the machine that hosts the instances.
An instance's network_interfaces only accept IPv4 addresses.
Therefore, the Kraftlet hands the rest of a plugin result—IPv6 addresses, in practice—to the guest through the unikraft.com/cni annotation, and the guest configures them on its own interfaces.
Kraftlet does this for the Pod's default network, a secondary network contributes its IPv4 addresses alone.
You can write the same annotation yourself on an instance you create through the API, for an interface without an address whose addressing an IPAM system owns.
Annotations
Kraftlet reads the following annotations from Pod and Service objects to configure Unikraft Cloud resources.
Pod annotations
| Annotation | Type | Default | Description |
|---|---|---|---|
cloud.unikraft.v1.instances/autostart | boolean | true | Whether the instance starts automatically when Kraftlet schedules the Pod. |
cloud.unikraft.v1.instances/template | string | — | Name of a pre-existing Unikraft Cloud instance template to use instead of the container image. |
cloud.unikraft.v1.instances/ignore | string | — | Comma-separated container names that Kraftlet leaves out of Unikraft Cloud. |
cloud.unikraft.v1.instances/plugins | string | — | ConfigMap holding the plugin list, written as <configmap> or <configmap>/<key>. |
cloud.unikraft.v1.instances/plugins.<container> | string | — | Same as above, for a single container of a multi-container Pod. |
cloud.unikraft.v1.instances/scale_to_zero.policy | on | off | idle | on | Enables or disables scale-to-zero for instances that back a service or carry plugins. |
cloud.unikraft.v1.instances/scale_to_zero.stateful | boolean | false | When true, Kraftlet retains the instance state when scaling to zero. |
cloud.unikraft.v1.instances/scale_to_zero.cooldown_time_ms | integer | 1000 | Idle time in milliseconds before Kraftlet suspends the instance. |
The plugin ConfigMap holds a JSON array of plugin objects under the plugins.json key, or under the key you name in the annotation.
Kraftlet also accepts a ConfigMap with a single key of any name.
Code
Service annotations
| Annotation | Type | Default | Description |
|---|---|---|---|
cloud.unikraft.v1.services/domain | string | — | Custom domain for the Unikraft Cloud service. |
cloud.unikraft.v1.services/domain.<container> | string | — | Per-container domain for a multi-container Pod. |
A bare label such as my-app becomes a subdomain of the metro, giving my-app.fra.unikraft.app.
A fully qualified name such as app.example.com makes the platform request a certificate for it.
For a multi-container Pod, the global annotation applies to every container as <container>-<domain>, and the per-container form overrides it.
Status annotations
Kraftlet writes a few annotations back onto the Pod as it reports status:
| Annotation | Description |
|---|---|
cloud.unikraft.v1.instances/plugins.<plugin>.url | Address of a running plugin, prefixed with the container name for multi-container Pods. |
cloud.unikraft.v1.instances/fqdns | JSON object mapping each container to the private and service FQDN of its instance. |
The FQDN annotation stays off until you install the chart with --set kraftlet.enableInstanceFqdnAnnotations=true.
Helm chart values
The values below cover the settings most deployments touch.
| Value | Default | Description |
|---|---|---|
ukc.metro | — | Metro name such as fra, or a full API endpoint address. |
ukc.token | — | Unikraft Cloud token, which the chart stores in a Secret. |
image.name and image.tag | ghcr.io/unikraft-cloud/kraftlet and latest | Kraftlet image. |
node.name | kraftlet | Name Kraftlet registers with the control plane. |
node.architecture | amd64 | Architecture the node advertises. |
node.taint.enable | true | Whether Kraftlet taints its node. |
node.taint.key, node.taint.value, node.taint.effect | virtual-kubelet.io/provider, ukc, NoSchedule | The taint Kraftlet applies. |
node.labels and node.annotations | — | Extra labels and annotations for the node object. |
node.providerId | — | Provider ID of the machine backing the node. |
kraftlet.replication.enabled and kraftlet.replication.replicas | false and 1 | Run more than one Kraftlet, each registering its own node. |
kraftlet.enablePvcWatcher | false | Manage the lifecycle of ukc-volume claims. |
kraftlet.enableInstanceFqdnAnnotations | false | Annotate Pods with the FQDNs of their instances. |
kraftlet.enableFailedPodReclaim | true | Delete a Pod that fails because the live memory quota is full, so its controller replaces it. |
kraftlet.podStatusUpdateInterval | 15s | How often Kraftlet refreshes Pod status from the platform. |
kraftlet.podSyncWorkers | 1 | Number of Pod reconcile workers. |
kraftlet.logLevel and kraftlet.logType | info and json | Log verbosity and format. |
kraftlet.port | 10250 | Port for the kubelet API that serves logs and Pod listings. |
kraftlet.k8s.qps and kraftlet.k8s.burst | — | Rate limits for the API server client. |
kraftlet.env | — | Extra environment variables for the Kraftlet container. |
csi.plugins | — | CSI drivers Kraftlet calls, as driverName: host:port pairs. |
csi.stagingBasePath | /var/lib/kubelet/plugins/kubernetes.io/csi/staging | Where Kraftlet stages CSI volumes. |
csi.healthCheckInterval | 10s | How often Kraftlet health-checks each CSI driver. |
tls.secretName and tls.secretKeys | — | Serving certificate for the kubelet API. |
resources | — | Resource requests and limits for the Kraftlet pod. |
priorityClassName | — | PriorityClass for the Kraftlet pod, which keeps its node registered under pressure. |
Current limitations
A Kraftlet node is a virtual kubelet in front of a remote platform, so a few kubelet behaviors have no counterpart:
kubectl execandkubectl attachdon't reach an instance.kubectl top podreturns nothing, since Kraftlet serves no stats summary. Read instance metrics instead.- Liveness, readiness, and startup probes never run. Kraftlet reports readiness from the instance state.
- Kraftlet applies no in-place updates to a Pod specification. Recreate the Pod to change the instance behind it.
- A Pod may back at most one Kubernetes Service.
subPathon a volume mount has no effect.- An
emptyDirwithmedium: Memorygets no backing volume.