Taking Firecracker for a spin

Doing semi-meaningful things with Firecracker

Firecracker[1] is recently making rounds on the internet as this relatively new, awesome technology for running lightweight VMs.

As something coming from AWS and powering AWS Lambda, my original perception was that it’s not easy to set up and use. However, this write from Julia Evans[2] proved me wrong. So, as I have recently picked up a used Dell R720 with decent amount of RAM and CPUs, it was time to take these two for a spin together.

Sipping the first coffee this gloomy Nürburgring weather morning, the thought of putting Vault on Firecracker seemed somewhat amusing, … and the day was gone.

I’m going to show you how I’ve done it. Do I like Firecracker? It’s been only one day but yes, it’s pretty neat and easy to use.

§Environment

Clean HWE Ubuntu 18.04.5 Server installation with the password-less sudoer user.

§Intro

A Firecracker VM requires a Linux Linux Kernel and a root file system. Most of the people who talk about Firecracker use example hello-vmlinux and a root file system from AWS. I wanted to learn how to build these myself so I went with building my own 5.8 kernel and a root file system extracted from the default official HashiCorp Vault Docker image. The steps:

  • install all the software required to build Linux kernel and install Docker
  • setup directory structure to work with
  • install Firecracker
  • get, configure and build Linux kernel
  • extract Vault file system from a running container
  • run Vault Firecracker microVM

§Dependencies

To avoid multiple apt-get updates, add Docker repository first:

1
2
3
4
5
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

Next, install all the software required to compile the kernel:

1
2
3
4
5
6
7
8
9
sudo apt-get update
sudo apt-get install \
    bison \
    build-essential \
    flex \
    git \
    libelf-dev \
    libncurses5-dev \
    libssl-dev -y

followed by Docker dependencies:

1
2
3
4
5
6
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common -y

and finally, Docker itself:

1
2
3
4
5
6
sudo apt-get install \
    docker-ce \
    docker-ce-cli \
    containerd.io -y
sudo groupadd docker # this may report that the group already exists
sudo usermod -aG docker $USER

§Setup the directory structure

To have everything in one place, create the following directories:

1
2
3
sudo mkdir -p /firecracker
sudo chown -R ${USER}:${USER} /firecracker
mkdir -p /firecracker/{configs,filesystems,kernels,linux.git,releases}
  • configs: this is where we will put the VM config JSON files
  • filesystems: this is where the extracted file systems will reside
  • kernels: this directory contains pre-built Linux kernels
  • linux.git: Linux sources go here
  • releases: Firecracker releases will be installed here

§Install Firecracker

This is as simple as downloading a pre-built binary release from GitHub and putting it on the PATH. I wanted to have versioning available for ease of upgrading in the future so I’ve built a shell program to manage this for me. You can find the program in this repository on GitHub[3]. Long story, short:

  • download install-firecracker.sh program and put it in /firecracker directory; technically does not matter where but the program assumes the /firecracker/... directory structure from the previous step
  • chmod +x /firecracker/install-firecracker.sh
  • run: sudo /firecracker/install-firecracker.sh

This will download the latest Firecracker release, install the release in /firecracker/releases directory and create /usr/bin/firecracker-<version> and /usr/bin/jailer-<version> links. You’ll also get /usr/bin/firecracker and /usr/bin/jailer links pointing to the version links. You can now run Firecracker:

1
firecracker --help

§Get Linux Kernel

I’m going to use 5.8 kernel. Do I need one? Not sure but why not.

1
export KERNEL_VERSION=v5.8

Clone sources from GitHub:

1
2
3
cd /firecracker/linux.git
git clone https://github.com/torvalds/linux.git .
git checkout ${KERNEL_VERSION}

§Configure the kernel

So, I’m not really fluent at this and…

I have no idea what I’m doing

This is the first time I’m building the kernel but fortunately one of the the Firecracker getting started documents points to a recommended kernel config. What’s less fortunate, the document talks about kernel v4.20 and the config is for v4.14.174. I’ve downloaded the file and placed it in /firecracker/linux.git/.config anyway but when I tried building the v5.8 kernel, make was insisting on recreating the config and asked me a lot of questions about what I want.

