Dynamic Reconfiguration of Unikernels at Boot Time with Firecracker

One of the first things that stands out to people when they first start playing around with unikernels is that the concept of an interactive server that you can pop into and start running all sorts of arbitrary programs (also known as 'commands') is supplanted by just one program - yours. So even though it's running 'as a server' it's really just your program running much the same way you might start it normally on your laptop. It's through the magic of virtualization that we can even do this.

Engineers have developed different patterns to deal with configuration differences and environment changes. Some will create a package which is essentially just a tarball of a root filesystem layout and some configuration dressing on top. Some create multiple packages per deploy target while others provide an additional config to be used for each deploy in their CI system. Done this way images are created on-demand at deploy time, which for most public clouds is how things work. This sounds like it would take a long time but a deploy done this way to Amazon only takes tens of seconds to build a brand new AMI and spin it up as an instance.

Ops run is pretty fast and by default re-generates images on the fly which is great for dev/test but no one uses 'ops run' in production. Even if you are running your own infrastructure you'd still use 'ops image create' and 'ops instance create' - just with the onprem target instead.

Other options exist too such as the cloud_init klib which allows you to inject env vars at boot time. This is a common practice to slurp down secrets such as TLS certs. We've even added vsock support in the past.

However there are still some cases where you might want to dynamically configure various options even before booting without having to rework the image and without having to hook up a secret store.

Enter Firecracker

Nanos has had firecracker support for quite a long time now. I originally wrote about it a few years ago and we of course have documentation on booting unikernels with firecracker. Back then a lot of people weren't necessarily using firecracker as most of our existing users were on the cloud and you need virtualization for firecracker but now we are starting to see more and more firecracker based platform-as-a-services pop up and we wanted the ability to make that experience even better.

One of our users noticed that it was increasingly common for people to pass in configuration via the kernel init line with firecracker and was wondering why we didn't support it. The answer is relatively simple. 99% of our current user base deploys to GCP, AWS, Azure or some other public cloud. When you deploy to these clouds you typically don't have the capability to inject various kernel parameters into the image at boot time so we've just done things differently.

To cut to the chase - we added support for changing arbitrary settings in the root tuple via the kernel command line, which is retrieved by the kernel when booting under AWS Firecracker.

Example Time

This capability is a lot more extensible than one might think at first blush because of Nanos unique TFS (tuple file system).

Even this simple hello world go app which we dumped shows the mix of fs and metadata:

eyberg@venus:~$ ~/.ops/0.1.48/dump ~/.ops/images/g
detected filesystem at 0xc10600
Label:
UUID: 73f8c8d6-df99-c3a0-0942-a0bf8dc25874
metadata
(..:
 arguments:[g]
 booted:
 children:(etc:(..:
                children:(passwd:(..:
                                  extents:(0:(allocated:1
                                              length:1
                                              offset:5973))
                                  filelength:33)
                          resolv.conf:(..:
                                       extents:(0:(allocated:1
                                                   length:1
                                                   offset:5972))
                                       filelength:20)
                          ssl:(..:
                               children:(certs:(..:
                                                children:(ca-certificates.crt:(..:
                                                                               extents:(0:(allocated:406
                                                                                           length:406
                                                                                           offset:5566))
                                                                               filelength:207436)))))))
           g:(..:
              extents:(0:(allocated:13458
                          length:13458
                          offset:5974))
              filelength:6890290)
           lib:(..:
                children:(x86_64-linux-gnu:(..:
                                            children:(libc.so.6:(..:
                                                                 extents:(0:(allocated:4049
                                                                             length:4049
                                                                             offset:1464))
                                                                 filelength:2072888)
                                                      libnss_dns.so.2:(..:
                                                                       extents:(0:(allocated:53
                                                                                   length:53
                                                                                   offset:5513))
                                                                       filelength:26936)))))
           lib64:(..:
                  children:(ld-linux-x86-64.so.2:(..:
                                                  extents:(0:(allocated:439
                                                              length:439
                                                              offset:1025))
                                                  filelength:224376)))
           proc:(..:
                 children:(self:(..:
                                 atime:7313690904789804366
                                 children:(exe:(..:
                                                atime:7313690904791658778
                                                linktarget:/g
                                                mtime:7313690904791658778))
                                 mtime:7313690904789804366)
                           sys:(..:
                                children:(kernel:(..:
                                                  children:(hostname:(..:
                                                                      extents:(0:(allocated:1
                                                                                  length:1
                                                                                  offset:19432))
                                                                      filelength:7))))))))
 environment:(IMAGE_NAME:g
              NANOS_ARCH:amd64
              NANOS_VERSION:0.1.48
              OPS_PORT:8080
              PWD:/
              USER:root)
 program:/g)

However, we can theoretically change any configuration you might find in our extremely extensive configuration documentation. Typically, this would constitute an image rebuild, even if an 'ops run' is more or less instant. Now, an operator can take the same image and dynamically change this at boot time, which becomes very handy if you are an operator of a firecracker based paas.

Let's take a look at few examples using the firecracker vm_config.json following along from our documentation:

Changing an IPv4 Address:

"boot-source": {
    "kernel_image_path": "/Users/bob/.ops/nightly/kernel.img",
    "boot_args": "en1.ipaddr=10.3.3.6 en1.netmask=255.255.0.0 en1.gateway=10.3.0.1"
}

We can put a lot of different things in that 'boot_args' config.

Change an IPv6 Address:

en1.ip6addr=20::A8FC:FF:7600:AA

Overwrite an Existing Env Var:

environment.PROD=1

Adjust a New Argument:

arguments.0=/python3 arguments.1=my_new_entrypoint.py

You Can Even Turn on Debug Support Dynamically:

"boot_args": "trace=t debugsyscalls=t"

Stop saying that! YoU CaNT DeBUG unIkernelSZ!!!!

Getting Started

Ops was built to make building and deploying unikernels easily and takes care of networking locally if you're just using ops. However, in this example we want to use firecracker so there's a different flow. If you want to try it out - let's create a new network (bridge) first:

ops network create

This will create new bridge and a dhcp listener along with it. Then we'll add a tap interface to it.

sudo ip tuntap add dev tap0 mode tap
sudo ip link set tap0 up
sudo brctl addif br0 tap0

From there we can start firecracker normally and read any of our logs.

 ./read_fifo.sh log.fifo

As with most changes in Nanos we typically end up making several other types of changes to get the changes we actually want done.

For example we had to add a new region to hold the command line itself:

#define REGION_INITIAL_PAGES     10 /* for page table allocations in stage2 and early stage3 */
#define REGION_CMDLINE           11 /* kernel command line */
#define REGION_FILESYSTEM        12 /* offset on disk for the
filesystem, see if we can get disk info from the bios */

We had to make a change in the id heap which in return made us have to make a change in the bitmap allocator to ensure alignment. These changes allowed us to remove padding which was wasteful.

Another change we made was to modify the tuple_notifer to operate on a composite (tuple or vector) value without modifying nested values. This was necessary to add the ability to modify the root config tuple.

This is all to say that a seemingly innocuous, yet reasonable, feature request can both touch quite a lot, yet at the same time deliver immense benefits.

Are you using firecracker? Are you building a firecracker Paas? If so reach out.

Deploy Your First Open Source Unikernel In Seconds

Get Started Now.