- 容器运行时
- 云原生
- 网络
【免费下载链接】rkt
[Project ended] rkt is a pod-native container engine for Linux. It is composable, secure, and built on standards.
rktexport子命令负责把一个已经退出(exited)的单应用 Pod的根文件系统连同其 Image Manifest 重新打包成一个标准的 App Container Image(.aci)文件,从而实现「容器运行态 → 可分发镜像」的逆向转化。本文以 Documentation/subcommands/export.md 为骨架,结合 rkt/export.go、pkg/pod/pods.go 等源码实现,系统讲解rkt export的调用方式、前置条件、应用选择规则、--overwrite行为以及底层 OverlayFS 挂载与 ACI 打包原理,帮助你理解并正确使用这一镜像导出能力。
命令概览与基本用法
rkt export的用法非常简洁,命令形式为:
rkt export UUID OUTPUT_ACI_FILE其中UUID是已退出 Pod的 UUID(支持前缀匹配,见后文),OUTPUT_ACI_FILE是要写入的.aci文件路径。最简单的调用示例如下:
$ rkt export UUID .aci即:指定 Pod 的 UUID 与输出 ACI 文件名,rkt 就会把该 Pod 中应用的根文件系统与清单打包为 ACI。从 rkt/export.go 的 cobra 命令定义可以看到其完整形式:
export [--app=APPNAME] UUID OUTPUT_ACI_FILE--app用于在包含多个应用的 Pod 中指定要导出的应用,默认行为见「应用选择逻辑」一节。
命令的入口逻辑位于 rkt/export.go 的runExport函数:它首先校验参数个数(必须是两个),然后校验输出文件扩展名,解析 Pod UUID,检查 Pod 状态,选择应用,最后根据 Pod 是否使用 OverlayFS 走不同的文件收集路径并调用buildAci完成打包。
前置条件:只能导出已退出的 Pod
rkt export只接受已退出的 Pod。源码中,rkt 通过 Pod 在数据目录中的位置与锁状态判定其生命周期状态(详见 pkg/pod/pods.go 中定义的状态常量,如Embryo、Preparing、Prepared、Running、Exited、ExitedGarbage等):
state := p.State() if state != pkgPod.Exited && state != pkgPod.ExitedGarbage { stderr.Print("pod is not exited. Only exited pods can be exported") return 254 }也就是说,只有状态为exited或exited garbage的 Pod 才能被导出;仍处于running、prepared或preparing等状态的 Pod 会被拒绝,并打印提示pod is not exited. Only exited pods can be exported。
在实际操作中,一个常见的「导出工作流」是:
$ rkt prepare coreos.com/etcd:v2.3.4 # 先准备 Pod $ rkt run-prepared <UUID> # 运行 Pod $ rkt list # 查看 Pod 状态与 UUID $ rkt export <UUID> my-app.aci # Pod 退出后导出为 ACIUUID 支持前缀匹配
runExport通过pkgPod.PodFromUUIDString解析 UUID(见 rkt/export.go)。该函数内部会先把传入字符串转为小写,再对所有 Pod 目录做前缀匹配(见 pkg/pod/uuid.go):
- 无匹配:报错
no matches found for "..."; - 多个匹配:报错
ambiguous uuid, N matches; - 恰好一个匹配:返回完整的 UUID。
因此你可以像rkt export bc3c export.aci这样使用 UUID 前缀,只要前缀是唯一的即可。
应用选择逻辑:单应用自动选中,多应用必须指定--app
一个 Pod 中可以包含多个应用(App)。导出时必须明确「导出哪个应用」。rkt/export.go 中getApp函数实现了三段式选择逻辑:
- 若通过
--app=APPNAME显式指定了应用名,则从 Pod Manifest 的Apps列表中按名字匹配;若该名字不存在,报错app <name> is not present in pod; - 若未指定
--app,且 Pod 中恰好只有一个应用,则直接导出该应用; - 若未指定
--app,而 Pod 中包含多个应用,则把所有应用名打印到stderr,并要求用户用rkt export --app=...明确指定,否则报错退出。
也就是说,单应用 Pod 可以省略--app,多应用 Pod 则必须显式指定。应用名来自 Pod Manifest 中runtimeApp的name字段(schema.RuntimeApp),导出时会据此定位该应用的 rootfs 与清单文件。
--overwrite:控制是否覆盖已存在的输出文件
rkt export唯一专属选项是--overwrite。原文档中的参数表如下:
| Flag | Default | Options | Description |
|---|---|---|---|
--overwrite | false | trueorfalse | Overwrite the output ACI if it exists |
该选项默认关闭(false)。源码层面,rkt/export.go 的buildAci在打开输出文件时根据该标志组合文件打开模式:
mode := os.O_CREATE | os.O_WRONLY if flagOverwriteACI { mode |= os.O_TRUNC } else { mode |= os.O_EXCL } aciFile, err := os.OpenFile(target, mode, 0644) if err != nil { if os.IsExist(err) { return errors.New("target file exists (try --overwrite)") } ... }- 不传
--overwrite时使用O_EXCL,若目标文件已存在则直接报错target file exists (try --overwrite); - 传入
--overwrite时改用O_TRUNC,目标文件会被截断并覆盖写入。
因此,重复导出到同一路径时要么先删除旧文件,要么加上--overwrite标志。
输出文件必须使用.aci扩展名
runExport会强制校验输出文件扩展名(rkt/export.go):
ext := filepath.Ext(outACI) if ext != schema.ACIExtension { stderr.Printf("extension must be %s (given %s)", schema.ACIExtension, outACI) return 254 }schema.ACIExtension来自 appc spec,即.aci。如果输出文件名不是.aci结尾(例如export.tar),命令会以退出码 254 报错:extension must be .aci (given ...)。
底层导出流程:OverlayFS 与非 Overlay 两条路径
rkt export的打包核心是buildAci,但在进入打包之前,它必须先从已退出的 Pod 中收集到应用的「完整 rootfs 快照」。由于 rkt 支持用 OverlayFS 运行 Pod,导出逻辑依据p.UsesOverlay()(即 Pod 目录下是否存在overlay-prepared标记文件,见 pkg/pod/pods.go 与 common/common.go 的OverlayPreparedFilename)分两条路径处理。
路径一:OverlayFS Pod —— 重新挂载合并视图
当 Pod 使用 OverlayFS 时,应用的最终 rootfs 由底层镜像(lower)与应用运行期写入的上层(upper)叠加而成。为了导出完整内容,runExport会:
- 在数据目录下创建临时目录
$dataDir/tmp/rkt-export-<UUID>,并在其中建立rootfs子目录(rkt/export.go); - 调用
mountOverlay把该应用的真实文件系统挂载到这个临时目录上(rkt/export.go)。
mountOverlay通过 imagestore 与 treestore 定位底层:从 Pod 的appsinfo/<app>/treeStoreID文件读取 treestore ID(GetAppTreeStoreID),以其 rootfs 作为 overlay 的lowerdir;upper 层位于 Pod 目录的overlay/<treeStoreID>/upper/<app>,work 层位于overlay/<treeStoreID>/work/<app>。随后调用 common/overlay/overlay.go 的Mount执行mount("overlay", dest, "overlay", 0, opts)系统调用:
func Mount(cfg *MountCfg) error { err := syscall.Mount("overlay", cfg.Dest, "overlay", 0, cfg.Opts()) ... }其中挂载选项由MountCfg(Lower/Upper/Work/Dest/Lbl)构造为lowerdir=...,upperdir=...,workdir=...,目录名中的冒号与逗号会被转义以避免被内核当作分隔符(sanitize)。挂载完成后,临时挂载点便呈现出应用在退出时的完整文件系统视图。
在runExport中,临时挂载点与临时目录通过defer保证在函数返回前被卸载(syscall.Unmount)和删除(os.RemoveAll)。这一设计确保导出过程不污染 Pod 数据目录。
路径二:非 OverlayFS Pod —— 校验无残留挂载点
当 Pod 未使用 OverlayFS 时,应用的 rootfs 直接存在于磁盘上($poddir/opt/stage2/<app>/rootfs),理论上可以直接打包。但若 Pod 退出后仍残留未清理的挂载点,直接打包会得到不完整的文件系统。因此runExport会先用mountinfo.ParseMounts解析宿主挂载表,并按应用的 rootfs 路径前缀过滤(rkt/export.go):
appRootfs := common.AppRootfsPath(p.Path(), app.Name) + string(filepath.Separator) mnts, err := mountinfo.ParseMounts(0) ... mnts = mnts.Filter(mountinfo.HasPrefix(appRootfs)) if len(mnts) > 0 { stderr.Printf("pod has remaining mountpoints. Only pods using overlayfs or with no mountpoints can be exported") return 254 }注意 rootfs 路径后追加了路径分隔符,目的是避免误匹配到appRootfs前缀相同的其他路径。如果检测到残留挂载点,则报错pod has remaining mountpoints. Only pods using overlayfs or with no mountpoints can be exported。换言之,要么使用 OverlayFS,要么 Pod 中不存在任何残留挂载点,二者满足其一即可导出。
ACI 打包:gzip + tar + appc ImageWriter
无论走哪条路径,最终都汇聚到buildAci(rkt/export.go),其打包流程如下:
- 以相应打开模式创建输出
.aci文件(权限0644); - 依次构造
gzip.Writer与tar.Writer,即 ACI 实际是 gzip 压缩的 tar 归档; - 从
$poddir/appsinfo/<app>/manifest(即common.AppInfoPath+aci.ManifestFile)读取并解析该应用的 Image Manifest(schema.ImageManifest); - 使用 appc 规范提供的
aci.NewImageWriter(im, tr)创建镜像写入器; - 通过
filepath.Walk(root, aci.BuildWalker(root, iw, walkerCb))递归遍历 rootfs,将每个文件以 tar 头写入; - 调用
iw.Close()收尾,写入 ACI 布局所需的补充条目; - 若中途出错,defer 中会关闭所有 writer 并删除目标文件,避免留下不完整的 ACI。
用户命名空间支持:导出时对 uid/gid 反偏移
如果 Pod 是以--private-users启动的(即使用了用户命名空间),Pod 目录下会存在private-users-prepared文件(见 common/common.go 的PrivateUsersPreparedFilename)。runExport会读取并反序列化其中的shift:count偏移信息(rkt/export.go),构造*user.UidRange:
privUserFile := filepath.Join(p.Path(), common.PrivateUsersPreparedFilename) privUserContent, err := ioutil.ReadFile(privUserFile) if err == nil { uidRange = user.NewBlankUidRange() if err := uidRange.Deserialize(privUserContent); err != nil { ... } }UidRange的定义与序列化/反序列化实现位于 pkg/user/uid_range.go。该结构记录Shift(偏移量)与Count(范围大小),启动时由--private-users触发SetRandomUidRange(user.DefaultRangeCount)(默认范围0x10000,见 rkt/run.go 与 pkg/user/uid_range.go)。
导出时,由于 Pod 内文件的所有者 uid/gid 是「宿主 uid + 偏移量」后的值,直接打包会得到错误的属主。因此buildAci在写入每个 tar 头时通过uidRange.UnshiftRange(uid, gid)把 uid/gid 减去偏移量,还原成镜像内部的原始属主(rkt/export.go):
var walkerCb aci.TarHeaderWalkFunc = func(hdr *tar.Header) bool { if uidRange != nil { uid, gid, err := uidRange.UnshiftRange(uint32(hdr.Uid), uint32(hdr.Gid)) ... hdr.Uid, hdr.Gid = int(uid), int(gid) } return true }这样导出的 ACI 在普通命名空间下重新运行时,文件属主是正确且一致的。
全局选项
与 rkt 的所有子命令一样,rkt export也接受一组全局选项(原文档通过[global-options]链接指向 Documentation/commands.md,此处摘录关键项):
| Flag | Default | Options | Description |
|---|---|---|---|
--debug | false | trueorfalse | 向stderr输出更多调试信息 |
--dir | /var/lib/rkt | 目录路径 | rkt 数据目录路径 |
--local-config | /etc/rkt | 目录路径 | 本地配置目录路径 |
--system-config | /usr/lib/rkt | 目录路径 | 系统配置目录路径 |
--user-config | '' | 目录路径 | 用户配置目录路径 |
--insecure-options | none | none、http、image、tls、pubkey、capabilities、paths、seccomp、all-fetch、all-run、all | 逗号分隔的需关闭的安全特性列表 |
--trust-keys-from-https | false | trueorfalse | 自动信任从 HTTPS(或配合pubkey时从 HTTP)获取的 GPG 公钥 |
其中--dir与导出关系最为密切:它决定了 Pod 数据目录的位置,进而决定runExport查找 Pod、创建临时目录以及访问 imagestore/treestore 的根路径。
错误处理与退出码
rkt export的错误路径统一返回退出码254(源码中以return 254表示失败),典型失败场景包括:
- 参数个数不为 2(打印用法并退出);
- 输出文件扩展名不是
.aci; - UUID 无法解析、无匹配或存在歧义;
- Pod 尚未退出(仍处于 running/prepared 等状态);
- 指定的
--app不存在,或多应用 Pod 未指定--app; - 非 OverlayFS Pod 存在残留挂载点;
- 目标 ACI 已存在但未加
--overwrite; - 打开、打包或卸载过程中出现文件系统错误。
此外,清理临时目录或卸载临时挂载点时若发生错误,exit会被置为 1,作为非致命错误的温和提示。
测试验证:覆盖六类典型场景
仓库中的功能测试为rkt export的行为提供了完整的验证样本。核心测试定义在 tests/rkt_export_test.go 中,exportTestCases覆盖了六类场景:
noOverlaySimpleTest:--no-overlay启动,导出单应用 Pod 后运行新 ACI 校验文件内容;specifiedAppTest:使用--app=rkt-inspect显式指定应用;multiAppPodTest:多应用 Pod 中通过--app导出指定应用(另一个应用写入不同的文件内容以作区分);userNS:--private-users --no-overlay启动后导出,验证 uid/gid 反偏移逻辑;overlaySimpleTest:OverlayFS Pod 导出;overlaySimulateReboot:模拟重启后卸载 overlay 再导出。
每个用例的验证流程高度一致:用 inspect 镜像的--write-file在 Pod 内写入一个测试文件 → 运行 Pod 使其退出 →rkt export导出 ACI → 用rkt run运行新 ACI 并执行--read-file读取同一文件,确认内容一致(ThisIsATest)→ 最后执行rkt gc与rkt image gc清理。
测试按 stage1 风格分流执行:常规容器 stage1 在 tests/rkt_export_container_test.go 中遍历全部用例(并通过common.SupportsOverlay()、common.SupportsUserNS()探测运行环境能力,不满足条件的用例自动跳过);fly stage1 则在 tests/rkt_export_fly_test.go 中单独运行overlaySimpleTest。这些测试文件同时是「如何完整走通一次导出并验证产物」的最佳实操范例。
与rkt image export的区分
rkt export与镜像仓库导出命令rkt image export容易混淆,二者针对不同的对象:
| 命令 | 对象 | 行为 |
|---|---|---|
rkt export UUID OUTPUT_ACI_FILE | 已退出的 Pod | 把应用在 Pod 中的运行态文件系统(含运行时写入)重新打包为 ACI |
rkt image export IMAGE OUTPUT_ACI_FILE | 本地镜像仓库中的镜像 | 把已存储的原始镜像(按镜像 ID 或名字引用)直接复制为 ACI,不做任何重新打包 |
从实现上看,rkt/image_export.go 只是打开 imagestore 并io.Copy原始数据流到输出文件;而rkt export需要经过挂载合并、清单读取、tar/gzip 重建等完整流程。如果你需要的是「容器运行后产生的最终状态」,请使用rkt export;如果只是备份仓库中某个镜像,应使用rkt image export。
小结
rkt export是一个把运行态 Pod 逆向还原为可分发 ACI 镜像的实用工具。使用时需要记住四个关键点:只能导出已退出的 Pod、输出文件必须带.aci扩展名、多应用 Pod 必须用--app指定导出目标、目标文件已存在时需要--overwrite。在底层,rkt 依据 Pod 是否使用 OverlayFS 分别通过「临时挂载合并视图」或「残留挂载点校验」收集完整 rootfs,再借助 appc 规范的 ImageWriter 重建 gzip+tar 格式的 ACI,并针对--private-users启动的 Pod 自动完成 uid/gid 反偏移。相关实现与测试可作为进一步学习 rkt 生命周期、OverlayFS 与 ACI 规范的绝佳切入点。
- 容器运行时
- 云原生
- 网络
【免费下载链接】rkt
[Project ended] rkt is a pod-native container engine for Linux. It is composable, secure, and built on standards.
相关推荐
Podman Volume Export 实战指南:将命名卷导出为 tar 归档
Podman Volume Export 实战指南:将命名卷导出为 tar 归档 podman volume export 是 Podman 提供的卷备份与迁移
容器运行时云原生CLIuv export:将 uv.lock 导出为 requirements.txt、pylock.toml 与 CycloneDX SBOM 的实践指南
uv export:将 uv.lock 导出为 requirements.txt、pylock.toml 与 CycloneDX SBOM 的实践指南 本文基于
包管理器开发工具CLIdirty_sock exploit开发详解:如何构造恶意snap包实现root权限获取
dirty_sock exploit开发详解:如何构造恶意snap包实现root权限获取 dirty_sock是一个针对Linux系统的本地提权漏洞利用工具,通
容器运行时云原生网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考