<?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>DEV Community: Collins Adom Baffour</title>
    <description>The latest articles on DEV Community by Collins Adom Baffour (@collins224).</description>
    <link>https://dev.to/collins224</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F712892%2F36b32493-b41a-4d10-8a16-86f0aaf259fe.jpg</url>
      <title>DEV Community: Collins Adom Baffour</title>
      <link>https://dev.to/collins224</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/collins224"/>
    <language>en</language>
    <item>
      <title>Running a Container Without Docker (Bootstrapping with the Filesystem and a Bundle)</title>
      <dc:creator>Collins Adom Baffour</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:13:25 +0000</pubDate>
      <link>https://dev.to/collins224/running-a-container-without-docker-bootstrapping-with-the-filesystem-and-a-bundle-4fbc</link>
      <guid>https://dev.to/collins224/running-a-container-without-docker-bootstrapping-with-the-filesystem-and-a-bundle-4fbc</guid>
      <description>&lt;p&gt;Docker is convenient, but it's also a thick layer of abstraction sitting on top of a handful of Linux kernel primitives. If you strip away the daemon, the CLI, and the image registry, a "container" is really just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a process with restricted &lt;strong&gt;namespaces&lt;/strong&gt; (its own view of PIDs, mounts, network, hostname, etc.)&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;root filesystem&lt;/strong&gt; it's been &lt;code&gt;chroot&lt;/code&gt;/&lt;code&gt;pivot_root&lt;/code&gt;'d into&lt;/li&gt;
&lt;li&gt;optionally, &lt;strong&gt;cgroups&lt;/strong&gt; limiting what it can consume&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post we'll build a minimal container runtime by hand — no &lt;code&gt;dockerd&lt;/code&gt;, no &lt;code&gt;containerd&lt;/code&gt;, just &lt;code&gt;unshare&lt;/code&gt;, &lt;code&gt;chroot&lt;/code&gt;, and a plain directory tree we'll call a &lt;strong&gt;bundle&lt;/strong&gt;. This is roughly what &lt;code&gt;runc&lt;/code&gt; does under the hood when Docker "runs" a container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It demystifies what Docker is actually doing.&lt;/li&gt;
&lt;li&gt;It's useful when you're stuck on a machine with no Docker installed (e.g. a locked-down CI runner) but you have root and a kernel with namespace support.&lt;/li&gt;
&lt;li&gt;It's the mental model you need if you ever touch the OCI runtime spec, &lt;code&gt;runc&lt;/code&gt;, &lt;code&gt;crun&lt;/code&gt;, or write your own sandboxing tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The ingredients
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Linux kernel with namespace support (basically any modern distro).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unshare&lt;/code&gt; and &lt;code&gt;chroot&lt;/code&gt; (from &lt;code&gt;util-linux&lt;/code&gt; / &lt;code&gt;coreutils&lt;/code&gt; — almost always already installed).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;root filesystem&lt;/strong&gt; for the container — a directory containing &lt;code&gt;/bin&lt;/code&gt;, &lt;code&gt;/lib&lt;/code&gt;, &lt;code&gt;/etc&lt;/code&gt;, etc. This is our "bundle."&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 1: Build the bundle (rootfs)
&lt;/h3&gt;