I don’t know what I want and I don’t know if make took the values from the old .config so I basically held Enter down for a bit. make moved my .config to .config.old and gave me a new .config file for v5.8 kernel. I took the new generated file and compared it with the original v4.14.174 config.

These files have few thousand lines so I wrote a program in Golang which loads both versions and compares the values.

With a flag, it allows bringing non-existing values from the good config to the new one. You can find this program in the kernel-configs directory of this repository[]. To get the v5.8 config, I basically executed:

1
2
3
4
go run ./compare-configs.go \
  --good-config=./4.14.174.config \
  --new-config=./5.8.config \
  --bring-old-non-existing

Next, I’ve replaced the /firecracker/linux.git/.config file contents with the result of my compare configs program and re-run kernel build. This time, it did not insist on recreating anything and used my config, definitely. Happy times!

§Build the kernel

You probably want to change the value of -j to something like 4 or 8. The build will take a bit more time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
time make vmlinux -j32
...
  LD      vmlinux.o
  MODPOST vmlinux.symvers
  MODINFO modules.builtin.modinfo
  GEN     modules.builtin
  LD      .tmp_vmlinux.kallsyms1
  KSYM    .tmp_vmlinux.kallsyms1.o
  LD      .tmp_vmlinux.kallsyms2
  KSYM    .tmp_vmlinux.kallsyms2.o
  LD      vmlinux
  SORTTAB vmlinux
  SYSMAP  System.map

real	0m54.052s
user	23m51.313s
sys	2m35.287s

After the build is complete, copy the vmlinux binary to the /firecracker/kernels directory:

1
mv ./vmlinux /firecracker/kernels/vmlinux-${KERNEL_VERSION}

§Build the file system

Fun part starts here. The article from Julia Evans talks about getting an init system installed in the container. I have naively tried simply extracting the file system and running it bluntly without doing any additional configuration but Firecracker was complaining about not having /sbin/openrc available. Makes sense, Docker images generally don’t need an init system. So I had to find a method to get one in.

Fortunately, Vault Docker image is built from Alpine Linux and the Creating a rootfs Image instructions show how to add an init system to a file system of an Alpine based container.

We’ve building a file system for Vault:

1
export FS=vault

Create a file system file and format it as ext4:

1
2
3
rm /firecracker/filesystems/vault-root.ext4
dd if=/dev/zero of=/firecracker/filesystems/vault-root.ext4 bs=1M count=500
mkfs.ext4 /firecracker/filesystems/vault-root.ext4

I have no idea how much space I needed. 50 megs wasn’t enough so I gave it 500 instead.

Create a mount directory and mount the file system file:

1
2
3
mkdir -p /firecracker/filesystems/mnt-${FS}
sudo mount /firecracker/filesystems/${FS}-root.ext4 \
  /firecracker/filesystems/mnt-${FS}

Now, run the Vault container and fetch the container ID, attach file system mount directory to the container:

1
export CONTAINER_ID=$(docker run -t --rm -v /firecracker/filesystems/mnt-${FS}:/export-rootfs -d vault:latest)

Get the shell in the container:

1
docker exec -ti ${CONTAINER_ID} /bin/sh

Now, in the container shell, execute these commands:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# install the init system and some extra tools:
apk add openrc
apk add util-linux
# set up a login terminal on the serial console (ttyS0):
ln -s agetty /etc/init.d/agetty.ttyS0
echo ttyS0 > /etc/securetty
rc-update add agetty.ttyS0 default
# Make sure special file systems are mounted on boot:
rc-update add devfs boot
rc-update add procfs boot
rc-update add sysfs boot

