<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Lucas Jacques</title>
    <description>The latest articles on Forem by Lucas Jacques (@lucasjacques).</description>
    <link>https://forem.com/lucasjacques</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1165200%2F49fbb46d-f2b9-4320-9e48-63ac4ae7786c.jpeg</url>
      <title>Forem: Lucas Jacques</title>
      <link>https://forem.com/lucasjacques</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lucasjacques"/>
    <language>en</language>
    <item>
      <title>Managing Firecracker microVMs in Go</title>
      <dc:creator>Lucas Jacques</dc:creator>
      <pubDate>Thu, 28 Sep 2023 13:47:12 +0000</pubDate>
      <link>https://forem.com/lucasjacques/managing-firecracker-microvms-in-go-2ja0</link>
      <guid>https://forem.com/lucasjacques/managing-firecracker-microvms-in-go-2ja0</guid>
      <description>&lt;p&gt;&lt;strong&gt;This article is the second iteration of the "&lt;em&gt;Orchestrating MicroVMs, from first principles&lt;/em&gt;" series about the creation of an orchestrator of Firecracker microVMs in Go named &lt;a href="https://github.com/valyentdev/ravel" rel="noopener noreferrer"&gt;Ravel&lt;/a&gt;. You can find the first article &lt;a href="https://dev.to/lucasjacques/orchestrating-microvms-from-first-principles-introduction-262i"&gt;here&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generally, an orchestrator cluster is made up of 2 kinds of nodes. First, there are worker nodes which handle computing workload. Secondly, there are supervisor nodes which orchestrate and schedule the workload on worker nodes.&lt;/p&gt;

&lt;p&gt;Today we'll only talk about the worker node. Our worker will expose a REST API which will respond to the following needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create microVMs from a container image,&lt;/li&gt;
&lt;li&gt;Manage microVMs lifecycle (start, stop, delete microVMs),&lt;/li&gt;
&lt;li&gt;List and inspect microVMs,&lt;/li&gt;
&lt;li&gt;Expose microVMs logs (will be covered in a future article). &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;For the purpose of the article all the presented code is simplified and truncated of error management. You can find the full implementation on the &lt;a href="https://github.com/valyentdev/ravel" rel="noopener noreferrer"&gt;Ravel Github repository&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  From containers to microVMs
&lt;/h2&gt;

&lt;p&gt;A container image (OCI image) holds all binaries required to run a workload and metadata about the intended process at container start, but it lacks a real Linux distro and kernel. Instead, it relies on the host system’s kernel, with the container runtime, like Docker or containerd, managing the container's lifecycle and interactions with the host system.&lt;/p&gt;

&lt;p&gt;The metadata within the image, specifying entry points, environment variables, and command arguments, is crucial for initializing the container correctly by the runtime.&lt;/p&gt;

&lt;p&gt;So, to run a container image in a microVM we need to bring a Linux kernel and an init binary to execute at kernel boot.&lt;/p&gt;

&lt;p&gt;For now, our init binary will be mostly a copy of the &lt;a href="https://github.com/thi-startup/init" rel="noopener noreferrer"&gt;thi-startup one&lt;/a&gt;. This needs to be mounted as a root file system and accompanied by a config file providing the metadata of the container image. Then, we extract the image content into a EXT4 file system and mount it as a second file system. &lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhackmd.io%2F_uploads%2FHy_Ia0MxT.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fhackmd.io%2F_uploads%2FHy_Ia0MxT.png" alt="MicroVM illustration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The init binary is charged to make the OCI image usable like a kind of linux distro (some linux mounts among other things) and to spawn a process with of the intended worlkoad from the provided config file. &lt;/p&gt;

&lt;p&gt;That's about all we need to construct our microVM.&lt;/p&gt;
&lt;h3&gt;
  
  
  An overview of the Go implementation
&lt;/h3&gt;

