构建一个基于 Firecracker 的课程平台,用于学习 Docker 和 Kubernetes

2023年6月17日 Containers,Programming,Ranting

这是一篇关于 iximiuz Labs 内部运作情况的迟来的文章。它将涵盖我为什么决定构建自己的 DevOps、SRE 和平台工程师的边做边学平台,我是如何设计它的,选择了什么技术栈,以及该平台的各个组件是如何实现的。它还将涉及我在此过程中必须做出的一些权衡,并突出显示该平台架构中最有趣的部分。最后,我当然会分享我对路线图下一步的想法。听起来很有趣?那就准备好迎接一篇长文吧!

我如何构建自己的边做边学平台

提升你的服务器端技能——加入 9,000 名工程师的行列,直接在收件箱中获取有见地的学习资料。 订阅! Built with ConvertKit

为什么要有自己的在线演练场?

在我决定全力投入 iximiuz Labs 之前,我已经~~写博客~~制作技术教育内容好几年了。我的主要表达方式是纯粹的文字和(相当多的)绘图。然而,还有另一个经常被忽视但很重要的部分——提供可重现的指令,说明如何重复我文章中的实验,以帮助学生获得自己的实践经验

我希望学生们不是仅仅阅读关于某项技术的文章,而是亲自尝试、摆弄它,并亲眼看看它是如何工作的。这尤其需要我为每篇文章花费大量时间来设计一套设置实验环境的步骤……但残酷的现实是,无论我付出多大的努力,总会有人缺乏合适的硬件、软件和/或技能来实际运行这些指令。所以,我总是觉得可以做得更有效率。

一个相当常见的解决方案是,直接在文章页面上提供一个连接到预配置环境的 Web 终端。这无疑消除了学生们的大部分障碍。他们不再需要在自己的机器上安装任何东西,并且可以在不离开浏览器的情况下按照文章中的说明进行操作。

带有集成 Web 终端的博客文章

然而,我个人在使用提供这种用户体验的平台时的体验并不是很积极。我尝试过几个,其中最著名的是 Katacoda,但它并没有给我留下深刻的印象。在我遇到的例子中,主要的关注点通常是演练场的功能,而不是教育内容。学生们通常会在左侧获得~~枯燥的~~文字材料,然后他们必须将命令复制粘贴到右侧的终端中,这使得它与附带云 VM 的普通博客文章没有太大的区别。

我想做得更好!🤪

如果我要用演练场来增强我的文章(这对我来说绝非易事),我希望它们成为学习体验中真正不可或缺的一部分,而不仅仅是我博客上的一个助手。

对我来说,技术教育的最终目标是让学生们做好准备,以解决实际生产环境中的问题,在这些环境中,通常没有指导员来指导他们完成整个过程。因此,高效的演练场应该以这样一种方式设计,即允许学生们通过自己的实践来学习,而不是简单地按照指示进行操作。但是,如果学生们当前技能与解决问题所需的技能之间的差距太大,学生们很可能会卡住并放弃。这意味着我必须提出一种混合方法,允许学生们以结构化的方式(由内容作者组织)学习理论,然后让他们有机会在不太受指导的、类似生产的环境中应用他们刚刚学到的知识。

在我理想的边做边学平台上,学生们首先要完成内容的理论部分,在那里他们可以学习概念并熟悉解决问题所需的工具和技术。然后,他们将获得一系列练习来尝试,并更好地内化他们新获得的技能

你可以将第一部分(理论部分)看作是我传统的深入式博客文章,其主要关注点是文本和视觉效果,而侧面的终端只是一个补充(因此,与众所周知的 KataCoda 没有太大的区别)。然而,第二部分(实践部分)应该是我在实践中很少见到的东西——可以将其视为 LeetCode 甚至 CodinGame 风格的练习(具有交互式解决方案检查、游戏化元素等等),但适用于 Docker、Kubernetes 以及 DevOps、SRE 或平台工程师使用的其他典型工具和技术

有趣的是,作为一名学生,我可能会更期待演练场在实践学习阶段带给我的新能力。然而,作为一名老师,我对演练场为实践和理论教育部分解锁的新可能性感到兴奋。

对于内容作者来说,理论部分的演练场成为了一种新的表达方式——例如,指导员可以使用演练场来动画化课程中的流程和概念。看看我提出的这个可视化工具的 PoC,它用于解释 Kubernetes 的工作原理——它利用演练场来运行 Kubernetes 集群,并在 Web 画布上实时渲染其状态变化,因为学生与它进行交互:

所有这些加在一起,听起来值得一试——如果我要尝试,我不会构建另一个 KataCoda,而是一个我以前从未见过的、并且我作为学生和老师都会喜欢使用的新型教育平台。

高级设计目标

上面的长篇大论是不可原谅的,但我希望它能让你更好地了解我试图通过该平台实现的目标,并证明我将在下面描述的一些设计决策是合理的。

与系统设计一样,条条大路通罗马。我的主要目标是交付一些足够好的东西,以便在可用于该项目的非常有限的资源的情况下开始使用。我的指导原则是(并且仍然是):

这是我在上述约束条件下提出的平台的高级设计:

iximiuz Labs v1 架构

高级的想法是,有一个(静态的)Web 应用程序,一个名为 Foreman 的中央组件,负责除演练场之外的所有事情,以及它后面的一个可水平扩展的裸机服务器(worker)机群,每个服务器运行多个 microVM。

Foreman 组件

厨房水槽 Foreman 组件包括一个前端应用程序、一个用于它的后端服务以及许多用于各种簿记任务的后台作业。这绝不是最优雅的方法,但以这种方式构建事物也并不少见,而且由于只有一个开发人员(我)在平台上工作,因此绝对没有必要将其拆分为微服务。

Foreman 的主要职责是:

所有 Foreman 的子系统都是无状态的,并且它们的操作大多是幂等的。即使我部署了多个 Foreman 实例,它们也可以在没有(多)个冲突的情况下使用相同的数据库。

从安全角度来看,Foreman 可能是该平台最敏感的组件。获得对它的访问权限将允许攻击者篡改内容和用户数据,并启动任意数量的演练场。然而,它并不像听起来那么糟糕。

篡改内容并不好,但缓解措施很简单——我可以随时清除它并从源头恢复它(所有内容都位于 Git 存储库中)。用户数据有点令人担忧,但该平台目前收集的唯一信息是用户的 GitHub 配置文件 ID(用于身份验证目的),这是公开信息,其余数据(如 microVM 的状态或用户的课程进度)在该平台上下文之外并没有那么有价值。启动任意数量的演练场也不是什么大问题——该平台旨在仅运行允许的 microVM 镜像,而 Foreman 无法控制允许什么和不允许什么。因此,攻击者将能够启动一堆 microVM,但该平台的任何其他(高级)用户也可以这样做。

Worker 机群

这是我必须最具创造力的地方。与 Foreman(一种非常典型的软件)不同,worker 要奇异得多,并且需要进行大量的实验和原型设计才能使其正常工作。

每个 worker 服务器都是一台裸机机器(因为它需要运行 Firecracker,并且嵌套虚拟化并不有趣),它具有许多守护程序,这些守护程序控制着 worker 和 microVM 生命周期的各个方面。

worker 服务器旨在可水平扩展并且彼此独立。它们在某种程度上也是无状态的——它们维护的唯一状态是它们当前正在运行的 microVM 的状态。虽然这可能会给用户带来一些暂时的不便,但如果 worker 服务器出现问题(包括服务器受到攻击),它可以简单地清除并替换为新的服务器,而不会对平台的其余部分产生连锁反应。

Bender 守护程序

worker 服务器上的关键组件是一个名为 Bender 的守护程序。它负责以下事项:

换句话说,Bender 之于 microVM 就像 Docker 或 containerd 之于容器一样——它是一个具有 REST API 的单主机 ~~容器~~ microVM 管理守护程序。

由于其性质,Bender 是一个高度特权的组件(它直接在主机上以 root 身份运行🙈),因此我的目标是使其尽可能简单和集中,以最大程度地减少攻击面。Bender 也不会直接暴露给最终用户(理想情况下,我甚至不会将其暴露给 Internet,但目前,由于我选择的托管提供商,Foreman 和 worker 之间没有 LAN)。

幸运的是,Bender 是唯一需要在 worker 服务器上以 root 身份运行的组件。

Firecracker microVM

iximiuz Labs 上的每个演练场都由一个或多个 microVM 组成。目标是让学生可以访问一个或一组连接到虚拟网络的临时 Linux 机器。

演练场的多节点性质,加上各种安全性和性能方面的考虑,使我在 Firecracker 之上编写了相当多的自定义代码:

多节点演练场架构

演练场的设计受到了 Weave Ignite 和 Kubernetes 的高度影响。

Weave Ignite 教会我在 Docker 容器中运行 Firecracker microVM——它有助于网络管理,并简化了清理逻辑。要为 microVM 配置网络,需要在主机上创建一个 tun/tap 设备,将其连接到 microVM,并添加一堆 iptables 规则以将流量路由到/从 microVM。Alex Ellis 有一个很棒的 GitHub 项目 演示了如何做到这一点。它并不太复杂,但是当需要在单个主机上为数十个 microVM 执行此操作,并结合出口和侧向流量过滤时,管理起来会有些麻烦。但是,包括创建 tun/tap 设备在内的大多数网络技巧都可以在 Docker 容器的隔离网络命名空间中完成。并且当 VM 终止时,容器也会终止,因此清理是自动的(好吧,Docker 会为你完成)。

Kubernetes 启发了我更进一步地采用在容器中运行 Firecracker microVM 的想法。在 Kubernetes 中,Pod 本质上是共享某些命名空间(最值得注意的是网络命名空间)的半融合容器组。但这听起来很像我的多节点演练场需要的——可以通过共享相同网络命名空间的容器组来表示可以通过虚拟网络相互通信的 microVM 组。并且每个这样的组都获得自己的 Docker 网络(带有一堆额外的防火墙规则)以将其与其他组隔离。

从安全角度来看,Firecracker microVM 是该平台最暴露的部分——我直接向 Internet 上的陌生人提供对它们的 shell 访问权限!但它们也是系统中最受限制和隔离的部分——每个 microVM 都会分配到一个临时的非特权用户,然后被限制并在此基础上进行容器化。即使突破 VM 仍然需要攻击者获得对主机的 root 访问权限。没有什么是不可能的,但我个人不知道该怎么做,并且我正在尽力使系统保持最新的安全补丁。

MicroVM 在资源和速率方面都受到限制,并且它们会在一定时间后自动终止。免费套餐 microVM 的出口流量不仅受到速率限制,而且还受到 DNS 和 HTTP(S) 的限制,并且仅限于允许的 host 集合(这部分实现起来有点棘手,但由于我在 Envoy 和 Istio 方面的丰富经验,经过几次迭代后,我似乎做对了)。

Conductor 守护程序

正如其名称应该清楚的那样,Conductor 守护程序负责将来自学生的命令传递给 microVM 并从 microVM 传递回来。目前,它包括以下内容:

当浏览器选项卡即将建立新的 Conductor 连接时,它会通过向 Foreman 发送经过身份验证的请求来 声明 一个连接槽。然后,Foreman 会为此连接颁发一个唯一的短期令牌,并在将该令牌返回给浏览器之前将其注册到相应的 Conductor 实例中。一旦浏览器收到令牌,它就可以使用目标 microVM 直接与 worker 服务器上的 Conductor 实例建立 WebSocket 连接。