# EXTRA: I had to add these lines:
# --------------------------------
# 1. enable local services:
# https://wiki.gentoo.org/wiki//etc/local.d
rc-update add local default
# 2. create a local service to Start Vault dev server on system boot:
echo "#!/bin/sh" >> /etc/local.d/HashiCorpVault.start
echo "/usr/local/bin/docker-entrypoint.sh server -dev && reboot || reboot" >> /etc/local.d/HashiCorpVault.start
# 3. make it executable:
chmod +x /etc/local.d/HashiCorpVault.start
# 4. For convenience, enable output from local service so I can see errors:
echo rc_verbose=yes > /etc/conf.d/local
# 5. make sure I also have /home and /vault directories in my exported file system
for d in home vault; do tar c "/$d" | tar x -C /export-rootfs; done
# EXTRA / end

# Then, copy the newly configured system to the rootfs image:
for d in bin etc lib root sbin usr; do tar c "/$d" | tar x -C /export-rootfs; done
for dir in dev proc run sys var; do mkdir /export-rootfs/${dir}; done

# All done, exit docker shell
exit

A few words about the HashiCorpVault.start service file. One method to shut the Firecracker VM gracefully down is to call reboot from inside of the VM. This is because Firecracker exits on CPU reset, more info here. Hence the command:

1
/usr/local/bin/docker-entrypoint.sh server -dev && reboot || reboot

will start the Vault server using the regular docker-entrypoint.sh from the original image.

The && reboot part will ensure the VM stops automatically after Vault exits gracefully. The || reboot part will stop the VM if Vault does not start for whatever reason.

This saves a hassle of doing ps -a and sudo kill <pid> dance when things go south.

You can now stop the container and unmount the file system:

1
2
docker stop ${CONTAINER_ID}
sudo umount /firecracker/filesystems/mnt-${FS}

§Launch Vault on Firecracker

Firecracker VMs can, by design, only use Linux tap devices for networking. There are tools that create ad-hoc devices using CNI plugins but I went with the method from Julia Evans. So, this part is directly lifted from Julia Evans.

Prepare kernel boot args:

1
2
3
4
5
export MASK_LONG="255.255.255.252"
export FC_IP="169.254.0.21"
export TAP_IP="169.254.0.22"
export KERNEL_BOOT_ARGS="ro console=ttyS0 noapic reboot=k panic=1 pci=off nomodules random.trust_cpu=on"
export KERNEL_BOOT_ARGS="${KERNEL_BOOT_ARGS} ip=${FC_IP}::${TAP_IP}:${MASK_LONG}::eth0:off"

Setup tap network interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export TAP_DEV="fc-88-tap0"
export MASK_SHORT="/30"
export FC_MAC="02:FC:00:00:00:05"

sudo ip link del "$TAP_DEV" 2> /dev/null || true
sudo ip tuntap add dev "$TAP_DEV" mode tap
sudo sysctl -w net.ipv4.conf.${TAP_DEV}.proxy_arp=1 > /dev/null
sudo sysctl -w net.ipv6.conf.${TAP_DEV}.disable_ipv6=1 > /dev/null
sudo ip addr add "${TAP_IP}${MASK_SHORT}" dev "$TAP_DEV"
sudo ip link set dev "$TAP_DEV" up

Write the VM config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cat <<EOF > /firecracker/configs/vault-config.json
{
  "boot-source": {
    "kernel_image_path": "/firecracker/kernels/vmlinux-${KERNEL_VERSION}",
    "boot_args": "$KERNEL_BOOT_ARGS"
  },
  "drives": [
    {
      "drive_id": "rootfs",
      "path_on_host": "/firecracker/filesystems/${FS}-root.ext4",
      "is_root_device": true,
      "is_read_only": false
    }
  ],
  "network-interfaces": [
      {
          "iface_id": "eth0",
          "guest_mac": "${FC_MAC}",
          "host_dev_name": "${TAP_DEV}"
      }
  ],
  "machine-config": {
    "vcpu_count": 1,
    "mem_size_mib": 128,
    "ht_enabled": false
  }
}
EOF

And launch:

1
firecracker --no-api --config-file /firecracker/configs/vault-config.json