&lt;p&gt;Here is a simplified version of our drive creation &lt;a href="https://github.com/valyentdev/ravel/tree/main/internal/worker/drives" rel="noopener noreferrer"&gt;implementation&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DrivesManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CreateDrive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="n"&gt;RavelDriveSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Drive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;driveId&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    
    &lt;span class="c"&gt;// Create a file and allocate it the desired size&lt;/span&gt;
    &lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fallocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getDrivePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driveId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// Format it as an ext4 file system&lt;/span&gt;
    &lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mkfs.ext4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;getDrivePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driveId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// Store it in a local store&lt;/span&gt;
    &lt;span class="n"&gt;dm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreRavelDrive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driveId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Drive&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;RavelDrive&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;drive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And our drive object interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Drive&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Mount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Unmount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;GetMountPath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;GetDrivePath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then here is how we build the VM extracted from the &lt;em&gt;&lt;a href="https://github.com/valyentdev/ravel/blob/main/internal/worker/machines/build_machine.go" rel="noopener noreferrer"&gt;original file&lt;/a&gt;&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineManager&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MachineManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;buildMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineSpec&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RavelMachineSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RavelMachine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;machineId&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// Get the desired image&lt;/span&gt;
    &lt;span class="n"&gt;machineManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineSpec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// Generate the config from image&lt;/span&gt;
    &lt;span class="n"&gt;imageInitConfig&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetInitImageConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// Build init drive and add the init binary and config into it&lt;/span&gt;
    &lt;span class="n"&gt;initDrive&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;machineManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildInitDrive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineSpec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;imageInitConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// Build main drive and extract container image into it&lt;/span&gt;
    &lt;span class="n"&gt;mainDrive&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;machineManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildMainDrive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RavelMachine&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;               &lt;span class="n"&gt;machineId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RavelMachineSpec&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;machineSpec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;InitDriveId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;initDrive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RootDriveId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;mainDrive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RavelMachineStatusCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Managing microVMs lifecycle
&lt;/h2&gt;

&lt;p&gt;Now that we have a file system and an init, we'll use Firecracker to spawn the microVMs. Each Firecracker microVM is attached to a unique Firecracker VMM (Virtual Machine Manager) process. So when we call &lt;code&gt;firecracker.NewMachine&lt;/code&gt; from the Go SDK, the returned &lt;code&gt;*firecracker.Machine&lt;/code&gt; object has methods to interact with this process.&lt;/p&gt;

&lt;p&gt;We need to track this object during all the lifetime of the Firecracker VMM which is attached to our Golang process so we'll keep it in memory with a &lt;code&gt;VMMManager&lt;/code&gt; which is just a little abstraction over the SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;VMMManager&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;machines&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;firecracker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Machine&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;VMMManagerInterface&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CreateMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machineId&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="n"&gt;firecracker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;firecracker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Machine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;StartMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machineId&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;StopMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machineId&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;GetMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineId&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;firecracker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Machine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;DeleteMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machineId&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;We'll probably need to extract this code into a pluggable "driver" and communicate with it under an unix socket, or something like that, to decorrelate the microVMs lifecycle from our worker API. This will allow us to keep the machines on even when turning off the API. This interface will also probably become more hypervisor agnostic in the future.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's create our first microVM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineManager&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MachineManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CreateMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ravelMachineSpec&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RavelMachineSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ravelMachine&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;machineManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buildMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ravelMachineSpec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;firecrackerConfig&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;GetFirecrackerConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ravelMachine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;machineManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;machines&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ravelMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firecrackerConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;machineManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreRavelMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ravelMachine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ravelMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineManager&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MachineManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;StartMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machineId&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;machineManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;machines&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;machineId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;machineManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdateRavelMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machineId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RavelMachine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;rm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RavelMachineStatusRunning&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;


    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then do the same to stop and delete microVMs.&lt;/p&gt;

&lt;p&gt;Here we are, so now let's expose theses features over this REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /machines              List machines
GET /machines/&amp;lt;id&amp;gt;         Inspect machines
POST /machines             Create a machine
POST /machines/&amp;lt;id&amp;gt;/start  Start a machine
POST /machines/&amp;lt;id&amp;gt;/stop   Stop a machine
DELETE /machines/&amp;lt;id&amp;gt;      Delete a machine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To do that we use a minimal HTTP router for Go named &lt;a href="https://github.com/alexedwards/flow" rel="noopener noreferrer"&gt;Flow&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/machines"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateMachineHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/machines"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListMachinesHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/machines/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetMachineHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/machines/:id/start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartMachineHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/machines/:id/stop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StopMachineHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/machines/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeleteMachineHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"DELETE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it, we have a REST API to manage firecracker microVMs. But it's still a work in progress...&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next ?
&lt;/h2&gt;

