L4Re on AWS EC2#
This page describes how to run L4Re on AWS EC2. EC2 instances boot via UEFI, thus L4Re images have to be UEFI accordingly.
Note
We experienced some instabilities for the EC2 target when using older gcc toolchains (~11). We’re working to resolve these issues. In the meantime if unexplained instabilities occur, consider using a newer compiler toolchain if possible.
EC2 Instance Types#
First, we need to select an instance type in EC2. In general the instance type can be any, however, there is an important detail to consider. As of this writing, EC2 does not support nested virtualization that would be required to run a hypervisor within an instance which is a VM itself. However, EC2 also offers “metal” instances which do not run EC2’s hypervisor but offer the whole machine to the user. With “metal” instances a user’s hypervisor can be run.
Thus, if virtualization shall be used with L4Re, a “metal” instance is required. In a VM-instance, L4Re will only work in a non-hypervisor configuration.
Configure for EC2#
This section gives you specific details about configuring your build for EC2. General information about configuring L4Re and the L4Re microkernel can be found in the section Configure L4Re and the Microkernel
Instance type m7g.metal#
The m7g.metal intance type is based on the ARM SBSA specification, which is why
we choose arm_sbsa
as the platform type. L4Re and the L4Re
microkernel require specific configurations to work on the m7g.metal instance
type hardware.
Necessary configuration options for L4Re:
CONFIG_BUILD_ARCH_arm64=y
CONFIG_PLATFORM_TYPE_arm_sbsa=y
Necessary configuration options for the L4Re microkernel:
CONFIG_ARM=y
CONFIG_PF_SBSA=y
CONFIG_ARM_NEOVERSE_N1=y
CONFIG_MP_MAX_CPUS=64
CONFIG_KERNEL_NX=y
CONFIG_ARM_PT48=y
x86-64 instance types#
For the majority of x86-64 hardware you should choose amd64
as architecture
and pc
as platform.
Necessary options for L4Re:
CONFIG_BUILD_ARCH_amd64=y
CONFIG_PLATFORM_TYPE_pc=y
Necessary options for the L4Re microkernel:
CONFIG_AMD64=y
CONFIG_PF_PC=y
Build EFI image#
In your finished L4Re build, in your build tree, for x86-64, generate a UEFI
image for the hello
target like this:
$ make efiimage E=hello
For arm64, use the SBSA platform and generate a UEFI image like this:
$ make efiimage E=hello PT=arm_sbsa
On x86-64 the resulting image is called bootx64.efi
and on arm64 it is
called bootaa64.efi
, and can be found in the images
folder of your
build directory.
Booting on EC2#
The generated EFI image of L4Re can be booted as any other OS kernel from within the AMI, with GRUB or directly from UEFI.
Booting from disk#
To boot your EFI image from disk, put it in the EFI partition (typically mounted
at /boot/efi
) on your AMI/your instance’s storage device /EFI/BOOT
under
the name BOOTX64.EFI
or BOOTAA64.EFI
respectively.
Network Booting#
EC2 also offers network booting which is useful when working with L4Re and needing to regenerate the image often.
For this, iPXE is used as a boot loader which is able to download an image via network and launch it.
To use it on EC2, please refer to the iPXE’s EC2 site at https://ipxe.org/howto/ec2 to configure your instance accordingly by picking the right AMI for your architecture choice and EC2 region.
As described on the iPXE page, the “user state” of an instance needs to have an iPXE script like this, for arm64:
#!ipxe
kernel http://l4re.org/download/snapshots/pre-built-images/arm64/l4re_vm-multi-p2p_sbsa.efi
boot
This example uses the “vm-multi-p2p” image from the pre-built image selection which uses virtualization and thus requires a “metal” Graviton instance.
For x86-64 the approach is similar.
Interacting with the Instance#
Head over to the “EC2 serial console” to interact with the instances, especially for seeing the output of it. More information on the EC2 serial console can be found in the EC2 documentation.
Stopping an Instance#
For stopping the instance, ensure to issue a “Force stop instance” to really shut down the instance.
Device pass-through#
With the help of io you can create virtual system busses (vbus for short), restricting access to a subset of the system’s hardware. These vbusses can then be passed to L4Re applications, like uvmm (a guest) or nvme-drv (nvme driver). L4Re applications, like the nvme-drv, can then in turn provide a more nuanced access to these devices, e.g. only one partition.
The details, such as PCI IDs, might differ between arm64 and x86_64. The following example was tested on the arm64 arm_sbsa platform.
Configuring io#
Io uses the scripting language Lua for its configuration. Within an io configuration you can dynamically define which devices will be assigned to which virtual system bus.
The following example detects NVMe devices and Network devices using their respective PCI IDs.
-- match returns matched devices as an array
local ami_device = Io.system_bus():match("PCI/VEN_1D0F&DEV_0061")
local ena_devices = Io.system_bus():match("PCI/VEN_1D0F&DEV_EC20")
local nvme_devices = Io.system_bus():match("PCI/VEN_1D0F&DEV_0065")
-- Create one vbus with all storage devices for nvme driver...
Io.add_vbusses
{
storage = Io.Vi.System_bus(function ()
Property.num_msis = 512;
PCI0 = Io.Vi.PCI_bus(function ()
ami = wrap(ami_device);
nvme = wrap(nvme_devices);
end);
end);
};
-- and create a vbus for one guest with two network devices
Io.add_vbusses {
guest1 = Io.Vi.System_bus(function()
Property.num_msis = 512;
PCI0 = Io.Vi.PCI_bus(function ()
network0 = wrap(ena_devices[0])
network1 = wrap(ena_devices[1])
end);
end);
};
Starting io from ned#
Using ned we can then start IO with this configuration. For each vbus defined in the io config, a corresponding IPC gate needs to be created of which we pass the server cap to io on start.
-- Platform ctrl (Can be passed to guest so that shutdowns/reboots are
-- passed through to host)
local platform_ctl = L4.default_loader:new_channel();
-- Storage vbus
local vbus_storage = L4.default_loader:new_channel();
-- Guest vbus
local vbus_guest1 = L4.default_loader:new_channel();
-- Start io
L4.default_loader:start({
scheduler = vmm.new_sched(0x40,0x2),
log = { "io", "red" },
caps = {
sigma0 = L4.cast(L4.Proto.Factory, L4.Env.sigma0):create(L4.Proto.Sigma0);
icu = L4.Env.icu;
iommu = L4.Env.iommu;
jdb = L4.Env.jdb;
-- Server side of platform_ctl cap, so IO responds to requests on it.
platform_ctl = platform_ctl:svr();
-- Server side of the storage vbus cap, nvme-driver uses this for
-- access to storage hardware
storage = vbus_storage:svr();
-- Server side of guest1 cap, to pass to uvmm directly
guest1 = vbus_guest1:svr();
},
}, "rom/io rom/config.io");
Nuanced storage access using nvme-drv#
The storage vbus from our previous example contains the AMI NVME device and several other configured NVMe devices. In order to not give access to all of them to one guest we can use the nvme-drv to create seperate virtio devices giving access to specific partitions and/or EC2 volumes.
local nvme = L4.default_loader:new_channel();
vmm.loader:start({
scheduler = vmm.new_sched(0x40,0x2),
log = { "nvme", "blue" },
caps = {
-- access to nvme devices via IO
vbus = vbus_storage,
-- endpoint for creating virtio devices
svr = nvme:svr(),
jdb = L4.Env.jdb,
},
}, "rom/nvme-drv");
-- device parameter can be a Partition UUID (not FS UUID) or ...
local nvme_part1 = nvme:create(0, "ds-max=5", "device=2FD29D59-FAFB-463E-8C4D-47B2931FA605");
-- ... an EC2 volume id followed by an nvme namespace id (usually n1)
local nvme_vol1 = nvme:create(0, "ds-max=5", "device=vol00489f52aed3a6549:n1");
Full storage access without using nvme-drv#
In cases where simpler, full storage access passthrough is desired, the nvme-drv
is not necessary. In this case IO can be configured without a vbus_storage
channel. Instead we add the devices the vbus_guest1 channel.
-- Platform ctrl (Can be passed to guest so that shutdowns/reboots are
-- passed through to host)
local platform_ctl = L4.default_loader:new_channel();
-- Guest vbus
local vbus_guest1 = L4.default_loader:new_channel();
-- Start io
L4.default_loader:start({
scheduler = vmm.new_sched(0x40,0x2),
log = { "io", "red" },
caps = {
sigma0 = L4.cast(L4.Proto.Factory, L4.Env.sigma0):create(L4.Proto.Sigma0);
icu = L4.Env.icu;
iommu = L4.Env.iommu;
jdb = L4.Env.jdb;
-- Server side of platform_ctl cap, so IO responds to requests on it.
platform_ctl = platform_ctl:svr();
-- Server side of guest1 cap, to pass to uvmm directly
guest1 = vbus_guest1:svr();
},
}, "rom/io rom/config.io");
The config.io
will also be simpler, without a storage
vbus
configuration, instead specifying ami
and nvme
in the guest vbus.
-- match returns matched devices as an array
local ami_device = Io.system_bus():match("PCI/VEN_1D0F&DEV_0061")
local ena_devices = Io.system_bus():match("PCI/VEN_1D0F&DEV_EC20")
local nvme_devices = Io.system_bus():match("PCI/VEN_1D0F&DEV_0065")
-- and create a vbus for one guest with all devices
Io.add_vbusses {
guest1 = Io.Vi.System_bus(function()
Property.num_msis = 512;
PCI0 = Io.Vi.PCI_bus(function ()
network = wrap(ena_devices);
ami = wrap(ami_device);
nvme = wrap(nvme_devices);
end);
end);
};
Pass vbusses to uvmm#
After creating the vbusses and virtio devices these can be passed to the corresponding guests. For this we can use the vmm.lua library provided as part of the uvmm package.
vmm.start_vm{
-- Other settings...
-- Device tree
fdt = "rom/virt-arm_sbsa.dtb",
-- Vbus
vbus = vbus_guest1,
ext_caps = {
-- Capability representing a virtio device provided by the nvme-drv
disk = nvme_vol1;
},
};
Note
For PCI passthrough to work on the arm_sbsa
machine you need to use a
device tree with a PCI bridge, such as the virt-arm_sbsa.dtb
provided as
part of the uvmm package. To add this file your L4Re image you need to
specify it in the modules.list for your target.
Note
For each virtio device passed to a guest a corresponding virtio-proxy node needs to exist in the device tree given to uvmm. This node also chooses the name of the capability which has to be specified here. The default arm64 device tree comes with one such node:
virtio_net@10000 {
compatible = "virtio,mmio";
reg = <0x10000 0x200>;
interrupt-parent = <&gic>;
interrupts = <0 123 4>;
l4vmm,vdev = "proxy";
l4vmm,virtiocap = "net";
};
Despite the name virtio_net
it can be used with any type of virtio
capability.
Using cloud-init in guests#
While running as part of a guest uvmm cloud-init might not detect the
presence of the EC2 environment and thus might give up. To tell cloud-init
it is within an EC2 environment, you can append the following to your Linux
kernel boot parameter: cc:{'datasource_list':['Ec2']}