浏览器中的 Web 终端是使用 Xterm.js 实现的,Conductor 使用类似于 [yudai/gotty](https://iximiuz.com/en/posts/iximiuz-labs-story/< https:/github.com/yudai/gotty>) 的方法将 TTY 控制的 shell 会话代理到浏览器。shell 会话本身是一个很好的旧 SSH 连接,它通过 Unix 套接字安装到 microVM 的容器中,然后由 Firestarter shim 组件代理到 microVM 的 tap 接口。

Conductor 守护程序在系统中的作用

tasks 也通过 WebSocket 连接传递到演练场。我将在下一节中更详细地介绍任务,但就目前而言,只需知道它们是由课程作者编写并按预定顺序在 microVM 上执行的一组命令。当任务更改其状态时(例如,从 pending 变为 running 或从 running 变为 finished),Conductor 会通过 WebSocket 消息通知浏览器页面。这就是在浏览器中跟踪和可视化学生的进度的方式。

自动解决方案检查子系统

自动解决方案检查是 iximiuz Labs 最有趣的部分之一。更重要的是,它可能是目前的关键差异化因素。该子系统值得一篇单独的博客文章,我肯定有一天应该写一篇,但现在,我将只给你一个高级概述,并重点介绍我必须克服的最有趣的技术挑战。

自动解决方案检查子系统基于 tasks 的概念。Tasks 是内容交互元素和实践部分中练习背后的低级构建块。内容作者可以定义一组命令,其方式类似于编写 GitHub Actions 管道的方式,这些命令将在启动课程(或练习)时发送到演练场 microVM。

Task 定义示例

在 microVM 端,命令被组织成有向无环图 (DAG),然后由一个名为 Examiner 的特殊守护程序定期执行。如果任务的命令以非零退出代码终止或超时,则会(几乎)立即重试。当命令最终成功完成时,浏览器页面会通过通过 Conductor 传递的 WebSocket 消息收到通知。

最初的想法是(重新)使用 SSH 会话(来自 Web 终端实现)将命令从 Conductor 传递到 Examiner 守护程序:

自动解决方案检查子系统

然而,事实证明该实现过于复杂且不可靠,尤其是在及时向浏览器报告任务状态更改时。令人惊讶的是,帮助来自最意想不到的地方——gRPC!gRPC 非常适合守护程序及其 CLI 客户端之间的本地通信——包括 containerd 在内的不同项目都将其用于此目的。Examiner 守护程序已经在使用 gRPC 与其 CLI 客户端 examinerctl 进行通信,这意味着:

将 gRPC 用于守护程序及其 CLI 客户端之间的本地通信

选择技术栈

不难注意到,上述组件在本质上差异很大。因此,为整个平台选择单一编程语言或框架不是一种选择。然而,由于工具链蔓延是一个真正的问题,因此我尝试尽可能地减少所涉及的技术数量。

iximiuz Labs 技术栈:34% Go,27% Vue,25% JavaScript,10% 其他 shell 脚本和 Dockerfiles

开发前端应用程序的需求使得选择 JavaScript(或 TypeScript)成为必然。假设我可以将 Node.js 也用于系统守护程序,但云原生领域中的大多数项目都是用 Go 编写的,我不想与生态系统作斗争。所以,选择了 Go。但这就是我停止的地方。尽管我对 Python 和 Rust 怀有热烈的感情,但我不想在组合中引入另一种语言。Go 和 JavaScript 的组合不是我最喜欢的,但它涵盖了所有基础,而且从历史上看,我在使用它时效率很高。

前端应用程序和 API 层

最初,我以为我会将 Vue.js 用于前端应用程序(我或多或少精通的唯一 Web 框架),并在其后放置一个用 Go 编写的 API 服务器。然而,我很快意识到我需要为 SEO 目的实现服务器端渲染 (SSR)——毕竟,这是我博客目前的主要流量来源之一。因此,这让我尝试了 Nuxt 3,而且相当出乎意料的是,我喜欢它!Nuxt 3 刚刚发布测试版,仍然有些粗糙,文档也非常有限,但尽管如此,我还是设法在几个小时内启动了一个带有 SSR 和 REST API 的简单应用程序。六个月后,我仍然在使用 Nuxt 3,而且我现在对我的选择更加满意。

为了避免处理大量的基本 UI 组件,我购买了 Tailwind UI kit 并使其与 daisyUI 友好——弗兰肯斯坦的怪物我并不感到自豪,但它奏效了!总的来说,我很惊喜,现在的硬核后端工程师可以通过现代前端堆栈实现多少目标:

我已经单独构建一个宠物项目几个月了,我很惊喜,现在的硬核后端工程师可以通过现代前端堆栈实现多少目标!💪关于帮助我开发 iximiuz Labs Web UI 的工具和技巧的线程 🧵 pic.twitter.com/nrpVLUVe1T — Ivan Velichko (@iximiuz) 2023 年 3 月 27 日

事实证明,Nuxt 3 中内置的 API 层非常方便,我决定至少现在不要在 Go 中实现单独的 API 服务器。但是如果没有单独的 API 服务器,为什么不尝试将后台作业也放入 Nuxt 应用程序中呢?这就是平台的主要业务逻辑最终以 JavaScript 🙈 实现的原因。

后端 worker 服务器和基础设施粘合剂

位于 worker 服务器上的两个主要守护程序是用 Go 编写的。其中一个作为 systemd 服务启动,另一个在 Docker 容器中运行。这两个守护程序都公开 HTTP API(通过 chi),其中一个守护程序还有一个内置的 WebSocket 服务器(通过存档的 gorilla/websocket 包)。

Docker 在整个平台上被大量(滥用):

有趣的是,目前的演练场也在 Dockerfile 中描述,然后构建为 Docker 镜像,并存储在私有 Docker 注册表中。当演练场管理守护程序需要启动新的演练场时,它会从注册表中拉取镜像,将其解压缩到目录中,并从其内容创建 Firecracker 驱动器镜像。

worker 服务器上使用的其他值得注意的技术包括:

托管和数据存储提供商

我考虑了相当多的平台托管选项,这不是一个容易的选择。目标是:

对于前端应用程序,我真诚地试图保持开放的心态,并考虑所有花哨的无服务器和边缘解决方案——Vercel、Netlify、Firebase 等等。

然而,我最终使用了 ~~不错的旧云 VM~~ 更传统的 fly.io Machines。Fly Machines 让我对前端应用程序的控制程度接近理想——我可以将任何东西(以及任何方式)打包在容器中,并在几秒钟内将其部署到多个区域中的多个廉价 VM。入口(包括 TLS 终止和负载平衡)和故障时的自动重启将由 fly.io 管理。fly.io 尚未涵盖的唯一内容是(或多或少高级的)可观察性,但我希望很快能解决这个问题。

对于存储层,我再次尝试保持开放的心态(考虑了 Supabase、Fauna、Xata、Upstash...),但最终,我决定选择最无聊的选项——MongoDB Atlas(带有时间点备份)。我几乎选择了 Fly 的(半托管)Postgres 产品,但在当时,(故障转移后的)故障恢复未按预期工作,我不想处理它。不过,我应该再次尝试一下,因为 MongoDB 非常适合生产,但不适合分析和报告(至少以我的经验)。

worker 服务器的选择要容易得多——Hetzner Auction 是我所知最便宜的裸机托管(我现在为一台具有 128GB RAM 和 6 个内核的服务器支付大约 40 美元/月),我可以将其装入数十个并发演练场。如果我需要扩展到其他地区,我可能会尝试在美洲和/或亚洲找到类似的托管提供商。

目前的每月基础设施费用约为 100 美元,其中 80-90% 是 worker 服务器的成本。

路线图的下一步是什么?

当然是内容!我已经完成了我的 containerd 课程 的第一个模块(带有两个扎实的课程),但是在写了将近五年的关于容器的博客之后,我已经积累了 soooo 多的内容创意……现在我终于有了一个平台来实现它们!

在功能方面,我计划接下来添加的内容包括:

一个梦想的目标是开源 Bender 和 Firecracker rootfs 生成工具。但是,在我找到一种可持续的方式来资助该平台的工作之前,我不确定我是否可以抽出时间来做这件事。

当然,用户也应该控制路线图,而不仅仅是我。说到用户,我保持开放的心态——最初,我为自己(作为作者)和我的博客的读者(作为学生)构建了该平台,但我可以看到它被其他教育者和/或学生自己用来创建和分享他们自己的实验室。公司可以使用它来培训他们的员工或进行自动化面试——类似于 HackerRank,但适用于 DevOps 主题。大学也可以使用它来教授 CS 学生。可能性是无限的 (tm) 🚀

致谢

我想感谢一些在我构建 iximiuz Labs 的过程中帮助过我的了不起的人:

附言: 如果你喜欢这篇博客文章并希望看到 iximiuz Labs 不断发展,请考虑在 Patreon 上支持我的工作——这将帮助我投入更多时间到该平台并撰写更多像这样的博客文章。赞助人当然可以获得对该平台的高级访问权限!😉

作者:Ivan Velichko

在 Twitter 上关注我 @iximiuz [](https://iximiuz.com/en/posts/iximiuz-labs-story/<https:/twitter.com/intent/tweet?via=iximiuz&text=Building a Firecracker-Powered Course Platform To Learn Docker and Kubernetes&hashtags=devops,architecture,experience>) 提升你的服务器端技能——加入 9,000 名工程师的行列,直接在收件箱中获取有见地的学习资料: 订阅!

分类

Containers (36)Programming (30)Kubernetes (16)Networking (16)Linux / Unix (15)Ranting (12)Observability (6)Serverless (1)

热门标签

docker (24)go (14)experience (12)container-image (11)container-runtime (10)containerd (9)rust (8)oci (7)architecture (7)python (7)更多…

系列

Mastering Container Networking (7)Computer Networking Fundamentals (7)Debugging Containers Like a Pro (6)Debunking Container Myths (5)[Learning Prometheus and PromQL (5)](https://iximiuz.