&lt;p&gt;The simplest way to get a working rootfs without pulling a Docker image is to export one from an existing image with &lt;code&gt;skopeo&lt;/code&gt;/&lt;code&gt;umoci&lt;/code&gt;, or — even simpler — just untar a minimal distro's rootfs tarball. Alpine publishes exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/mybundle/rootfs
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/mybundle
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-minirootfs-3.20.3-x86_64.tar.gz
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; alpine-minirootfs-3.20.3-x86_64.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; rootfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a directory that &lt;em&gt;is&lt;/em&gt; a Linux filesystem: &lt;code&gt;rootfs/bin&lt;/code&gt;, &lt;code&gt;rootfs/etc&lt;/code&gt;, &lt;code&gt;rootfs/lib&lt;/code&gt;, and so on. This directory is your bundle — the same idea as the &lt;code&gt;rootfs/&lt;/code&gt; + &lt;code&gt;config.json&lt;/code&gt; layout the &lt;a href="https://github.com/opencontainers/runtime-spec" rel="noopener noreferrer"&gt;OCI runtime spec&lt;/a&gt; formalizes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Isolate it with namespaces
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;unshare&lt;/code&gt; lets you spin up a process in new namespaces without any daemon involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;unshare &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="nt"&gt;--uts&lt;/span&gt; &lt;span class="nt"&gt;--ipc&lt;/span&gt; &lt;span class="nt"&gt;--net&lt;/span&gt; &lt;span class="nt"&gt;--pid&lt;/span&gt; &lt;span class="nt"&gt;--fork&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mount-proc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;chroot&lt;/span&gt; ~/mybundle/rootfs /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking that down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--mount&lt;/code&gt; — the container gets its own mount table; mounting/unmounting inside it won't affect the host.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--uts&lt;/code&gt; — its own hostname/domainname.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--ipc&lt;/code&gt; — isolated System V IPC and POSIX message queues.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--pid --fork&lt;/code&gt; — a new PID namespace; the shell becomes PID 1 &lt;em&gt;inside&lt;/em&gt; the container.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--net&lt;/code&gt; — its own network stack (no interfaces except &lt;code&gt;lo&lt;/code&gt; by default — more on this below).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--mount-proc&lt;/code&gt; — remounts &lt;code&gt;/proc&lt;/code&gt; inside the new mount namespace so &lt;code&gt;ps&lt;/code&gt; reports container-local PIDs instead of the host's.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chroot ~/mybundle/rootfs /bin/sh&lt;/code&gt; — pivots the process's filesystem root and execs a shell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run that, and &lt;code&gt;ps aux&lt;/code&gt; inside the shell will show basically nothing but your own shell as PID 1. &lt;code&gt;hostname somename&lt;/code&gt; will only affect this namespace. You are, for most practical purposes, "in a container."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Give it a real root (pivot_root instead of chroot)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;chroot&lt;/code&gt; alone is famously escapable and doesn't fully detach the process from the host's mount tree. The more correct approach — what &lt;code&gt;runc&lt;/code&gt; actually does — is &lt;code&gt;pivot_root&lt;/code&gt;, which swaps the entire root mount rather than just relabeling a path.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This replaces Step 2's &lt;code&gt;chroot&lt;/code&gt;, it doesn't chain after it.&lt;/strong&gt; If you &lt;code&gt;chroot&lt;/code&gt; into the rootfs first and &lt;em&gt;then&lt;/em&gt; try to run the commands below, they'll fail: once you've &lt;code&gt;chroot&lt;/code&gt;'d, the path &lt;code&gt;~/mybundle/rootfs&lt;/code&gt; no longer exists from your process's point of view (its root is now &lt;code&gt;/&lt;/code&gt;), so &lt;code&gt;mount --bind ~/mybundle/rootfs ~/mybundle/rootfs&lt;/code&gt; errors with "No such file or directory," and &lt;code&gt;pivot_root&lt;/code&gt; errors with "Resource busy" because the target isn't a proper mount point. Pick one entry method — &lt;code&gt;chroot&lt;/code&gt; (Step 2) or &lt;code&gt;pivot_root&lt;/code&gt; (this step) — not both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the full sequence run correctly, from the host, in one &lt;code&gt;unshare&lt;/code&gt; invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# run as root on the HOST — do not chroot first&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/mybundle/rootfs/oldroot

unshare &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="nt"&gt;--uts&lt;/span&gt; &lt;span class="nt"&gt;--ipc&lt;/span&gt; &lt;span class="nt"&gt;--net&lt;/span&gt; &lt;span class="nt"&gt;--pid&lt;/span&gt; &lt;span class="nt"&gt;--fork&lt;/span&gt; &lt;span class="nt"&gt;--mount-proc&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
    set -e
    ROOTFS=~/mybundle/rootfs
    mount --bind "$ROOTFS" "$ROOTFS"   # rootfs must be a mount point for pivot_root
    cd "$ROOTFS"
    pivot_root . oldroot
    mount -t proc proc /proc
    umount -l /oldroot
    rmdir /oldroot
    exec /bin/sh
  '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;mount --bind&lt;/code&gt; happens on the &lt;em&gt;host's original&lt;/em&gt; mount namespace before anything is switched — that's what makes &lt;code&gt;$ROOTFS&lt;/code&gt; a valid mount point in the first place. &lt;code&gt;pivot_root . oldroot&lt;/code&gt; (called with relative paths, after &lt;code&gt;cd&lt;/code&gt;ing into the rootfs) is the actual root swap; there's no separate &lt;code&gt;chroot&lt;/code&gt; call needed afterward, since the process's root is already the new filesystem.&lt;/p&gt;