&lt;p&gt;There is a lot of work to make this worker ready to handle workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add logs management and an endpoint for it&lt;/li&gt;
&lt;li&gt;Add a VSOCK device to enable &lt;code&gt;docker exec&lt;/code&gt; like stuff with the init.&lt;/li&gt;
&lt;li&gt;Use the Firecracker Jailer to improve security&lt;/li&gt;
&lt;li&gt;Add networking management to bring internet connectivity and, in the future, cluster wide connectivity.&lt;/li&gt;
&lt;li&gt;Add clean OCI images management decoupled from docker.&lt;/li&gt;
&lt;li&gt;Track status of the workload and failure recovery&lt;/li&gt;
&lt;li&gt;Add resource management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And many other things...&lt;/p&gt;

&lt;p&gt;I will publish  more blog posts in the future for some of these points. To stay tuned, you can follow me on  &lt;a href="https://x.com/lucas__jcq" rel="noopener noreferrer"&gt;X (formerly Twitter)&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;For Potential Contributors:&lt;/strong&gt; The world of open-source thrives on collaboration. If you're passionate about distributed systems and want to contribute &lt;a href="https://github.com/valyentdev/ravel" rel="noopener noreferrer"&gt;Ravel welcomes you&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Criticisms and Feedback:&lt;/strong&gt; Every system can be refined and improved. Your constructive feedback is invaluable to the development of Ravel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>go</category>
      <category>docker</category>
      <category>devops</category>
      <category>firecracker</category>
    </item>
    <item>
      <title>Orchestrating MicroVMs, from first principles: Introduction</title>
      <dc:creator>Lucas Jacques</dc:creator>
      <pubDate>Thu, 21 Sep 2023 14:19:18 +0000</pubDate>
      <link>https://forem.com/lucasjacques/orchestrating-microvms-from-first-principles-introduction-262i</link>
      <guid>https://forem.com/lucasjacques/orchestrating-microvms-from-first-principles-introduction-262i</guid>
      <description>&lt;p&gt;&lt;strong&gt;At Valyent, our mission is to spare you the pain of deploying your applications. To do this, we build an OCI image from your code and we run it on our infrastructure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We convey the right image to the right machine with the right compute resources for each of our customers. This is called orchestration.&lt;/p&gt;

&lt;p&gt;Orchestration is the automation of the deployment, scaling, and operation of applications. It manages resources allocation, health monitoring, ensures proper networking, handles load balancing, and oversees application updates and rollbacks.&lt;/p&gt;

&lt;p&gt;At Valyent we build a PaaS which could be partially summarized as a managed orchestration with good UX and good DX.&lt;/p&gt;

&lt;p&gt;Here are the &lt;strong&gt;specifics needs&lt;/strong&gt; of a PaaS:&lt;br&gt;
1) &lt;strong&gt;Orchestrating compute resources&lt;/strong&gt;: A PaaS must accommodate a large number of users and their computing needs, so an orchestration system is needed to support the workload efficiently.&lt;br&gt;
2) &lt;strong&gt;Networking&lt;/strong&gt;: A PaaS need a robust networking system to accomade internal networking between apps of an organization and external networking to allow traffic from internet.&lt;br&gt;
3) &lt;strong&gt;Isolation &amp;amp; multi-tenancy&lt;/strong&gt;: Each tenant have security and privacy needs. It is unthinkable that one tenant could access the resources of another. The PaaS must ensure the isolation between each tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  State of the art of orchestration
&lt;/h2&gt;