The result will be similar to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[    0.000000] Linux version 5.8.0 (radek@r720sas) (gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, GNU ld (GNU Binutils for Ubuntu) 2.30) #4 SMP Sat Feb 6 18:50:30 UTC 2021
[    0.000000] Command line: ro console=ttyS0 noapic reboot=k panic=1 pci=off nomodules random.trust_cpu=on ip=169.254.0.21::169.254.0.22:255.255.255.252::eth0:off root=/dev/vda rw virtio_mmio.device=4K@0xd0000000:5 virtio_mmio.device=4K@0xd0001000:6
[    0.000000] x86/fpu: Supporting XSAVE feature 0x001: 'x87 floating point registers'
[    0.000000] x86/fpu: Supporting XSAVE feature 0x002: 'SSE registers'
[    0.000000] x86/fpu: Supporting XSAVE feature 0x004: 'AVX registers'
[    0.000000] x86/fpu: xstate_offset[2]:  576, xstate_sizes[2]:  256
[    0.000000] x86/fpu: Enabled xstate features 0x7, context size is 832 bytes, using 'standard' format.
[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x0000000007ffffff] usable
...
 * Mounting persistent storage (pstore) filesystem ... [ ok ]
 * Starting local ... *   Executing "/etc/local.d/HashiCorpVault.start" ...==> Vault server configuration:

             Api Address: http://0.0.0.0:8200
                     Cgo: disabled
         Cluster Address: https://0.0.0.0:8201
              Go Version: go1.15.7
              Listener 1: tcp (addr: "0.0.0.0:8200", cluster address: "0.0.0.0:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
               Log Level: info
                   Mlock: supported: true, enabled: false
           Recovery Mode: false
                 Storage: inmem
                 Version: Vault v1.6.2
             Version Sha: be65a227ef2e80f8588b3b13584b5c0d9238c1d7

==> Vault server started! Log data will stream in below:

2021-02-06T21:13:22.006Z [INFO]  proxy environment: http_proxy= https_proxy= no_proxy=
2021-02-06T21:13:22.007Z [WARN]  no `api_addr` value specified in config or in VAULT_API_ADDR; falling back to detection if possible, but this value should be manually set
2021-02-06T21:13:22.020Z [INFO]  core: security barrier not initialized
2021-02-06T21:13:22.020Z [INFO]  core: security barrier initialized: stored=1 shares=1 threshold=1
...
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variable:

    $ export VAULT_ADDR='http://0.0.0.0:8200'

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Unseal Key: YqKuQlzPiMUQXphehp0M7DAvsqImqNJrvJqAn/R0nyc=
Root Token: s.F4erraVTvHnx3oU4Ac8zwaCP

Development mode should NOT be used in production installations!

Try it out by opening another terminal on the same machine and running:

1
2
curl http://169.254.0.21:8200/sys/health
{"errors":[]}

It’s alive.

You can stop the VM by simply pressing CTRL+C in the terminal window where Vault is running. The VM will shut gracefully down:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
^C==> Vault shutdown triggered
2021-02-06T21:13:33.863Z [INFO]  core: marked as sealed
2021-02-06T21:13:33.866Z [INFO]  core: pre-seal teardown starting
2021-02-06T21:13:33.869Z [INFO]  rollback: stopping rollback manager
2021-02-06T21:13:33.872Z [INFO]  core: pre-seal teardown complete
2021-02-06T21:13:33.873Z [INFO]  core: stopping cluster listeners
2021-02-06T21:13:33.874Z [INFO]  core.cluster-listener: forwarding rpc listeners stopped
2021-02-06T21:13:34.076Z [INFO]  core.cluster-listener: rpc listeners successfully shut down
2021-02-06T21:13:34.082Z [INFO]  core: cluster listeners successfully shut down
2021-02-06T21:13:34.085Z [INFO]  core: vault is sealed
 [ ok ]
 [ ok ]
 * Stopping local ... [ ok ]
The system is going down NOW!
Sent SIGTERM to all processes
Sent SIGKILL to all processes
Requesting system reboot
[   15.877742] Unregister pv shared memory for cpu 0
[   15.880260] reboot: Restarting system
[   15.881627] reboot: machine restart
radek@r720:/firecracker$

That’s all for today.