&lt;p&gt;Now the process genuinely cannot see the host filesystem at all — there's no &lt;code&gt;/oldroot&lt;/code&gt; path left to walk back through, unlike a plain &lt;code&gt;chroot&lt;/code&gt; where a determined process can sometimes break out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Limit resources with cgroups
&lt;/h3&gt;

&lt;p&gt;Namespaces isolate &lt;em&gt;view&lt;/em&gt;, cgroups limit &lt;em&gt;consumption&lt;/em&gt;. These commands are run &lt;strong&gt;on the host, as root&lt;/strong&gt; — not inside the chroot/pivoted rootfs (Alpine's minirootfs doesn't even ship &lt;code&gt;sudo&lt;/code&gt;, so running them post-pivot will just fail with "sudo: not found"). If you're already root (e.g. via &lt;code&gt;sudo su&lt;/code&gt;), drop &lt;code&gt;sudo&lt;/code&gt; entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; /sys/fs/cgroup/mybundle
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"+cpu +memory +pids"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/cgroup.subtree_control
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"50M"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/mybundle/memory.max
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"20"&lt;/span&gt;  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/mybundle/pids.max
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$$&lt;/span&gt;    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/mybundle/cgroup.procs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this &lt;em&gt;before&lt;/em&gt; the &lt;code&gt;unshare&lt;/code&gt; call that launches your namespaced shell, capturing the PID via &lt;code&gt;echo $$&lt;/code&gt; right before you invoke it (or write the container's PID into &lt;code&gt;cgroup.procs&lt;/code&gt; right after &lt;code&gt;unshare --fork&lt;/code&gt; returns it). Then the shell — and everything it spawns — is capped at 50 MB of memory and 20 processes, exactly like a &lt;code&gt;docker run --memory=50m --pids-limit=20&lt;/code&gt; would enforce.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Networking (optional, and the fiddly part)
&lt;/h3&gt;

&lt;p&gt;By default the &lt;code&gt;--net&lt;/code&gt; namespace only has loopback. To give the container real connectivity you create a veth pair, put one end inside the namespace, and NAT it — this is precisely what Docker's &lt;code&gt;docker0&lt;/code&gt; bridge and &lt;code&gt;iptables&lt;/code&gt; rules automate for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link &lt;/span&gt;add veth-host &lt;span class="nb"&gt;type &lt;/span&gt;veth peer name veth-ctr
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth-ctr netns &amp;lt;container-pid&amp;gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip addr add 10.200.1.1/24 dev veth-host
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth-host up
&lt;span class="c"&gt;# inside the namespace:&lt;/span&gt;
ip addr add 10.200.1.2/24 dev veth-ctr
ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth-ctr up
ip &lt;span class="nb"&gt;link set &lt;/span&gt;lo up
ip route add default via 10.200.1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a NAT rule on the host (&lt;code&gt;iptables -t nat -A POSTROUTING -s 10.200.1.0/24 -j MASQUERADE&lt;/code&gt;) and the container can reach the outside world. This is the one piece where "just do it by hand" starts to feel like reimplementing Docker's networking stack — which, in a sense, you are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together: a tiny launcher script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# run-container.sh &amp;lt;bundle-dir&amp;gt; -- &amp;lt;command&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;# run as root on the host (e.g. sudo ./run-container.sh ...)&lt;/span&gt;
&lt;span class="nv"&gt;BUNDLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;  &lt;span class="c"&gt;# drop the "--"&lt;/span&gt;
&lt;span class="nv"&gt;ROOTFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUNDLE&lt;/span&gt;&lt;span class="s2"&gt;/rootfs"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOTFS&lt;/span&gt;&lt;span class="s2"&gt;/oldroot"&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;unshare &lt;span class="nt"&gt;--mount&lt;/span&gt; &lt;span class="nt"&gt;--uts&lt;/span&gt; &lt;span class="nt"&gt;--ipc&lt;/span&gt; &lt;span class="nt"&gt;--pid&lt;/span&gt; &lt;span class="nt"&gt;--fork&lt;/span&gt; &lt;span class="nt"&gt;--mount-proc&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    set -e
    mount --bind '&lt;/span&gt;&lt;span class="nv"&gt;$ROOTFS&lt;/span&gt;&lt;span class="s2"&gt;' '&lt;/span&gt;&lt;span class="nv"&gt;$ROOTFS&lt;/span&gt;&lt;span class="s2"&gt;'
    cd '&lt;/span&gt;&lt;span class="nv"&gt;$ROOTFS&lt;/span&gt;&lt;span class="s2"&gt;'
    pivot_root . oldroot
    mount -t proc proc /proc
    umount -l /oldroot
    rmdir /oldroot
    exec &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;
  "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./run-container.sh ~/mybundle &lt;span class="nt"&gt;--&lt;/span&gt; /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real, working container in about ten lines of shell. It's missing image layering, a registry client, seccomp profiles, and a dozen other things &lt;code&gt;runc&lt;/code&gt;/&lt;code&gt;containerd&lt;/code&gt;/Docker handle for you — but the isolation primitives are the same ones Docker uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this maps to in OCI terms
&lt;/h2&gt;

&lt;p&gt;If you've seen &lt;code&gt;runc spec&lt;/code&gt; generate a &lt;code&gt;config.json&lt;/code&gt;, that file is just a declarative description of exactly the steps above: which namespaces to unshare, what the rootfs path is, what cgroup limits to apply, what mounts to bind in. A "bundle" in the OCI sense is literally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mybundle/
├── config.json     # namespaces, cgroup limits, mounts, entrypoint
└── rootfs/          # the filesystem tree, e.g. our Alpine extract
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;runc run mycontainer&lt;/code&gt; just reads that &lt;code&gt;config.json&lt;/code&gt; and does the &lt;code&gt;unshare&lt;/code&gt;/&lt;code&gt;pivot_root&lt;/code&gt;/cgroup dance for you, with a lot more edge-case handling (capabilities, seccomp, user namespaces, &lt;code&gt;/dev&lt;/code&gt; setup, hooks). Understanding it manually first makes reading &lt;code&gt;runc&lt;/code&gt;'s source — or debugging a weird container issue — a lot less intimidating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This needs root (or user namespaces configured for rootless operation — a whole extra topic).&lt;/li&gt;
&lt;li&gt;Skipping seccomp/capabilities means this is &lt;strong&gt;not&lt;/strong&gt; a security boundary equivalent to a real container runtime. Don't run untrusted code in it.&lt;/li&gt;
&lt;li&gt;Cgroup v1 vs v2 paths differ; check &lt;code&gt;mount | grep cgroup&lt;/code&gt; to see which your system uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to go further, the natural next step is reading &lt;code&gt;runc&lt;/code&gt;'s &lt;code&gt;libcontainer&lt;/code&gt; source or the &lt;a href="https://github.com/opencontainers/runtime-spec" rel="noopener noreferrer"&gt;OCI runtime-spec repo&lt;/a&gt; — everything above is the tip of what it formalizes.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>containers</category>
      <category>devops</category>
      <category>systemsprogramming</category>
    </item>
    <item>
      <title>Amazon Simple Storage Service (S3)</title>
      <dc:creator>Collins Adom Baffour</dc:creator>
      <pubDate>Wed, 13 Nov 2024 18:14:37 +0000</pubDate>
      <link>https://dev.to/collins224/amazon-simple-storage-service-411o</link>
      <guid>https://dev.to/collins224/amazon-simple-storage-service-411o</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Amazon S3&lt;/strong&gt; is an incredibly scalable, durable, and secure object service solution used by businesses worldwide to store and retrieve data. Whether you’re handling backups, websites, or big data, S3's flexibility makes it an ideal choice. Amazon S3 stores data as objects within buckets. &lt;strong&gt;Object&lt;/strong&gt; is a file and any metadata that describes the file. &lt;strong&gt;Bucket&lt;/strong&gt; is a container for objects. To store your data in Amazon S3, create a bucket and specify a bucket name and AWS Region. Then, you upload your data to that bucket as objects in Amazon S3. Each object has a key (or key name), the unique identifier for the object within the bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buckets vs Objects
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feaf6dcjmad6g1bpb85g4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feaf6dcjmad6g1bpb85g4.png" alt="Bucket Vs Object" width="712" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage Classes
&lt;/h2&gt;

&lt;p&gt;Amazon S3 offers a range of storage classes designed for different use cases. These classes are purpose-built to provide the lowest cost storage for different access patterns. These classes are ideal for virtually any use case, including those with demanding performance needs, &lt;strong&gt;data lakes&lt;/strong&gt;, &lt;strong&gt;residency requirements&lt;/strong&gt;, &lt;strong&gt;unknown or changing access patterns&lt;/strong&gt;, or &lt;strong&gt;archival storage&lt;/strong&gt;. The table below shows the various storage classes and their use cases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn39i2vche52gzjprdc70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn39i2vche52gzjprdc70.png" alt="S3 Storage Classes" width="709" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Control
&lt;/h2&gt;

&lt;p&gt;Amazon S3 is secure, and private by default, with extensive auditing capabilities to monitor access requests to resources. Access to S3 resources must be explicitly granted to an identity to ensure security.&lt;br&gt;
This access can be granted by the below Access Management tools&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bucket Policy&lt;/strong&gt;: JSON-based policies attached to buckets to specify access permissions, enabling fine-grained control over who can access specific resources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Identity-Based Policy&lt;/strong&gt;: Policies attached to AWS IAM identities (users, groups, roles) that define permissions to access S3 resources across the AWS account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;S3 Access Grants&lt;/strong&gt;: An access control tool that simplifies granting cross-account access to specific objects or buckets using access permissions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access Points&lt;/strong&gt;: Customized access control points with unique policies to simplify access for large data sets, especially in shared environments or multi-tenant architectures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access Control List&lt;/strong&gt; (ACL): Legacy method for managing access to buckets and objects by defining read and write permissions for users and groups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Object Ownership&lt;/strong&gt;: A setting that controls ownership of objects uploaded to a bucket, often used to ensure the bucket owner automatically owns all objects, simplifying permissions management. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data Protection
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Versioning&lt;/strong&gt;: Versioning in Amazon S3 is a means of keeping multiple variants of an object in the same bucket. You can use the S3 Versioning feature to preserve, retrieve, and restore every version of every object stored in your buckets. &lt;br&gt;
Buckets can be in one of three states: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unversioned (the default)&lt;/li&gt;
&lt;li&gt;Versioning-enabled&lt;/li&gt;
&lt;li&gt;Versioning-suspended&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replication&lt;/strong&gt;: You can use replication to enable automatic, asynchronous copying of objects across Amazon S3 buckets. Buckets that are configured for object replication can be owned by the same AWS account or by different accounts.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security and Encryption
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Encryption Options&lt;/strong&gt;: All Amazon S3 buckets have encryption configured by default, and all new objects that are uploaded to an S3 bucket are automatically encrypted at rest. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server-side encryption&lt;/strong&gt; - &lt;strong&gt;Amazon S3 managed keys (SSE-S3)&lt;/strong&gt; is the &lt;strong&gt;default&lt;/strong&gt; encryption configuration for every bucket in Amazon S3. To use a different type of encryption, you can either specify the type of server-side encryption to use in your S3 PUT requests, or you can set the default encryption configuration in the destination bucket. Other server-side encryption includes Server-Side Encryption with AWS Key Management Service and Server-Side Encryption with Customer-Provided Keys&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-side encryption&lt;/strong&gt; – You encrypt your data client-side and upload the encrypted data to Amazon S3. In this case, you manage the encryption process, encryption keys, and related tools.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In summary, Amazon S3 provides a scalable, secure, and versatile storage solution for a range of needs. With robust access controls, encryption options, and seamless AWS integration, S3 empowers businesses to efficiently manage and protect their data, supporting innovation and growth in the cloud.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article, please let me know in the comment section or send me a DM. I'm always happy to chat! ✌️&lt;/p&gt;

&lt;p&gt;Thank you so much for reading! 🙏 Keep an eye out for more AWS-related posts, and feel free to connect with me on LinkedIn 👉 &lt;br&gt;
&lt;a href="https://www.linkedin.com/in/collins-adom-baffour/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/collins-adom-baffour/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/data-protection.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/data-protection.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/security.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/security.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-management.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-management.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>s3</category>
      <category>security</category>
    </item>
    <item>
      <title>Security in Amazon Virtual Private Cloud</title>
      <dc:creator>Collins Adom Baffour</dc:creator>
      <pubDate>Sun, 12 May 2024 21:19:19 +0000</pubDate>
      <link>https://dev.to/collins224/security-in-amazon-virtual-private-cloud-2nh5</link>
      <guid>https://dev.to/collins224/security-in-amazon-virtual-private-cloud-2nh5</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;When it comes to securing resources within an AWS Virtual Private Cloud (VPC), both Security Groups and Network Access Control Lists (NACLs) play vital roles. However, they operate at different layers of the network stack and serve distinct purposes. Let's explore the differences between Security Groups and NACLs:&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Groups
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9cexglokpge3fjkdbb2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9cexglokpge3fjkdbb2v.png" alt="SG Diagram" width="481" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Layer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Operates at the Instance Level&lt;/strong&gt;: Security Groups are stateful firewalls that operate at the instance level. They control inbound and outbound traffic for individual EC2 instances, RDS instances, and other resources within the same VPC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Allow Rules Only&lt;/strong&gt;: Security Groups consist of rules that allow traffic based on specified criteria (e.g., IP addresses, protocols, ports). By default, all traffic is denied unless explicitly allowed by a rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stateful&lt;/strong&gt;: Security Groups are stateful, meaning they automatically allow return traffic for permitted inbound connections. For example, if an inbound rule allows traffic on port 80, the corresponding outbound traffic is allowed without requiring an explicit rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Updates
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Dynamic&lt;/strong&gt;: Changes to Security Group rules take effect immediately. This flexibility allows for quick adjustments to network access based on evolving requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Instance-level Security&lt;/strong&gt;: Security Groups are ideal for enforcing security policies specific to individual instances or resource groups. They provide granular control over network traffic and are well-suited for application-level security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network Access Control Lists (NACLs)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk78wz8kt6nzgtjp471zm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk78wz8kt6nzgtjp471zm.png" alt="NACL Diagram" width="481" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Layer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Operates at the Subnet Level&lt;/strong&gt;: NACLs are stateless firewalls that operate at the subnet level. They control traffic entering and exiting subnets within the VPC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Allow and Deny Rules&lt;/strong&gt;: NACLs consist of numbered rules that allow or deny traffic based on source and destination IP addresses, protocols, and ports. Unlike Security Groups, NACLs support both allow and deny rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stateless&lt;/strong&gt;: NACLs are stateless, meaning they do not automatically allow return traffic. For each packet, both inbound and outbound rules are evaluated independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Priority Order
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Evaluated in Order&lt;/strong&gt;: NACL rules are evaluated in ascending order based on their rule numbers. If a packet matches a rule, the action (allow or deny) specified by that rule is applied, and subsequent rules are not evaluated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Subnet-level Security&lt;/strong&gt;: NACLs provide an additional layer of security by controlling traffic flow at the subnet boundary. They are useful for implementing broad network security policies across multiple instances or services within a subnet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Both Security Groups and NACLs are essential components of network security in AWS, and they are often used together to create multi-layered defense strategies for protecting resources within a VPC.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vpc</category>
      <category>cloudcomputing</category>
      <category>security</category>
    </item>
    <item>
      <title>Amazon MSK (Managed) vs Apache Kafka (Self -Managed)</title>
      <dc:creator>Collins Adom Baffour</dc:creator>
      <pubDate>Sat, 11 May 2024 16:42:39 +0000</pubDate>
      <link>https://dev.to/collins224/amazon-msk-vs-kafka-5g4p</link>
      <guid>https://dev.to/collins224/amazon-msk-vs-kafka-5g4p</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the world of real-time data processing, Apache Kafka has long reigned as the go-to solution for building robust and scalable streaming platforms. However, with the rise of managed services like Amazon Managed Streaming for Apache Kafka (Amazon MSK), businesses face a choice: stick with the tried and true self-managed Kafka or embrace the convenience of a managed service. In this blog, we'll delve into the key differences, pros, and cons of Amazon MSK versus Kafka, helping you make an informed decision for your streaming needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Basics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Apache Kafka:
&lt;/h3&gt;

&lt;p&gt;Apache Kafka, developed by LinkedIn, is an open-source distributed event streaming platform used for building real-time data pipelines and streaming applications. It is designed for high throughput, fault tolerance, and horizontal scalability. Kafka operates on a distributed architecture consisting of brokers, topics, producers, and consumers. While powerful, Kafka requires manual provisioning, configuration, and maintenance, which can be complex and resource-intensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazon MSK:
&lt;/h3&gt;

&lt;p&gt;Amazon Managed Streaming for Apache Kafka (Amazon MSK) is a fully managed service that simplifies the deployment, management, and scaling of Apache Kafka clusters on AWS. With Amazon MSK, AWS handles infrastructure provisioning, software installation, maintenance, and monitoring, allowing users to focus on building applications rather than managing infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Differences:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Managed vs. Self-Managed: The most apparent difference between Amazon MSK and Kafka is the management approach. With Kafka, users are responsible for setting up and managing the infrastructure, including provisioning servers, configuring brokers, and ensuring high availability. In contrast, Amazon MSK abstracts away much of this complexity, providing a fully managed service where AWS handles cluster management, updates, and monitoring.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integration with AWS Ecosystem: Amazon MSK seamlessly integrates with other AWS services, such as Amazon CloudWatch, AWS CloudFormation, AWS Identity and Access Management (IAM), and Amazon VPC, enabling users to leverage existing AWS tools and services for monitoring, security, and networking. While Kafka can also be deployed on AWS, integrating it with AWS services requires additional setup and configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability and Elasticity: Both Amazon MSK and Kafka offer scalability and elasticity, allowing clusters to scale horizontally by adding or removing nodes to accommodate changes in workload. However, Amazon MSK simplifies the scaling process by automatically handling cluster resizing and rebalancing, whereas with Kafka, users must manually adjust cluster configuration and rebalance partitions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cost Structure: The cost structure differs between Amazon MSK and self-managed Kafka. With Kafka, users pay for infrastructure resources (e.g., EC2 instances, storage, networking) based on usage and configuration. In contrast, Amazon MSK follows a pay-as-you-go pricing model, where users pay for the managed service based on cluster usage (e.g., broker-hours, storage, data transfer).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Pros and Cons:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Kafka:
&lt;/h4&gt;

&lt;h4&gt;
  
  
  Pros:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Full control over infrastructure and configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flexibility to customize and optimize cluster performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No vendor lock-in, as Kafka is open-source and can be deployed on any infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Cons:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Requires expertise in deployment, configuration, and maintenance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Time-consuming and resource-intensive management tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limited integration with AWS services out-of-the-box.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Amazon MSK:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Pros:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fully managed service reduces operational overhead and complexity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Seamless integration with AWS ecosystem for monitoring, security, and networking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automated provisioning, scaling, and maintenance tasks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Vendor lock-in to AWS ecosystem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limited flexibility for customizing cluster configuration and performance optimizations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Potentially higher costs compared to self-managed Kafka, depending on usage patterns.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion:
&lt;/h2&gt;

&lt;p&gt;Choosing between Amazon MSK and Kafka depends on your organization's specific requirements, expertise, and preferences. While Kafka offers greater control and flexibility, it requires significant investment in management and maintenance. On the other hand, Amazon MSK provides convenience and simplicity, allowing users to focus on application development rather than infrastructure management. Ultimately, the decision boils down to striking the right balance between control, convenience, and cost-effectiveness for your streaming platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Further Reading&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/msk/" rel="noopener noreferrer"&gt;https://aws.amazon.com/msk/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kafka.apache.org/documentation/" rel="noopener noreferrer"&gt;https://kafka.apache.org/documentation/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.freecodecamp.org/news/apache-kafka-handbook/" rel="noopener noreferrer"&gt;https://www.freecodecamp.org/news/apache-kafka-handbook/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cloudcomputing</category>
      <category>kafka</category>
      <category>msk</category>
    </item>
  </channel>
</rss>