&lt;p&gt;The market itself presents a pantheon of tools, each with its merits and challenges :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt;, for instance is the catch-all tool offering a lot of features but not without dragging users through intricate labyrinths. Achieving multi-tenancy in Kubernetes is hard, with its namespaces providing limited isolation, potentially leading to security and resource concerns.&lt;/li&gt;
&lt;li&gt;Then there's &lt;strong&gt;Nomad&lt;/strong&gt;, open-source, albeit with asterisks (BSL). Its single-responsibility design necessitates external tools for functionalities like service discovery and load balancing (Consul), secrets management (Vault) etc. But most of multi-tenancy features are only included in Enterprise plan which is expensive and this for each product.&lt;/li&gt;
&lt;li&gt;While they are ready for multi tenancy, platforms like &lt;strong&gt;OpenStack&lt;/strong&gt; and &lt;strong&gt;OpenNebula&lt;/strong&gt; were primarily designed to manage giants clusters and weren’t precisely created for apps orchestration. Their focus on diverse workloads can make them seem like a maze with a lot of complex components.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Multi-Tenant Distributed System Bottleneck&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;As you have certainly understood, building a distributed system ready for multitenancy is not easy. The isolation requirements of a multi-tenant orchestrator can be divided into two main parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network:&lt;/strong&gt; Ensuring isolated and secure internal networking between the apps of a tenant is crucial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution:&lt;/strong&gt; We need to execute multiple customer, potentially unsafe, code on the same host. The application of one tenant must not interfere in any way with that of another and must only use the computing resources to which it is allocated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the scope of this article we'll focus on the execution part. The technology most currently used to run applications in isolation are containers, popularized by tools like Docker and widely used by Kubernetes, Nomad, etc.&lt;/p&gt;

&lt;h4&gt;
  
  
  The problem with containers
&lt;/h4&gt;

&lt;p&gt;Containers are offering numerous benefits in terms of efficiency, portability, and modularity. However, when it comes to security, containers have intrinsic challenges that set them apart from traditional virtual machines (VMs). &lt;/p&gt;

&lt;p&gt;Let's explore why containers might not be as secure as one might think, and how they differ from VMs in this regard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shared Kernel:&lt;/strong&gt; Containers, unlike VMs, share the host's operating system kernel. This means that if a malicious actor manages to exploit a vulnerability in the kernel, all containers running on that host could potentially be compromised. In contrast, VMs have their own full-fledged OS and kernel, providing a stronger isolation boundary and thiner attack surface.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weak Isolation:&lt;/strong&gt; By design, containers are less isolated from each other than VMs. If an attacker can break out of a container, they might gain access to other containers or even the host system. Such container escape vulnerabilities can be catastrophic in a shared environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Resources management&lt;/strong&gt;: Containers sharing a host can impact each other's performance, leading to the 'noisy neighbor' phenomenon. Memory allocation can be tricky; if a container surpasses its limit, it may be terminated. Shared kernel resources, like open file descriptors, can become exhausted by one container, affecting others. Resource isolation isn't foolproof; shared resources, such as CPU cache, can be exploited.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While containers offer a plethora of benefits like speed, they intrinsically possess vulnerabilities and challenges not present in the VM realm.&lt;/p&gt;

&lt;h4&gt;
  
  
  Firecracker
&lt;/h4&gt;

&lt;p&gt;Enter Amazon Firecracker, a cloud-native alternative. Designed for lightweight, multi-tenant environments, Firecracker offers speed, agility, and efficiency, setting it apart from counterparts like QEMU.&lt;/p&gt;

&lt;p&gt;So it's magic, just replace the containers with Firecracker microVMs? Not really.&lt;/p&gt;

&lt;p&gt;First, Firecracker was designed to execute serverless tasks and is the underlying engine for Amazon Lambdas. By default, it's not really made for long-running task. Second, orchestrating firecracker microVMs is not easy. There is not as much tooling as for containers. Moreover, by default, Firecracker does not natively allow you to execute OCI images.&lt;/p&gt;

&lt;p&gt;So what do we do next ? &lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing &lt;strong&gt;Ravel&lt;/strong&gt;, an Open-Source Firecracker Orchestrator
&lt;/h2&gt;

&lt;p&gt;This post is the first iteration of a long series detailing our journey to build our own microVMs orchestrator: Ravel. Here are the main characteristics expected from Ravel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Distributed Firecracker microVMs&lt;/strong&gt; orchestration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCI images compatible&lt;/strong&gt; out of the box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant&lt;/strong&gt; by default: secured private internal networking included&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to use&lt;/strong&gt; and to install&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open-source&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next article will cover the implementation of the first building block of Ravel: &lt;br&gt;
A small golang programm to manage multiple firecracker microVMs easily.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're interested by Valyent and Ravel you can join the waitlist on &lt;a href="https://valyent.dev/waitlist"&gt;www.valyent.dev&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thanks for reading &amp;amp; let’s embark on this journey together.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>containers</category>
      <category>docker</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
